Compare commits

..

18 Commits

Author SHA1 Message Date
16bd82de39 Improve tables behavior on small screen (mobile) 2026-01-02 22:19:22 +01:00
2ae7f2685d Make documents UI modular 2026-01-02 19:12:00 +01:00
29f9a213b4 Make the TOC table responsive 2025-12-31 18:16:08 +01:00
884661d4e1 Enforce page number in TOC 2025-12-31 14:57:15 +01:00
c7cace96f7 Extend lenght of TOC items 2025-12-31 14:49:37 +01:00
d3c099c05b Extend search to toc titles 2025-12-30 22:21:43 +01:00
903633b5a7 Extend TOC to books 2025-12-30 22:15:29 +01:00
ee775d737e Make sure that cache is always cleaned while performing an update 2025-12-30 21:53:04 +01:00
8087ab5997 Implement TOC in UI 2025-12-30 00:44:06 +01:00
1899747909 Minor fix 2025-12-29 12:16:58 +01:00
0880bd0817 Initial implemntation of TOC for books et al. 2025-12-29 12:05:37 +01:00
74d7df2c8b Add an icon for fetured items 2025-12-29 11:44:54 +01:00
c81508bbd5 Fix a bug in featured count limit 2025-12-25 19:46:18 +01:00
b4f69d8a34 Fix a regression in a template 2025-12-25 11:13:14 +01:00
676418cb67 Code refactoring to simplify template data contexts (#55)
* Fix a search filter when no catalogs are returned
* Code refactoring to simplify templates
* Remove duplicated code
* Remove dead code
* More improvements, clean up and add featured items in homepage
* Fix a type and better page navigation
2025-12-24 15:38:07 +01:00
98d2e7beab Extend search to catalogs and scales 2025-12-23 12:19:26 +01:00
fb17dc2a7c Add some utils to generate cards via imagemagick 2025-12-21 23:01:04 +01:00
5a71dc36fa Improve sorting and extend search to magazines 2025-12-21 22:56:45 +01:00
43 changed files with 855 additions and 499 deletions

View File

@@ -11,7 +11,7 @@ from portal.utils import get_site_conf
from repository.models import ( from repository.models import (
BookDocument, BookDocument,
CatalogDocument, CatalogDocument,
MagazineIssueDocument MagazineIssueDocument,
) )
from bookshelf.models import ( from bookshelf.models import (
BaseBookProperty, BaseBookProperty,
@@ -22,6 +22,7 @@ from bookshelf.models import (
Catalog, Catalog,
Magazine, Magazine,
MagazineIssue, MagazineIssue,
TocEntry,
) )
@@ -58,9 +59,23 @@ class MagazineIssueDocInline(BookDocInline):
model = MagazineIssueDocument model = MagazineIssueDocument
class BookTocInline(admin.TabularInline):
model = TocEntry
min_num = 0
extra = 0
fields = (
"title",
"subtitle",
"authors",
"page",
"featured",
)
@admin.register(Book) @admin.register(Book)
class BookAdmin(SortableAdminBase, admin.ModelAdmin): class BookAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = ( inlines = (
BookTocInline,
BookPropertyInline, BookPropertyInline,
BookImageInline, BookImageInline,
BookDocInline, BookDocInline,
@@ -135,8 +150,8 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
if obj.invoice.exists(): if obj.invoice.exists():
html = format_html_join( html = format_html_join(
"<br>", "<br>",
"<a href=\"{}\" target=\"_blank\">{}</a>", '<a href="{}" target="_blank">{}</a>',
((i.file.url, i) for i in obj.invoice.all()) ((i.file.url, i) for i in obj.invoice.all()),
) )
else: else:
html = "-" html = "-"
@@ -212,11 +227,11 @@ class AuthorAdmin(admin.ModelAdmin):
@admin.register(Publisher) @admin.register(Publisher)
class PublisherAdmin(admin.ModelAdmin): class PublisherAdmin(admin.ModelAdmin):
list_display = ("name", "country_flag") list_display = ("name", "country_flag_name")
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag_name(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
@@ -240,10 +255,10 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
readonly_fields = ("invoices", "creation_time", "updated_time") readonly_fields = ("invoices", "creation_time", "updated_time")
search_fields = ("manufacturer__name", "years", "scales__scale") search_fields = ("manufacturer__name", "years", "scales__scale")
list_filter = ( list_filter = (
"published",
"manufacturer__name", "manufacturer__name",
"publication_year", "publication_year",
"scales__scale", "scales__scale",
"published",
) )
fieldsets = ( fieldsets = (
@@ -303,8 +318,8 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
if obj.invoice.exists(): if obj.invoice.exists():
html = format_html_join( html = format_html_join(
"<br>", "<br>",
"<a href=\"{}\" target=\"_blank\">{}</a>", '<a href="{}" target="_blank">{}</a>',
((i.file.url, i) for i in obj.invoice.all()) ((i.file.url, i) for i in obj.invoice.all()),
) )
else: else:
html = "-" html = "-"
@@ -366,6 +381,7 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.register(MagazineIssue) @admin.register(MagazineIssue)
class MagazineIssueAdmin(SortableAdminBase, admin.ModelAdmin): class MagazineIssueAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = ( inlines = (
BookTocInline,
BookPropertyInline, BookPropertyInline,
BookImageInline, BookImageInline,
MagazineIssueDocInline, MagazineIssueDocInline,
@@ -449,14 +465,12 @@ class MagazineIssueInline(admin.TabularInline):
readonly_fields = ("preview",) readonly_fields = ("preview",)
class Media: class Media:
js = ('admin/js/magazine_issue_defaults.js',) js = ("admin/js/magazine_issue_defaults.js",)
@admin.register(Magazine) @admin.register(Magazine)
class MagazineAdmin(SortableAdminBase, admin.ModelAdmin): class MagazineAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = ( inlines = (MagazineIssueInline,)
MagazineIssueInline,
)
list_display = ( list_display = (
"__str__", "__str__",
@@ -466,7 +480,10 @@ class MagazineAdmin(SortableAdminBase, admin.ModelAdmin):
autocomplete_fields = ("publisher",) autocomplete_fields = ("publisher",)
readonly_fields = ("creation_time", "updated_time") readonly_fields = ("creation_time", "updated_time")
search_fields = ("name", "publisher__name") search_fields = ("name", "publisher__name")
list_filter = ("publisher__name", "published") list_filter = (
"published",
"publisher__name",
)
fieldsets = ( fieldsets = (
( (

View File

@@ -0,0 +1,29 @@
# Generated by Django 6.0 on 2025-12-21 21:56
import django.db.models.functions.text
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0027_magazine_website"),
]
operations = [
migrations.AlterModelOptions(
name="magazine",
options={"ordering": [django.db.models.functions.text.Lower("name")]},
),
migrations.AlterModelOptions(
name="magazineissue",
options={
"ordering": [
"magazine",
"publication_year",
"publication_month",
"issue_number",
]
},
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 6.0 on 2025-12-23 11:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0028_alter_magazine_options_alter_magazineissue_options"),
("metadata", "0025_alter_company_options_alter_manufacturer_options_and_more"),
]
operations = [
migrations.AlterField(
model_name="catalog",
name="manufacturer",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="catalogs",
to="metadata.manufacturer",
),
),
migrations.AlterField(
model_name="catalog",
name="scales",
field=models.ManyToManyField(related_name="catalogs", to="metadata.scale"),
),
]

View File

@@ -0,0 +1,53 @@
# Generated by Django 6.0 on 2025-12-29 11:02
import django.db.models.deletion
import tinymce.models
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0029_alter_catalog_manufacturer_alter_catalog_scales"),
]
operations = [
migrations.CreateModel(
name="TocEntry",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("description", tinymce.models.HTMLField(blank=True)),
("notes", tinymce.models.HTMLField(blank=True)),
("creation_time", models.DateTimeField(auto_now_add=True)),
("updated_time", models.DateTimeField(auto_now=True)),
("published", models.BooleanField(default=True)),
("title", models.CharField(max_length=200)),
("subtitle", models.CharField(blank=True, max_length=200)),
("authors", models.CharField(blank=True, max_length=256)),
("page", models.SmallIntegerField()),
("featured", models.BooleanField(default=False)),
(
"book",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="toc",
to="bookshelf.basebook",
),
),
],
options={
"verbose_name": "Table of Contents Entry",
"verbose_name_plural": "Table of Contents Entries",
"ordering": ["page"],
},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 6.0 on 2025-12-31 13:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0030_tocentry"),
]
operations = [
migrations.AlterField(
model_name="tocentry",
name="authors",
field=models.CharField(blank=True),
),
migrations.AlterField(
model_name="tocentry",
name="subtitle",
field=models.CharField(blank=True),
),
migrations.AlterField(
model_name="tocentry",
name="title",
field=models.CharField(),
),
]

View File

@@ -5,6 +5,7 @@ from django.db import models
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.utils.dates import MONTHS from django.utils.dates import MONTHS
from django.db.models.functions import Lower
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django_countries.fields import CountryField from django_countries.fields import CountryField
@@ -59,36 +60,24 @@ class BaseBook(BaseModel):
blank=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
)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
shutil.rmtree( shutil.rmtree(
os.path.join( os.path.join(
settings.MEDIA_ROOT, "images", "books", str(self.uuid) settings.MEDIA_ROOT, "images", "books", str(self.uuid)
), ),
ignore_errors=True ignore_errors=True,
) )
super(BaseBook, self).delete(*args, **kwargs) super(BaseBook, self).delete(*args, **kwargs)
def book_image_upload(instance, filename): def book_image_upload(instance, filename):
return os.path.join( return os.path.join("images", "books", str(instance.book.uuid), filename)
"images",
"books",
str(instance.book.uuid),
filename
)
def magazine_image_upload(instance, filename): def magazine_image_upload(instance, filename):
return os.path.join( return os.path.join("images", "magazines", str(instance.uuid), filename)
"images",
"magazines",
str(instance.uuid),
filename
)
class BaseBookImage(Image): class BaseBookImage(Image):
@@ -132,8 +121,7 @@ class Book(BaseBook):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"bookshelf_item", "bookshelf_item", kwargs={"selector": "book", "uuid": self.uuid}
kwargs={"selector": "book", "uuid": self.uuid}
) )
@@ -141,9 +129,10 @@ class Catalog(BaseBook):
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="catalogs",
) )
years = models.CharField(max_length=12) years = models.CharField(max_length=12)
scales = models.ManyToManyField(Scale) scales = models.ManyToManyField(Scale, related_name="catalogs")
class Meta: class Meta:
ordering = ["manufacturer", "publication_year"] ordering = ["manufacturer", "publication_year"]
@@ -158,12 +147,12 @@ class Catalog(BaseBook):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"bookshelf_item", "bookshelf_item", kwargs={"selector": "catalog", "uuid": self.uuid}
kwargs={"selector": "catalog", "uuid": self.uuid}
) )
def get_scales(self): def get_scales(self):
return "/".join([s.scale for s in self.scales.all()]) return "/".join([s.scale for s in self.scales.all()])
get_scales.short_description = "Scales" get_scales.short_description = "Scales"
@@ -180,32 +169,36 @@ class Magazine(BaseModel):
language = models.CharField( language = models.CharField(
max_length=7, max_length=7,
choices=sorted(settings.LANGUAGES, key=lambda s: s[1]), choices=sorted(settings.LANGUAGES, key=lambda s: s[1]),
default='en' default="en",
)
tags = models.ManyToManyField(
Tag, related_name="magazine", blank=True
) )
tags = models.ManyToManyField(Tag, related_name="magazine", blank=True)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
shutil.rmtree( shutil.rmtree(
os.path.join( os.path.join(
settings.MEDIA_ROOT, "images", "magazines", str(self.uuid) settings.MEDIA_ROOT, "images", "magazines", str(self.uuid)
), ),
ignore_errors=True ignore_errors=True,
) )
super(Magazine, self).delete(*args, **kwargs) super(Magazine, self).delete(*args, **kwargs)
class Meta: class Meta:
ordering = ["name"] ordering = [Lower("name")]
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse("magazine", kwargs={"uuid": self.uuid})
"magazine",
kwargs={"uuid": self.uuid} def get_cover(self):
) if self.image:
return self.image
else:
cover_issue = self.issue.filter(published=True).first()
if cover_issue and cover_issue.image.exists():
return cover_issue.image.first().image
return None
def website_short(self): def website_short(self):
if self.website: if self.website:
@@ -218,14 +211,17 @@ class MagazineIssue(BaseBook):
) )
issue_number = models.CharField(max_length=100) issue_number = models.CharField(max_length=100)
publication_month = models.SmallIntegerField( publication_month = models.SmallIntegerField(
null=True, null=True, blank=True, choices=MONTHS.items()
blank=True,
choices=MONTHS.items()
) )
class Meta: class Meta:
unique_together = ("magazine", "issue_number") unique_together = ("magazine", "issue_number")
ordering = ["magazine", "issue_number"] ordering = [
"magazine",
"publication_year",
"publication_month",
"issue_number",
]
def __str__(self): def __str__(self):
return f"{self.magazine.name} - {self.issue_number}" return f"{self.magazine.name} - {self.issue_number}"
@@ -237,6 +233,10 @@ class MagazineIssue(BaseBook):
"published." "published."
) )
@property
def obj_label(self):
return "Magazine Issue"
def preview(self): def preview(self):
return self.image.first().image_thumbnail(100) return self.image.first().image_thumbnail(100)
@@ -246,9 +246,43 @@ class MagazineIssue(BaseBook):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"issue", "issue", kwargs={"uuid": self.uuid, "magazine": self.magazine.uuid}
kwargs={
"uuid": self.uuid,
"magazine": self.magazine.uuid
}
) )
class TocEntry(BaseModel):
book = models.ForeignKey(
BaseBook, on_delete=models.CASCADE, related_name="toc"
)
title = models.CharField()
subtitle = models.CharField(blank=True)
authors = models.CharField(blank=True)
page = models.SmallIntegerField()
featured = models.BooleanField(
default=False,
)
class Meta:
ordering = ["page"]
verbose_name = "Table of Contents Entry"
verbose_name_plural = "Table of Contents Entries"
def __str__(self):
if self.subtitle:
title = f"{self.title}: {self.subtitle}"
else:
title = self.title
return f"{title} (p. {self.page})"
def clean(self):
if self.page is None:
raise ValidationError("Page number is required.")
if self.page < 1:
raise ValidationError("Page number is invalid.")
try:
if self.page > self.book.number_of_pages:
raise ValidationError(
"Page number exceeds the publication's number of pages."
)
except TypeError:
pass # number_of_pages is None

View File

@@ -2,6 +2,7 @@ import html
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
# from django.forms import BaseInlineFormSet # for future reference # from django.forms import BaseInlineFormSet # for future reference
from django.utils.html import format_html, strip_tags from django.utils.html import format_html, strip_tags
from adminsortable2.admin import ( from adminsortable2.admin import (
@@ -46,15 +47,22 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"creation_time", "creation_time",
"updated_time", "updated_time",
) )
list_filter = ("company__name", "era", "scale", "published") list_filter = ("published", "company__name", "era", "scale")
list_display = ("__str__",) + list_filter + ("country_flag",) list_display = (
"__str__",
"company__name",
"era",
"scale",
"country_flag",
"published",
)
search_fields = ("identifier",) + list_filter search_fields = ("identifier",) + list_filter
save_as = True save_as = True
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country '<img src="{}" title="{}" />', obj.country.flag, obj.country.name
) )
fieldsets = ( fieldsets = (
@@ -138,6 +146,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
) )
return generate_csv(header, data, "consists.csv") return generate_csv(header, data, "consists.csv")
download_csv.short_description = "Download selected items as CSV" download_csv.short_description = "Download selected items as CSV"
actions = [publish, unpublish, download_csv] actions = [publish, unpublish, download_csv]

View File

@@ -47,12 +47,12 @@ class ScaleAdmin(admin.ModelAdmin):
@admin.register(Company) @admin.register(Company)
class CompanyAdmin(admin.ModelAdmin): class CompanyAdmin(admin.ModelAdmin):
readonly_fields = ("logo_thumbnail",) readonly_fields = ("logo_thumbnail",)
list_display = ("name", "country_flag") list_display = ("name", "country_flag_name")
list_filter = ("name", "country") list_filter = ("name", "country")
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag_name(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
@@ -61,12 +61,12 @@ class CompanyAdmin(admin.ModelAdmin):
@admin.register(Manufacturer) @admin.register(Manufacturer)
class ManufacturerAdmin(admin.ModelAdmin): class ManufacturerAdmin(admin.ModelAdmin):
readonly_fields = ("logo_thumbnail",) readonly_fields = ("logo_thumbnail",)
list_display = ("name", "category", "country_flag") list_display = ("name", "category", "country_flag_name")
list_filter = ("category",) list_filter = ("category",)
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag_name(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
@@ -88,6 +88,12 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
@admin.register(Shop) @admin.register(Shop)
class ShopAdmin(admin.ModelAdmin): class ShopAdmin(admin.ModelAdmin):
list_display = ("name", "on_line", "active") list_display = ("name", "on_line", "active", "country_flag_name")
list_filter = ("on_line", "active") list_filter = ("on_line", "active")
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country")
def country_flag_name(self, obj):
return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name
)

View File

@@ -7,11 +7,12 @@ from django.dispatch.dispatcher import receiver
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django_countries.fields import CountryField from django_countries.fields import CountryField
from ram.models import SimpleBaseModel
from ram.utils import DeduplicatedStorage, get_image_preview, slugify from ram.utils import DeduplicatedStorage, get_image_preview, slugify
from ram.managers import PublicManager from ram.managers import PublicManager
class Property(models.Model): class Property(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
private = models.BooleanField( private = models.BooleanField(
default=False, default=False,
@@ -28,7 +29,7 @@ class Property(models.Model):
objects = PublicManager() objects = PublicManager()
class Manufacturer(models.Model): class Manufacturer(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True, editable=False) slug = models.CharField(max_length=128, unique=True, editable=False)
category = models.CharField( category = models.CharField(
@@ -68,7 +69,7 @@ class Manufacturer(models.Model):
logo_thumbnail.short_description = "Preview" logo_thumbnail.short_description = "Preview"
class Company(models.Model): class Company(SimpleBaseModel):
name = models.CharField(max_length=64, unique=True) name = models.CharField(max_length=64, unique=True)
slug = models.CharField(max_length=64, unique=True, editable=False) slug = models.CharField(max_length=64, unique=True, editable=False)
extended_name = models.CharField(max_length=128, blank=True) extended_name = models.CharField(max_length=128, blank=True)
@@ -106,7 +107,7 @@ class Company(models.Model):
logo_thumbnail.short_description = "Preview" logo_thumbnail.short_description = "Preview"
class Decoder(models.Model): class Decoder(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
@@ -142,7 +143,7 @@ def calculate_ratio(ratio):
raise ValidationError("Invalid ratio format") raise ValidationError("Invalid ratio format")
class Scale(models.Model): class Scale(SimpleBaseModel):
scale = models.CharField(max_length=32, unique=True) scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False) slug = models.CharField(max_length=32, unique=True, editable=False)
ratio = models.CharField(max_length=16, validators=[calculate_ratio]) ratio = models.CharField(max_length=16, validators=[calculate_ratio])
@@ -177,7 +178,7 @@ def scale_save(sender, instance, **kwargs):
instance.ratio_int = calculate_ratio(instance.ratio) instance.ratio_int = calculate_ratio(instance.ratio)
class RollingStockType(models.Model): class RollingStockType(SimpleBaseModel):
type = models.CharField(max_length=64) type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField() order = models.PositiveSmallIntegerField()
category = models.CharField( category = models.CharField(
@@ -207,7 +208,7 @@ class RollingStockType(models.Model):
return "{0} {1}".format(self.type, self.category) return "{0} {1}".format(self.type, self.category)
class Tag(models.Model): class Tag(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True) slug = models.CharField(max_length=128, unique=True)
@@ -227,7 +228,7 @@ class Tag(models.Model):
) )
class Shop(models.Model): class Shop(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
country = CountryField(blank=True) country = CountryField(blank=True)
website = models.URLField(blank=True) website = models.URLField(blank=True)

View File

@@ -2,23 +2,23 @@
{% load dynamic_url %} {% load dynamic_url %}
{% block header %} {% block header %}
{% if book.tags.all %} {% if data.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
{% for t in book.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in data.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
{% if not book.published %} {% if not data.published %}
<span class="badge text-bg-warning">Unpublished</span> | <span class="badge text-bg-warning">Unpublished</span> |
{% endif %} {% endif %}
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small> <small class="text-body-secondary">Updated {{ data.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %} {% endblock %}
{% block carousel %} {% block carousel %}
<div class="row"> <div class="row">
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000"> <div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000">
<div class="carousel-inner"> <div class="carousel-inner">
{% for t in book.image.all %} {% for t in data.image.all %}
{% if forloop.first %} {% if forloop.first %}
<div class="carousel-item active"> <div class="carousel-item active">
{% else %} {% else %}
@@ -28,7 +28,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% if book.image.count > 1 %} {% if data.image.count > 1 %}
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev"> <button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden"><i class="bi bi-chevron-left"></i></span> <span class="visually-hidden"><i class="bi bi-chevron-left"></i></span>
@@ -49,10 +49,12 @@
<div class="mx-auto"> <div class="mx-auto">
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist"> <nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button> <button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
{% if data.toc.all %}<button class="nav-link" id="nav-toc-tab" data-bs-toggle="tab" data-bs-target="#nav-toc" type="button" role="tab" aria-controls="nav-toc" aria-selected="true">Table of contents</button>{% endif %}
{% if documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %} {% if documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
</nav> </nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector"> <select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option> <option value="nav-summary" selected>Summary</option>
{% if data.toc.all %}<option value="nav-toc">Table of contents</option>{% endif %}
{% if documents %}<option value="nav-documents">Documents</option>{% endif %} {% if documents %}<option value="nav-documents">Documents</option>{% endif %}
</select> </select>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
@@ -61,86 +63,86 @@
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row"> <th colspan="2" scope="row">
{{ label|capfirst }} {{ data.obj_label|capfirst }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if type == "catalog" %} {% if data.obj_type == "catalog" %}
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td> <td>
<a href="{% url 'filtered' _filter="manufacturer" search=book.manufacturer.slug %}">{{ book.manufacturer }}{% if book.manufacturer.website %}</a> <a href="{{ book.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} <a href="{% url 'filtered' _filter="manufacturer" search=data.manufacturer.slug %}">{{ data.manufacturer }}{% if data.manufacturer.website %}</a> <a href="{{ data.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Scales</th> <th class="w-33" scope="row">Scales</th>
<td>{{ book.get_scales }}</td> <td>{{ data.get_scales }}</td>
</tr> </tr>
{% elif type == "book" %} {% elif data.obj_type == "book" %}
<tr> <tr>
<th class="w-33" scope="row">Title</th> <th class="w-33" scope="row">Title</th>
<td>{{ book.title }}</td> <td>{{ data.title }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Authors</th> <th class="w-33" scope="row">Authors</th>
<td> <td>
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul> <ul class="mb-0 list-unstyled">{% for a in data.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Publisher</th> <th class="w-33" scope="row">Publisher</th>
<td> <td>
<img src="{{ book.publisher.country.flag }}" alt="{{ book.publisher.country }}"> {{ book.publisher }} <img src="{{ data.publisher.country.flag }}" alt="{{ data.publisher.country }}"> {{ data.publisher }}
{% if book.publisher.website %} <a href="{{ book.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if data.publisher.website %} <a href="{{ data.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
{% elif type == "magazineissue" %} {% elif data.obj_type == "magazineissue" %}
<tr> <tr>
<th class="w-33" scope="row">Magazine</th> <th class="w-33" scope="row">Magazine</th>
<td> <td>
<a href="{% url 'magazine' book.magazine.pk %}">{{ book.magazine }}</a> <a href="{% url 'magazine' data.magazine.pk %}">{{ data.magazine }}</a>
{% if book.magazine.website %} <a href="{{ book.magazine.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if data.magazine.website %} <a href="{{ data.magazine.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Publisher</th> <th class="w-33" scope="row">Publisher</th>
<td> <td>
<img src="{{ book.publisher.country.flag }}" alt="{{ book.publisher.country }}"> {{ book.publisher }} <img src="{{ data.publisher.country.flag }}" alt="{{ data.publisher.country }}"> {{ data.publisher }}
{% if book.publisher.website %} <a href="{{ book.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if data.publisher.website %} <a href="{{ data.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Issue</th> <th class="w-33" scope="row">Issue</th>
<td>{{ book.issue_number }}</td> <td>{{ data.issue_number }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Date</th> <th class="w-33" scope="row">Date</th>
<td>{{ book.publication_year|default:"-" }} / {{ book.get_publication_month_display|default:"-" }}</td> <td>{{ data.publication_year|default:"-" }} / {{ data.get_publication_month_display|default:"-" }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th scope="row">ISBN</th> <th scope="row">ISBN</th>
<td>{{ book.ISBN|default:"-" }}</td> <td>{{ data.ISBN|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Language</th> <th scope="row">Language</th>
<td>{{ book.get_language_display }}</td> <td>{{ data.get_language_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Number of pages</th> <th scope="row">Number of pages</th>
<td>{{ book.number_of_pages|default:"-" }}</td> <td>{{ data.number_of_pages|default:"-" }}</td>
</tr> </tr>
{% if type == "boook" or type == "catalog" %} {% if data.obj_type == "book" or data.obj_type == "catalog" %}
<tr> <tr>
<th scope="row">Publication year</th> <th scope="row">Publication year</th>
<td>{{ book.publication_year|default:"-" }}</td> <td>{{ data.publication_year|default:"-" }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if book.description %} {% if data.description %}
<tr> <tr>
<th class="w-33" scope="row">Description</th> <th class="w-33" scope="row">Description</th>
<td>{{ book.description | safe }}</td> <td>{{ data.description | safe }}</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
@@ -156,17 +158,17 @@
<tr> <tr>
<th class="w-33" scope="row">Shop</th> <th class="w-33" scope="row">Shop</th>
<td> <td>
{{ book.shop|default:"-" }} {{ data.shop|default:"-" }}
{% if book.shop.website %} <a href="{{ book.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if data.shop.website %} <a href="{{ data.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Purchase date</th> <th class="w-33" scope="row">Purchase date</th>
<td>{{ book.purchase_date|default:"-" }}</td> <td>{{ data.purchase_date|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Price ({{ site_conf.currency }})</th> <th scope="row">Price ({{ site_conf.currency }})</th>
<td>{{ book.price|default:"-" }}</td> <td>{{ data.price|default:"-" }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -189,27 +191,36 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab"> <div class="tab-pane table-responsive" id="nav-toc" role="tabpanel" aria-labelledby="nav-toc-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="3" scope="row">Documents</th> <th scope="row">Title</th>
<th scope="row">Subtitle</th>
<th scope="row">Authors</th>
<th scope="row">Page</th>
<th scope="row"><abbr title="Featured article"><i class="bi bi-star-fill"></i></abbr></th>
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for d in documents.all %} {% for toc in data.toc.all %}
<tr> <tr>
<td class="w-33">{{ d.description }}</td> <td class="w-33">{{ toc.title }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td> <td class="w-33">{{ toc.subtitle }}</td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td> <td>{{ toc.authors }}</td>
<td>{{ toc.page }}</td>
<td>{% if toc.featured %}<abbr title="Featured article"><i class="bi bi-star-fill text-warning"></i></abbr>{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
{% include "includes/documents.html" %}
</div>
</div> </div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <div class="d-grid gap-2 d-md-flex justify-content-md-end">
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' type book.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' data.obj_type data.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,21 +6,21 @@
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
{% block cards %} {% block cards %}
{% for d in data %} {% for d in data %}
{% if d.type == "roster" %} {% if d.obj_type == "rollingstock" %}
{% include "cards/roster.html" %} {% include "cards/roster.html" %}
{% elif d.type == "company" %} {% elif d.obj_type == "company" %}
{% include "cards/company.html" %} {% include "cards/company.html" %}
{% elif d.type == "rolling_stock_type" %} {% elif d.obj_type == "rollingstocktype" %}
{% include "cards/rolling_stock_type.html" %} {% include "cards/rolling_stock_type.html" %}
{% elif d.type == "scale" %} {% elif d.obj_type == "scale" %}
{% include "cards/scale.html" %} {% include "cards/scale.html" %}
{% elif d.type == "consist" %} {% elif d.obj_type == "consist" %}
{% include "cards/consist.html" %} {% include "cards/consist.html" %}
{% elif d.type == "manufacturer" %} {% elif d.obj_type == "manufacturer" %}
{% include "cards/manufacturer.html" %} {% include "cards/manufacturer.html" %}
{% elif d.type == "magazine" or d.type == "magazineissue" %} {% elif d.obj_type == "magazine" or d.obj_type == "magazineissue" %}
{% include "cards/magazine.html" %} {% include "cards/magazine.html" %}
{% elif d.type == "book" or d.type == "catalog" %} {% elif d.obj_type == "book" or d.obj_type == "catalog" %}
{% include "cards/book.html" %} {% include "cards/book.html" %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@@ -2,31 +2,31 @@
{% load dynamic_url %} {% load dynamic_url %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if d.item.image.exists %} {% if d.image.exists %}
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a> <a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a>
{% else %} {% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) --> <!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}"></a> <a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
{% endif %} {% endif %}
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong> <strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p> </p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small> <p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% empty %}
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
{% endfor %} {% endfor %}
</p> </p>
{% endif %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row"> <th colspan="2" scope="row">
{{ d.label|capfirst }} {{ d.obj_label|capfirst }}
<div class="float-end"> <div class="float-end">
{% if not d.item.published %} {% if not d.published %}
<span class="badge text-bg-warning">Unpublished</span> <span class="badge text-bg-warning">Unpublished</span>
{% endif %} {% endif %}
</div> </div>
@@ -34,46 +34,46 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.type == "catalog" %} {% if d.obj_type == "catalog" %}
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td> <td>
<a href="{% url 'filtered' _filter="manufacturer" search=d.item.manufacturer.slug %}">{{ d.item.manufacturer }}{% if d.item.manufacturer.website %}</a> <a href="{{ d.item.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} <a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.slug %}">{{ d.manufacturer }}{% if d.manufacturer.website %}</a> <a href="{{ d.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Scales</th> <th class="w-33" scope="row">Scales</th>
<td>{{ d.item.get_scales }}</td> <td>{{ d.get_scales }}</td>
</tr> </tr>
{% elif d.type == "book" %} {% elif d.obj_type == "book" %}
<tr> <tr>
<th class="w-33" scope="row">Authors</th> <th class="w-33" scope="row">Authors</th>
<td> <td>
<ul class="mb-0 list-unstyled">{% for a in d.item.authors.all %}<li>{{ a }}</li>{% endfor %}</ul> <ul class="mb-0 list-unstyled">{% for a in d.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Publisher</th> <th class="w-33" scope="row">Publisher</th>
<td><img src="{{ d.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }}</td> <td><img src="{{ d.publisher.country.flag }}" alt="{{ d.publisher.country }}"> {{ d.publisher }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th scope="row">Language</th> <th scope="row">Language</th>
<td>{{ d.item.get_language_display }}</td> <td>{{ d.get_language_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Pages</th> <th scope="row">Pages</th>
<td>{{ d.item.number_of_pages|default:"-" }}</td> <td>{{ d.number_of_pages|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Year</th> <th scope="row">Year</th>
<td>{{ d.item.publication_year|default:"-" }}</td> <td>{{ d.publication_year|default:"-" }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' d.type d.item.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' d.obj_type d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ d.item.name }}</strong> <strong>{{ d.name }}</strong>
</p> </p>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@@ -10,7 +10,7 @@
<th colspan="2" scope="row"> <th colspan="2" scope="row">
Company Company
<div class="float-end"> <div class="float-end">
{% if d.item.freelance %} {% if d.freelance %}
<span class="badge text-bg-secondary">Freelance</span> <span class="badge text-bg-secondary">Freelance</span>
{% endif %} {% endif %}
</div> </div>
@@ -18,30 +18,30 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.item.logo %} {% if d.logo %}
<tr> <tr>
<th class="w-33" scope="row">Logo</th> <th class="w-33" scope="row">Logo</th>
<td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td> <td><img class="logo" src="{{ d.logo.url }}" alt="{{ d.name }} logo"></td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th class="w-33" scope="row">Name</th> <th class="w-33" scope="row">Name</th>
<td>{{ d.item.extended_name }}</td> <td>{{ d.extended_name }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Abbreviation</th> <th class="w-33" scope="row">Abbreviation</th>
<td>{{ d.item.name }}</td> <td>{{ d.name }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Country</th> <th class="w-33" scope="row">Country</th>
<td><img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}"> {{ d.item.country.name }}</td> <td><img src="{{ d.country.flag }}" alt="{{ d.country }}"> {{ d.country.name }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
{% with items=d.item.num_items %} {% with items=d.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="company" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a> <a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="company" search=d.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.item.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.pk %}">Edit</a>{% endif %}
{% endwith %} {% endwith %}
</div> </div>
</div> </div>

View File

@@ -1,36 +1,36 @@
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
<a href="{{ d.item.get_absolute_url }}"> <a href="{{ d.get_absolute_url }}">
{% if d.item.image %} {% if d.image %}
<img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}"> <img class="card-img-top" src="{{ d.image.url }}" alt="{{ d }}">
{% else %} {% else %}
{% with d.item.consist_item.first.rolling_stock as r %} {% with d.consist_item.first.rolling_stock as r %}
<img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d.item }}"> <img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d }}">
{% endwith %} {% endwith %}
{% endif %} {% endif %}
</a> </a>
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong> <strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p> </p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small> <p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% empty %}
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
{% endfor %} {% endfor %}
</p> </p>
{% endif %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row"> <th colspan="2" scope="row">
Consist Consist
<div class="float-end"> <div class="float-end">
{% if not d.item.published %} {% if not d.published %}
<span class="badge text-bg-warning">Unpublished</span> <span class="badge text-bg-warning">Unpublished</span>
{% endif %} {% endif %}
{% if d.item.company.freelance %} {% if d.company.freelance %}
<span class="badge text-bg-secondary">Freelance</span> <span class="badge text-bg-secondary">Freelance</span>
{% endif %} {% endif %}
</div> </div>
@@ -38,32 +38,32 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.item.address %} {% if d.address %}
<tr> <tr>
<th class="w-33" scope="row">Address</th> <th class="w-33" scope="row">Address</th>
<td>{{ d.item.address }}</td> <td>{{ d.address }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th class="w-33" scope="row">Company</th> <th class="w-33" scope="row">Company</th>
<td> <td>
<img src="{{ d.item.company.country.flag }}" alt="{{ d.item.company.country }}"> <img src="{{ d.company.country.flag }}" alt="{{ d.company.country }}">
<abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr> <abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
<td>{{ d.item.era }}</td> <td>{{ d.era }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Length</th> <th scope="row">Length</th>
<td>{{ d.item.length }}</td> <td>{{ d.length }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.item.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,50 +2,46 @@
{% load dynamic_url %} {% load dynamic_url %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if d.type == "magazine" %} {% if d.obj_type == "magazine" %}
<a href="{{ d.item.get_absolute_url }}"> <a href="{{ d.get_absolute_url }}">
{% if d.item.image and d.type == "magazine" %} {% if d.get_cover %}
<img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}"> <img class="card-img-top" src="{{ d.get_cover.url }}" alt="{{ d }}">
{% elif d.item.issue.first.image.exists %}
{% with d.item.issue.first as i %}
<img class="card-img-top" src="{{ i.image.first.image.url }}" alt="{{ d.item }}">
{% endwith %}
{% else %} {% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) --> <!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}"> <img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}">
{% endif %} {% endif %}
</a> </a>
{% elif d.type == "magazineissue" %} {% elif d.obj_type == "magazineissue" %}
<a href="{{ d.item.get_absolute_url }}"> <a href="{{ d.get_absolute_url }}">
{% if d.item.image.exists %} {% if d.image.exists %}
<img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"> <img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}">
{% else %} {% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) --> <!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}"> <img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}">
{% endif %} {% endif %}
</a> </a>
{% endif %} {% endif %}
</a> </a>
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong> <strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p> </p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small> <p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% empty %}
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
{% endfor %} {% endfor %}
</p> </p>
{% endif %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row"> <th colspan="2" scope="row">
{{ d.label|capfirst }} {{ d.obj_label|capfirst }}
<div class="float-end"> <div class="float-end">
{% if not d.item.published %} {% if not d.published %}
<span class="badge text-bg-warning">Unpublished</span> <span class="badge text-bg-warning">Unpublished</span>
{% endif %} {% endif %}
</div> </div>
@@ -53,51 +49,51 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.type == "magazineissue" %} {% if d.obj_type == "magazineissue" %}
<tr> <tr>
<th class="w-33" scope="row">Magazine</th> <th class="w-33" scope="row">Magazine</th>
<td>{{ d.item.magazine }}</td> <td>{{ d.magazine }}</td>
</tr> </tr>
{% else %} {% else %}
<tr> <tr>
<th class="w-33" scope="row">Website</th> <th class="w-33" scope="row">Website</th>
<td>{% if d.item.website %}<a href="{{ d.item.website }}" target="_blank">{{ d.item.website_short }}</td>{% else %}-{% endif %}</td> <td>{% if d.website %}<a href="{{ d.website }}" target="_blank">{{ d.website_short }}</td>{% else %}-{% endif %}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th class="w-33" scope="row">Publisher</th> <th class="w-33" scope="row">Publisher</th>
<td> <td>
<img src="{{ d.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }} <img src="{{ d.publisher.country.flag }}" alt="{{ d.publisher.country }}"> {{ d.publisher }}
{% if d.item.publisher.website %} <a href="{{ d.item.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if d.publisher.website %} <a href="{{ d.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
{% if d.type == "magazineissue" %} {% if d.obj_type == "magazineissue" %}
<tr> <tr>
<th class="w-33" scope="row">Issue</th> <th class="w-33" scope="row">Issue</th>
<td>{{ d.item.issue_number }}</td> <td>{{ d.issue_number }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Date</th> <th class="w-33" scope="row">Date</th>
<td>{{ d.item.publication_year|default:"-" }} / {{ d.item.get_publication_month_display|default:"-" }}</td> <td>{{ d.publication_year|default:"-" }} / {{ d.get_publication_month_display|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Pages</th> <th class="w-33" scope="row">Pages</th>
<td>{{ d.item.number_of_pages|default:"-" }}</td> <td>{{ d.number_of_pages|default:"-" }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th class="w-33" scope="row">Language</th> <th class="w-33" scope="row">Language</th>
<td>{{ d.item.get_language_display }}</td> <td>{{ d.get_language_display }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
{% if d.type == "magazine" %} {% if d.obj_type == "magazine" %}
<a class="btn btn-sm btn-outline-primary{% if d.item.issues == 0 %} disabled{% endif %}" href="{{ d.item.get_absolute_url }}">Show {{ d.item.issues }} issue{{ d.item.issues|pluralize }}</a> <a class="btn btn-sm btn-outline-primary{% if d.issues == 0 %} disabled{% endif %}" href="{{ d.get_absolute_url }}">Show {{ d.issues }} issue{{ d.issues|pluralize }}</a>
{% else %} {% else %}
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
{% endif %} {% endif %}
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' d.type d.item.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' d.obj_type d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,7 @@
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ d.item.name }}</strong> <strong>{{ d.name }}</strong>
</p> </p>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@@ -11,26 +11,26 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.item.logo %} {% if d.logo %}
<tr> <tr>
<th class="w-33" scope="row">Logo</th> <th class="w-33" scope="row">Logo</th>
<td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td> <td><img class="logo" src="{{ d.logo.url }}" alt="{{ d.name }} logo"></td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th class="w-33" scope="row">Website</th> <th class="w-33" scope="row">Website</th>
<td>{% if d.item.website %}<a href="{{ d.item.website }}" target="_blank">{{ d.item.website_short }}</td>{% else %}-{% endif %}</td> <td>{% if d.website %}<a href="{{ d.website }}" target="_blank">{{ d.website_short }}</td>{% else %}-{% endif %}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Category</th> <th class="w-33" scope="row">Category</th>
<td>{{ d.item.category | title }}</td> <td>{{ d.category | title }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
{% with items=d.item.num_items %} {% with items=d.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="manufacturer" search=d.item.slug %}">Show {{ items }} item{{ items|pluralize }}</a> <a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="manufacturer" search=d.slug %}">Show {{ items }} item{{ items|pluralize }}</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.item.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.pk %}">Edit</a>{% endif %}
{% endwith %} {% endwith %}
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>{{ d.item }}</strong></p> <p class="card-text"><strong>{{ d }}</strong></p>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -11,18 +11,18 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Type</th> <th class="w-33" scope="row">Type</th>
<td>{{ d.item.type }}</td> <td>{{ d.type }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Category</th> <th class="w-33" scope="row">Category</th>
<td>{{ d.item.category | title}}</td> <td>{{ d.category | title}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
{% with items=d.item.num_items %} {% with items=d.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="type" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a> <a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="type" search=d.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_rollingstocktype_change' d.item.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_rollingstocktype_change' d.pk %}">Edit</a>{% endif %}
{% endwith %} {% endwith %}
</div> </div>
</div> </div>

View File

@@ -3,34 +3,41 @@
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if d.item.image.exists %} <div id="card-img-container" class="position-relative">
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a> {% if d.featured %}
{% else %} <span class="position-absolute translate-middle top-0 start-0 m-3 text-danger">
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) --> <abbr title="Featured item"><i class="bi bi-heart-fill"></i></abbr>
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}"></a> </span>
{% endif %} {% endif %}
{% if d.image.exists %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a>
{% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
{% endif %}
</div>
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong> <strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p> </p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small> <p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% empty %}
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
{% endfor %} {% endfor %}
</p> </p>
{% endif %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row"> <th colspan="2" scope="row">
Rolling stock Rolling stock
<div class="float-end"> <div class="float-end">
{% if not d.item.published %} {% if not d.published %}
<span class="badge text-bg-warning">Unpublished</span> <span class="badge text-bg-warning">Unpublished</span>
{% endif %} {% endif %}
{% if d.item.company.freelance %} {% if d.company.freelance %}
<span class="badge text-bg-secondary">Freelance</span> <span class="badge text-bg-secondary">Freelance</span>
{% endif %} {% endif %}
</div> </div>
@@ -40,50 +47,50 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Type</th> <th class="w-33" scope="row">Type</th>
<td>{{ d.item.rolling_class.type }}</td> <td>{{ d.rolling_class.type }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Company</th> <th scope="row">Company</th>
<td> <td>
<img src="{{ d.item.company.country.flag }}" alt="{{ d.item.company.country }}"> <img src="{{ d.company.country.flag }}" alt="{{ d.company.country }}">
<a href="{% url 'filtered' _filter="company" search=d.item.company.slug %}"><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></a> <a href="{% url 'filtered' _filter="company" search=d.company.slug %}"><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></a>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Class</th> <th scope="row">Class</th>
<td>{{ d.item.rolling_class.identifier }}</td> <td>{{ d.rolling_class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Road number</th> <th scope="row">Road number</th>
<td>{{ d.item.road_number }}</td> <td>{{ d.road_number }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
<td>{{ d.item.era }}</td> <td>{{ d.era }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td>{%if d.item.manufacturer %} <td>{%if d.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.item.manufacturer.slug %}">{{ d.item.manufacturer }}{% if d.item.manufacturer.website %}</a> <a href="{{ d.item.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} <a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.slug %}">{{ d.manufacturer }}{% if d.manufacturer.website %}</a> <a href="{{ d.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td> {% endif %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=d.item.scale.slug %}"><abbr title="{{ d.item.scale.ratio }} - {{ d.item.scale.tracks }} mm">{{ d.item.scale }}</abbr></a></td> <td><a href="{% url 'filtered' _filter="scale" search=d.scale.slug %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }} mm">{{ d.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">Item number</th> <th scope="row">Item number</th>
<td>{{ d.item.item_number }}{%if d.item.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=d.item.manufacturer.slug search=d.item.item_number_slug %}">SET</a>{% endif %}</td> <td>{{ d.item_number }}{%if d.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=d.manufacturer.slug search=d.item_number_slug %}">SET</a>{% endif %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">DCC</th> <th scope="row">DCC</th>
<td><a class="text-reset text-decoration-none" title="Symbols" href="" data-bs-toggle="modal" data-bs-target="#symbolsModal">{% dcc d.item %}</a></td> <td><a class="text-reset text-decoration-none" title="Symbols" href="" data-bs-toggle="modal" data-bs-target="#symbolsModal">{% dcc d %}</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{d.item.get_absolute_url}}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{d.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.item.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>{{ d.item }}</strong></p> <p class="card-text"><strong>{{ d }}</strong></p>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -11,26 +11,26 @@
<tbody> <tbody>
<tr> <tr>
<th class="w-33" scope="row">Name</th> <th class="w-33" scope="row">Name</th>
<td>{{ d.item.scale }}</td> <td>{{ d.scale }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Ratio</th> <th class="w-33" scope="row">Ratio</th>
<td>{{ d.item.ratio }}</td> <td>{{ d.ratio }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Tracks</th> <th class="w-33" scope="row">Tracks</th>
<td>{{ d.item.tracks }} mm</td> <td>{{ d.tracks }} mm</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Gauge</th> <th class="w-33" scope="row">Gauge</th>
<td>{{ d.item.gauge }}</td> <td>{{ d.gauge }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
{% with items=d.item.num_items %} {% with items=d.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a> <a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %}
{% endwith %} {% endwith %}
</div> </div>
</div> </div>

View File

@@ -32,7 +32,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0"> <ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <a class="page-link" href="{% url 'consist' uuid=consist.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -48,13 +48,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'consist' uuid=consist.uuid page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <a class="page-link" href="{% url 'consist' uuid=consist.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -76,7 +76,7 @@
<option value="nav-summary" selected>Summary</option> <option value="nav-summary" selected>Summary</option>
</select> </select>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active table-responsive" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>

View File

@@ -5,7 +5,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0"> <ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <a class="page-link" href="{% url 'filtered' _filter=filter search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -21,13 +21,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'filtered' _filter=filter search=search page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <a class="page-link" href="{% url 'filtered' _filter=filter search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -3,3 +3,18 @@
{% block header %} {% block header %}
<div class="text-body-secondary">{{ site_conf.about | safe }}</div> <div class="text-body-secondary">{{ site_conf.about | safe }}</div>
{% endblock %} {% endblock %}
{% block cards %}
{% for d in data %}
{% include "cards/roster.html" %}
{% endfor %}
{% endblock %}
{% block pagination %}
<nav aria-label="Page navigation">
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
<li class="page-item">
<a class="page-link" href="{% url "roster" %}#main-content" tabindex="-1">Go to the roster <i class="bi bi-chevron-right"></i></a>
</li>
</ul>
</nav>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% if documents %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="3" scope="row">{{ header|default:"Documents" }}</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% for d in documents.all %}
<tr>
<td class="w-33">{{ d.description }}</td>
<td class="text-nowrap">
{% if d.private %}
<i class="bi bi-file-earmark-lock2"></i>
{% else %}
<i class="bi bi-file-earmark-text"></i>
{% endif %}
<a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a>
</td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}

View File

@@ -32,7 +32,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0"> <ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'magazine_pagination' uuid=magazine.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <a class="page-link" href="{% url 'magazine' uuid=magazine.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -48,13 +48,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'magazine_pagination' uuid=magazine.uuid page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'magazine' uuid=magazine.uuid page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'magazine_pagination' uuid=magazine.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <a class="page-link" href="{% url 'magazine' uuid=magazine.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -5,7 +5,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0"> <ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <a class="page-link" href="{% url 'manufacturer' manufacturer=manufacturer.slug search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -21,13 +21,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'manufacturer' manufacturer=manufacturer.slug search=search page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <a class="page-link" href="{% url 'manufacturer' manufacturer=manufacturer.slug search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -7,7 +7,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0"> <ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% dynamic_pagination type page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <a class="page-link" href="{% url request.resolver_match.url_name page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -23,13 +23,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% dynamic_pagination type page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url request.resolver_match.url_name page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% dynamic_pagination type page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <a class="page-link" href="{% url request.resolver_match.url_name page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,12 +1,12 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
{% with data.0.item.category as c %} {% with data.0.category as c %}
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0"> <ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <a class="page-link" href="{% url 'manufacturers' category=c page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -22,13 +22,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'manufacturers_pagination' category=c page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'manufacturers' category=c page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <a class="page-link" href="{% url 'manufacturers' category=c page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -402,43 +402,9 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab"> <div class="tab-pane table-responsive" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
{% if documents %} {% include "includes/documents.html" %}
<table class="table table-striped"> {% include "includes/documents.html" with documents=decoder_documents header="Decoder documents" %}
<thead>
<tr>
<th colspan="3" scope="row">Documents</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% for d in documents.all %}
<tr>
<td class="w-33">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if decoder_documents %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="3" scope="row">Decoder documents</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% for d in decoder_documents.all %}
<tr>
<td class="w-33">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div> </div>
<div class="tab-pane" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab"> <div class="tab-pane" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
<table class="table table-striped"> <table class="table table-striped">

View File

@@ -6,7 +6,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0"> <ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <a class="page-link" href="{% url 'search' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -22,13 +22,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'search_pagination' search=encoded_search page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'search' search=encoded_search page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <a class="page-link" href="{% url 'search' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -12,10 +12,3 @@ def dynamic_admin_url(app_name, model_name, object_id=None):
args=[object_id] args=[object_id]
) )
return reverse(f'admin:{app_name}_{model_name}_changelist') return reverse(f'admin:{app_name}_{model_name}_changelist')
@register.simple_tag
def dynamic_pagination(reverse_name, page):
if reverse_name.endswith('y'):
return reverse(f'{reverse_name[:-1]}ies_pagination', args=[page])
return reverse(f'{reverse_name}s_pagination', args=[page])

View File

@@ -0,0 +1,11 @@
import random
from django import template
register = template.Library()
@register.filter
def shuffle(items):
shuffled_items = list(items)
random.shuffle(shuffled_items)
return shuffled_items

View File

@@ -1,7 +1,7 @@
from django.urls import path from django.urls import path
from portal.views import ( from portal.views import (
GetData, GetHome,
GetRoster, GetRoster,
GetObjectsFiltered, GetObjectsFiltered,
GetManufacturerItem, GetManufacturerItem,
@@ -23,123 +23,75 @@ from portal.views import (
) )
urlpatterns = [ urlpatterns = [
path("", GetData.as_view(template="home.html"), name="index"), path("", GetHome.as_view(), name="index"),
path("roster", GetRoster.as_view(), name="roster"), path("roster", GetRoster.as_view(), name="roster"),
path( path("roster/page/<int:page>", GetRoster.as_view(), name="roster"),
"roster/page/<int:page>",
GetRoster.as_view(),
name="rosters_pagination"
),
path( path(
"page/<str:flatpage>", "page/<str:flatpage>",
GetFlatpage.as_view(), GetFlatpage.as_view(),
name="flatpage", name="flatpage",
), ),
path( path("consists", Consists.as_view(), name="consists"),
"consists", path("consists/page/<int:page>", Consists.as_view(), name="consists"),
Consists.as_view(),
name="consists"
),
path(
"consists/page/<int:page>",
Consists.as_view(),
name="consists_pagination"
),
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"), path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
path( path(
"consist/<uuid:uuid>/page/<int:page>", "consist/<uuid:uuid>/page/<int:page>",
GetConsist.as_view(), GetConsist.as_view(),
name="consist_pagination", name="consist",
),
path(
"companies",
Companies.as_view(),
name="companies"
), ),
path("companies", Companies.as_view(), name="companies"),
path( path(
"companies/page/<int:page>", "companies/page/<int:page>",
Companies.as_view(), Companies.as_view(),
name="companies_pagination", name="companies",
), ),
path( path(
"manufacturers/<str:category>", "manufacturers/<str:category>",
Manufacturers.as_view(template="pagination_manufacturers.html"), Manufacturers.as_view(template="pagination_manufacturers.html"),
name="manufacturers" name="manufacturers",
), ),
path( path(
"manufacturers/<str:category>/page/<int:page>", "manufacturers/<str:category>/page/<int:page>",
Manufacturers.as_view(template="pagination_manufacturers.html"), Manufacturers.as_view(template="pagination_manufacturers.html"),
name="manufacturers_pagination", name="manufacturers",
),
path(
"scales",
Scales.as_view(),
name="scales"
),
path(
"scales/page/<int:page>",
Scales.as_view(),
name="scales_pagination"
),
path(
"types",
Types.as_view(),
name="rolling_stock_types"
),
path(
"types/page/<int:page>",
Types.as_view(),
name="rolling_stock_types_pagination"
),
path(
"bookshelf/books",
Books.as_view(),
name="books"
),
path(
"bookshelf/books/page/<int:page>",
Books.as_view(),
name="books_pagination"
), ),
path("scales", Scales.as_view(), name="scales"),
path("scales/page/<int:page>", Scales.as_view(), name="scales"),
path("types", Types.as_view(), name="rolling_stock_types"),
path("types/page/<int:page>", Types.as_view(), name="rolling_stock_types"),
path("bookshelf/books", Books.as_view(), name="books"),
path("bookshelf/books/page/<int:page>", Books.as_view(), name="books"),
path( path(
"bookshelf/magazine/<uuid:uuid>", "bookshelf/magazine/<uuid:uuid>",
GetMagazine.as_view(), GetMagazine.as_view(),
name="magazine" name="magazine",
), ),
path( path(
"bookshelf/magazine/<uuid:uuid>/page/<int:page>", "bookshelf/magazine/<uuid:uuid>/page/<int:page>",
GetMagazine.as_view(), GetMagazine.as_view(),
name="magazine_pagination", name="magazine",
), ),
path( path(
"bookshelf/magazine/<uuid:magazine>/issue/<uuid:uuid>", "bookshelf/magazine/<uuid:magazine>/issue/<uuid:uuid>",
GetMagazineIssue.as_view(), GetMagazineIssue.as_view(),
name="issue", name="issue",
), ),
path( path("bookshelf/magazines", Magazines.as_view(), name="magazines"),
"bookshelf/magazines",
Magazines.as_view(),
name="magazines"
),
path( path(
"bookshelf/magazines/page/<int:page>", "bookshelf/magazines/page/<int:page>",
Magazines.as_view(), Magazines.as_view(),
name="magazines_pagination" name="magazines",
), ),
path( path(
"bookshelf/<str:selector>/<uuid:uuid>", "bookshelf/<str:selector>/<uuid:uuid>",
GetBookCatalog.as_view(), GetBookCatalog.as_view(),
name="bookshelf_item" name="bookshelf_item",
),
path(
"bookshelf/catalogs",
Catalogs.as_view(),
name="catalogs"
), ),
path("bookshelf/catalogs", Catalogs.as_view(), name="catalogs"),
path( path(
"bookshelf/catalogs/page/<int:page>", "bookshelf/catalogs/page/<int:page>",
Catalogs.as_view(), Catalogs.as_view(),
name="catalogs_pagination" name="catalogs",
), ),
path( path(
"search", "search",
@@ -149,7 +101,7 @@ urlpatterns = [
path( path(
"search/<str:search>/page/<int:page>", "search/<str:search>/page/<int:page>",
SearchObjects.as_view(), SearchObjects.as_view(),
name="search_pagination", name="search",
), ),
path( path(
"manufacturer/<str:manufacturer>", "manufacturer/<str:manufacturer>",
@@ -159,7 +111,7 @@ urlpatterns = [
path( path(
"manufacturer/<str:manufacturer>/page/<int:page>", "manufacturer/<str:manufacturer>/page/<int:page>",
GetManufacturerItem.as_view(), GetManufacturerItem.as_view(),
name="manufacturer_pagination", name="manufacturer",
), ),
path( path(
"manufacturer/<str:manufacturer>/<str:search>", "manufacturer/<str:manufacturer>/<str:search>",
@@ -169,7 +121,7 @@ urlpatterns = [
path( path(
"manufacturer/<str:manufacturer>/<str:search>/page/<int:page>", "manufacturer/<str:manufacturer>/<str:search>/page/<int:page>",
GetManufacturerItem.as_view(), GetManufacturerItem.as_view(),
name="manufacturer_pagination", name="manufacturer",
), ),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
@@ -179,7 +131,7 @@ urlpatterns = [
path( path(
"<str:_filter>/<str:search>/page/<int:page>", "<str:_filter>/<str:search>/page/<int:page>",
GetObjectsFiltered.as_view(), GetObjectsFiltered.as_view(),
name="filtered_pagination", name="filtered",
), ),
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"), path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),
] ]

View File

@@ -4,10 +4,12 @@ from itertools import chain
from functools import reduce from functools import reduce
from urllib.parse import unquote from urllib.parse import unquote
from django.conf import settings
from django.views import View from django.views import View
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError from django.db.utils import OperationalError, ProgrammingError
from django.db.models import F, Q, Count from django.db.models import F, Q, Count
from django.db.models.functions import Lower
from django.shortcuts import render, get_object_or_404, get_list_or_404 from django.shortcuts import render, get_object_or_404, get_list_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator from django.core.paginator import Paginator
@@ -31,7 +33,7 @@ def get_items_per_page():
items_per_page = get_site_conf().items_per_page items_per_page = get_site_conf().items_per_page
except (OperationalError, ProgrammingError): except (OperationalError, ProgrammingError):
items_per_page = 6 items_per_page = 6
return items_per_page return int(items_per_page)
def get_order_by_field(): def get_order_by_field():
@@ -61,9 +63,8 @@ class Render404(View):
class GetData(View): class GetData(View):
title = "Home" title = None
template = "pagination.html" template = "pagination.html"
item_type = "roster"
filter = Q() # empty filter by default filter = Q() # empty filter by default
def get_data(self, request): def get_data(self, request):
@@ -74,13 +75,10 @@ class GetData(View):
) )
def get(self, request, page=1): def get(self, request, page=1):
data = [] if self.title is None or self.template is None:
for item in self.get_data(request): raise Exception("title and template must be defined")
data.append({
"type": self.item_type, data = list(self.get_data(request))
"label": self.item_type.capitalize(),
"item": item
})
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
@@ -93,7 +91,6 @@ class GetData(View):
self.template, self.template,
{ {
"title": self.title, "title": self.title,
"type": self.item_type,
"data": data, "data": data,
"matches": paginator.count, "matches": paginator.count,
"page_range": page_range, "page_range": page_range,
@@ -101,18 +98,36 @@ class GetData(View):
) )
class GetRoster(GetData): class GetHome(GetData):
title = "The Roster" title = "Home"
item_type = "roster" template = "home.html"
def get_data(self, request): def get_data(self, request):
return RollingStock.objects.get_published(request.user).order_by( max_items = min(settings.FEATURED_ITEMS_MAX, get_items_per_page())
*get_order_by_field() return (
) RollingStock.objects.get_published(request.user)
.filter(featured=True)
.order_by(*get_order_by_field())[:max_items]
) or super().get_data(request)
class GetRoster(GetData):
title = "The Roster"
class SearchObjects(View): class SearchObjects(View):
def run_search(self, request, search, _filter, page=1): def run_search(self, request, search, _filter, page=1):
"""
Run the search query on the database and return the results.
param request: HTTP request
param search: search string
param _filter: filter to apply (type, company, manufacturer, scale)
param page: page number for pagination
return: tuple (data, matches, page_range)
1. data: list of dicts with keys "type" and "item"
2. matches: total number of matches
3. page_range: elided page range for pagination
"""
if _filter is None: if _filter is None:
query = reduce( query = reduce(
operator.or_, operator.or_,
@@ -155,15 +170,13 @@ class SearchObjects(View):
# FIXME duplicated code! # FIXME duplicated code!
# FIXME see if it makes sense to filter calatogs and books by scale # FIXME see if it makes sense to filter calatogs and books by scale
# and manufacturer as well # and manufacturer as well
data = []
roster = ( roster = (
RollingStock.objects.get_published(request.user) RollingStock.objects.get_published(request.user)
.filter(query) .filter(query)
.distinct() .distinct()
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) )
for item in roster: data = list(roster)
data.append({"type": "roster", "item": item})
if _filter is None: if _filter is None:
consists = ( consists = (
@@ -176,20 +189,41 @@ class SearchObjects(View):
) )
.distinct() .distinct()
) )
for item in consists: data = list(chain(data, consists))
data.append({"type": "consist", "item": item})
books = ( books = (
Book.objects.get_published(request.user) Book.objects.get_published(request.user)
.filter(title__icontains=search) .filter(
Q(
Q(title__icontains=search)
| Q(description__icontains=search)
| Q(toc__title__icontains=search)
)
)
.distinct() .distinct()
) )
catalogs = ( catalogs = (
Catalog.objects.get_published(request.user) Catalog.objects.get_published(request.user)
.filter(manufacturer__name__icontains=search) .filter(
Q(
Q(manufacturer__name__icontains=search)
| Q(description__icontains=search)
)
)
.distinct() .distinct()
) )
for item in list(chain(books, catalogs)): data = list(chain(data, books, catalogs))
data.append({"type": "book", "item": item}) magazine_issues = (
MagazineIssue.objects.get_published(request.user)
.filter(
Q(
Q(magazine__name__icontains=search)
| Q(description__icontains=search)
| Q(toc__title__icontains=search)
)
)
.distinct()
)
data = list(chain(data, magazine_issues))
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
@@ -245,10 +279,25 @@ class SearchObjects(View):
class GetManufacturerItem(View): class GetManufacturerItem(View):
def get(self, request, manufacturer, search="all", page=1): def get(self, request, manufacturer, search="all", page=1):
"""
Get all items from a specific manufacturer. If `search` is not "all",
filter by item number as well, for example to get all itmes from the
same set.
The view returns both rolling stock and catalogs.
param request: HTTP request
param manufacturer: Manufacturer slug
param search: item number slug or "all"
param page: page number for pagination
return: rendered template
1. manufacturer: Manufacturer object
2. search: item number slug or "all"
3. data: list of dicts with keys "type" and "item"
4. matches: total number of matches
5. page_range: elided page range for pagination
"""
manufacturer = get_object_or_404( manufacturer = get_object_or_404(
Manufacturer, slug__iexact=manufacturer Manufacturer, slug__iexact=manufacturer
) )
if search != "all": if search != "all":
roster = get_list_or_404( roster = get_list_or_404(
RollingStock.objects.get_published(request.user).order_by( RollingStock.objects.get_published(request.user).order_by(
@@ -259,6 +308,7 @@ class GetManufacturerItem(View):
& Q(item_number_slug__exact=search) & Q(item_number_slug__exact=search)
), ),
) )
catalogs = [] # no catalogs when searching for a specific item
title = "{0}: {1}".format( title = "{0}: {1}".format(
manufacturer, manufacturer,
# all returned records must have the same `item_number``; # all returned records must have the same `item_number``;
@@ -275,12 +325,12 @@ class GetManufacturerItem(View):
.distinct() .distinct()
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) )
catalogs = Catalog.objects.get_published(request.user).filter(
manufacturer=manufacturer
)
title = "Manufacturer: {0}".format(manufacturer) title = "Manufacturer: {0}".format(manufacturer)
data = [] data = list(chain(roster, catalogs))
for item in roster:
data.append({"type": "roster", "item": item})
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
@@ -329,9 +379,15 @@ class GetObjectsFiltered(View):
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) )
data = [] data = list(roster)
for item in roster:
data.append({"type": "roster", "item": item}) if _filter == "scale":
catalogs = (
Catalog.objects.get_published(request.user)
.filter(scales__slug=search)
.distinct()
)
data = list(chain(data, catalogs))
try: # Execute only if query_2nd is defined try: # Execute only if query_2nd is defined
consists = ( consists = (
@@ -339,23 +395,24 @@ class GetObjectsFiltered(View):
.filter(query_2nd) .filter(query_2nd)
.distinct() .distinct()
) )
for item in consists: data = list(chain(data, consists))
data.append({"type": "consist", "item": item})
if _filter == "tag": # Books can be filtered only by tag if _filter == "tag": # Books can be filtered only by tag
books = ( books = (
Book.objects.get_published(request.user) Book.objects.get_published(request.user)
.filter(query_2nd) .filter(query_2nd)
.distinct() .distinct()
) )
for item in books:
data.append({"type": "book", "item": item})
catalogs = ( catalogs = (
Catalog.objects.get_published(request.user) Catalog.objects.get_published(request.user)
.filter(query_2nd) .filter(query_2nd)
.distinct() .distinct()
) )
for item in catalogs: magazine_issues = (
data.append({"type": "catalog", "item": item}) MagazineIssue.objects.get_published(request.user)
.filter(query_2nd)
.distinct()
)
data = list(chain(data, books, catalogs, magazine_issues))
except NameError: except NameError:
pass pass
@@ -409,16 +466,14 @@ class GetRollingStock(View):
request.user request.user
) )
consists = [ consists = list(
{"type": "consist", "item": c} Consist.objects.get_published(request.user).filter(
for c in Consist.objects.get_published(request.user).filter(
consist_item__rolling_stock=rolling_stock consist_item__rolling_stock=rolling_stock
) )
] # A dict with "item" is required by the consists card )
set = [ trainset = list(
{"type": "set", "item": s} RollingStock.objects.get_published(request.user)
for s in RollingStock.objects.get_published(request.user)
.filter( .filter(
Q( Q(
Q(item_number__exact=rolling_stock.item_number) Q(item_number__exact=rolling_stock.item_number)
@@ -426,7 +481,7 @@ class GetRollingStock(View):
) )
) )
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
] )
return render( return render(
request, request,
@@ -439,7 +494,7 @@ class GetRollingStock(View):
"decoder_documents": decoder_documents, "decoder_documents": decoder_documents,
"documents": documents, "documents": documents,
"journal": journal, "journal": journal,
"set": set, "set": trainset,
"consists": consists, "consists": consists,
}, },
) )
@@ -447,7 +502,6 @@ class GetRollingStock(View):
class Consists(GetData): class Consists(GetData):
title = "Consists" title = "Consists"
item_type = "consist"
def get_data(self, request): def get_data(self, request):
return Consist.objects.get_published(request.user).all() return Consist.objects.get_published(request.user).all()
@@ -461,16 +515,13 @@ class GetConsist(View):
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
data = [
{
"type": "roster",
"item": RollingStock.objects.get_published(request.user).get(
uuid=r.rolling_stock_id
),
}
for r in consist.consist_item.all()
]
data = list(
RollingStock.objects.get_published(request.user).get(
uuid=r.rolling_stock_id
)
for r in consist.consist_item.all()
)
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
@@ -491,7 +542,6 @@ class GetConsist(View):
class Manufacturers(GetData): class Manufacturers(GetData):
title = "Manufacturers" title = "Manufacturers"
item_type = "manufacturer"
def get_data(self, request): def get_data(self, request):
return ( return (
@@ -526,7 +576,26 @@ class Manufacturers(GetData):
) )
) )
) )
.annotate(num_items=F("num_rollingstock") + F("num_rollingclass")) .annotate(
num_catalogs=(
Count(
"catalogs",
filter=Q(
catalogs__in=(
Catalog.objects.get_published(request.user)
),
),
distinct=True,
)
)
)
.annotate(
num_items=(
F("num_rollingstock")
+ F("num_rollingclass")
+ F("num_catalogs")
)
)
.order_by("name") .order_by("name")
) )
@@ -541,7 +610,6 @@ class Manufacturers(GetData):
class Companies(GetData): class Companies(GetData):
title = "Companies" title = "Companies"
item_type = "company"
def get_data(self, request): def get_data(self, request):
return ( return (
@@ -580,7 +648,6 @@ class Companies(GetData):
class Scales(GetData): class Scales(GetData):
title = "Scales" title = "Scales"
item_type = "scale"
def get_data(self, request): def get_data(self, request):
return ( return (
@@ -601,15 +668,21 @@ class Scales(GetData):
), ),
distinct=True, distinct=True,
), ),
num_catalogs=Count("catalogs", distinct=True),
)
.annotate(
num_items=(
F("num_rollingstock")
+ F("num_consists")
+ F("num_catalogs")
)
) )
.annotate(num_items=F("num_rollingstock") + F("num_consists"))
.order_by("-ratio_int", "-tracks", "scale") .order_by("-ratio_int", "-tracks", "scale")
) )
class Types(GetData): class Types(GetData):
title = "Types" title = "Types"
item_type = "rolling_stock_type"
def get_data(self, request): def get_data(self, request):
return RollingStockType.objects.annotate( return RollingStockType.objects.annotate(
@@ -626,7 +699,6 @@ class Types(GetData):
class Books(GetData): class Books(GetData):
title = "Books" title = "Books"
item_type = "book"
def get_data(self, request): def get_data(self, request):
return Book.objects.get_published(request.user).all() return Book.objects.get_published(request.user).all()
@@ -634,7 +706,6 @@ class Books(GetData):
class Catalogs(GetData): class Catalogs(GetData):
title = "Catalogs" title = "Catalogs"
item_type = "catalog"
def get_data(self, request): def get_data(self, request):
return Catalog.objects.get_published(request.user).all() return Catalog.objects.get_published(request.user).all()
@@ -642,12 +713,11 @@ class Catalogs(GetData):
class Magazines(GetData): class Magazines(GetData):
title = "Magazines" title = "Magazines"
item_type = "magazine"
def get_data(self, request): def get_data(self, request):
return ( return (
Magazine.objects.get_published(request.user) Magazine.objects.get_published(request.user)
.all() .order_by(Lower("name"))
.annotate( .annotate(
issues=Count( issues=Count(
"issue", "issue",
@@ -669,14 +739,7 @@ class GetMagazine(View):
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
data = [ data = list(magazine.issue.get_published(request.user).all())
{
"type": "magazineissue",
"label": "Magazine issue",
"item": i,
}
for i in magazine.issue.get_published(request.user).all()
]
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
@@ -712,11 +775,9 @@ class GetMagazineIssue(View):
"bookshelf/book.html", "bookshelf/book.html",
{ {
"title": issue, "title": issue,
"book": issue, "data": issue,
"documents": documents, "documents": documents,
"properties": properties, "properties": properties,
"type": "magazineissue",
"label": "Magazine issue",
}, },
) )
@@ -743,11 +804,9 @@ class GetBookCatalog(View):
"bookshelf/book.html", "bookshelf/book.html",
{ {
"title": book, "title": book,
"book": book, "data": book,
"documents": documents, "documents": documents,
"properties": properties, "properties": properties,
"type": selector,
"label": selector.capitalize(),
}, },
) )

View File

@@ -1,4 +1,4 @@
from ram.utils import git_suffix from ram.utils import git_suffix
__version__ = "0.18.5" __version__ = "0.19.5"
__version__ += git_suffix(__file__) __version__ += git_suffix(__file__)

View File

@@ -1,22 +1,60 @@
from django.contrib import admin
from django.conf import settings from django.conf import settings
from django.contrib import admin
from django.core.cache import cache
admin.site.site_header = settings.SITE_NAME admin.site.site_header = settings.SITE_NAME
def publish(modeladmin, request, queryset): def publish(modeladmin, request, queryset):
for obj in queryset: queryset.update(published=True)
obj.published = True cache.clear()
obj.save()
publish.short_description = "Publish selected items" publish.short_description = "Publish selected items"
def unpublish(modeladmin, request, queryset): def unpublish(modeladmin, request, queryset):
for obj in queryset: queryset.update(published=False)
obj.published = False cache.clear()
obj.save()
unpublish.short_description = "Unpublish selected items" unpublish.short_description = "Unpublish selected items"
def set_featured(modeladmin, request, queryset):
count = queryset.count()
if count > settings.FEATURED_ITEMS_MAX:
modeladmin.message_user(
request,
"You can only mark up to {} items as featured.".format(
settings.FEATURED_ITEMS_MAX
),
level="error",
)
return
featured = modeladmin.model.objects.filter(featured=True).count()
if featured + count > settings.FEATURED_ITEMS_MAX:
modeladmin.message_user(
request,
"There are already {} featured items. You can only mark {} more items as featured.".format( # noqa: E501
featured,
settings.FEATURED_ITEMS_MAX - featured,
),
level="error",
)
return
queryset.update(featured=True)
cache.clear()
set_featured.short_description = "Mark selected items as featured"
def unset_featured(modeladmin, request, queryset):
queryset.update(featured=False)
cache.clear()
unset_featured.short_description = (
"Unmark selected items as featured"
)

View File

@@ -9,6 +9,19 @@ from ram.utils import DeduplicatedStorage, get_image_preview
from ram.managers import PublicManager from ram.managers import PublicManager
class SimpleBaseModel(models.Model):
class Meta:
abstract = True
@property
def obj_type(self):
return self._meta.model_name
@property
def obj_label(self):
return self._meta.object_name
class BaseModel(models.Model): class BaseModel(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False) uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
description = tinymce.HTMLField(blank=True) description = tinymce.HTMLField(blank=True)
@@ -20,6 +33,14 @@ class BaseModel(models.Model):
class Meta: class Meta:
abstract = True abstract = True
@property
def obj_type(self):
return self._meta.model_name
@property
def obj_label(self):
return self._meta.object_name
objects = PublicManager() objects = PublicManager()

View File

@@ -204,6 +204,8 @@ ROLLING_STOCK_TYPES = [
("other", "Other"), ("other", "Other"),
] ]
FEATURED_ITEMS_MAX = 6
try: try:
from ram.local_settings import * from ram.local_settings import *
except ImportError: except ImportError:

View File

@@ -6,8 +6,8 @@ from django.utils.html import format_html, format_html_join, strip_tags
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from ram.admin import publish, unpublish
from ram.utils import generate_csv from ram.utils import generate_csv
from ram.admin import publish, unpublish, set_featured, unset_featured
from repository.models import RollingStockDocument from repository.models import RollingStockDocument
from portal.utils import get_site_conf from portal.utils import get_site_conf
from roster.models import ( from roster.models import (
@@ -44,7 +44,9 @@ class RollingClass(admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name '<img src="{}" title="{}" />',
obj.company.country.flag,
obj.company.country.name,
) )
@@ -128,9 +130,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"item_number", "item_number",
"company", "company",
"country_flag", "country_flag",
"featured",
"published", "published",
) )
list_filter = ( list_filter = (
"featured",
"published",
"rolling_class__type__category", "rolling_class__type__category",
"rolling_class__type", "rolling_class__type",
"rolling_class__company__name", "rolling_class__company__name",
@@ -152,7 +157,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name '<img src="{}" title="{}" />', obj.country.flag, obj.country.name
) )
fieldsets = ( fieldsets = (
@@ -162,6 +167,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"fields": ( "fields": (
"preview", "preview",
"published", "published",
"featured",
"rolling_class", "rolling_class",
"road_number", "road_number",
"scale", "scale",
@@ -224,8 +230,8 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
if obj.invoice.exists(): if obj.invoice.exists():
html = format_html_join( html = format_html_join(
"<br>", "<br>",
"<a href=\"{}\" target=\"_blank\">{}</a>", '<a href="{}" target="_blank">{}</a>',
((i.file.url, i) for i in obj.invoice.all()) ((i.file.url, i) for i in obj.invoice.all()),
) )
else: else:
html = "-" html = "-"
@@ -296,4 +302,5 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
return generate_csv(header, data, "rolling_stock.csv") return generate_csv(header, data, "rolling_stock.csv")
download_csv.short_description = "Download selected items as CSV" download_csv.short_description = "Download selected items as CSV"
actions = [publish, unpublish, download_csv]
actions = [publish, unpublish, set_featured, unset_featured, download_csv]

View File

@@ -0,0 +1,21 @@
# Generated by Django 6.0 on 2025-12-24 13:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0038_alter_rollingstock_rolling_class"),
]
operations = [
migrations.AddField(
model_name="rollingstock",
name="featured",
field=models.BooleanField(
default=False,
help_text="Featured rolling stock will appear on the homepage",
),
),
]

View File

@@ -5,6 +5,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.core.exceptions import ValidationError
from tinymce import models as tinymce from tinymce import models as tinymce
@@ -82,9 +83,7 @@ class RollingStock(BaseModel):
help_text="Catalog item number or code", help_text="Catalog item number or code",
) )
item_number_slug = models.CharField( item_number_slug = models.CharField(
max_length=32, max_length=32, blank=True, editable=False
blank=True,
editable=False
) )
set = models.BooleanField( set = models.BooleanField(
default=False, default=False,
@@ -113,6 +112,10 @@ class RollingStock(BaseModel):
null=True, null=True,
blank=True, blank=True,
) )
featured = models.BooleanField(
default=False,
help_text="Featured rolling stock will appear on the homepage",
)
tags = models.ManyToManyField( tags = models.ManyToManyField(
Tag, related_name="rolling_stock", blank=True Tag, related_name="rolling_stock", blank=True
) )
@@ -165,10 +168,23 @@ class RollingStock(BaseModel):
os.path.join( os.path.join(
settings.MEDIA_ROOT, "images", "rollingstock", str(self.uuid) settings.MEDIA_ROOT, "images", "rollingstock", str(self.uuid)
), ),
ignore_errors=True ignore_errors=True,
) )
super(RollingStock, self).delete(*args, **kwargs) super(RollingStock, self).delete(*args, **kwargs)
def clean(self, *args, **kwargs):
if self.featured:
MAX = settings.FEATURED_ITEMS_MAX
featured_count = (
RollingStock.objects.filter(featured=True)
.exclude(uuid=self.uuid)
.count()
)
if featured_count > MAX - 1:
raise ValidationError(
"There are already {} featured items".format(MAX)
)
@receiver(models.signals.pre_save, sender=RollingStock) @receiver(models.signals.pre_save, sender=RollingStock)
def pre_save_internal_fields(sender, instance, *args, **kwargs): def pre_save_internal_fields(sender, instance, *args, **kwargs):
@@ -185,10 +201,7 @@ def pre_save_internal_fields(sender, instance, *args, **kwargs):
def rolling_stock_image_upload(instance, filename): def rolling_stock_image_upload(instance, filename):
return os.path.join( return os.path.join(
"images", "images", "rollingstock", str(instance.rolling_stock.uuid), filename
"rollingstock",
str(instance.rolling_stock.uuid),
filename
) )

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,12 @@
#!/bin/bash
mkdir -p output
for img in input/*.{jpg,png}; do
[ -e "$img" ] || continue # skip if no files
name=$(basename "${img%.*}").jpg
magick convert background.png \
\( "$img" -resize x820 \) \
-gravity center -composite \
-quality 85 -sampling-factor 4:4:4 \
"output/$name"
done