From ddcf06994d6c50e5431da868d9210f513317c886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sat, 18 Jan 2025 15:37:56 +0100 Subject: [PATCH] Improve user experience in admin and UI (#45) --- ram/bookshelf/admin.py | 98 ++++++++--------- ...basebookdocument_creation_time_and_more.py | 34 ++++++ ram/consist/admin.py | 9 +- ram/consist/models.py | 4 + ram/metadata/admin.py | 49 ++++++++- ..._decoderdocument_creation_time_and_more.py | 66 +++++++++++ ram/metadata/models.py | 4 + ram/portal/templates/cards/company.html | 7 +- ram/portal/templates/cards/consist.html | 5 +- ram/portal/templates/cards/roster.html | 5 +- ram/portal/templates/consist.html | 5 +- ram/portal/templates/rollingstock.html | 32 ++++-- ram/ram/__init__.py | 2 +- ram/ram/models.py | 7 +- ram/roster/admin.py | 103 ++++++++++++------ ...ingstockdocument_creation_time_and_more.py | 34 ++++++ ram/roster/models.py | 6 +- 17 files changed, 364 insertions(+), 106 deletions(-) create mode 100644 ram/bookshelf/migrations/0021_basebookdocument_creation_time_and_more.py create mode 100644 ram/metadata/migrations/0022_decoderdocument_creation_time_and_more.py create mode 100644 ram/roster/migrations/0032_rollingstockdocument_creation_time_and_more.py diff --git a/ram/bookshelf/admin.py b/ram/bookshelf/admin.py index abae703..b5efe57 100644 --- a/ram/bookshelf/admin.py +++ b/ram/bookshelf/admin.py @@ -2,7 +2,7 @@ import html from django.conf import settings from django.contrib import admin -from django.utils.html import strip_tags +from django.utils.html import format_html, strip_tags from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from ram.admin import publish, unpublish @@ -93,12 +93,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin): ), ( "Notes", - { - "classes": ("collapse",), - "fields": ( - "notes", - ) - }, + {"classes": ("collapse",), "fields": ("notes",)}, ), ( "Audit", @@ -150,23 +145,25 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin): "{}:{}".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, - ]) + 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") @@ -185,9 +182,15 @@ class AuthorAdmin(admin.ModelAdmin): @admin.register(Publisher) class PublisherAdmin(admin.ModelAdmin): - list_display = ("name", "country") + list_display = ("name", "country_flag") search_fields = ("name",) + @admin.display(description="Country") + def country_flag(self, obj): + return format_html( + ' {}'.format(obj.country.flag, obj.country.name) + ) + @admin.register(Catalog) class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): @@ -237,12 +240,7 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): ), ( "Notes", - { - "classes": ("collapse",), - "fields": ( - "notes", - ) - }, + {"classes": ("collapse",), "fields": ("notes",)}, ), ( "Audit", @@ -287,24 +285,26 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin): "{}:{}".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, - ]) + 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") diff --git a/ram/bookshelf/migrations/0021_basebookdocument_creation_time_and_more.py b/ram/bookshelf/migrations/0021_basebookdocument_creation_time_and_more.py new file mode 100644 index 0000000..bf228b5 --- /dev/null +++ b/ram/bookshelf/migrations/0021_basebookdocument_creation_time_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.4 on 2025-01-18 11:20 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookshelf", "0020_alter_basebookdocument_unique_together_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="basebookdocument", + name="creation_time", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="basebookdocument", + name="updated_time", + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name="basebookdocument", + name="private", + field=models.BooleanField( + default=False, help_text="Document will be visible only to logged users" + ), + ), + ] diff --git a/ram/consist/admin.py b/ram/consist/admin.py index 3551770..58cb60b 100644 --- a/ram/consist/admin.py +++ b/ram/consist/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.utils.html import format_html from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from ram.admin import publish, unpublish @@ -28,10 +29,16 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin): "updated_time", ) list_filter = ("company", "era", "published") - list_display = ("__str__",) + list_filter + list_display = ("__str__",) + list_filter + ("country_flag",) search_fields = ("identifier",) + list_filter save_as = True + @admin.display(description="Country") + def country_flag(self, obj): + return format_html( + ' {}'.format(obj.country.flag, obj.country) + ) + fieldsets = ( ( None, diff --git a/ram/consist/models.py b/ram/consist/models.py index b97b375..a6ce884 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -39,6 +39,10 @@ class Consist(BaseModel): def get_absolute_url(self): return reverse("consist", kwargs={"uuid": self.uuid}) + @property + def country(self): + return self.company.country + def clean(self): if self.consist_item.filter(rolling_stock__published=False).exists(): raise ValidationError( diff --git a/ram/metadata/admin.py b/ram/metadata/admin.py index 81ec1a8..5cdfec0 100644 --- a/ram/metadata/admin.py +++ b/ram/metadata/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.utils.html import format_html from adminsortable2.admin import SortableAdminMixin from ram.admin import publish, unpublish @@ -47,18 +48,30 @@ class ScaleAdmin(admin.ModelAdmin): @admin.register(Company) class CompanyAdmin(admin.ModelAdmin): readonly_fields = ("logo_thumbnail",) - list_display = ("name", "country") - list_filter = list_display + list_display = ("name", "country_flag") + list_filter = ("name", "country") search_fields = ("name",) + @admin.display(description="Country") + def country_flag(self, obj): + return format_html( + ' {}'.format(obj.country.flag, obj.country.name) + ) + @admin.register(Manufacturer) class ManufacturerAdmin(admin.ModelAdmin): readonly_fields = ("logo_thumbnail",) - list_display = ("name", "category") + list_display = ("name", "category", "country_flag") list_filter = ("category",) search_fields = ("name",) + @admin.display(description="Country") + def country_flag(self, obj): + return format_html( + ' {}'.format(obj.country.flag, obj.country.name) + ) + @admin.register(Tag) class TagAdmin(admin.ModelAdmin): @@ -76,7 +89,7 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin): @admin.register(GenericDocument) class GenericDocumentAdmin(admin.ModelAdmin): - readonly_fields = ("size",) + readonly_fields = ("size", "creation_time", "updated_time") list_display = ( "__str__", "description", @@ -88,4 +101,32 @@ class GenericDocumentAdmin(admin.ModelAdmin): "description", "file", ) + fieldsets = ( + ( + None, + { + "fields": ( + "private", + "description", + "file", + "size", + "tags", + ) + }, + ), + ( + "Notes", + {"classes": ("collapse",), "fields": ("notes",)}, + ), + ( + "Audit", + { + "classes": ("collapse",), + "fields": ( + "creation_time", + "updated_time", + ), + }, + ), + ) actions = [publish, unpublish] diff --git a/ram/metadata/migrations/0022_decoderdocument_creation_time_and_more.py b/ram/metadata/migrations/0022_decoderdocument_creation_time_and_more.py new file mode 100644 index 0000000..3032c5a --- /dev/null +++ b/ram/metadata/migrations/0022_decoderdocument_creation_time_and_more.py @@ -0,0 +1,66 @@ +# Generated by Django 5.1.4 on 2025-01-18 11:20 + +import django.utils.timezone +import django_countries.fields +import tinymce.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("metadata", "0021_genericdocument"), + ] + + operations = [ + migrations.AddField( + model_name="decoderdocument", + name="creation_time", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="decoderdocument", + name="updated_time", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="genericdocument", + name="creation_time", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="genericdocument", + name="notes", + field=tinymce.models.HTMLField(blank=True), + ), + migrations.AddField( + model_name="genericdocument", + name="updated_time", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="manufacturer", + name="country", + field=django_countries.fields.CountryField(blank=True, max_length=2), + ), + migrations.AlterField( + model_name="decoderdocument", + name="private", + field=models.BooleanField( + default=False, help_text="Document will be visible only to logged users" + ), + ), + migrations.AlterField( + model_name="genericdocument", + name="private", + field=models.BooleanField( + default=False, help_text="Document will be visible only to logged users" + ), + ), + ] diff --git a/ram/metadata/models.py b/ram/metadata/models.py index 23fe45f..0a5e880 100644 --- a/ram/metadata/models.py +++ b/ram/metadata/models.py @@ -6,6 +6,8 @@ from django.dispatch.dispatcher import receiver from django.core.exceptions import ValidationError from django_countries.fields import CountryField +from tinymce import models as tinymce + from ram.models import Document from ram.utils import DeduplicatedStorage, get_image_preview, slugify from ram.managers import PublicManager @@ -34,6 +36,7 @@ class Manufacturer(models.Model): category = models.CharField( max_length=64, choices=settings.MANUFACTURER_TYPES ) + country = CountryField(blank=True) website = models.URLField(blank=True) logo = models.ImageField( upload_to=os.path.join("images", "manufacturers"), @@ -237,6 +240,7 @@ class Tag(models.Model): class GenericDocument(Document): + notes = tinymce.HTMLField(blank=True) tags = models.ManyToManyField(Tag, blank=True) class Meta: diff --git a/ram/portal/templates/cards/company.html b/ram/portal/templates/cards/company.html index 2ef9306..04307b8 100644 --- a/ram/portal/templates/cards/company.html +++ b/ram/portal/templates/cards/company.html @@ -7,7 +7,10 @@ - + + {% if d.item.freelance %} + + {% endif %} @@ -27,7 +30,7 @@ - {% if d.item.freelance %} diff --git a/ram/portal/templates/cards/consist.html b/ram/portal/templates/cards/consist.html index 4caffa2..85bc81e 100644 --- a/ram/portal/templates/cards/consist.html +++ b/ram/portal/templates/cards/consist.html @@ -24,7 +24,10 @@
CompanyCompanyFreelance
Country{{ d.item.country.name }} {{ d.item.country }} + {{ d.item.country }} {{ d.item.country.name }}
- + + {% if d.item.company.freelance %} + + {% endif %} diff --git a/ram/portal/templates/cards/roster.html b/ram/portal/templates/cards/roster.html index c2ebc8c..3936cdb 100644 --- a/ram/portal/templates/cards/roster.html +++ b/ram/portal/templates/cards/roster.html @@ -22,7 +22,10 @@
ConsistConsistFreelance
- + + {% if d.item.rolling_class.company.freelance %} + + {% endif %} diff --git a/ram/portal/templates/consist.html b/ram/portal/templates/consist.html index 3b7da7d..33d48c4 100644 --- a/ram/portal/templates/consist.html +++ b/ram/portal/templates/consist.html @@ -79,7 +79,10 @@
Rolling stockRolling stockFreelance
- + + {% if consist.company.freelance %} + + {% endif %} diff --git a/ram/portal/templates/rollingstock.html b/ram/portal/templates/rollingstock.html index 30271f8..f3d0b39 100644 --- a/ram/portal/templates/rollingstock.html +++ b/ram/portal/templates/rollingstock.html @@ -73,7 +73,10 @@
DataConsistFreelance
- + + {% if company.freelance %} + + {% endif %} @@ -84,7 +87,13 @@ + + + + @@ -281,7 +290,10 @@
Rolling stockRolling stockFreelance
Company - {{ company }} {{ company.extended_name_pp }} + {{ company }} {{ company.extended_name_pp }} +
Country + {{ company.country }} {{ company.country.name }}
- + + {% if company.freelance %} + + {% endif %} @@ -293,18 +305,16 @@ {% endif %} - + - - {% if company.freelance %} - - - - - {% endif %}
Company dataCompany dataFreelance
Name{{ company.name }} {{ company.extended_name_pp }} + {{ company.name }} {{ company.extended_name_pp }} +
Country{{ company.country.name }} {{ company.country }} + + {{ company.country }} {{ company.country.name }} +
NotesA freelance company
diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index a995164..90c1856 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.16.0" +__version__ = "0.16.1" __version__ += git_suffix(__file__) diff --git a/ram/ram/models.py b/ram/ram/models.py index 63d7c65..ee5e36b 100644 --- a/ram/ram/models.py +++ b/ram/ram/models.py @@ -28,7 +28,12 @@ class Document(models.Model): upload_to="files/", storage=DeduplicatedStorage(), ) - private = models.BooleanField(default=False) + private = models.BooleanField( + default=False, + help_text="Document will be visible only to logged users", + ) + creation_time = models.DateTimeField(auto_now_add=True) + updated_time = models.DateTimeField(auto_now=True) class Meta: abstract = True diff --git a/ram/roster/admin.py b/ram/roster/admin.py index 388852a..026c56e 100644 --- a/ram/roster/admin.py +++ b/ram/roster/admin.py @@ -2,7 +2,7 @@ import html from django.conf import settings from django.contrib import admin -from django.utils.html import strip_tags +from django.utils.html import format_html, strip_tags from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin @@ -31,7 +31,7 @@ class RollingClassPropertyInline(admin.TabularInline): class RollingClass(admin.ModelAdmin): inlines = (RollingClassPropertyInline,) autocomplete_fields = ("manufacturer",) - list_display = ("__str__", "type", "company") + list_display = ("__str__", "type", "company", "country_flag") list_filter = ("company", "type__category", "type") search_fields = ( "identifier", @@ -40,6 +40,12 @@ class RollingClass(admin.ModelAdmin): ) save_as = True + @admin.display(description="Country") + def country_flag(self, obj): + return format_html( + ' {}'.format(obj.country.flag, obj.country) + ) + class RollingStockDocInline(admin.TabularInline): model = RollingStockDocument @@ -87,6 +93,21 @@ class RollingStockDocumentAdmin(admin.ModelAdmin): "description", "file", ) + autocomplete_fields = ("rolling_stock",) + fieldsets = ( + ( + None, + { + "fields": ( + "private", + "rolling_stock", + "description", + "file", + "size", + ) + }, + ), + ) @admin.register(RollingStockJournal) @@ -94,19 +115,32 @@ class RollingJournalDocumentAdmin(admin.ModelAdmin): list_display = ( "__str__", "date", - "rolling_stock", "private", ) list_filter = ( "date", "private", ) + autocomplete_fields = ("rolling_stock",) search_fields = ( "rolling_stock__rolling_class__identifier", "rolling_stock__road_number", "rolling_stock__item_number", "log", ) + fieldsets = ( + ( + None, + { + "fields": ( + "private", + "rolling_stock", + "log", + "date", + ) + }, + ), + ) @admin.register(RollingStock) @@ -126,7 +160,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): "scale", "item_number", "company", - "country", + "country_flag", "published", ) list_filter = ( @@ -146,6 +180,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): ) save_as = True + @admin.display(description="Country") + def country_flag(self, obj): + return format_html( + ' {}'.format(obj.country.flag, obj.country) + ) + fieldsets = ( ( None, @@ -187,12 +227,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): ), ( "Notes", - { - "classes": ("collapse",), - "fields": ( - "notes", - ) - }, + {"classes": ("collapse",), "fields": ("notes",)}, ), ( "Audit", @@ -241,29 +276,31 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): "{}:{}".format(property.property.name, property.value) for property in obj.property.all() ) - data.append([ - obj.__str__(), - 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, - ]) + data.append( + [ + obj.__str__(), + 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") diff --git a/ram/roster/migrations/0032_rollingstockdocument_creation_time_and_more.py b/ram/roster/migrations/0032_rollingstockdocument_creation_time_and_more.py new file mode 100644 index 0000000..b3f69bf --- /dev/null +++ b/ram/roster/migrations/0032_rollingstockdocument_creation_time_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.4 on 2025-01-18 11:20 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("roster", "0031_alter_rollingstockdocument_unique_together_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="rollingstockdocument", + name="creation_time", + field=models.DateTimeField( + auto_now_add=True, default=django.utils.timezone.now + ), + preserve_default=False, + ), + migrations.AddField( + model_name="rollingstockdocument", + name="updated_time", + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name="rollingstockdocument", + name="private", + field=models.BooleanField( + default=False, help_text="Document will be visible only to logged users" + ), + ), + ] diff --git a/ram/roster/models.py b/ram/roster/models.py index 3d5217d..637495c 100644 --- a/ram/roster/models.py +++ b/ram/roster/models.py @@ -42,6 +42,10 @@ class RollingClass(models.Model): def __str__(self): return "{0} {1}".format(self.company, self.identifier) + @property + def country(self): + return self.company.country + class RollingClassProperty(PropertyInstance): rolling_class = models.ForeignKey( @@ -127,7 +131,7 @@ class RollingStock(BaseModel): @property def country(self): - return str(self.rolling_class.company.country) + return self.rolling_class.company.country @property def company(self):