Compare commits

9 Commits

41 changed files with 443 additions and 696 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,
@@ -66,12 +66,12 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
BookDocInline, BookDocInline,
) )
list_display = ( list_display = (
"published",
"title", "title",
"get_authors", "get_authors",
"get_publisher", "get_publisher",
"publication_year", "publication_year",
"number_of_pages", "number_of_pages",
"published",
) )
autocomplete_fields = ("authors", "publisher", "shop") autocomplete_fields = ("authors", "publisher", "shop")
readonly_fields = ("invoices", "creation_time", "updated_time") readonly_fields = ("invoices", "creation_time", "updated_time")
@@ -135,8 +135,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 +212,11 @@ class AuthorAdmin(admin.ModelAdmin):
@admin.register(Publisher) @admin.register(Publisher)
class PublisherAdmin(admin.ModelAdmin): class PublisherAdmin(admin.ModelAdmin):
list_display = ("name", "country_flag_name") list_display = ("name", "country_flag")
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country") @admin.display(description="Country")
def country_flag_name(self, obj): def country_flag(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 +240,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 +303,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 = "-"
@@ -449,12 +449,14 @@ 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 = (MagazineIssueInline,) inlines = (
MagazineIssueInline,
)
list_display = ( list_display = (
"__str__", "__str__",
@@ -464,10 +466,7 @@ 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 = ( list_filter = ("publisher__name", "published")
"published",
"publisher__name",
)
fieldsets = ( fieldsets = (
( (
@@ -476,7 +475,6 @@ class MagazineAdmin(SortableAdminBase, admin.ModelAdmin):
"fields": ( "fields": (
"published", "published",
"name", "name",
"website",
"publisher", "publisher",
"ISBN", "ISBN",
"language", "language",

View File

@@ -1,18 +0,0 @@
# Generated by Django 6.0 on 2025-12-12 14:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0026_alter_basebook_language_alter_magazine_image_and_more"),
]
operations = [
migrations.AddField(
model_name="magazine",
name="website",
field=models.URLField(blank=True),
),
]

View File

@@ -1,29 +0,0 @@
# 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

@@ -1,29 +0,0 @@
# 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

@@ -1,11 +1,9 @@
import os import os
import shutil import shutil
from urllib.parse import urlparse
from django.db import models 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
@@ -60,24 +58,36 @@ 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(Tag, related_name="bookshelf", blank=True) tags = models.ManyToManyField(
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("images", "books", str(instance.book.uuid), filename) return os.path.join(
"images",
"books",
str(instance.book.uuid),
filename
)
def magazine_image_upload(instance, filename): def magazine_image_upload(instance, filename):
return os.path.join("images", "magazines", str(instance.uuid), filename) return os.path.join(
"images",
"magazines",
str(instance.uuid),
filename
)
class BaseBookImage(Image): class BaseBookImage(Image):
@@ -121,7 +131,8 @@ class Book(BaseBook):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"bookshelf_item", kwargs={"selector": "book", "uuid": self.uuid} "bookshelf_item",
kwargs={"selector": "book", "uuid": self.uuid}
) )
@@ -129,10 +140,9 @@ 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, related_name="catalogs") scales = models.ManyToManyField(Scale)
class Meta: class Meta:
ordering = ["manufacturer", "publication_year"] ordering = ["manufacturer", "publication_year"]
@@ -147,19 +157,18 @@ class Catalog(BaseBook):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"bookshelf_item", kwargs={"selector": "catalog", "uuid": self.uuid} "bookshelf_item",
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"
class Magazine(BaseModel): class Magazine(BaseModel):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
website = models.URLField(blank=True)
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
image = models.ImageField( image = models.ImageField(
blank=True, blank=True,
@@ -169,31 +178,32 @@ 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 = [Lower("name")] ordering = ["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("magazine", kwargs={"uuid": self.uuid}) return reverse(
"magazine",
def website_short(self): kwargs={"uuid": self.uuid}
if self.website: )
return urlparse(self.website).netloc.replace("www.", "")
class MagazineIssue(BaseBook): class MagazineIssue(BaseBook):
@@ -202,17 +212,14 @@ 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, blank=True, choices=MONTHS.items() null=True,
blank=True,
choices=MONTHS.items()
) )
class Meta: class Meta:
unique_together = ("magazine", "issue_number") unique_together = ("magazine", "issue_number")
ordering = [ ordering = ["magazine", "issue_number"]
"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}"
@@ -224,10 +231,6 @@ 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)
@@ -237,5 +240,9 @@ class MagazineIssue(BaseBook):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"issue", kwargs={"uuid": self.uuid, "magazine": self.magazine.uuid} "issue",
kwargs={
"uuid": self.uuid,
"magazine": self.magazine.uuid
}
) )

View File

@@ -49,5 +49,3 @@ class CatalogSerializer(serializers.ModelSerializer):
"price", "price",
) )
read_only_fields = ("creation_time", "updated_time") read_only_fields = ("creation_time", "updated_time")
# FIXME: add Magazine and MagazineIssue serializers

View File

@@ -38,5 +38,3 @@ class CatalogGet(RetrieveAPIView):
def get_queryset(self): def get_queryset(self):
return Book.objects.get_published(self.request.user) return Book.objects.get_published(self.request.user)
# FIXME: add Magazine and MagazineIssue views

View File

@@ -2,7 +2,6 @@ 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 (
@@ -47,22 +46,15 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"creation_time", "creation_time",
"updated_time", "updated_time",
) )
list_filter = ("published", "company__name", "era", "scale") list_filter = ("company__name", "era", "scale", "published")
list_display = ( list_display = ("__str__",) + list_filter + ("country_flag",)
"__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="{}" title="{}" />', obj.country.flag, obj.country.name '<img src="{}" /> {}', obj.country.flag, obj.country
) )
fieldsets = ( fieldsets = (
@@ -146,7 +138,6 @@ 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_name") list_display = ("name", "country_flag")
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_name(self, obj): def country_flag(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_name") list_display = ("name", "category", "country_flag")
list_filter = ("category",) list_filter = ("category",)
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country") @admin.display(description="Country")
def country_flag_name(self, obj): def country_flag(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,12 +88,6 @@ 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", "country_flag_name") list_display = ("name", "on_line", "active")
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

@@ -1,5 +1,4 @@
import os import os
from urllib.parse import urlparse
from django.db import models 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
@@ -7,12 +6,11 @@ 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(SimpleBaseModel): class Property(models.Model):
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,
@@ -29,7 +27,7 @@ class Property(SimpleBaseModel):
objects = PublicManager() objects = PublicManager()
class Manufacturer(SimpleBaseModel): class Manufacturer(models.Model):
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(
@@ -59,17 +57,13 @@ class Manufacturer(SimpleBaseModel):
}, },
) )
def website_short(self):
if self.website:
return urlparse(self.website).netloc.replace("www.", "")
def logo_thumbnail(self): def logo_thumbnail(self):
return get_image_preview(self.logo.url) return get_image_preview(self.logo.url)
logo_thumbnail.short_description = "Preview" logo_thumbnail.short_description = "Preview"
class Company(SimpleBaseModel): class Company(models.Model):
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)
@@ -107,7 +101,7 @@ class Company(SimpleBaseModel):
logo_thumbnail.short_description = "Preview" logo_thumbnail.short_description = "Preview"
class Decoder(SimpleBaseModel): class Decoder(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
@@ -143,7 +137,7 @@ def calculate_ratio(ratio):
raise ValidationError("Invalid ratio format") raise ValidationError("Invalid ratio format")
class Scale(SimpleBaseModel): class Scale(models.Model):
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])
@@ -178,7 +172,7 @@ def scale_save(sender, instance, **kwargs):
instance.ratio_int = calculate_ratio(instance.ratio) instance.ratio_int = calculate_ratio(instance.ratio)
class RollingStockType(SimpleBaseModel): class RollingStockType(models.Model):
type = models.CharField(max_length=64) type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField() order = models.PositiveSmallIntegerField()
category = models.CharField( category = models.CharField(
@@ -208,7 +202,7 @@ class RollingStockType(SimpleBaseModel):
return "{0} {1}".format(self.type, self.category) return "{0} {1}".format(self.type, self.category)
class Tag(SimpleBaseModel): class Tag(models.Model):
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)
@@ -228,7 +222,7 @@ class Tag(SimpleBaseModel):
) )
class Shop(SimpleBaseModel): class Shop(models.Model):
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 data.tags.all %} {% if book.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
{% for t in data.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in book.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 data.published %} {% if not book.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 {{ data.updated_time | date:"M d, Y H:i" }}</small> <small class="text-body-secondary">Updated {{ book.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 data.image.all %} {% for t in book.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 data.image.count > 1 %} {% if book.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>
@@ -61,86 +61,82 @@
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row"> <th colspan="2" scope="row">
{{ data.obj_label|capfirst }} {% if type == "catalog" %}Catalog
{% elif type == "book" %}Book{% endif %}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if data.obj_type == "catalog" %} {% if type == "catalog" %}
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td> <td>{{ book.manufacturer }}</td>
<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>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Scales</th> <th class="w-33" scope="row">Scales</th>
<td>{{ data.get_scales }}</td> <td>{{ book.get_scales }}</td>
</tr> </tr>
{% elif data.obj_type == "book" %} {% elif type == "book" %}
<tr> <tr>
<th class="w-33" scope="row">Title</th> <th class="w-33" scope="row">Title</th>
<td>{{ data.title }}</td> <td>{{ book.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 data.authors.all %}<li>{{ a }}</li>{% endfor %}</ul> <ul class="mb-0 list-unstyled">{% for a in book.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="{{ data.publisher.country.flag }}" alt="{{ data.publisher.country }}"> {{ data.publisher }} <img src="{{ book.publisher.country.flag }}" alt="{{ book.publisher.country }}"> {{ book.publisher }}
{% if data.publisher.website %} <a href="{{ data.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if book.publisher.website %} <a href="{{ book.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
{% elif data.obj_type == "magazineissue" %} {% elif 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></td>
<a href="{% url 'magazine' data.magazine.pk %}">{{ data.magazine }}</a>
{% if data.magazine.website %} <a href="{{ data.magazine.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</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="{{ data.publisher.country.flag }}" alt="{{ data.publisher.country }}"> {{ data.publisher }} <img src="{{ book.publisher.country.flag }}" alt="{{ book.publisher.country }}"> {{ book.publisher }}
{% if data.publisher.website %} <a href="{{ data.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if book.publisher.website %} <a href="{{ book.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>{{ data.issue_number }}</td> <td>{{ book.issue_number }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Date</th> <th class="w-33" scope="row">Date</th>
<td>{{ data.publication_year|default:"-" }} / {{ data.get_publication_month_display|default:"-" }}</td> <td>{{ book.publication_year|default:"-" }} / {{ book.publication_month|default:"-" }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th scope="row">ISBN</th> <th scope="row">ISBN</th>
<td>{{ data.ISBN|default:"-" }}</td> <td>{{ book.ISBN|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Language</th> <th scope="row">Language</th>
<td>{{ data.get_language_display }}</td> <td>{{ book.get_language_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Number of pages</th> <th scope="row">Number of pages</th>
<td>{{ data.number_of_pages|default:"-" }}</td> <td>{{ book.number_of_pages|default:"-" }}</td>
</tr> </tr>
{% if data.obj_type == "book" or data.obj_type == "catalog" %} {% if type == "boook" or type == "catalog" %}
<tr> <tr>
<th scope="row">Publication year</th> <th scope="row">Publication year</th>
<td>{{ data.publication_year|default:"-" }}</td> <td>{{ book.publication_year|default:"-" }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if data.description %} {% if book.description %}
<tr> <tr>
<th class="w-33" scope="row">Description</th> <th class="w-33" scope="row">Description</th>
<td>{{ data.description | safe }}</td> <td>{{ book.description | safe }}</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
@@ -156,17 +152,17 @@
<tr> <tr>
<th class="w-33" scope="row">Shop</th> <th class="w-33" scope="row">Shop</th>
<td> <td>
{{ data.shop|default:"-" }} {{ book.shop|default:"-" }}
{% if data.shop.website %} <a href="{{ data.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if book.shop.website %} <a href="{{ book.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>{{ data.purchase_date|default:"-" }}</td> <td>{{ book.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>{{ data.price|default:"-" }}</td> <td>{{ book.price|default:"-" }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -209,7 +205,7 @@
</div> </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' data.obj_type data.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' type book.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.obj_type == "rollingstock" %} {% if d.type == "roster" %}
{% include "cards/roster.html" %} {% include "cards/roster.html" %}
{% elif d.obj_type == "company" %} {% elif d.type == "company" %}
{% include "cards/company.html" %} {% include "cards/company.html" %}
{% elif d.obj_type == "rollingstocktype" %} {% elif d.type == "rolling_stock_type" %}
{% include "cards/rolling_stock_type.html" %} {% include "cards/rolling_stock_type.html" %}
{% elif d.obj_type == "scale" %} {% elif d.type == "scale" %}
{% include "cards/scale.html" %} {% include "cards/scale.html" %}
{% elif d.obj_type == "consist" %} {% elif d.type == "consist" %}
{% include "cards/consist.html" %} {% include "cards/consist.html" %}
{% elif d.obj_type == "manufacturer" %} {% elif d.type == "manufacturer" %}
{% include "cards/manufacturer.html" %} {% include "cards/manufacturer.html" %}
{% elif d.obj_type == "magazine" or d.obj_type == "magazineissue" %} {% elif d.type == "magazine" or d.type == "magazineissue" %}
{% include "cards/magazine.html" %} {% include "cards/magazine.html" %}
{% elif d.obj_type == "book" or d.obj_type == "catalog" %} {% elif d.type == "book" or d.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.image.exists %} {% if d.item.image.exists %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a> <a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></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.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a> <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>
{% 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 }}</strong> <strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.item.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.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.item.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.obj_label|capfirst }} {{ d.type | capfirst }}
<div class="float-end"> <div class="float-end">
{% if not d.published %} {% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span> <span class="badge text-bg-warning">Unpublished</span>
{% endif %} {% endif %}
</div> </div>
@@ -34,46 +34,44 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.obj_type == "catalog" %} {% if d.type == "catalog" %}
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td> <td>{{ d.item.manufacturer }}</td>
<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>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Scales</th> <th class="w-33" scope="row">Scales</th>
<td>{{ d.get_scales }}</td> <td>{{ d.item.get_scales }}</td>
</tr> </tr>
{% elif d.obj_type == "book" %} {% elif d.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.authors.all %}<li>{{ a }}</li>{% endfor %}</ul> <ul class="mb-0 list-unstyled">{% for a in d.item.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.publisher.country.flag }}" alt="{{ d.publisher.country }}"> {{ d.publisher }}</td> <td><img src="{{ d.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th scope="row">Language</th> <th scope="row">Language</th>
<td>{{ d.get_language_display }}</td> <td>{{ d.item.get_language_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Pages</th> <th scope="row">Pages</th>
<td>{{ d.number_of_pages|default:"-" }}</td> <td>{{ d.item.number_of_pages|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Year</th> <th scope="row">Year</th>
<td>{{ d.publication_year|default:"-" }}</td> <td>{{ d.item.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.get_absolute_url }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ d.item.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.obj_type d.pk %}">Edit</a>{% 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 %}
</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.name }}</strong> <strong>{{ d.item.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.freelance %} {% if d.item.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.logo %} {% if d.item.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.logo.url }}" alt="{{ d.name }} logo"></td> <td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.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.extended_name }}</td> <td>{{ d.item.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.name }}</td> <td>{{ d.item.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.country.flag }}" alt="{{ d.country }}"> {{ d.country.name }}</td> <td><img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}"> {{ d.item.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.num_items %} {% with items=d.item.num_items %}
<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> <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>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.pk %}">Edit</a>{% endif %} {% 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 %}
{% 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.get_absolute_url }}"> <a href="{{ d.item.get_absolute_url }}">
{% if d.image %} {% if d.item.image %}
<img class="card-img-top" src="{{ d.image.url }}" alt="{{ d }}"> <img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}">
{% else %} {% else %}
{% with d.consist_item.first.rolling_stock as r %} {% with d.item.consist_item.first.rolling_stock as r %}
<img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d }}"> <img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d.item }}">
{% 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 }}</strong> <strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.item.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.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.item.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.published %} {% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span> <span class="badge text-bg-warning">Unpublished</span>
{% endif %} {% endif %}
{% if d.company.freelance %} {% if d.item.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.address %} {% if d.item.address %}
<tr> <tr>
<th class="w-33" scope="row">Address</th> <th class="w-33" scope="row">Address</th>
<td>{{ d.address }}</td> <td>{{ d.item.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.company.country.flag }}" alt="{{ d.company.country }}"> <img src="{{ d.item.company.country.flag }}" alt="{{ d.item.company.country }}">
<abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr> <abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
<td>{{ d.era }}</td> <td>{{ d.item.era }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Length</th> <th scope="row">Length</th>
<td>{{ d.length }}</td> <td>{{ d.item.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.get_absolute_url }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ d.item.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.pk %}">Edit</a>{% endif %} {% 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 %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,51 +1,49 @@
{% load static %} {% load static %}
{% load dynamic_url %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if d.obj_type == "magazine" %} {% if d.type == "magazine" %}
<a href="{{ d.get_absolute_url }}"> <a href="{{ d.item.get_absolute_url }}">
{% if d.image and d.obj_type == "magazine" %} {% if d.item.image and d.type == "magazine" %}
<img class="card-img-top" src="{{ d.image.url }}" alt="{{ d }}"> <img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}">
{% elif d.issue.first.image.exists %} {% elif d.item.issue.first.image.exists %}
{% with d.issue.first as i %} {% with d.item.issue.first as i %}
<img class="card-img-top" src="{{ i.image.first.image.url }}" alt="{{ d }}"> <img class="card-img-top" src="{{ i.image.first.image.url }}" alt="{{ d.item }}">
{% endwith %} {% 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 }}"> <img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}">
{% endif %} {% endif %}
</a> </a>
{% elif d.obj_type == "magazineissue" %} {% elif d.type == "magazineissue" %}
<a href="{{ d.get_absolute_url }}"> <a href="{{ d.item.get_absolute_url }}">
{% if d.image.exists %} {% if d.item.image.exists %}
<img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"> <img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}">
{% 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 }}"> <img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}">
{% 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 }}</strong> <strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.item.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.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.item.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.obj_label|capfirst }} {{ d.type | capfirst }}
<div class="float-end"> <div class="float-end">
{% if not d.published %} {% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span> <span class="badge text-bg-warning">Unpublished</span>
{% endif %} {% endif %}
</div> </div>
@@ -53,51 +51,46 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.obj_type == "magazineissue" %} {% if d.type == "magazineissue" %}
<tr> <tr>
<th class="w-33" scope="row">Magazine</th> <th class="w-33" scope="row">Magazine</th>
<td>{{ d.magazine }}</td> <td>{{ d.item.magazine }}</td>
</tr>
{% else %}
<tr>
<th class="w-33" scope="row">Website</th>
<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.publisher.country.flag }}" alt="{{ d.publisher.country }}"> {{ d.publisher }} <img src="{{ d.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }}
{% if d.publisher.website %} <a href="{{ d.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if d.item.publisher.website %} <a href="{{ d.item.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
{% if d.obj_type == "magazineissue" %} {% if d.type == "magazineissue" %}
<tr> <tr>
<th class="w-33" scope="row">Issue</th> <th class="w-33" scope="row">Issue</th>
<td>{{ d.issue_number }}</td> <td>{{ d.item.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.publication_year|default:"-" }} / {{ d.get_publication_month_display|default:"-" }}</td> <td>{{ d.item.publication_year|default:"-" }} / {{ d.item.publication_month|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Pages</th> <th class="w-33" scope="row">Pages</th>
<td>{{ d.number_of_pages|default:"-" }}</td> <td>{{ d.item.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.get_language_display }}</td> <td>{{ d.item.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.obj_type == "magazine" %} {% if d.type == "magazine" %}
<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> <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>
{% else %} {% else %}
<a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ d.item.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.obj_type d.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:bookshelf_magazineissue_change' d.item.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.name }}</strong> <strong>{{ d.item.name }}</strong>
</p> </p>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@@ -11,26 +11,28 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if d.logo %} {% if d.item.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.logo.url }}" alt="{{ d.name }} logo"></td> <td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td>
</tr>
{% endif %}
{% if d.item.website %}
<tr>
<th class="w-33" scope="row">Website</th>
<td><a href="{{ d.item.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a></td>
</tr> </tr>
{% endif %} {% endif %}
<tr>
<th class="w-33" scope="row">Website</th>
<td>{% if d.website %}<a href="{{ d.website }}" target="_blank">{{ d.website_short }}</td>{% else %}-{% endif %}</td>
</tr>
<tr> <tr>
<th class="w-33" scope="row">Category</th> <th class="w-33" scope="row">Category</th>
<td>{{ d.category | title }}</td> <td>{{ d.item.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.num_items %} {% with items=d.item.num_items %}
<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> <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>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.pk %}">Edit</a>{% endif %} {% 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 %}
{% 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 }}</strong></p> <p class="card-text"><strong>{{ d.item }}</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.type }}</td> <td>{{ d.item.type }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Category</th> <th class="w-33" scope="row">Category</th>
<td>{{ d.category | title}}</td> <td>{{ d.item.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.num_items %} {% with items=d.item.num_items %}
<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> <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>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_rollingstocktype_change' d.pk %}">Edit</a>{% endif %} {% 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 %}
{% endwith %} {% endwith %}
</div> </div>
</div> </div>

View File

@@ -3,34 +3,34 @@
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
{% if d.image.exists %} {% if d.item.image.exists %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a> <a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></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.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a> <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>
{% 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 }}</strong> <strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.item.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.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.item.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.published %} {% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span> <span class="badge text-bg-warning">Unpublished</span>
{% endif %} {% endif %}
{% if d.company.freelance %} {% if d.item.company.freelance %}
<span class="badge text-bg-secondary">Freelance</span> <span class="badge text-bg-secondary">Freelance</span>
{% endif %} {% endif %}
</div> </div>
@@ -40,50 +40,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.rolling_class.type }}</td> <td>{{ d.item.rolling_class.type }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Company</th> <th scope="row">Company</th>
<td> <td>
<img src="{{ d.company.country.flag }}" alt="{{ d.company.country }}"> <img src="{{ d.item.company.country.flag }}" alt="{{ d.item.company.country }}">
<a href="{% url 'filtered' _filter="company" search=d.company.slug %}"><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></a> <a href="{% url 'filtered' _filter="company" search=d.item.company.slug %}"><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></a>
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Class</th> <th scope="row">Class</th>
<td>{{ d.rolling_class.identifier }}</td> <td>{{ d.item.rolling_class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Road number</th> <th scope="row">Road number</th>
<td>{{ d.road_number }}</td> <td>{{ d.item.road_number }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
<td>{{ d.era }}</td> <td>{{ d.item.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.manufacturer %} <td>{%if d.item.manufacturer %}
<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 %} <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 %}
{% 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.scale.slug %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }} mm">{{ d.scale }}</abbr></a></td> <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>
</tr> </tr>
<tr> <tr>
<th scope="row">Item number</th> <th scope="row">Item number</th>
<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> <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>
</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 %}</a></td> <td><a class="text-reset text-decoration-none" title="Symbols" href="" data-bs-toggle="modal" data-bs-target="#symbolsModal">{% dcc d.item %}</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.get_absolute_url}}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{d.item.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.pk %}">Edit</a>{% endif %} {% 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 %}
</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 }}</strong></p> <p class="card-text"><strong>{{ d.item }}</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.scale }}</td> <td>{{ d.item.scale }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Ratio</th> <th class="w-33" scope="row">Ratio</th>
<td>{{ d.ratio }}</td> <td>{{ d.item.ratio }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Tracks</th> <th class="w-33" scope="row">Tracks</th>
<td>{{ d.tracks }} mm</td> <td>{{ d.item.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.gauge }}</td> <td>{{ d.item.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.num_items %} {% with items=d.item.num_items %}
<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> <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>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %} {% 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 %}
{% 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' 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_pagination' 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' uuid=consist.uuid page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'consist_pagination' 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' 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_pagination' 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">

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' _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_pagination' _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' _filter=filter search=search page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'filtered_pagination' _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' _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_pagination' _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,18 +3,3 @@
{% 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

@@ -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' 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_pagination' 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' uuid=magazine.uuid page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'magazine_pagination' 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' 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_pagination' 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">
@@ -88,7 +88,7 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Name</th> <th class="w-33" scope="row">Name</th>
<td>{{ magazine }}</td> <td>{{ magazine }} </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Publisher</th> <th class="w-33" scope="row">Publisher</th>
@@ -97,14 +97,6 @@
{% if magazine.publisher.website %} <a href="{{ magazine.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if magazine.publisher.website %} <a href="{{ magazine.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
<tr>
<th class="w-33" scope="row">Website</th>
<td>{% if magazine.website %}<a href="{{ magazine.website }}" target="_blank">{{ magazine.website_short }}</td>{% else %}-{% endif %}</td>
</tr>
<tr>
<th class="w-33" scope="row">Language</th>
<td>{{ magazine.get_language_display }}</td>
</tr>
<tr> <tr>
<th scope="row">ISBN</th> <th scope="row">ISBN</th>
<td>{{ magazine.ISBN | default:"-" }}</td> <td>{{ magazine.ISBN | default:"-" }}</td>

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' 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_pagination' 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' manufacturer=manufacturer.slug search=search page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'manufacturer_pagination' 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' 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_pagination' 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="{% url request.resolver_match.url_name page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a> <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>
</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="{% url request.resolver_match.url_name page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% dynamic_pagination type 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 request.resolver_match.url_name page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a> <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>
</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.category as c %} {% with data.0.item.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' 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_pagination' 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' category=c page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'manufacturers_pagination' 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' 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_pagination' 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

@@ -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' 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_pagination' 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' search=encoded_search page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'search_pagination' 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' 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_pagination' 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,3 +12,10 @@ 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

@@ -1,11 +0,0 @@
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 (
GetHome, GetData,
GetRoster, GetRoster,
GetObjectsFiltered, GetObjectsFiltered,
GetManufacturerItem, GetManufacturerItem,
@@ -23,75 +23,123 @@ from portal.views import (
) )
urlpatterns = [ urlpatterns = [
path("", GetHome.as_view(), name="index"), path("", GetData.as_view(template="home.html"), name="index"),
path("roster", GetRoster.as_view(), name="roster"), path("roster", GetRoster.as_view(), name="roster"),
path("roster/page/<int:page>", GetRoster.as_view(), name="roster"), path(
"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("consists", Consists.as_view(), name="consists"), path(
path("consists/page/<int:page>", Consists.as_view(), name="consists"), "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", name="consist_pagination",
),
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", name="companies_pagination",
), ),
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", name="manufacturers_pagination",
),
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", name="magazine_pagination",
), ),
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("bookshelf/magazines", Magazines.as_view(), name="magazines"), path(
"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", name="magazines_pagination"
), ),
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", name="catalogs_pagination"
), ),
path( path(
"search", "search",
@@ -101,7 +149,7 @@ urlpatterns = [
path( path(
"search/<str:search>/page/<int:page>", "search/<str:search>/page/<int:page>",
SearchObjects.as_view(), SearchObjects.as_view(),
name="search", name="search_pagination",
), ),
path( path(
"manufacturer/<str:manufacturer>", "manufacturer/<str:manufacturer>",
@@ -111,7 +159,7 @@ urlpatterns = [
path( path(
"manufacturer/<str:manufacturer>/page/<int:page>", "manufacturer/<str:manufacturer>/page/<int:page>",
GetManufacturerItem.as_view(), GetManufacturerItem.as_view(),
name="manufacturer", name="manufacturer_pagination",
), ),
path( path(
"manufacturer/<str:manufacturer>/<str:search>", "manufacturer/<str:manufacturer>/<str:search>",
@@ -121,7 +169,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", name="manufacturer_pagination",
), ),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
@@ -131,7 +179,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", name="filtered_pagination",
), ),
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"), path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),
] ]

View File

@@ -4,12 +4,10 @@ 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
@@ -33,7 +31,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 int(items_per_page) return items_per_page
def get_order_by_field(): def get_order_by_field():
@@ -63,8 +61,9 @@ class Render404(View):
class GetData(View): class GetData(View):
title = None title = "Home"
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,11 +73,11 @@ class GetData(View):
.filter(self.filter) .filter(self.filter)
) )
def get(self, request, page=1): def get(self, request, filter=Q(), page=1):
if self.title is None or self.template is None: self.filter = filter
raise Exception("title and template must be defined") data = []
for item in self.get_data(request):
data = list(self.get_data(request)) data.append({"type": self.item_type, "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)
@@ -91,6 +90,7 @@ 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,
@@ -98,36 +98,18 @@ class GetData(View):
) )
class GetHome(GetData):
title = "Home"
template = "home.html"
def get_data(self, request):
max_items = min(settings.FEATURED_ITEMS_MAX, get_items_per_page())
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): class GetRoster(GetData):
title = "The Roster" title = "The Roster"
item_type = "roster"
def get_data(self, request):
return RollingStock.objects.get_published(request.user).order_by(
*get_order_by_field()
)
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_,
@@ -170,13 +152,15 @@ 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())
) )
data = list(roster) for item in roster:
data.append({"type": "roster", "item": item})
if _filter is None: if _filter is None:
consists = ( consists = (
@@ -189,39 +173,20 @@ class SearchObjects(View):
) )
.distinct() .distinct()
) )
data = list(chain(data, consists)) for item in consists:
data.append({"type": "consist", "item": item})
books = ( books = (
Book.objects.get_published(request.user) Book.objects.get_published(request.user)
.filter( .filter(title__icontains=search)
Q(
Q(title__icontains=search)
| Q(description__icontains=search)
)
)
.distinct() .distinct()
) )
catalogs = ( catalogs = (
Catalog.objects.get_published(request.user) Catalog.objects.get_published(request.user)
.filter( .filter(manufacturer__name__icontains=search)
Q(
Q(manufacturer__name__icontains=search)
| Q(description__icontains=search)
)
)
.distinct() .distinct()
) )
data = list(chain(data, books, catalogs)) for item in list(chain(books, catalogs)):
magazine_issues = ( data.append({"type": "book", "item": item})
MagazineIssue.objects.get_published(request.user)
.filter(
Q(
Q(magazine__name__icontains=search)
| Q(description__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)
@@ -277,25 +242,10 @@ 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(
@@ -306,7 +256,6 @@ 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``;
@@ -323,12 +272,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 = list(chain(roster, catalogs)) data = []
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(
@@ -377,15 +326,9 @@ class GetObjectsFiltered(View):
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) )
data = list(roster) data = []
for item in roster:
if _filter == "scale": data.append({"type": "roster", "item": item})
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 = (
@@ -393,24 +336,23 @@ class GetObjectsFiltered(View):
.filter(query_2nd) .filter(query_2nd)
.distinct() .distinct()
) )
data = list(chain(data, consists)) for item in 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()
) )
magazine_issues = ( for item in catalogs:
MagazineIssue.objects.get_published(request.user) data.append({"type": "catalog", "item": item})
.filter(query_2nd)
.distinct()
)
data = list(chain(data, books, catalogs, magazine_issues))
except NameError: except NameError:
pass pass
@@ -464,14 +406,16 @@ class GetRollingStock(View):
request.user request.user
) )
consists = list( consists = [
Consist.objects.get_published(request.user).filter( {"type": "consist", "item": c}
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
trainset = list( set = [
RollingStock.objects.get_published(request.user) {"type": "set", "item": s}
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)
@@ -479,7 +423,7 @@ class GetRollingStock(View):
) )
) )
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) ]
return render( return render(
request, request,
@@ -492,7 +436,7 @@ class GetRollingStock(View):
"decoder_documents": decoder_documents, "decoder_documents": decoder_documents,
"documents": documents, "documents": documents,
"journal": journal, "journal": journal,
"set": trainset, "set": set,
"consists": consists, "consists": consists,
}, },
) )
@@ -500,6 +444,7 @@ 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()
@@ -513,13 +458,16 @@ class GetConsist(View):
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
data = [
data = list( {
RollingStock.objects.get_published(request.user).get( "type": "roster",
uuid=r.rolling_stock_id "item": RollingStock.objects.get_published(request.user).get(
) uuid=r.rolling_stock_id
),
}
for r in consist.consist_item.all() 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(
@@ -540,6 +488,7 @@ 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 (
@@ -574,26 +523,7 @@ class Manufacturers(GetData):
) )
) )
) )
.annotate( .annotate(num_items=F("num_rollingstock") + F("num_rollingclass"))
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")
) )
@@ -608,6 +538,7 @@ 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 (
@@ -646,6 +577,7 @@ 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 (
@@ -666,21 +598,15 @@ 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(
@@ -697,6 +623,7 @@ 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()
@@ -704,6 +631,7 @@ 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()
@@ -711,11 +639,12 @@ 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)
.order_by(Lower("name")) .all()
.annotate( .annotate(
issues=Count( issues=Count(
"issue", "issue",
@@ -737,7 +666,13 @@ class GetMagazine(View):
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
data = list(magazine.issue.get_published(request.user).all()) data = [
{
"type": "magazineissue",
"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(
@@ -773,9 +708,10 @@ class GetMagazineIssue(View):
"bookshelf/book.html", "bookshelf/book.html",
{ {
"title": issue, "title": issue,
"data": issue, "book": issue,
"documents": documents, "documents": documents,
"properties": properties, "properties": properties,
"type": "magazineissue",
}, },
) )
@@ -802,9 +738,10 @@ class GetBookCatalog(View):
"bookshelf/book.html", "bookshelf/book.html",
{ {
"title": book, "title": book,
"data": book, "book": book,
"documents": documents, "documents": documents,
"properties": properties, "properties": properties,
"type": selector,
}, },
) )

View File

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

View File

@@ -9,19 +9,6 @@ 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)
@@ -33,14 +20,6 @@ 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,8 +204,6 @@ 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

@@ -44,9 +44,7 @@ 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="{}" title="{}" />', '<img src="{}" /> {}', obj.country.flag, obj.country.name
obj.company.country.flag,
obj.company.country.name,
) )
@@ -130,12 +128,9 @@ 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",
@@ -157,7 +152,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="{}" title="{}" />', obj.country.flag, obj.country.name '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
fieldsets = ( fieldsets = (
@@ -167,7 +162,6 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"fields": ( "fields": (
"preview", "preview",
"published", "published",
"featured",
"rolling_class", "rolling_class",
"road_number", "road_number",
"scale", "scale",
@@ -230,8 +224,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 = "-"
@@ -302,38 +296,4 @@ 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]
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 = RollingStock.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)
set_featured.short_description = "Mark selected rolling stock as featured"
def unset_featured(modeladmin, request, queryset):
queryset.update(featured=False)
unset_featured.short_description = (
"Unmark selected rolling stock as featured"
)
actions = [publish, unpublish, set_featured, unset_featured, download_csv]

View File

@@ -1,21 +0,0 @@
# 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,7 +5,6 @@ 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
@@ -83,7 +82,9 @@ 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, blank=True, editable=False max_length=32,
blank=True,
editable=False
) )
set = models.BooleanField( set = models.BooleanField(
default=False, default=False,
@@ -112,10 +113,6 @@ 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
) )
@@ -168,18 +165,10 @@ 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
if RollingStock.objects.filter(featured=True).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):
@@ -196,7 +185,10 @@ 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", "rollingstock", str(instance.rolling_stock.uuid), filename "images",
"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.

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -1,12 +0,0 @@
#!/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