mirror of
https://github.com/daniviga/django-ram.git
synced 2025-08-04 05:07:50 +02:00
Add a CSV export functionality in admin and add price fields (#41)
* Implement an action do download data in csv * Refactor CSV download * Move price to main models and add csv to bookshelf * Update template and API * Small refactoring
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
|
import html
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.utils.html import strip_tags
|
||||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||||
|
|
||||||
|
from ram.utils import generate_csv
|
||||||
|
from portal.utils import get_site_conf
|
||||||
from bookshelf.models import (
|
from bookshelf.models import (
|
||||||
BaseBookProperty,
|
BaseBookProperty,
|
||||||
BaseBookImage,
|
BaseBookImage,
|
||||||
@@ -71,12 +77,28 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"number_of_pages",
|
"number_of_pages",
|
||||||
"publication_year",
|
"publication_year",
|
||||||
"description",
|
"description",
|
||||||
"purchase_date",
|
|
||||||
"notes",
|
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Purchase data",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"purchase_date",
|
||||||
|
"price",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"notes",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"Audit",
|
"Audit",
|
||||||
{
|
{
|
||||||
@@ -89,13 +111,66 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
form.base_fields["price"].label = "Price ({})".format(
|
||||||
|
get_site_conf().currency
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
|
||||||
@admin.display(description="Publisher")
|
@admin.display(description="Publisher")
|
||||||
def get_publisher(self, obj):
|
def get_publisher(self, obj):
|
||||||
return obj.publisher.name
|
return obj.publisher.name
|
||||||
|
|
||||||
@admin.display(description="Authors")
|
@admin.display(description="Authors")
|
||||||
def get_authors(self, obj):
|
def get_authors(self, obj):
|
||||||
return ", ".join(a.short_name() for a in obj.authors.all())
|
return obj.authors_list
|
||||||
|
|
||||||
|
def download_csv(modeladmin, request, queryset):
|
||||||
|
header = [
|
||||||
|
"Title",
|
||||||
|
"Authors",
|
||||||
|
"Publisher",
|
||||||
|
"ISBN",
|
||||||
|
"Language",
|
||||||
|
"Number of Pages",
|
||||||
|
"Publication Year",
|
||||||
|
"Description",
|
||||||
|
"Tags",
|
||||||
|
"Purchase Date",
|
||||||
|
"Price ({})".format(get_site_conf().currency),
|
||||||
|
"Notes",
|
||||||
|
"Properties",
|
||||||
|
]
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for obj in queryset:
|
||||||
|
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
"{}:{}".format(property.property.name, property.value)
|
||||||
|
for property in obj.property.all()
|
||||||
|
)
|
||||||
|
data.append([
|
||||||
|
obj.title,
|
||||||
|
obj.authors_list.replace(",", settings.CSV_SEPARATOR_ALT),
|
||||||
|
obj.publisher.name,
|
||||||
|
obj.ISBN,
|
||||||
|
dict(settings.LANGUAGES)[obj.language],
|
||||||
|
obj.number_of_pages,
|
||||||
|
obj.publication_year,
|
||||||
|
html.unescape(strip_tags(obj.description)),
|
||||||
|
settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
t.name for t in obj.tags.all()
|
||||||
|
),
|
||||||
|
obj.purchase_date,
|
||||||
|
obj.price,
|
||||||
|
html.unescape(strip_tags(obj.notes)),
|
||||||
|
properties,
|
||||||
|
])
|
||||||
|
|
||||||
|
return generate_csv(header, data, "bookshelf_books.csv")
|
||||||
|
|
||||||
|
download_csv.short_description = "Download selected items as CSV"
|
||||||
|
actions = [download_csv]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Author)
|
@admin.register(Author)
|
||||||
@@ -146,12 +221,28 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"number_of_pages",
|
"number_of_pages",
|
||||||
"publication_year",
|
"publication_year",
|
||||||
"description",
|
"description",
|
||||||
"purchase_date",
|
|
||||||
"notes",
|
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Purchase data",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"purchase_date",
|
||||||
|
"price",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"notes",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"Audit",
|
"Audit",
|
||||||
{
|
{
|
||||||
@@ -164,6 +255,61 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
form.base_fields["price"].label = "Price ({})".format(
|
||||||
|
get_site_conf().currency
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
|
||||||
@admin.display(description="Scales")
|
@admin.display(description="Scales")
|
||||||
def get_scales(self, obj):
|
def get_scales(self, obj):
|
||||||
return "/".join(s.scale for s in obj.scales.all())
|
return "/".join(s.scale for s in obj.scales.all())
|
||||||
|
|
||||||
|
def download_csv(modeladmin, request, queryset):
|
||||||
|
header = [
|
||||||
|
"Catalog",
|
||||||
|
"Manufacturer",
|
||||||
|
"Years",
|
||||||
|
"Scales",
|
||||||
|
"ISBN",
|
||||||
|
"Language",
|
||||||
|
"Number of Pages",
|
||||||
|
"Publication Year",
|
||||||
|
"Description",
|
||||||
|
"Tags",
|
||||||
|
"Purchase Date",
|
||||||
|
"Price ({})".format(get_site_conf().currency),
|
||||||
|
"Notes",
|
||||||
|
"Properties",
|
||||||
|
]
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for obj in queryset:
|
||||||
|
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
"{}:{}".format(property.property.name, property.value)
|
||||||
|
for property in obj.property.all()
|
||||||
|
)
|
||||||
|
data.append([
|
||||||
|
obj.__str__,
|
||||||
|
obj.manufacturer.name,
|
||||||
|
obj.years,
|
||||||
|
obj.get_scales,
|
||||||
|
obj.ISBN,
|
||||||
|
dict(settings.LANGUAGES)[obj.language],
|
||||||
|
obj.number_of_pages,
|
||||||
|
obj.publication_year,
|
||||||
|
html.unescape(strip_tags(obj.description)),
|
||||||
|
settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
t.name for t in obj.tags.all()
|
||||||
|
),
|
||||||
|
obj.purchase_date,
|
||||||
|
obj.price,
|
||||||
|
html.unescape(strip_tags(obj.notes)),
|
||||||
|
properties,
|
||||||
|
])
|
||||||
|
|
||||||
|
return generate_csv(header, data, "bookshelf_catalogs.csv")
|
||||||
|
|
||||||
|
download_csv.short_description = "Download selected items as CSV"
|
||||||
|
actions = [download_csv]
|
||||||
|
36
ram/bookshelf/migrations/0019_basebook_price.py
Normal file
36
ram/bookshelf/migrations/0019_basebook_price.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2024-12-29 17:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def price_to_property(apps, schema_editor):
|
||||||
|
basebook = apps.get_model("bookshelf", "BaseBook")
|
||||||
|
for row in basebook.objects.all():
|
||||||
|
prop = row.property.filter(property__name__icontains="price")
|
||||||
|
for p in prop:
|
||||||
|
try:
|
||||||
|
row.price = float(p.value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
row.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0018_alter_basebookdocument_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="basebook",
|
||||||
|
name="price",
|
||||||
|
field=models.DecimalField(
|
||||||
|
blank=True, decimal_places=2, max_digits=10, null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
price_to_property,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@@ -49,6 +49,12 @@ class BaseBook(BaseModel):
|
|||||||
number_of_pages = models.SmallIntegerField(null=True, blank=True)
|
number_of_pages = models.SmallIntegerField(null=True, blank=True)
|
||||||
publication_year = models.SmallIntegerField(null=True, blank=True)
|
publication_year = models.SmallIntegerField(null=True, blank=True)
|
||||||
description = tinymce.HTMLField(blank=True)
|
description = tinymce.HTMLField(blank=True)
|
||||||
|
price = models.DecimalField(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
purchase_date = models.DateField(null=True, blank=True)
|
purchase_date = models.DateField(null=True, blank=True)
|
||||||
tags = models.ManyToManyField(
|
tags = models.ManyToManyField(
|
||||||
Tag, related_name="bookshelf", blank=True
|
Tag, related_name="bookshelf", blank=True
|
||||||
@@ -114,9 +120,14 @@ class Book(BaseBook):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
@property
|
||||||
def publisher_name(self):
|
def publisher_name(self):
|
||||||
return self.publisher.name
|
return self.publisher.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def authors_list(self):
|
||||||
|
return ", ".join(a.short_name() for a in self.authors.all())
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"bookshelf_item",
|
"bookshelf_item",
|
||||||
|
@@ -1,6 +1,10 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from bookshelf.models import Book, Author, Publisher
|
from bookshelf.models import Book, Catalog, Author, Publisher
|
||||||
from metadata.serializers import TagSerializer
|
from metadata.serializers import (
|
||||||
|
ScaleSerializer,
|
||||||
|
ManufacturerSerializer,
|
||||||
|
TagSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthorSerializer(serializers.ModelSerializer):
|
class AuthorSerializer(serializers.ModelSerializer):
|
||||||
@@ -22,5 +26,16 @@ class BookSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
fields = "__all__"
|
exclude = ("price",)
|
||||||
|
read_only_fields = ("creation_time", "updated_time")
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogSerializer(serializers.ModelSerializer):
|
||||||
|
scales = ScaleSerializer(many=True)
|
||||||
|
manufacturer = ManufacturerSerializer()
|
||||||
|
tags = TagSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Catalog
|
||||||
|
exclude = ("price",)
|
||||||
read_only_fields = ("creation_time", "updated_time")
|
read_only_fields = ("creation_time", "updated_time")
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from bookshelf.views import BookList, BookGet
|
from bookshelf.views import BookList, BookGet, CatalogList, CatalogGet
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("book/list", BookList.as_view()),
|
path("book/list", BookList.as_view()),
|
||||||
path("book/get/<uuid:uuid>", BookGet.as_view()),
|
path("book/get/<uuid:uuid>", BookGet.as_view()),
|
||||||
|
path("catalog/list", CatalogList.as_view()),
|
||||||
|
path("catalog/get/<uuid:uuid>", CatalogGet.as_view()),
|
||||||
]
|
]
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||||
from rest_framework.schemas.openapi import AutoSchema
|
from rest_framework.schemas.openapi import AutoSchema
|
||||||
|
|
||||||
from bookshelf.models import Book
|
from bookshelf.models import Book, Catalog
|
||||||
from bookshelf.serializers import BookSerializer
|
from bookshelf.serializers import BookSerializer, CatalogSerializer
|
||||||
|
|
||||||
|
|
||||||
class BookList(ListAPIView):
|
class BookList(ListAPIView):
|
||||||
@@ -19,3 +19,19 @@ class BookGet(RetrieveAPIView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Book.objects.get_published(self.request.user)
|
return Book.objects.get_published(self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogList(ListAPIView):
|
||||||
|
serializer_class = CatalogSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Catalog.objects.get_published(self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogGet(RetrieveAPIView):
|
||||||
|
serializer_class = CatalogSerializer
|
||||||
|
lookup_field = "uuid"
|
||||||
|
schema = AutoSchema(operation_id_base="retrieveCatalogByUUID")
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Book.objects.get_published(self.request.user)
|
||||||
|
@@ -17,6 +17,7 @@ class SiteConfigurationAdmin(SingletonModelAdmin):
|
|||||||
"about",
|
"about",
|
||||||
"items_per_page",
|
"items_per_page",
|
||||||
"items_ordering",
|
"items_ordering",
|
||||||
|
"currency",
|
||||||
"footer",
|
"footer",
|
||||||
"footer_extended",
|
"footer_extended",
|
||||||
)
|
)
|
||||||
|
21
ram/portal/migrations/0018_siteconfiguration_currency.py
Normal file
21
ram/portal/migrations/0018_siteconfiguration_currency.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2024-12-29 15:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"portal",
|
||||||
|
"0017_alter_flatpage_content_alter_siteconfiguration_about_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="siteconfiguration",
|
||||||
|
name="currency",
|
||||||
|
field=models.CharField(default="EUR", max_length=3),
|
||||||
|
),
|
||||||
|
]
|
@@ -30,6 +30,7 @@ class SiteConfiguration(SingletonModel):
|
|||||||
],
|
],
|
||||||
default="type",
|
default="type",
|
||||||
)
|
)
|
||||||
|
currency = models.CharField(max_length=3, default="EUR")
|
||||||
footer = tinymce.HTMLField(blank=True)
|
footer = tinymce.HTMLField(blank=True)
|
||||||
footer_extended = tinymce.HTMLField(blank=True)
|
footer_extended = tinymce.HTMLField(blank=True)
|
||||||
show_version = models.BooleanField(default=True)
|
show_version = models.BooleanField(default=True)
|
||||||
|
@@ -113,6 +113,12 @@
|
|||||||
<th scope="row">Purchase date</th>
|
<th scope="row">Purchase date</th>
|
||||||
<td>{{ book.purchase_date|default:"-" }}</td>
|
<td>{{ book.purchase_date|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Price ({{ site_conf.currency }})</th>
|
||||||
|
<td>{{ book.price|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if properties %}
|
{% if properties %}
|
||||||
|
@@ -187,6 +187,12 @@
|
|||||||
<th scope="row">Purchase date</th>
|
<th scope="row">Purchase date</th>
|
||||||
<td>{{ rolling_stock.purchase_date|default:"-" }}</td>
|
<td>{{ rolling_stock.purchase_date|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Price ({{ site_conf.currency }})</th>
|
||||||
|
<td>{{ rolling_stock.price|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if properties %}
|
{% if properties %}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
from ram.utils import git_suffix
|
from ram.utils import git_suffix
|
||||||
|
|
||||||
__version__ = "0.14.3"
|
__version__ = "0.15.0"
|
||||||
__version__ += git_suffix(__file__)
|
__version__ += git_suffix(__file__)
|
||||||
|
@@ -170,6 +170,9 @@ SITE_NAME = "Railroad Assets Manger"
|
|||||||
# The file must be placed in the root of the 'static' folder
|
# The file must be placed in the root of the 'static' folder
|
||||||
DEFAULT_CARD_IMAGE = "coming_soon.svg"
|
DEFAULT_CARD_IMAGE = "coming_soon.svg"
|
||||||
|
|
||||||
|
CSV_SEPARATOR = ","
|
||||||
|
CSV_SEPARATOR_ALT = ";"
|
||||||
|
|
||||||
DECODER_INTERFACES = [
|
DECODER_INTERFACES = [
|
||||||
(0, "Built-in"),
|
(0, "Built-in"),
|
||||||
(1, "NEM651"),
|
(1, "NEM651"),
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
|
import csv
|
||||||
import hashlib
|
import hashlib
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.text import slugify as django_slugify
|
from django.utils.text import slugify as django_slugify
|
||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
@@ -57,3 +60,15 @@ def slugify(string, custom_separator=None):
|
|||||||
if custom_separator is not None:
|
if custom_separator is not None:
|
||||||
string = string.replace("-", custom_separator)
|
string = string.replace("-", custom_separator)
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
def generate_csv(header, data, filename, separator=settings.CSV_SEPARATOR):
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="{}"'.format(
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
writer = csv.writer(response)
|
||||||
|
writer.writerow(header)
|
||||||
|
for row in data:
|
||||||
|
writer.writerow(row)
|
||||||
|
return response
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
|
import html
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||||
|
|
||||||
|
from ram.utils import generate_csv
|
||||||
|
from portal.utils import get_site_conf
|
||||||
from roster.models import (
|
from roster.models import (
|
||||||
RollingClass,
|
RollingClass,
|
||||||
RollingClassProperty,
|
RollingClassProperty,
|
||||||
@@ -152,8 +159,6 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"era",
|
"era",
|
||||||
"description",
|
"description",
|
||||||
"production_year",
|
"production_year",
|
||||||
"purchase_date",
|
|
||||||
"notes",
|
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -168,6 +173,24 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Purchase data",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"purchase_date",
|
||||||
|
"price",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"notes",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"Audit",
|
"Audit",
|
||||||
{
|
{
|
||||||
@@ -179,3 +202,65 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
form.base_fields["price"].label = "Price ({})".format(
|
||||||
|
get_site_conf().currency
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
|
||||||
|
def download_csv(modeladmin, request, queryset):
|
||||||
|
header = [
|
||||||
|
"Company",
|
||||||
|
"Identifier",
|
||||||
|
"Road Number",
|
||||||
|
"Manufacturer",
|
||||||
|
"Scale",
|
||||||
|
"Item Number",
|
||||||
|
"Set",
|
||||||
|
"Era",
|
||||||
|
"Description",
|
||||||
|
"Production Year",
|
||||||
|
"Notes",
|
||||||
|
"Tags",
|
||||||
|
"Decoder Interface",
|
||||||
|
"Decoder",
|
||||||
|
"Address",
|
||||||
|
"Purchase Date",
|
||||||
|
"Price ({})".format(get_site_conf().currency),
|
||||||
|
"Properties",
|
||||||
|
]
|
||||||
|
data = []
|
||||||
|
for obj in queryset:
|
||||||
|
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
"{}:{}".format(property.property.name, property.value)
|
||||||
|
for property in obj.property.all()
|
||||||
|
)
|
||||||
|
data.append([
|
||||||
|
obj.rolling_class.company.name,
|
||||||
|
obj.rolling_class.identifier,
|
||||||
|
obj.road_number,
|
||||||
|
obj.manufacturer.name,
|
||||||
|
obj.scale.scale,
|
||||||
|
obj.item_number,
|
||||||
|
obj.set,
|
||||||
|
obj.era,
|
||||||
|
html.unescape(strip_tags(obj.description)),
|
||||||
|
obj.production_year,
|
||||||
|
html.unescape(strip_tags(obj.notes)),
|
||||||
|
settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
t.name for t in obj.tags.all()
|
||||||
|
),
|
||||||
|
obj.decoder_interface,
|
||||||
|
obj.decoder,
|
||||||
|
obj.address,
|
||||||
|
obj.purchase_date,
|
||||||
|
obj.price,
|
||||||
|
properties,
|
||||||
|
])
|
||||||
|
|
||||||
|
return generate_csv(header, data, "rolling_stock.csv")
|
||||||
|
|
||||||
|
download_csv.short_description = "Download selected items as CSV"
|
||||||
|
actions = [download_csv]
|
||||||
|
36
ram/roster/migrations/0030_rollingstock_price.py
Normal file
36
ram/roster/migrations/0030_rollingstock_price.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2024-12-29 15:23
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def price_to_property(apps, schema_editor):
|
||||||
|
rollingstock = apps.get_model("roster", "RollingStock")
|
||||||
|
for row in rollingstock.objects.all():
|
||||||
|
prop = row.property.filter(property__name__icontains="price")
|
||||||
|
for p in prop:
|
||||||
|
try:
|
||||||
|
row.price = float(p.value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
row.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("roster", "0029_alter_rollingstockimage_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="rollingstock",
|
||||||
|
name="price",
|
||||||
|
field=models.DecimalField(
|
||||||
|
blank=True, decimal_places=2, max_digits=10, null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
price_to_property,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@@ -101,6 +101,12 @@ class RollingStock(BaseModel):
|
|||||||
)
|
)
|
||||||
production_year = models.SmallIntegerField(null=True, blank=True)
|
production_year = models.SmallIntegerField(null=True, blank=True)
|
||||||
purchase_date = models.DateField(null=True, blank=True)
|
purchase_date = models.DateField(null=True, blank=True)
|
||||||
|
price = models.DecimalField(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
description = tinymce.HTMLField(blank=True)
|
description = tinymce.HTMLField(blank=True)
|
||||||
tags = models.ManyToManyField(
|
tags = models.ManyToManyField(
|
||||||
Tag, related_name="rolling_stock", blank=True
|
Tag, related_name="rolling_stock", blank=True
|
||||||
|
@@ -28,5 +28,5 @@ class RollingStockSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RollingStock
|
model = RollingStock
|
||||||
fields = "__all__"
|
exclude = ("price",)
|
||||||
read_only_fields = ("creation_time", "updated_time")
|
read_only_fields = ("creation_time", "updated_time")
|
||||||
|
Reference in New Issue
Block a user