From 647894bca7b199b5e2af9feb5e6affd75154a6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Thu, 20 Mar 2025 22:07:12 +0100 Subject: [PATCH 01/34] Add country flag to cards --- ram/portal/templates/cards/consist.html | 5 ++++- ram/portal/templates/cards/roster.html | 1 + ram/ram/__init__.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ram/portal/templates/cards/consist.html b/ram/portal/templates/cards/consist.html index 3af1cc0..d8075c5 100644 --- a/ram/portal/templates/cards/consist.html +++ b/ram/portal/templates/cards/consist.html @@ -46,7 +46,10 @@ {% endif %} Company - {{ d.item.company }} + + {{ d.item.company.country }} + {{ d.item.company }} + Era diff --git a/ram/portal/templates/cards/roster.html b/ram/portal/templates/cards/roster.html index 544cba4..bdb57fe 100644 --- a/ram/portal/templates/cards/roster.html +++ b/ram/portal/templates/cards/roster.html @@ -43,6 +43,7 @@ Company + {{ d.item.company.country }} {{ d.item.company }} diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index 2629044..abac0a2 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.17.1" +__version__ = "0.17.2" __version__ += git_suffix(__file__) From a2c857a3cddce8eab10fead94b0d5454ded9b623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Fri, 25 Apr 2025 23:14:10 +0200 Subject: [PATCH 02/34] Fix a couple of bugs --- ram/bookshelf/models.py | 4 ++++ ram/ram/__init__.py | 2 +- ram/roster/admin.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ram/bookshelf/models.py b/ram/bookshelf/models.py index fba7aa5..3469a31 100644 --- a/ram/bookshelf/models.py +++ b/ram/bookshelf/models.py @@ -137,6 +137,10 @@ class Catalog(BaseBook): ordering = ["manufacturer", "publication_year"] def __str__(self): + # if the object is new, return an empty string to avoid + # calling self.scales.all() which would raise a infinite recursion + if self.pk is None: + return str() # empty string scales = self.get_scales() return "%s %s %s" % (self.manufacturer.name, self.years, scales) diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index abac0a2..c84cd75 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.17.2" +__version__ = "0.17.3" __version__ += git_suffix(__file__) diff --git a/ram/roster/admin.py b/ram/roster/admin.py index f1962aa..7c79b75 100644 --- a/ram/roster/admin.py +++ b/ram/roster/admin.py @@ -273,11 +273,11 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): settings.CSV_SEPARATOR_ALT.join( t.name for t in obj.tags.all() ), - obj.decoder_interface, + obj.get_decoder_interface_display(), obj.decoder, obj.address, - obj.purchase_date, obj.shop, + obj.purchase_date, obj.price, properties, ] From 88d718fa948b875499dce0044a6404a23468d072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sat, 26 Apr 2025 00:16:09 +0200 Subject: [PATCH 03/34] Minor footer improvement on large screens --- ram/portal/templates/includes/footer.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ram/portal/templates/includes/footer.html b/ram/portal/templates/includes/footer.html index fd53602..44ba82b 100644 --- a/ram/portal/templates/includes/footer.html +++ b/ram/portal/templates/includes/footer.html @@ -12,9 +12,11 @@

Made with ❤️ for 🚂 and django-ram {% if site_conf.show_version %}
Version {{ site_conf.version }}{% endif %}

-

- {% if site_conf.disclaimer %} {% endif %} - +

+ {% if site_conf.disclaimer %} + Disclaimer | + {% endif %} + Back to top

From 85741f090ccff2f24e49607780a9b27c79b1297b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 27 Apr 2025 18:22:13 +0200 Subject: [PATCH 04/34] Provide consist composition --- ram/consist/models.py | 14 ++++++++++++++ ram/portal/templates/cards/consist.html | 2 +- ram/portal/templates/consist.html | 2 +- ram/ram/__init__.py | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/ram/consist/models.py b/ram/consist/models.py index f89b5b9..f9d5ac8 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -39,6 +39,20 @@ class Consist(BaseModel): def get_absolute_url(self): return reverse("consist", kwargs={"uuid": self.uuid}) + @property + def length(self): + return self.consist_item.count() + + def get_type_count(self): + return self.consist_item.annotate( + type=models.F("rolling_stock__rolling_class__type__type") + ).values( + "type" + ).annotate( + count=models.Count("rolling_stock"), + category=models.F("rolling_stock__rolling_class__type__category") + ).order_by("rolling_stock__rolling_class__type__order") + @property def country(self): return self.company.country diff --git a/ram/portal/templates/cards/consist.html b/ram/portal/templates/cards/consist.html index d8075c5..7b11d6b 100644 --- a/ram/portal/templates/cards/consist.html +++ b/ram/portal/templates/cards/consist.html @@ -57,7 +57,7 @@ Length - {{ d.item.consist_item.count }} + {{ d.item.length }} diff --git a/ram/portal/templates/consist.html b/ram/portal/templates/consist.html index 254de34..79b85db 100644 --- a/ram/portal/templates/consist.html +++ b/ram/portal/templates/consist.html @@ -109,7 +109,7 @@ {% endif %} Length - {{ data | length }} + {{ consist.length }}: {% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} + {% endif %}{% endfor %} diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index c84cd75..21933ee 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.17.3" +__version__ = "0.17.4" __version__ += git_suffix(__file__) From 122211687412b1577efd9d0ff40727ded030a599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 27 Apr 2025 22:12:15 +0200 Subject: [PATCH 05/34] Improve consist counter, fix a bug with unpublished stock --- .../0016_alter_consistitem_order.py | 18 +++++++++++++++ ram/consist/models.py | 23 ++++++++----------- ram/ram/__init__.py | 2 +- 3 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 ram/consist/migrations/0016_alter_consistitem_order.py diff --git a/ram/consist/migrations/0016_alter_consistitem_order.py b/ram/consist/migrations/0016_alter_consistitem_order.py new file mode 100644 index 0000000..68cc677 --- /dev/null +++ b/ram/consist/migrations/0016_alter_consistitem_order.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-04-27 19:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("consist", "0015_consist_description"), + ] + + operations = [ + migrations.AlterField( + model_name="consistitem", + name="order", + field=models.PositiveIntegerField(), + ), + ] diff --git a/ram/consist/models.py b/ram/consist/models.py index f9d5ac8..e6467aa 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -50,19 +50,14 @@ class Consist(BaseModel): "type" ).annotate( count=models.Count("rolling_stock"), - category=models.F("rolling_stock__rolling_class__type__category") - ).order_by("rolling_stock__rolling_class__type__order") + category=models.F("rolling_stock__rolling_class__type__category"), + order=models.Max("order"), + ).order_by("order") @property def country(self): return self.company.country - def clean(self): - if self.consist_item.filter(rolling_stock__published=False).exists(): - raise ValidationError( - "You must publish all items in the consist before publishing the consist." # noqa: E501 - ) - class Meta: ordering = ["company", "-creation_time"] @@ -72,11 +67,7 @@ class ConsistItem(models.Model): Consist, on_delete=models.CASCADE, related_name="consist_item" ) rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE) - order = models.PositiveIntegerField( - default=1000, # make sure it is always added at the end - blank=False, - null=False - ) + order = models.PositiveIntegerField(blank=False, null=False) class Meta: ordering = ["order"] @@ -90,6 +81,12 @@ class ConsistItem(models.Model): def __str__(self): return "{0}".format(self.rolling_stock) + def clean(self): + if self.consist.published and not self.rolling_stock.published: + raise ValidationError( + "You must unpublish the the consist before using this item." + ) + def published(self): return self.rolling_stock.published published.boolean = True diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index 21933ee..da1f3ee 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.17.4" +__version__ = "0.17.5" __version__ += git_suffix(__file__) From e9ec126ada14bab7b543b188f84814171ebb47c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 27 Apr 2025 22:22:47 +0200 Subject: [PATCH 06/34] Fix a bug in the consist admin search --- ram/consist/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ram/consist/admin.py b/ram/consist/admin.py index fa774f6..f898b4f 100644 --- a/ram/consist/admin.py +++ b/ram/consist/admin.py @@ -28,7 +28,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin): "creation_time", "updated_time", ) - list_filter = ("company", "era", "published") + list_filter = ("company__name", "era", "published") list_display = ("__str__",) + list_filter + ("country_flag",) search_fields = ("identifier",) + list_filter save_as = True From ece8d1ad948b5932caa694c6447c3702803bb0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Tue, 29 Apr 2025 22:34:46 +0200 Subject: [PATCH 07/34] Minor UI improvement --- ram/portal/templates/consist.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ram/portal/templates/consist.html b/ram/portal/templates/consist.html index 79b85db..3c50c15 100644 --- a/ram/portal/templates/consist.html +++ b/ram/portal/templates/consist.html @@ -109,7 +109,7 @@ {% endif %} Length - {{ consist.length }}: {% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} + {% endif %}{% endfor %} + {{ consist.length }} | {% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} + {% endif %}{% endfor %} From 2e06e94fdeef396af75820577f29341ef6ee6a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Wed, 30 Apr 2025 22:50:43 +0200 Subject: [PATCH 08/34] Add counters to cards --- ram/portal/templates/cards/company.html | 4 +- ram/portal/templates/cards/manufacturer.html | 4 +- .../templates/cards/rolling_stock_type.html | 6 +- ram/portal/templates/cards/scale.html | 4 +- ram/portal/templates/consist.html | 6 +- ram/portal/views.py | 59 ++++++++++++++++--- ram/ram/__init__.py | 2 +- 7 files changed, 71 insertions(+), 14 deletions(-) diff --git a/ram/portal/templates/cards/company.html b/ram/portal/templates/cards/company.html index 9467c79..fc0b5af 100644 --- a/ram/portal/templates/cards/company.html +++ b/ram/portal/templates/cards/company.html @@ -39,8 +39,10 @@
- Show all rolling stock + {% with items=d.item.num_items %} + Show {{ items }} item{{ items | pluralize}} {% if request.user.is_staff %}Edit{% endif %} + {% endwith %}
diff --git a/ram/portal/templates/cards/manufacturer.html b/ram/portal/templates/cards/manufacturer.html index e809b57..c7e41e0 100644 --- a/ram/portal/templates/cards/manufacturer.html +++ b/ram/portal/templates/cards/manufacturer.html @@ -30,8 +30,10 @@
- Show all rolling stock + {% with items=d.item.num_items %} + Show {{ items }} item{{ items | pluralize}} {% if request.user.is_staff %}Edit{% endif %} + {% endwith %}
diff --git a/ram/portal/templates/cards/rolling_stock_type.html b/ram/portal/templates/cards/rolling_stock_type.html index 52a45f8..30665c9 100644 --- a/ram/portal/templates/cards/rolling_stock_type.html +++ b/ram/portal/templates/cards/rolling_stock_type.html @@ -20,8 +20,10 @@
- Show all rolling stock - {% if request.user.is_staff %}Edit{% endif %} + {% with items=d.item.num_items %} + Show {{ items }} item{{ items | pluralize}} + {% if request.user.is_staff %}Edit{% endif %} + {% endwith %}
diff --git a/ram/portal/templates/cards/scale.html b/ram/portal/templates/cards/scale.html index b80f1f2..4c1118a 100644 --- a/ram/portal/templates/cards/scale.html +++ b/ram/portal/templates/cards/scale.html @@ -28,8 +28,10 @@
- Show all rolling stock + {% with items=d.item.num_items %} + Show {{ items }} item{{ items | pluralize}} {% if request.user.is_staff %}Edit{% endif %} + {% endwith %}
diff --git a/ram/portal/templates/consist.html b/ram/portal/templates/consist.html index 3c50c15..25d1e3b 100644 --- a/ram/portal/templates/consist.html +++ b/ram/portal/templates/consist.html @@ -109,7 +109,11 @@ {% endif %} Length - {{ consist.length }} | {% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} + {% endif %}{% endfor %} + {{ consist.length }} + + + Composition + {% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} » {% endif %}{% endfor %} diff --git a/ram/portal/views.py b/ram/portal/views.py index 221de45..22d4776 100644 --- a/ram/portal/views.py +++ b/ram/portal/views.py @@ -7,7 +7,7 @@ from urllib.parse import unquote from django.views import View from django.http import Http404, HttpResponseBadRequest from django.db.utils import OperationalError, ProgrammingError -from django.db.models import Q, Count +from django.db.models import F, Q, Count from django.shortcuts import render, get_object_or_404, get_list_or_404 from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import Paginator @@ -490,7 +490,11 @@ class Manufacturers(GetData): item_type = "manufacturer" def get_data(self, request): - return Manufacturer.objects.filter(self.filter) + return Manufacturer.objects.filter( + self.filter + ).annotate( + num_items=Count("rollingstock") + Count("rollingclass"), + ).order_by("name") # overload get method to filter by category def get(self, request, category, page=1): @@ -506,18 +510,50 @@ class Companies(GetData): item_type = "company" def get_data(self, request): - return Company.objects.all() + return Company.objects.annotate( + num_rollingstock=( + Count( + "rollingclass__rolling_class", + filter=Q( + rollingclass__rolling_class__in=( + RollingStock.objects.get_published(request.user) + ) + ), + distinct=True + ) + ) + ).annotate( + num_consists=( + Count( + "consist", + filter=Q( + consist__in=( + Consist.objects.get_published(request.user) + ), + ), + distinct=True + ) + ) + ).annotate( + num_items=F("num_rollingstock") + F("num_consists") + ).order_by("name") class Scales(GetData): title = "Scales" item_type = "scale" - queryset = Scale.objects.all() def get_data(self, request): return Scale.objects.annotate( - num_items=Count("rollingstock") - ) # .filter(num_items__gt=0) to filter data with no items + num_items=Count( + "rollingstock", + filter=Q( + rollingstock__in=RollingStock.objects.get_published( + request.user + ) + ), + ), + ).order_by("-ratio_int", "-tracks", "scale") class Types(GetData): @@ -525,7 +561,16 @@ class Types(GetData): item_type = "rolling_stock_type" def get_data(self, request): - return RollingStockType.objects.all() + return RollingStockType.objects.annotate( + num_items=Count( + "rollingclass__rolling_class", + filter=Q( + rollingclass__rolling_class__in=( + RollingStock.objects.get_published(request.user) + ) + ), + ) + ).order_by("order") class Books(GetData): diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index da1f3ee..bbf4658 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.17.5" +__version__ = "0.17.6" __version__ += git_suffix(__file__) From 40f42a9ee92d3b46d15a64558f19af97afebab1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Wed, 30 Apr 2025 22:52:51 +0200 Subject: [PATCH 09/34] Reformat portal/views.py --- ram/portal/views.py | 67 +++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/ram/portal/views.py b/ram/portal/views.py index 22d4776..9437d1b 100644 --- a/ram/portal/views.py +++ b/ram/portal/views.py @@ -490,11 +490,13 @@ class Manufacturers(GetData): item_type = "manufacturer" def get_data(self, request): - return Manufacturer.objects.filter( - self.filter - ).annotate( - num_items=Count("rollingstock") + Count("rollingclass"), - ).order_by("name") + return ( + Manufacturer.objects.filter(self.filter) + .annotate( + num_items=Count("rollingstock") + Count("rollingclass"), + ) + .order_by("name") + ) # overload get method to filter by category def get(self, request, category, page=1): @@ -510,33 +512,38 @@ class Companies(GetData): item_type = "company" def get_data(self, request): - return Company.objects.annotate( - num_rollingstock=( - Count( - "rollingclass__rolling_class", - filter=Q( - rollingclass__rolling_class__in=( - RollingStock.objects.get_published(request.user) - ) - ), - distinct=True - ) - ) - ).annotate( - num_consists=( - Count( - "consist", - filter=Q( - consist__in=( - Consist.objects.get_published(request.user) + return ( + Company.objects.annotate( + num_rollingstock=( + Count( + "rollingclass__rolling_class", + filter=Q( + rollingclass__rolling_class__in=( + RollingStock.objects.get_published( + request.user + ) + ) ), - ), - distinct=True + distinct=True, + ) ) ) - ).annotate( - num_items=F("num_rollingstock") + F("num_consists") - ).order_by("name") + .annotate( + num_consists=( + Count( + "consist", + filter=Q( + consist__in=( + Consist.objects.get_published(request.user) + ), + ), + distinct=True, + ) + ) + ) + .annotate(num_items=F("num_rollingstock") + F("num_consists")) + .order_by("name") + ) class Scales(GetData): @@ -614,7 +621,7 @@ class GetBookCatalog(View): "book": book, "documents": documents, "properties": properties, - "type": selector + "type": selector, }, ) From 7673f0514a8f2656f8ca2a375cbc500ea071e315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Thu, 1 May 2025 23:49:22 +0200 Subject: [PATCH 10/34] Fix filter by scale counters and add consist constrains Still to be improved, see FIXME --- ram/consist/admin.py | 4 +- ram/consist/migrations/0017_consist_scale.py | 42 ++++++++++++++++++++ ram/consist/models.py | 36 ++++++++++++++++- ram/portal/views.py | 32 ++++++++++----- ram/ram/__init__.py | 2 +- 5 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 ram/consist/migrations/0017_consist_scale.py diff --git a/ram/consist/admin.py b/ram/consist/admin.py index f898b4f..2ec67ef 100644 --- a/ram/consist/admin.py +++ b/ram/consist/admin.py @@ -25,10 +25,11 @@ class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline): class ConsistAdmin(SortableAdminBase, admin.ModelAdmin): inlines = (ConsistItemInline,) readonly_fields = ( + "scale", "creation_time", "updated_time", ) - list_filter = ("company__name", "era", "published") + list_filter = ("company__name", "era", "scale", "published") list_display = ("__str__",) + list_filter + ("country_flag",) search_fields = ("identifier",) + list_filter save_as = True @@ -49,6 +50,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin): "consist_address", "company", "era", + "scale", "description", "image", "tags", diff --git a/ram/consist/migrations/0017_consist_scale.py b/ram/consist/migrations/0017_consist_scale.py new file mode 100644 index 0000000..4e2fcaf --- /dev/null +++ b/ram/consist/migrations/0017_consist_scale.py @@ -0,0 +1,42 @@ +# Generated by Django 5.1.4 on 2025-05-01 09:51 + +import django.db.models.deletion +from django.db import migrations, models + + +def set_scale(apps, schema_editor): + Consist = apps.get_model("consist", "Consist") + + for consist in Consist.objects.all(): + try: + consist.scale = consist.consist_item.first().rolling_stock.scale + consist.save() + except AttributeError: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("consist", "0016_alter_consistitem_order"), + ( + "metadata", + "0024_remove_genericdocument_tags_delete_decoderdocument_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="consist", + name="scale", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="metadata.scale", + ), + ), + migrations.RunPython( + set_scale, + reverse_code=migrations.RunPython.noop + ), + ] diff --git a/ram/consist/models.py b/ram/consist/models.py index e6467aa..9a7f61f 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError from ram.models import BaseModel from ram.utils import DeduplicatedStorage -from metadata.models import Company, Tag +from metadata.models import Company, Scale, Tag from roster.models import RollingStock @@ -26,6 +26,7 @@ class Consist(BaseModel): blank=True, help_text="Era or epoch of the consist", ) + scale = models.ForeignKey(Scale, null=True, on_delete=models.CASCADE) image = models.ImageField( upload_to=os.path.join("images", "consists"), storage=DeduplicatedStorage, @@ -81,8 +82,39 @@ class ConsistItem(models.Model): def __str__(self): return "{0}".format(self.rolling_stock) + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if self.consist.scale != self.rolling_stock.scale: + self.consist.scale = self.rolling_stock.scale + self.consist.save() + + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + if not self.consist.consist_item.exists(): + self.consist.scale = None + self.consist.save() + def clean(self): - if self.consist.published and not self.rolling_stock.published: + rolling_stock = getattr(self, "rolling_stock", False) + if not rolling_stock: + return # exit if no inline are present + + # FIXME this does not work when creating a new consist, + # because the consist is not saved yet and it must be moved + # to the admin form validation via InlineFormSet.clean() + consist = self.consist + items = consist.consist_item + if ( + consist.pk # if we are not creating a new consist + and items.exists() # if there's at least one item + and self != items.first() # if we are not changing the first item + # if scale is different from the first item + and rolling_stock.scale != items.first().rolling_stock.scale + ): + raise ValidationError( + "The rolling stock and consist must be of the same scale." + ) + if self.consist.published and not rolling_stock.published: raise ValidationError( "You must unpublish the the consist before using this item." ) diff --git a/ram/portal/views.py b/ram/portal/views.py index 9437d1b..4685f5a 100644 --- a/ram/portal/views.py +++ b/ram/portal/views.py @@ -551,16 +551,30 @@ class Scales(GetData): item_type = "scale" def get_data(self, request): - return Scale.objects.annotate( - num_items=Count( - "rollingstock", - filter=Q( - rollingstock__in=RollingStock.objects.get_published( - request.user - ) + return ( + Scale.objects.annotate( + num_rollingstock=Count( + "rollingstock", + filter=Q( + rollingstock__in=RollingStock.objects.get_published( + request.user + ) + ), + distinct=True, ), - ), - ).order_by("-ratio_int", "-tracks", "scale") + num_consists=Count( + "consist", + filter=Q( + consist__in=Consist.objects.get_published( + request.user + ) + ), + distinct=True, + ), + ) + .annotate(num_items=F("num_rollingstock") + F("num_consists")) + .order_by("-ratio_int", "-tracks", "scale") + ) class Types(GetData): diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index bbf4658..30051e6 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.17.6" +__version__ = "0.17.7" __version__ += git_suffix(__file__) From 60195bc99f6178b9888e9d7e4f0049d22e743738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Fri, 2 May 2025 13:39:42 +0200 Subject: [PATCH 11/34] Simplify the logic about scales in the consist and remove async updates --- ram/consist/admin.py | 1 - .../migrations/0018_alter_consist_scale.py | 25 +++++++++++++++++++ ram/consist/models.py | 23 ++--------------- ram/ram/__init__.py | 2 +- 4 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 ram/consist/migrations/0018_alter_consist_scale.py diff --git a/ram/consist/admin.py b/ram/consist/admin.py index 2ec67ef..e480318 100644 --- a/ram/consist/admin.py +++ b/ram/consist/admin.py @@ -25,7 +25,6 @@ class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline): class ConsistAdmin(SortableAdminBase, admin.ModelAdmin): inlines = (ConsistItemInline,) readonly_fields = ( - "scale", "creation_time", "updated_time", ) diff --git a/ram/consist/migrations/0018_alter_consist_scale.py b/ram/consist/migrations/0018_alter_consist_scale.py new file mode 100644 index 0000000..48810d9 --- /dev/null +++ b/ram/consist/migrations/0018_alter_consist_scale.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.4 on 2025-05-02 11:33 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("consist", "0017_consist_scale"), + ( + "metadata", + "0024_remove_genericdocument_tags_delete_decoderdocument_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="consist", + name="scale", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="metadata.scale" + ), + ), + ] diff --git a/ram/consist/models.py b/ram/consist/models.py index 9a7f61f..01e842e 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -26,7 +26,7 @@ class Consist(BaseModel): blank=True, help_text="Era or epoch of the consist", ) - scale = models.ForeignKey(Scale, null=True, on_delete=models.CASCADE) + scale = models.ForeignKey(Scale, on_delete=models.CASCADE) image = models.ImageField( upload_to=os.path.join("images", "consists"), storage=DeduplicatedStorage, @@ -82,18 +82,6 @@ class ConsistItem(models.Model): def __str__(self): return "{0}".format(self.rolling_stock) - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - if self.consist.scale != self.rolling_stock.scale: - self.consist.scale = self.rolling_stock.scale - self.consist.save() - - def delete(self, *args, **kwargs): - super().delete(*args, **kwargs) - if not self.consist.consist_item.exists(): - self.consist.scale = None - self.consist.save() - def clean(self): rolling_stock = getattr(self, "rolling_stock", False) if not rolling_stock: @@ -103,14 +91,7 @@ class ConsistItem(models.Model): # because the consist is not saved yet and it must be moved # to the admin form validation via InlineFormSet.clean() consist = self.consist - items = consist.consist_item - if ( - consist.pk # if we are not creating a new consist - and items.exists() # if there's at least one item - and self != items.first() # if we are not changing the first item - # if scale is different from the first item - and rolling_stock.scale != items.first().rolling_stock.scale - ): + if rolling_stock.scale != consist.scale: raise ValidationError( "The rolling stock and consist must be of the same scale." ) diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index 30051e6..d918b03 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.17.7" +__version__ = "0.17.8" __version__ += git_suffix(__file__) From dea7a594bc9aa4b48ab7ddc58bb8a5dfca819769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Fri, 2 May 2025 22:25:59 +0200 Subject: [PATCH 12/34] Implement CSV export for cosists --- ram/consist/admin.py | 81 +++++++++++++++++++++++++++++++++++++++---- ram/consist/models.py | 4 +++ ram/roster/admin.py | 11 ++++-- 3 files changed, 87 insertions(+), 9 deletions(-) diff --git a/ram/consist/admin.py b/ram/consist/admin.py index e480318..33df151 100644 --- a/ram/consist/admin.py +++ b/ram/consist/admin.py @@ -1,11 +1,26 @@ +import html + +from django.conf import settings from django.contrib import admin -from django.utils.html import format_html -from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin +# from django.forms import BaseInlineFormSet # for future reference +from django.utils.html import format_html, strip_tags +from adminsortable2.admin import ( + SortableAdminBase, + SortableInlineAdminMixin, + # CustomInlineFormSetMixin, # for future reference +) from ram.admin import publish, unpublish +from ram.utils import generate_csv from consist.models import Consist, ConsistItem +# for future reference +# class ConsistItemInlineFormSet(CustomInlineFormSetMixin, BaseInlineFormSet): +# def clean(self): +# super().clean() + + class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline): model = ConsistItem min_num = 1 @@ -14,10 +29,11 @@ class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline): readonly_fields = ( "preview", "published", - "address", - "type", + "scale", "company", + "type", "era", + "address", ) @@ -46,10 +62,10 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin): "fields": ( "published", "identifier", - "consist_address", "company", - "era", "scale", + "era", + "consist_address", "description", "image", "tags", @@ -71,4 +87,55 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin): }, ), ) - actions = [publish, unpublish] + + def download_csv(modeladmin, request, queryset): + header = [ + "ID", + "Name", + "Published", + "Company", + "Country", + "Address", + "Scale", + "Era", + "Description", + "Tags", + "Length", + "Composition", + "Item name", + "Item type", + "Item ID", + ] + data = [] + for obj in queryset: + for item in obj.consist_item.all(): + types = " + ".join( + "{}x {}".format(t["count"], t["type"]) + for t in obj.get_type_count() + ) + data.append( + [ + obj.uuid, + obj.__str__(), + "X" if obj.published else "", + obj.company.name, + obj.company.country, + obj.consist_address, + obj.scale.scale, + obj.era, + html.unescape(strip_tags(obj.description)), + settings.CSV_SEPARATOR_ALT.join( + t.name for t in obj.tags.all() + ), + obj.length, + types, + item.rolling_stock.__str__(), + item.type, + item.rolling_stock.uuid, + ] + ) + + return generate_csv(header, data, "consists.csv") + download_csv.short_description = "Download selected items as CSV" + + actions = [publish, unpublish, download_csv] diff --git a/ram/consist/models.py b/ram/consist/models.py index 01e842e..7dfd464 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -107,6 +107,10 @@ class ConsistItem(models.Model): def preview(self): return self.rolling_stock.image.first().image_thumbnail(100) + @property + def scale(self): + return self.rolling_stock.scale + @property def type(self): return self.rolling_stock.rolling_class.type diff --git a/ram/roster/admin.py b/ram/roster/admin.py index 7c79b75..9aabe52 100644 --- a/ram/roster/admin.py +++ b/ram/roster/admin.py @@ -140,6 +140,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): "rolling_class__identifier", "rolling_class__company__name", "manufacturer__name", + "scale", "road_number", "address", "item_number", @@ -229,9 +230,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): def download_csv(modeladmin, request, queryset): header = [ + "ID", "Name", + "Class", + "Type", "Company", - "Identifier", + "Country", "Road Number", "Manufacturer", "Scale", @@ -258,9 +262,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): ) data.append( [ + obj.uuid, obj.__str__(), - obj.rolling_class.company.name, obj.rolling_class.identifier, + obj.rolling_class.type, + obj.rolling_class.company.name, + obj.rolling_class.company.country, obj.road_number, obj.manufacturer.name, obj.scale.scale, From 292b95b8ed4e65f522519e75b6511d9154cc91cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sat, 3 May 2025 21:02:44 +0200 Subject: [PATCH 13/34] Fix a bug in roster search via scale --- ram/roster/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ram/roster/admin.py b/ram/roster/admin.py index 9aabe52..739def0 100644 --- a/ram/roster/admin.py +++ b/ram/roster/admin.py @@ -140,7 +140,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): "rolling_class__identifier", "rolling_class__company__name", "manufacturer__name", - "scale", + "scale__scale", "road_number", "address", "item_number", From d0d25424fb46bd864728d6dea5031819ba07dab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 4 May 2025 19:12:05 +0200 Subject: [PATCH 14/34] Fix `road_number_int` field sizing --- .../0037_alter_rollingstock_road_number_int.py | 18 ++++++++++++++++++ ram/roster/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 ram/roster/migrations/0037_alter_rollingstock_road_number_int.py diff --git a/ram/roster/migrations/0037_alter_rollingstock_road_number_int.py b/ram/roster/migrations/0037_alter_rollingstock_road_number_int.py new file mode 100644 index 0000000..04ac49b --- /dev/null +++ b/ram/roster/migrations/0037_alter_rollingstock_road_number_int.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-05-04 17:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("roster", "0036_delete_rollingstockdocument"), + ] + + operations = [ + migrations.AlterField( + model_name="rollingstock", + name="road_number_int", + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/ram/roster/models.py b/ram/roster/models.py index 72fc261..7e1534d 100644 --- a/ram/roster/models.py +++ b/ram/roster/models.py @@ -67,7 +67,7 @@ class RollingStock(BaseModel): verbose_name="Class", ) road_number = models.CharField(max_length=128, unique=False) - road_number_int = models.PositiveSmallIntegerField(default=0, unique=False) + road_number_int = models.PositiveIntegerField(default=0, unique=False) manufacturer = models.ForeignKey( Manufacturer, on_delete=models.CASCADE, From 86657a3b9f5cc714a0f01d56102e932d7d5af184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 4 May 2025 19:12:50 +0200 Subject: [PATCH 15/34] Manually fix a migration to correctly bootsrap a new DB --- ram/bookshelf/migrations/0022_basebook_shop.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ram/bookshelf/migrations/0022_basebook_shop.py b/ram/bookshelf/migrations/0022_basebook_shop.py index 5457845..a791450 100644 --- a/ram/bookshelf/migrations/0022_basebook_shop.py +++ b/ram/bookshelf/migrations/0022_basebook_shop.py @@ -29,6 +29,10 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RemoveConstraint( + model_name="basebookdocument", + name="unique_book_file", + ), migrations.AddField( model_name="basebook", name="shop", From 76b266b1f97f5caab3f5886100e80267999103e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 4 May 2025 19:13:43 +0200 Subject: [PATCH 16/34] Extend search on company to slug field to better manage accented and utf names --- ram/ram/__init__.py | 2 +- ram/roster/admin.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ram/ram/__init__.py b/ram/ram/__init__.py index d918b03..1372aec 100644 --- a/ram/ram/__init__.py +++ b/ram/ram/__init__.py @@ -1,4 +1,4 @@ from ram.utils import git_suffix -__version__ = "0.17.8" +__version__ = "0.17.9" __version__ += git_suffix(__file__) diff --git a/ram/roster/admin.py b/ram/roster/admin.py index 739def0..e7908fa 100644 --- a/ram/roster/admin.py +++ b/ram/roster/admin.py @@ -36,6 +36,7 @@ class RollingClass(admin.ModelAdmin): search_fields = ( "identifier", "company__name", + "company__slug", "type__type", ) save_as = True @@ -139,6 +140,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin): search_fields = ( "rolling_class__identifier", "rolling_class__company__name", + "rolling_class__company__slug", "manufacturer__name", "scale__scale", "road_number", From b81c63898f0d52c39b5a5052d47076594899be5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 4 May 2025 22:05:47 +0200 Subject: [PATCH 17/34] Add more information in consist_item rows --- ram/consist/admin.py | 2 ++ ram/consist/models.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ram/consist/admin.py b/ram/consist/admin.py index 33df151..472b6a0 100644 --- a/ram/consist/admin.py +++ b/ram/consist/admin.py @@ -30,6 +30,8 @@ class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline): "preview", "published", "scale", + "manufacturer", + "item_number", "company", "type", "era", diff --git a/ram/consist/models.py b/ram/consist/models.py index 7dfd464..8077c3d 100644 --- a/ram/consist/models.py +++ b/ram/consist/models.py @@ -2,6 +2,7 @@ import os from django.db import models from django.urls import reverse +from django.utils.text import Truncator from django.dispatch import receiver from django.core.exceptions import ValidationError @@ -107,13 +108,21 @@ class ConsistItem(models.Model): def preview(self): return self.rolling_stock.image.first().image_thumbnail(100) + @property + def manufacturer(self): + return Truncator(self.rolling_stock.manufacturer).chars(10) + + @property + def item_number(self): + return self.rolling_stock.item_number + @property def scale(self): return self.rolling_stock.scale @property def type(self): - return self.rolling_stock.rolling_class.type + return self.rolling_stock.rolling_class.type.type @property def address(self): From b5c57dcd94d6195b0fcf312ec76ef662b0080814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 4 May 2025 22:46:23 +0200 Subject: [PATCH 18/34] Rely on slugs to have a more natural sorting --- ...ons_alter_manufacturer_options_and_more.py | 28 +++++++++++++++++++ ram/metadata/models.py | 6 ++-- 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 ram/metadata/migrations/0025_alter_company_options_alter_manufacturer_options_and_more.py diff --git a/ram/metadata/migrations/0025_alter_company_options_alter_manufacturer_options_and_more.py b/ram/metadata/migrations/0025_alter_company_options_alter_manufacturer_options_and_more.py new file mode 100644 index 0000000..0a1afe6 --- /dev/null +++ b/ram/metadata/migrations/0025_alter_company_options_alter_manufacturer_options_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.4 on 2025-05-04 20:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "metadata", + "0024_remove_genericdocument_tags_delete_decoderdocument_and_more", + ), + ] + + operations = [ + migrations.AlterModelOptions( + name="company", + options={"ordering": ["slug"], "verbose_name_plural": "Companies"}, + ), + migrations.AlterModelOptions( + name="manufacturer", + options={"ordering": ["category", "slug"]}, + ), + migrations.AlterModelOptions( + name="tag", + options={"ordering": ["slug"]}, + ), + ] diff --git a/ram/metadata/models.py b/ram/metadata/models.py index 29a3ae8..23f5f63 100644 --- a/ram/metadata/models.py +++ b/ram/metadata/models.py @@ -43,7 +43,7 @@ class Manufacturer(models.Model): ) class Meta: - ordering = ["category", "name"] + ordering = ["category", "slug"] def __str__(self): return self.name @@ -78,7 +78,7 @@ class Company(models.Model): class Meta: verbose_name_plural = "Companies" - ordering = ["name"] + ordering = ["slug"] def __str__(self): return self.name @@ -207,7 +207,7 @@ class Tag(models.Model): slug = models.CharField(max_length=128, unique=True) class Meta: - ordering = ["name"] + ordering = ["slug"] def __str__(self): return self.name From 222e2075ec78c84aad685402e611530810bf3fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Mon, 12 May 2025 13:51:31 +0200 Subject: [PATCH 19/34] Change the behavior of the modal --- ram/portal/templates/includes/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ram/portal/templates/includes/footer.html b/ram/portal/templates/includes/footer.html index 44ba82b..769a245 100644 --- a/ram/portal/templates/includes/footer.html +++ b/ram/portal/templates/includes/footer.html @@ -21,7 +21,7 @@