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 (
BookDocument,
CatalogDocument,
MagazineIssueDocument,
MagazineIssueDocument
)
from bookshelf.models import (
BaseBookProperty,
@@ -66,12 +66,12 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
BookDocInline,
)
list_display = (
"published",
"title",
"get_authors",
"get_publisher",
"publication_year",
"number_of_pages",
"published",
)
autocomplete_fields = ("authors", "publisher", "shop")
readonly_fields = ("invoices", "creation_time", "updated_time")
@@ -135,8 +135,8 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
if obj.invoice.exists():
html = format_html_join(
"<br>",
'<a href="{}" target="_blank">{}</a>',
((i.file.url, i) for i in obj.invoice.all()),
"<a href=\"{}\" target=\"_blank\">{}</a>",
((i.file.url, i) for i in obj.invoice.all())
)
else:
html = "-"
@@ -212,11 +212,11 @@ class AuthorAdmin(admin.ModelAdmin):
@admin.register(Publisher)
class PublisherAdmin(admin.ModelAdmin):
list_display = ("name", "country_flag_name")
list_display = ("name", "country_flag")
search_fields = ("name",)
@admin.display(description="Country")
def country_flag_name(self, obj):
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name
)
@@ -240,10 +240,10 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
readonly_fields = ("invoices", "creation_time", "updated_time")
search_fields = ("manufacturer__name", "years", "scales__scale")
list_filter = (
"published",
"manufacturer__name",
"publication_year",
"scales__scale",
"published",
)
fieldsets = (
@@ -303,8 +303,8 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
if obj.invoice.exists():
html = format_html_join(
"<br>",
'<a href="{}" target="_blank">{}</a>',
((i.file.url, i) for i in obj.invoice.all()),
"<a href=\"{}\" target=\"_blank\">{}</a>",
((i.file.url, i) for i in obj.invoice.all())
)
else:
html = "-"
@@ -449,12 +449,14 @@ class MagazineIssueInline(admin.TabularInline):
readonly_fields = ("preview",)
class Media:
js = ("admin/js/magazine_issue_defaults.js",)
js = ('admin/js/magazine_issue_defaults.js',)
@admin.register(Magazine)
class MagazineAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (MagazineIssueInline,)
inlines = (
MagazineIssueInline,
)
list_display = (
"__str__",
@@ -464,10 +466,7 @@ class MagazineAdmin(SortableAdminBase, admin.ModelAdmin):
autocomplete_fields = ("publisher",)
readonly_fields = ("creation_time", "updated_time")
search_fields = ("name", "publisher__name")
list_filter = (
"published",
"publisher__name",
)
list_filter = ("publisher__name", "published")
fieldsets = (
(
@@ -476,7 +475,6 @@ class MagazineAdmin(SortableAdminBase, admin.ModelAdmin):
"fields": (
"published",
"name",
"website",
"publisher",
"ISBN",
"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 shutil
from urllib.parse import urlparse
from django.db import models
from django.conf import settings
from django.urls import reverse
from django.utils.dates import MONTHS
from django.db.models.functions import Lower
from django.core.exceptions import ValidationError
from django_countries.fields import CountryField
@@ -60,24 +58,36 @@ class BaseBook(BaseModel):
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):
shutil.rmtree(
os.path.join(
settings.MEDIA_ROOT, "images", "books", str(self.uuid)
),
ignore_errors=True,
ignore_errors=True
)
super(BaseBook, self).delete(*args, **kwargs)
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):
return os.path.join("images", "magazines", str(instance.uuid), filename)
return os.path.join(
"images",
"magazines",
str(instance.uuid),
filename
)
class BaseBookImage(Image):
@@ -121,7 +131,8 @@ class Book(BaseBook):
def get_absolute_url(self):
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,
on_delete=models.CASCADE,
related_name="catalogs",
)
years = models.CharField(max_length=12)
scales = models.ManyToManyField(Scale, related_name="catalogs")
scales = models.ManyToManyField(Scale)
class Meta:
ordering = ["manufacturer", "publication_year"]
@@ -147,19 +157,18 @@ class Catalog(BaseBook):
def get_absolute_url(self):
return reverse(
"bookshelf_item", kwargs={"selector": "catalog", "uuid": self.uuid}
"bookshelf_item",
kwargs={"selector": "catalog", "uuid": self.uuid}
)
def get_scales(self):
return "/".join([s.scale for s in self.scales.all()])
get_scales.short_description = "Scales"
class Magazine(BaseModel):
name = models.CharField(max_length=200)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
website = models.URLField(blank=True)
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
image = models.ImageField(
blank=True,
@@ -169,31 +178,32 @@ class Magazine(BaseModel):
language = models.CharField(
max_length=7,
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):
shutil.rmtree(
os.path.join(
settings.MEDIA_ROOT, "images", "magazines", str(self.uuid)
),
ignore_errors=True,
ignore_errors=True
)
super(Magazine, self).delete(*args, **kwargs)
class Meta:
ordering = [Lower("name")]
ordering = ["name"]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("magazine", kwargs={"uuid": self.uuid})
def website_short(self):
if self.website:
return urlparse(self.website).netloc.replace("www.", "")
return reverse(
"magazine",
kwargs={"uuid": self.uuid}
)
class MagazineIssue(BaseBook):
@@ -202,17 +212,14 @@ class MagazineIssue(BaseBook):
)
issue_number = models.CharField(max_length=100)
publication_month = models.SmallIntegerField(
null=True, blank=True, choices=MONTHS.items()
null=True,
blank=True,
choices=MONTHS.items()
)
class Meta:
unique_together = ("magazine", "issue_number")
ordering = [
"magazine",
"publication_year",
"publication_month",
"issue_number",
]
ordering = ["magazine", "issue_number"]
def __str__(self):
return f"{self.magazine.name} - {self.issue_number}"
@@ -224,10 +231,6 @@ class MagazineIssue(BaseBook):
"published."
)
@property
def obj_label(self):
return "Magazine Issue"
def preview(self):
return self.image.first().image_thumbnail(100)
@@ -237,5 +240,9 @@ class MagazineIssue(BaseBook):
def get_absolute_url(self):
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",
)
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):
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.contrib import admin
# from django.forms import BaseInlineFormSet # for future reference
from django.utils.html import format_html, strip_tags
from adminsortable2.admin import (
@@ -47,22 +46,15 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"creation_time",
"updated_time",
)
list_filter = ("published", "company__name", "era", "scale")
list_display = (
"__str__",
"company__name",
"era",
"scale",
"country_flag",
"published",
)
list_filter = ("company__name", "era", "scale", "published")
list_display = ("__str__",) + list_filter + ("country_flag",)
search_fields = ("identifier",) + list_filter
save_as = True
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" title="{}" />', obj.country.flag, obj.country.name
'<img src="{}" /> {}', obj.country.flag, obj.country
)
fieldsets = (
@@ -146,7 +138,6 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
)
return generate_csv(header, data, "consists.csv")
download_csv.short_description = "Download selected items as CSV"
actions = [publish, unpublish, download_csv]

View File

@@ -47,12 +47,12 @@ class ScaleAdmin(admin.ModelAdmin):
@admin.register(Company)
class CompanyAdmin(admin.ModelAdmin):
readonly_fields = ("logo_thumbnail",)
list_display = ("name", "country_flag_name")
list_display = ("name", "country_flag")
list_filter = ("name", "country")
search_fields = ("name",)
@admin.display(description="Country")
def country_flag_name(self, obj):
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name
)
@@ -61,12 +61,12 @@ class CompanyAdmin(admin.ModelAdmin):
@admin.register(Manufacturer)
class ManufacturerAdmin(admin.ModelAdmin):
readonly_fields = ("logo_thumbnail",)
list_display = ("name", "category", "country_flag_name")
list_display = ("name", "category", "country_flag")
list_filter = ("category",)
search_fields = ("name",)
@admin.display(description="Country")
def country_flag_name(self, obj):
def country_flag(self, obj):
return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name
)
@@ -88,12 +88,6 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
@admin.register(Shop)
class ShopAdmin(admin.ModelAdmin):
list_display = ("name", "on_line", "active", "country_flag_name")
list_display = ("name", "on_line", "active")
list_filter = ("on_line", "active")
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
from urllib.parse import urlparse
from django.db import models
from django.urls import reverse
from django.conf import settings
@@ -7,12 +6,11 @@ from django.dispatch.dispatcher import receiver
from django.core.exceptions import ValidationError
from django_countries.fields import CountryField
from ram.models import SimpleBaseModel
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
from ram.managers import PublicManager
class Property(SimpleBaseModel):
class Property(models.Model):
name = models.CharField(max_length=128, unique=True)
private = models.BooleanField(
default=False,
@@ -29,7 +27,7 @@ class Property(SimpleBaseModel):
objects = PublicManager()
class Manufacturer(SimpleBaseModel):
class Manufacturer(models.Model):
name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True, editable=False)
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):
return get_image_preview(self.logo.url)
logo_thumbnail.short_description = "Preview"
class Company(SimpleBaseModel):
class Company(models.Model):
name = models.CharField(max_length=64, unique=True)
slug = models.CharField(max_length=64, unique=True, editable=False)
extended_name = models.CharField(max_length=128, blank=True)
@@ -107,7 +101,7 @@ class Company(SimpleBaseModel):
logo_thumbnail.short_description = "Preview"
class Decoder(SimpleBaseModel):
class Decoder(models.Model):
name = models.CharField(max_length=128, unique=True)
manufacturer = models.ForeignKey(
Manufacturer,
@@ -143,7 +137,7 @@ def calculate_ratio(ratio):
raise ValidationError("Invalid ratio format")
class Scale(SimpleBaseModel):
class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False)
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)
class RollingStockType(SimpleBaseModel):
class RollingStockType(models.Model):
type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField()
category = models.CharField(
@@ -208,7 +202,7 @@ class RollingStockType(SimpleBaseModel):
return "{0} {1}".format(self.type, self.category)
class Tag(SimpleBaseModel):
class Tag(models.Model):
name = 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)
country = CountryField(blank=True)
website = models.URLField(blank=True)

View File

@@ -2,23 +2,23 @@
{% load dynamic_url %}
{% block header %}
{% if data.tags.all %}
{% if book.tags.all %}
<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 #}
{% endfor %}
</p>
{% endif %}
{% if not data.published %}
{% if not book.published %}
<span class="badge text-bg-warning">Unpublished</span> |
{% 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 %}
{% block carousel %}
<div class="row">
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000">
<div class="carousel-inner">
{% for t in data.image.all %}
{% for t in book.image.all %}
{% if forloop.first %}
<div class="carousel-item active">
{% else %}
@@ -28,7 +28,7 @@
</div>
{% endfor %}
</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">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden"><i class="bi bi-chevron-left"></i></span>
@@ -61,86 +61,82 @@
<thead>
<tr>
<th colspan="2" scope="row">
{{ data.obj_label|capfirst }}
{% if type == "catalog" %}Catalog
{% elif type == "book" %}Book{% endif %}
</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if data.obj_type == "catalog" %}
{% if type == "catalog" %}
<tr>
<th class="w-33" scope="row">Manufacturer</th>
<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>
<td>{{ book.manufacturer }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Scales</th>
<td>{{ data.get_scales }}</td>
<td>{{ book.get_scales }}</td>
</tr>
{% elif data.obj_type == "book" %}
{% elif type == "book" %}
<tr>
<th class="w-33" scope="row">Title</th>
<td>{{ data.title }}</td>
<td>{{ book.title }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Authors</th>
<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>
</tr>
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>
<img src="{{ data.publisher.country.flag }}" alt="{{ data.publisher.country }}"> {{ data.publisher }}
{% if data.publisher.website %} <a href="{{ data.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
<img src="{{ book.publisher.country.flag }}" alt="{{ book.publisher.country }}"> {{ book.publisher }}
{% if book.publisher.website %} <a href="{{ book.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td>
</tr>
{% elif data.obj_type == "magazineissue" %}
{% elif type == "magazineissue" %}
<tr>
<th class="w-33" scope="row">Magazine</th>
<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>
<td><a href="{% url 'magazine' book.magazine.pk %}">{{ book.magazine }}</a></td>
</tr>
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>
<img src="{{ data.publisher.country.flag }}" alt="{{ data.publisher.country }}"> {{ data.publisher }}
{% if data.publisher.website %} <a href="{{ data.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
<img src="{{ book.publisher.country.flag }}" alt="{{ book.publisher.country }}"> {{ book.publisher }}
{% if book.publisher.website %} <a href="{{ book.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td>
</tr>
<tr>
<th class="w-33" scope="row">Issue</th>
<td>{{ data.issue_number }}</td>
<td>{{ book.issue_number }}</td>
</tr>
<tr>
<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>
{% endif %}
<tr>
<th scope="row">ISBN</th>
<td>{{ data.ISBN|default:"-" }}</td>
<td>{{ book.ISBN|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Language</th>
<td>{{ data.get_language_display }}</td>
<td>{{ book.get_language_display }}</td>
</tr>
<tr>
<th scope="row">Number of pages</th>
<td>{{ data.number_of_pages|default:"-" }}</td>
<td>{{ book.number_of_pages|default:"-" }}</td>
</tr>
{% if data.obj_type == "book" or data.obj_type == "catalog" %}
{% if type == "boook" or type == "catalog" %}
<tr>
<th scope="row">Publication year</th>
<td>{{ data.publication_year|default:"-" }}</td>
<td>{{ book.publication_year|default:"-" }}</td>
</tr>
{% endif %}
{% if data.description %}
{% if book.description %}
<tr>
<th class="w-33" scope="row">Description</th>
<td>{{ data.description | safe }}</td>
<td>{{ book.description | safe }}</td>
</tr>
{% endif %}
</tbody>
@@ -156,17 +152,17 @@
<tr>
<th class="w-33" scope="row">Shop</th>
<td>
{{ data.shop|default:"-" }}
{% if data.shop.website %} <a href="{{ data.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{{ book.shop|default:"-" }}
{% if book.shop.website %} <a href="{{ book.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td>
</tr>
<tr>
<th class="w-33" scope="row">Purchase date</th>
<td>{{ data.purchase_date|default:"-" }}</td>
<td>{{ book.purchase_date|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Price ({{ site_conf.currency }})</th>
<td>{{ data.price|default:"-" }}</td>
<td>{{ book.price|default:"-" }}</td>
</tr>
</tbody>
</table>
@@ -209,7 +205,7 @@
</div>
</div>
<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>

View File

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

View File

@@ -2,31 +2,31 @@
{% load dynamic_url %}
<div class="col">
<div class="card shadow-sm">
{% if d.image.exists %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a>
{% if d.item.image.exists %}
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a>
{% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
<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 %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<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 #}
{% empty %}
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">
{{ d.obj_label|capfirst }}
{{ d.type | capfirst }}
<div class="float-end">
{% if not d.published %}
{% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span>
{% endif %}
</div>
@@ -34,46 +34,44 @@
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.obj_type == "catalog" %}
{% if d.type == "catalog" %}
<tr>
<th class="w-33" scope="row">Manufacturer</th>
<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>
<td>{{ d.item.manufacturer }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Scales</th>
<td>{{ d.get_scales }}</td>
<td>{{ d.item.get_scales }}</td>
</tr>
{% elif d.obj_type == "book" %}
{% elif d.type == "book" %}
<tr>
<th class="w-33" scope="row">Authors</th>
<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>
</tr>
<tr>
<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>
{% endif %}
<tr>
<th scope="row">Language</th>
<td>{{ d.get_language_display }}</td>
<td>{{ d.item.get_language_display }}</td>
</tr>
<tr>
<th scope="row">Pages</th>
<td>{{ d.number_of_pages|default:"-" }}</td>
<td>{{ d.item.number_of_pages|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Year</th>
<td>{{ d.publication_year|default:"-" }}</td>
<td>{{ d.item.publication_year|default:"-" }}</td>
</tr>
</tbody>
</table>
<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>
{% 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 %}
<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.type d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>

View File

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

View File

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

View File

@@ -1,51 +1,49 @@
{% load static %}
{% load dynamic_url %}
<div class="col">
<div class="card shadow-sm">
{% if d.obj_type == "magazine" %}
<a href="{{ d.get_absolute_url }}">
{% if d.image and d.obj_type == "magazine" %}
<img class="card-img-top" src="{{ d.image.url }}" alt="{{ d }}">
{% elif d.issue.first.image.exists %}
{% with d.issue.first as i %}
<img class="card-img-top" src="{{ i.image.first.image.url }}" alt="{{ d }}">
{% if d.type == "magazine" %}
<a href="{{ d.item.get_absolute_url }}">
{% if d.item.image and d.type == "magazine" %}
<img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}">
{% elif d.item.issue.first.image.exists %}
{% with d.item.issue.first as i %}
<img class="card-img-top" src="{{ i.image.first.image.url }}" alt="{{ d.item }}">
{% endwith %}
{% else %}
<!-- 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 %}
</a>
{% elif d.obj_type == "magazineissue" %}
<a href="{{ d.get_absolute_url }}">
{% if d.image.exists %}
<img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}">
{% elif d.type == "magazineissue" %}
<a href="{{ d.item.get_absolute_url }}">
{% if d.item.image.exists %}
<img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}">
{% else %}
<!-- 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 %}
</a>
{% endif %}
</a>
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<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 #}
{% empty %}
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">
{{ d.obj_label|capfirst }}
{{ d.type | capfirst }}
<div class="float-end">
{% if not d.published %}
{% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span>
{% endif %}
</div>
@@ -53,51 +51,46 @@
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.obj_type == "magazineissue" %}
{% if d.type == "magazineissue" %}
<tr>
<th class="w-33" scope="row">Magazine</th>
<td>{{ d.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>
<td>{{ d.item.magazine }}</td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>
<img src="{{ d.publisher.country.flag }}" alt="{{ d.publisher.country }}"> {{ d.publisher }}
{% if d.publisher.website %} <a href="{{ d.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
<img src="{{ d.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }}
{% if d.item.publisher.website %} <a href="{{ d.item.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td>
</tr>
{% if d.obj_type == "magazineissue" %}
{% if d.type == "magazineissue" %}
<tr>
<th class="w-33" scope="row">Issue</th>
<td>{{ d.issue_number }}</td>
<td>{{ d.item.issue_number }}</td>
</tr>
<tr>
<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>
<th class="w-33" scope="row">Pages</th>
<td>{{ d.number_of_pages|default:"-" }}</td>
<td>{{ d.item.number_of_pages|default:"-" }}</td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Language</th>
<td>{{ d.get_language_display }}</td>
<td>{{ d.item.get_language_display }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
{% if d.obj_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>
{% if d.type == "magazine" %}
<a class="btn btn-sm btn-outline-primary{% if d.item.issues == 0 %} disabled{% endif %}" href="{{ d.item.get_absolute_url }}">Show {{ d.item.issues }} issue{{ d.item.issues|pluralize }}</a>
{% 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 %}
{% 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>

View File

@@ -2,7 +2,7 @@
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.name }}</strong>
<strong>{{ d.item.name }}</strong>
</p>
<table class="table table-striped">
<thead>
@@ -11,26 +11,28 @@
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.logo %}
{% if d.item.logo %}
<tr>
<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>
{% 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>
<th class="w-33" scope="row">Category</th>
<td>{{ d.category | title }}</td>
<td>{{ d.item.category | title }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
{% with items=d.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="manufacturer" search=d.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 %}
{% 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.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.item.pk %}">Edit</a>{% endif %}
{% endwith %}
</div>
</div>

View File

@@ -1,7 +1,7 @@
<div class="col">
<div class="card shadow-sm">
<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">
<thead>
<tr>
@@ -11,18 +11,18 @@
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.type }}</td>
<td>{{ d.item.type }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Category</th>
<td>{{ d.category | title}}</td>
<td>{{ d.item.category | title}}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
{% with items=d.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="type" search=d.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 %}
{% 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.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.item.pk %}">Edit</a>{% endif %}
{% endwith %}
</div>
</div>

View File

@@ -3,34 +3,34 @@
<div class="col">
<div class="card shadow-sm">
{% if d.image.exists %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a>
{% if d.item.image.exists %}
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a>
{% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
<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 %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<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 #}
{% empty %}
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">
Rolling stock
<div class="float-end">
{% if not d.published %}
{% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span>
{% endif %}
{% if d.company.freelance %}
{% if d.item.company.freelance %}
<span class="badge text-bg-secondary">Freelance</span>
{% endif %}
</div>
@@ -40,50 +40,50 @@
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.rolling_class.type }}</td>
<td>{{ d.item.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td>
<img src="{{ d.company.country.flag }}" alt="{{ d.company.country }}">
<a href="{% url 'filtered' _filter="company" search=d.company.slug %}"><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></a>
<img src="{{ d.item.company.country.flag }}" alt="{{ d.item.company.country }}">
<a href="{% url 'filtered' _filter="company" search=d.item.company.slug %}"><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></a>
</td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ d.rolling_class.identifier }}</td>
<td>{{ d.item.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ d.road_number }}</td>
<td>{{ d.item.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.era }}</td>
<td>{{ d.item.era }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Manufacturer</th>
<td>{%if d.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 %}
<td>{%if d.item.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.item.manufacturer.slug %}">{{ d.item.manufacturer }}{% if d.item.manufacturer.website %}</a> <a href="{{ d.item.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
<tr>
<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>
<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>
<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>
</tbody>
</table>
<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>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.pk %}">Edit</a>{% endif %}
<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.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<div class="col">
<div class="card shadow-sm">
<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">
<thead>
<tr>
@@ -11,26 +11,26 @@
<tbody>
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.scale }}</td>
<td>{{ d.item.scale }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Ratio</th>
<td>{{ d.ratio }}</td>
<td>{{ d.item.ratio }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Tracks</th>
<td>{{ d.tracks }} mm</td>
<td>{{ d.item.tracks }} mm</td>
</tr>
<tr>
<th class="w-33" scope="row">Gauge</th>
<td>{{ d.gauge }}</td>
<td>{{ d.item.gauge }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
{% with items=d.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.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 %}
{% 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.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.item.pk %}">Edit</a>{% endif %}
{% endwith %}
</div>
</div>

View File

@@ -32,7 +32,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<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>
{% else %}
<li class="page-item disabled">
@@ -48,13 +48,13 @@
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% 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 %}
{% endfor %}
{% if data.has_next %}
<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>
{% else %}
<li class="page-item disabled">

View File

@@ -5,7 +5,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<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>
{% else %}
<li class="page-item disabled">
@@ -21,13 +21,13 @@
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% 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 %}
{% endfor %}
{% if data.has_next %}
<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>
{% else %}
<li class="page-item disabled">

View File

@@ -3,18 +3,3 @@
{% block header %}
<div class="text-body-secondary">{{ site_conf.about | safe }}</div>
{% 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">
{% if data.has_previous %}
<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>
{% else %}
<li class="page-item disabled">
@@ -48,13 +48,13 @@
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% 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 %}
{% endfor %}
{% if data.has_next %}
<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>
{% else %}
<li class="page-item disabled">
@@ -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 %}
</td>
</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>
<th scope="row">ISBN</th>
<td>{{ magazine.ISBN | default:"-" }}</td>

View File

@@ -5,7 +5,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<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>
{% else %}
<li class="page-item disabled">
@@ -21,13 +21,13 @@
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% 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 %}
{% endfor %}
{% if data.has_next %}
<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>
{% else %}
<li class="page-item disabled">

View File

@@ -7,7 +7,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<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>
{% else %}
<li class="page-item disabled">
@@ -23,13 +23,13 @@
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% 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 %}
{% endfor %}
{% if data.has_next %}
<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>
{% else %}
<li class="page-item disabled">

View File

@@ -1,12 +1,12 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
{% with data.0.category as c %}
{% with data.0.item.category as c %}
<nav aria-label="Page navigation">
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<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>
{% else %}
<li class="page-item disabled">
@@ -22,13 +22,13 @@
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% 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 %}
{% endfor %}
{% if data.has_next %}
<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>
{% else %}
<li class="page-item disabled">

View File

@@ -6,7 +6,7 @@
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<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>
{% else %}
<li class="page-item disabled">
@@ -22,13 +22,13 @@
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% 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 %}
{% endfor %}
{% if data.has_next %}
<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>
{% else %}
<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]
)
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 portal.views import (
GetHome,
GetData,
GetRoster,
GetObjectsFiltered,
GetManufacturerItem,
@@ -23,75 +23,123 @@ from portal.views import (
)
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/page/<int:page>", GetRoster.as_view(), name="roster"),
path(
"roster/page/<int:page>",
GetRoster.as_view(),
name="rosters_pagination"
),
path(
"page/<str:flatpage>",
GetFlatpage.as_view(),
name="flatpage",
),
path("consists", Consists.as_view(), name="consists"),
path("consists/page/<int:page>", Consists.as_view(), name="consists"),
path(
"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>/page/<int:page>",
GetConsist.as_view(),
name="consist",
name="consist_pagination",
),
path(
"companies",
Companies.as_view(),
name="companies"
),
path("companies", Companies.as_view(), name="companies"),
path(
"companies/page/<int:page>",
Companies.as_view(),
name="companies",
name="companies_pagination",
),
path(
"manufacturers/<str:category>",
Manufacturers.as_view(template="pagination_manufacturers.html"),
name="manufacturers",
name="manufacturers"
),
path(
"manufacturers/<str:category>/page/<int:page>",
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(
"bookshelf/magazine/<uuid:uuid>",
GetMagazine.as_view(),
name="magazine",
name="magazine"
),
path(
"bookshelf/magazine/<uuid:uuid>/page/<int:page>",
GetMagazine.as_view(),
name="magazine",
name="magazine_pagination",
),
path(
"bookshelf/magazine/<uuid:magazine>/issue/<uuid:uuid>",
GetMagazineIssue.as_view(),
name="issue",
),
path("bookshelf/magazines", Magazines.as_view(), name="magazines"),
path(
"bookshelf/magazines",
Magazines.as_view(),
name="magazines"
),
path(
"bookshelf/magazines/page/<int:page>",
Magazines.as_view(),
name="magazines",
name="magazines_pagination"
),
path(
"bookshelf/<str:selector>/<uuid:uuid>",
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(
"bookshelf/catalogs/page/<int:page>",
Catalogs.as_view(),
name="catalogs",
name="catalogs_pagination"
),
path(
"search",
@@ -101,7 +149,7 @@ urlpatterns = [
path(
"search/<str:search>/page/<int:page>",
SearchObjects.as_view(),
name="search",
name="search_pagination",
),
path(
"manufacturer/<str:manufacturer>",
@@ -111,7 +159,7 @@ urlpatterns = [
path(
"manufacturer/<str:manufacturer>/page/<int:page>",
GetManufacturerItem.as_view(),
name="manufacturer",
name="manufacturer_pagination",
),
path(
"manufacturer/<str:manufacturer>/<str:search>",
@@ -121,7 +169,7 @@ urlpatterns = [
path(
"manufacturer/<str:manufacturer>/<str:search>/page/<int:page>",
GetManufacturerItem.as_view(),
name="manufacturer",
name="manufacturer_pagination",
),
path(
"<str:_filter>/<str:search>",
@@ -131,7 +179,7 @@ urlpatterns = [
path(
"<str:_filter>/<str:search>/page/<int:page>",
GetObjectsFiltered.as_view(),
name="filtered",
name="filtered_pagination",
),
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),
]

View File

@@ -4,12 +4,10 @@ from itertools import chain
from functools import reduce
from urllib.parse import unquote
from django.conf import settings
from django.views import View
from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError
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.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
@@ -33,7 +31,7 @@ def get_items_per_page():
items_per_page = get_site_conf().items_per_page
except (OperationalError, ProgrammingError):
items_per_page = 6
return int(items_per_page)
return items_per_page
def get_order_by_field():
@@ -63,8 +61,9 @@ class Render404(View):
class GetData(View):
title = None
title = "Home"
template = "pagination.html"
item_type = "roster"
filter = Q() # empty filter by default
def get_data(self, request):
@@ -74,11 +73,11 @@ class GetData(View):
.filter(self.filter)
)
def get(self, request, page=1):
if self.title is None or self.template is None:
raise Exception("title and template must be defined")
data = list(self.get_data(request))
def get(self, request, filter=Q(), page=1):
self.filter = filter
data = []
for item in self.get_data(request):
data.append({"type": self.item_type, "item": item})
paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page)
@@ -91,6 +90,7 @@ class GetData(View):
self.template,
{
"title": self.title,
"type": self.item_type,
"data": data,
"matches": paginator.count,
"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):
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):
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:
query = reduce(
operator.or_,
@@ -170,13 +152,15 @@ class SearchObjects(View):
# FIXME duplicated code!
# FIXME see if it makes sense to filter calatogs and books by scale
# and manufacturer as well
data = []
roster = (
RollingStock.objects.get_published(request.user)
.filter(query)
.distinct()
.order_by(*get_order_by_field())
)
data = list(roster)
for item in roster:
data.append({"type": "roster", "item": item})
if _filter is None:
consists = (
@@ -189,39 +173,20 @@ class SearchObjects(View):
)
.distinct()
)
data = list(chain(data, consists))
for item in consists:
data.append({"type": "consist", "item": item})
books = (
Book.objects.get_published(request.user)
.filter(
Q(
Q(title__icontains=search)
| Q(description__icontains=search)
)
)
.filter(title__icontains=search)
.distinct()
)
catalogs = (
Catalog.objects.get_published(request.user)
.filter(
Q(
Q(manufacturer__name__icontains=search)
| Q(description__icontains=search)
)
)
.filter(manufacturer__name__icontains=search)
.distinct()
)
data = list(chain(data, books, catalogs))
magazine_issues = (
MagazineIssue.objects.get_published(request.user)
.filter(
Q(
Q(magazine__name__icontains=search)
| Q(description__icontains=search)
)
)
.distinct()
)
data = list(chain(data, magazine_issues))
for item in list(chain(books, catalogs)):
data.append({"type": "book", "item": item})
paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page)
@@ -277,25 +242,10 @@ class SearchObjects(View):
class GetManufacturerItem(View):
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, slug__iexact=manufacturer
)
if search != "all":
roster = get_list_or_404(
RollingStock.objects.get_published(request.user).order_by(
@@ -306,7 +256,6 @@ class GetManufacturerItem(View):
& Q(item_number_slug__exact=search)
),
)
catalogs = [] # no catalogs when searching for a specific item
title = "{0}: {1}".format(
manufacturer,
# all returned records must have the same `item_number``;
@@ -323,12 +272,12 @@ class GetManufacturerItem(View):
.distinct()
.order_by(*get_order_by_field())
)
catalogs = Catalog.objects.get_published(request.user).filter(
manufacturer=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())
data = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
@@ -377,15 +326,9 @@ class GetObjectsFiltered(View):
.order_by(*get_order_by_field())
)
data = list(roster)
if _filter == "scale":
catalogs = (
Catalog.objects.get_published(request.user)
.filter(scales__slug=search)
.distinct()
)
data = list(chain(data, catalogs))
data = []
for item in roster:
data.append({"type": "roster", "item": item})
try: # Execute only if query_2nd is defined
consists = (
@@ -393,24 +336,23 @@ class GetObjectsFiltered(View):
.filter(query_2nd)
.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
books = (
Book.objects.get_published(request.user)
.filter(query_2nd)
.distinct()
)
for item in books:
data.append({"type": "book", "item": item})
catalogs = (
Catalog.objects.get_published(request.user)
.filter(query_2nd)
.distinct()
)
magazine_issues = (
MagazineIssue.objects.get_published(request.user)
.filter(query_2nd)
.distinct()
)
data = list(chain(data, books, catalogs, magazine_issues))
for item in catalogs:
data.append({"type": "catalog", "item": item})
except NameError:
pass
@@ -464,14 +406,16 @@ class GetRollingStock(View):
request.user
)
consists = list(
Consist.objects.get_published(request.user).filter(
consists = [
{"type": "consist", "item": c}
for c in Consist.objects.get_published(request.user).filter(
consist_item__rolling_stock=rolling_stock
)
)
] # A dict with "item" is required by the consists card
trainset = list(
RollingStock.objects.get_published(request.user)
set = [
{"type": "set", "item": s}
for s in RollingStock.objects.get_published(request.user)
.filter(
Q(
Q(item_number__exact=rolling_stock.item_number)
@@ -479,7 +423,7 @@ class GetRollingStock(View):
)
)
.order_by(*get_order_by_field())
)
]
return render(
request,
@@ -492,7 +436,7 @@ class GetRollingStock(View):
"decoder_documents": decoder_documents,
"documents": documents,
"journal": journal,
"set": trainset,
"set": set,
"consists": consists,
},
)
@@ -500,6 +444,7 @@ class GetRollingStock(View):
class Consists(GetData):
title = "Consists"
item_type = "consist"
def get_data(self, request):
return Consist.objects.get_published(request.user).all()
@@ -513,13 +458,16 @@ class GetConsist(View):
)
except ObjectDoesNotExist:
raise Http404
data = list(
RollingStock.objects.get_published(request.user).get(
data = [
{
"type": "roster",
"item": RollingStock.objects.get_published(request.user).get(
uuid=r.rolling_stock_id
)
),
}
for r in consist.consist_item.all()
)
]
paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
@@ -540,6 +488,7 @@ class GetConsist(View):
class Manufacturers(GetData):
title = "Manufacturers"
item_type = "manufacturer"
def get_data(self, request):
return (
@@ -574,26 +523,7 @@ class Manufacturers(GetData):
)
)
)
.annotate(
num_catalogs=(
Count(
"catalogs",
filter=Q(
catalogs__in=(
Catalog.objects.get_published(request.user)
),
),
distinct=True,
)
)
)
.annotate(
num_items=(
F("num_rollingstock")
+ F("num_rollingclass")
+ F("num_catalogs")
)
)
.annotate(num_items=F("num_rollingstock") + F("num_rollingclass"))
.order_by("name")
)
@@ -608,6 +538,7 @@ class Manufacturers(GetData):
class Companies(GetData):
title = "Companies"
item_type = "company"
def get_data(self, request):
return (
@@ -646,6 +577,7 @@ class Companies(GetData):
class Scales(GetData):
title = "Scales"
item_type = "scale"
def get_data(self, request):
return (
@@ -666,21 +598,15 @@ class Scales(GetData):
),
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")
)
class Types(GetData):
title = "Types"
item_type = "rolling_stock_type"
def get_data(self, request):
return RollingStockType.objects.annotate(
@@ -697,6 +623,7 @@ class Types(GetData):
class Books(GetData):
title = "Books"
item_type = "book"
def get_data(self, request):
return Book.objects.get_published(request.user).all()
@@ -704,6 +631,7 @@ class Books(GetData):
class Catalogs(GetData):
title = "Catalogs"
item_type = "catalog"
def get_data(self, request):
return Catalog.objects.get_published(request.user).all()
@@ -711,11 +639,12 @@ class Catalogs(GetData):
class Magazines(GetData):
title = "Magazines"
item_type = "magazine"
def get_data(self, request):
return (
Magazine.objects.get_published(request.user)
.order_by(Lower("name"))
.all()
.annotate(
issues=Count(
"issue",
@@ -737,7 +666,13 @@ class GetMagazine(View):
)
except ObjectDoesNotExist:
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())
data = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
@@ -773,9 +708,10 @@ class GetMagazineIssue(View):
"bookshelf/book.html",
{
"title": issue,
"data": issue,
"book": issue,
"documents": documents,
"properties": properties,
"type": "magazineissue",
},
)
@@ -802,9 +738,10 @@ class GetBookCatalog(View):
"bookshelf/book.html",
{
"title": book,
"data": book,
"book": book,
"documents": documents,
"properties": properties,
"type": selector,
},
)

View File

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

View File

@@ -9,19 +9,6 @@ from ram.utils import DeduplicatedStorage, get_image_preview
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):
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
description = tinymce.HTMLField(blank=True)
@@ -33,14 +20,6 @@ class BaseModel(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
objects = PublicManager()

View File

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

View File

@@ -44,9 +44,7 @@ class RollingClass(admin.ModelAdmin):
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" title="{}" />',
obj.company.country.flag,
obj.company.country.name,
'<img src="{}" /> {}', obj.country.flag, obj.country.name
)
@@ -130,12 +128,9 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"item_number",
"company",
"country_flag",
"featured",
"published",
)
list_filter = (
"featured",
"published",
"rolling_class__type__category",
"rolling_class__type",
"rolling_class__company__name",
@@ -157,7 +152,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Country")
def country_flag(self, obj):
return format_html(
'<img src="{}" title="{}" />', obj.country.flag, obj.country.name
'<img src="{}" /> {}', obj.country.flag, obj.country.name
)
fieldsets = (
@@ -167,7 +162,6 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"fields": (
"preview",
"published",
"featured",
"rolling_class",
"road_number",
"scale",
@@ -230,8 +224,8 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
if obj.invoice.exists():
html = format_html_join(
"<br>",
'<a href="{}" target="_blank">{}</a>',
((i.file.url, i) for i in obj.invoice.all()),
"<a href=\"{}\" target=\"_blank\">{}</a>",
((i.file.url, i) for i in obj.invoice.all())
)
else:
html = "-"
@@ -302,38 +296,4 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
return generate_csv(header, data, "rolling_stock.csv")
download_csv.short_description = "Download selected items as 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]
actions = [publish, unpublish, 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.conf import settings
from django.dispatch import receiver
from django.core.exceptions import ValidationError
from tinymce import models as tinymce
@@ -83,7 +82,9 @@ class RollingStock(BaseModel):
help_text="Catalog item number or code",
)
item_number_slug = models.CharField(
max_length=32, blank=True, editable=False
max_length=32,
blank=True,
editable=False
)
set = models.BooleanField(
default=False,
@@ -112,10 +113,6 @@ class RollingStock(BaseModel):
null=True,
blank=True,
)
featured = models.BooleanField(
default=False,
help_text="Featured rolling stock will appear on the homepage",
)
tags = models.ManyToManyField(
Tag, related_name="rolling_stock", blank=True
)
@@ -168,18 +165,10 @@ class RollingStock(BaseModel):
os.path.join(
settings.MEDIA_ROOT, "images", "rollingstock", str(self.uuid)
),
ignore_errors=True,
ignore_errors=True
)
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)
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):
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