Compare commits

12 Commits

Author SHA1 Message Date
b4f69d8a34 Fix a regression in a template 2025-12-25 11:13:14 +01:00
676418cb67 Code refactoring to simplify template data contexts (#55)
* Fix a search filter when no catalogs are returned
* Code refactoring to simplify templates
* Remove duplicated code
* Remove dead code
* More improvements, clean up and add featured items in homepage
* Fix a type and better page navigation
2025-12-24 15:38:07 +01:00
98d2e7beab Extend search to catalogs and scales 2025-12-23 12:19:26 +01:00
fb17dc2a7c Add some utils to generate cards via imagemagick 2025-12-21 23:01:04 +01:00
5a71dc36fa Improve sorting and extend search to magazines 2025-12-21 22:56:45 +01:00
c539255bf9 More UI improvements and fix a regression on manufacturer filtering 2025-12-12 23:55:09 +01:00
fc527d5cd1 Minor fixes to labels and dates 2025-12-12 00:08:43 +01:00
f45d754c91 More fixes to lables 2025-12-10 23:38:04 +01:00
e9c9ede357 Fix a bug in magazine edit 2025-12-10 23:03:48 +01:00
39b0a9378b Magazine UI (#54)
* Work in progress to implement magazines and issues UI

* Fully implement UI for magazines
2025-12-10 22:58:39 +01:00
6b10051bc4 Add support for magazines, backend only (#53)
* Initial work to support magazines

* Change editor default height to 300px from 500px

* Stabilize the magazine repository app

* Switch from stacked to tabular inlines for magazines

* Update submodules
2025-12-08 23:18:57 +01:00
3804c3379b Add 'repository' to the login menu 2025-12-08 14:06:17 +01:00
53 changed files with 1779 additions and 359 deletions

View File

@@ -8,7 +8,11 @@ from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from ram.admin import publish, unpublish from ram.admin import publish, unpublish
from ram.utils import generate_csv from ram.utils import generate_csv
from portal.utils import get_site_conf from portal.utils import get_site_conf
from repository.models import BookDocument, CatalogDocument from repository.models import (
BookDocument,
CatalogDocument,
MagazineIssueDocument,
)
from bookshelf.models import ( from bookshelf.models import (
BaseBookProperty, BaseBookProperty,
BaseBookImage, BaseBookImage,
@@ -16,6 +20,8 @@ from bookshelf.models import (
Author, Author,
Publisher, Publisher,
Catalog, Catalog,
Magazine,
MagazineIssue,
) )
@@ -48,6 +54,10 @@ class CatalogDocInline(BookDocInline):
model = CatalogDocument model = CatalogDocument
class MagazineIssueDocInline(BookDocInline):
model = MagazineIssueDocument
@admin.register(Book) @admin.register(Book)
class BookAdmin(SortableAdminBase, admin.ModelAdmin): class BookAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = ( inlines = (
@@ -56,17 +66,17 @@ 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")
search_fields = ("title", "publisher__name", "authors__last_name") search_fields = ("title", "publisher__name", "authors__last_name")
list_filter = ("publisher__name", "authors") list_filter = ("publisher__name", "authors", "published")
fieldsets = ( fieldsets = (
( (
@@ -125,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 = "-"
@@ -202,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") list_display = ("name", "country_flag_name")
search_fields = ("name",) search_fields = ("name",)
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag_name(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
@@ -229,7 +239,12 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
autocomplete_fields = ("manufacturer",) autocomplete_fields = ("manufacturer",)
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 = ("manufacturer__name", "publication_year", "scales__scale") list_filter = (
"published",
"manufacturer__name",
"publication_year",
"scales__scale",
)
fieldsets = ( fieldsets = (
( (
@@ -288,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 = "-"
@@ -346,3 +361,144 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
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]
@admin.register(MagazineIssue)
class MagazineIssueAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (
BookPropertyInline,
BookImageInline,
MagazineIssueDocInline,
)
list_display = (
"__str__",
"issue_number",
"published",
)
autocomplete_fields = ("shop",)
readonly_fields = ("magazine", "creation_time", "updated_time")
def get_model_perms(self, request):
"""
Return empty perms dict thus hiding the model from admin index.
"""
return {}
fieldsets = (
(
None,
{
"fields": (
"published",
"magazine",
"issue_number",
"publication_year",
"publication_month",
"ISBN",
"language",
"number_of_pages",
"description",
"tags",
)
},
),
(
"Purchase data",
{
"classes": ("collapse",),
"fields": (
"shop",
"purchase_date",
"price",
),
},
),
(
"Notes",
{"classes": ("collapse",), "fields": ("notes",)},
),
(
"Audit",
{
"classes": ("collapse",),
"fields": (
"creation_time",
"updated_time",
),
},
),
)
actions = [publish, unpublish]
class MagazineIssueInline(admin.TabularInline):
model = MagazineIssue
min_num = 0
extra = 0
autocomplete_fields = ("shop",)
show_change_link = True
fields = (
"preview",
"published",
"issue_number",
"publication_year",
"publication_month",
"number_of_pages",
"language",
)
readonly_fields = ("preview",)
class Media:
js = ("admin/js/magazine_issue_defaults.js",)
@admin.register(Magazine)
class MagazineAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (MagazineIssueInline,)
list_display = (
"__str__",
"publisher",
"published",
)
autocomplete_fields = ("publisher",)
readonly_fields = ("creation_time", "updated_time")
search_fields = ("name", "publisher__name")
list_filter = (
"published",
"publisher__name",
)
fieldsets = (
(
None,
{
"fields": (
"published",
"name",
"website",
"publisher",
"ISBN",
"language",
"description",
"image",
"tags",
)
},
),
(
"Notes",
{"classes": ("collapse",), "fields": ("notes",)},
),
(
"Audit",
{
"classes": ("collapse",),
"fields": (
"creation_time",
"updated_time",
),
},
),
)
actions = [publish, unpublish]

View File

@@ -0,0 +1,224 @@
# Generated by Django 6.0 on 2025-12-08 17:47
import bookshelf.models
import django.db.models.deletion
import ram.utils
import tinymce.models
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0024_alter_basebook_language"),
("metadata", "0025_alter_company_options_alter_manufacturer_options_and_more"),
]
operations = [
migrations.CreateModel(
name="Magazine",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("description", tinymce.models.HTMLField(blank=True)),
("notes", tinymce.models.HTMLField(blank=True)),
("creation_time", models.DateTimeField(auto_now_add=True)),
("updated_time", models.DateTimeField(auto_now=True)),
("published", models.BooleanField(default=True)),
("name", models.CharField(max_length=200)),
("ISBN", models.CharField(blank=True, max_length=17)),
(
"image",
models.ImageField(
blank=True,
storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.book_image_upload,
),
),
(
"language",
models.CharField(
choices=[
("af", "Afrikaans"),
("ar", "Arabic"),
("ar-dz", "Algerian Arabic"),
("ast", "Asturian"),
("az", "Azerbaijani"),
("bg", "Bulgarian"),
("be", "Belarusian"),
("bn", "Bengali"),
("br", "Breton"),
("bs", "Bosnian"),
("ca", "Catalan"),
("ckb", "Central Kurdish (Sorani)"),
("cs", "Czech"),
("cy", "Welsh"),
("da", "Danish"),
("de", "German"),
("dsb", "Lower Sorbian"),
("el", "Greek"),
("en", "English"),
("en-au", "Australian English"),
("en-gb", "British English"),
("eo", "Esperanto"),
("es", "Spanish"),
("es-ar", "Argentinian Spanish"),
("es-co", "Colombian Spanish"),
("es-mx", "Mexican Spanish"),
("es-ni", "Nicaraguan Spanish"),
("es-ve", "Venezuelan Spanish"),
("et", "Estonian"),
("eu", "Basque"),
("fa", "Persian"),
("fi", "Finnish"),
("fr", "French"),
("fy", "Frisian"),
("ga", "Irish"),
("gd", "Scottish Gaelic"),
("gl", "Galician"),
("he", "Hebrew"),
("hi", "Hindi"),
("hr", "Croatian"),
("hsb", "Upper Sorbian"),
("ht", "Haitian Creole"),
("hu", "Hungarian"),
("hy", "Armenian"),
("ia", "Interlingua"),
("id", "Indonesian"),
("ig", "Igbo"),
("io", "Ido"),
("is", "Icelandic"),
("it", "Italian"),
("ja", "Japanese"),
("ka", "Georgian"),
("kab", "Kabyle"),
("kk", "Kazakh"),
("km", "Khmer"),
("kn", "Kannada"),
("ko", "Korean"),
("ky", "Kyrgyz"),
("lb", "Luxembourgish"),
("lt", "Lithuanian"),
("lv", "Latvian"),
("mk", "Macedonian"),
("ml", "Malayalam"),
("mn", "Mongolian"),
("mr", "Marathi"),
("ms", "Malay"),
("my", "Burmese"),
("nb", "Norwegian Bokmål"),
("ne", "Nepali"),
("nl", "Dutch"),
("nn", "Norwegian Nynorsk"),
("os", "Ossetic"),
("pa", "Punjabi"),
("pl", "Polish"),
("pt", "Portuguese"),
("pt-br", "Brazilian Portuguese"),
("ro", "Romanian"),
("ru", "Russian"),
("sk", "Slovak"),
("sl", "Slovenian"),
("sq", "Albanian"),
("sr", "Serbian"),
("sr-latn", "Serbian Latin"),
("sv", "Swedish"),
("sw", "Swahili"),
("ta", "Tamil"),
("te", "Telugu"),
("tg", "Tajik"),
("th", "Thai"),
("tk", "Turkmen"),
("tr", "Turkish"),
("tt", "Tatar"),
("udm", "Udmurt"),
("ug", "Uyghur"),
("uk", "Ukrainian"),
("ur", "Urdu"),
("uz", "Uzbek"),
("vi", "Vietnamese"),
("zh-hans", "Simplified Chinese"),
("zh-hant", "Traditional Chinese"),
],
default="en",
max_length=7,
),
),
(
"publisher",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="bookshelf.publisher",
),
),
(
"tags",
models.ManyToManyField(
blank=True, related_name="magazine", to="metadata.tag"
),
),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="MagazineIssue",
fields=[
(
"basebook_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="bookshelf.basebook",
),
),
("issue_number", models.CharField(max_length=100)),
(
"publication_month",
models.SmallIntegerField(
blank=True,
choices=[
(1, "January"),
(2, "February"),
(3, "March"),
(4, "April"),
(5, "May"),
(6, "June"),
(7, "July"),
(8, "August"),
(9, "September"),
(10, "October"),
(11, "November"),
(12, "December"),
],
null=True,
),
),
(
"magazine",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="issue",
to="bookshelf.magazine",
),
),
],
options={
"ordering": ["magazine", "issue_number"],
"unique_together": {("magazine", "issue_number")},
},
bases=("bookshelf.basebook",),
),
]

View File

@@ -0,0 +1,244 @@
# Generated by Django 6.0 on 2025-12-10 20:59
import bookshelf.models
import ram.utils
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0025_magazine_magazineissue"),
]
operations = [
migrations.AlterField(
model_name="basebook",
name="language",
field=models.CharField(
choices=[
("af", "Afrikaans"),
("sq", "Albanian"),
("ar-dz", "Algerian Arabic"),
("ar", "Arabic"),
("es-ar", "Argentinian Spanish"),
("hy", "Armenian"),
("ast", "Asturian"),
("en-au", "Australian English"),
("az", "Azerbaijani"),
("eu", "Basque"),
("be", "Belarusian"),
("bn", "Bengali"),
("bs", "Bosnian"),
("pt-br", "Brazilian Portuguese"),
("br", "Breton"),
("en-gb", "British English"),
("bg", "Bulgarian"),
("my", "Burmese"),
("ca", "Catalan"),
("ckb", "Central Kurdish (Sorani)"),
("es-co", "Colombian Spanish"),
("hr", "Croatian"),
("cs", "Czech"),
("da", "Danish"),
("nl", "Dutch"),
("en", "English"),
("eo", "Esperanto"),
("et", "Estonian"),
("fi", "Finnish"),
("fr", "French"),
("fy", "Frisian"),
("gl", "Galician"),
("ka", "Georgian"),
("de", "German"),
("el", "Greek"),
("ht", "Haitian Creole"),
("he", "Hebrew"),
("hi", "Hindi"),
("hu", "Hungarian"),
("is", "Icelandic"),
("io", "Ido"),
("ig", "Igbo"),
("id", "Indonesian"),
("ia", "Interlingua"),
("ga", "Irish"),
("it", "Italian"),
("ja", "Japanese"),
("kab", "Kabyle"),
("kn", "Kannada"),
("kk", "Kazakh"),
("km", "Khmer"),
("ko", "Korean"),
("ky", "Kyrgyz"),
("lv", "Latvian"),
("lt", "Lithuanian"),
("dsb", "Lower Sorbian"),
("lb", "Luxembourgish"),
("mk", "Macedonian"),
("ms", "Malay"),
("ml", "Malayalam"),
("mr", "Marathi"),
("es-mx", "Mexican Spanish"),
("mn", "Mongolian"),
("ne", "Nepali"),
("es-ni", "Nicaraguan Spanish"),
("nb", "Norwegian Bokmål"),
("nn", "Norwegian Nynorsk"),
("os", "Ossetic"),
("fa", "Persian"),
("pl", "Polish"),
("pt", "Portuguese"),
("pa", "Punjabi"),
("ro", "Romanian"),
("ru", "Russian"),
("gd", "Scottish Gaelic"),
("sr", "Serbian"),
("sr-latn", "Serbian Latin"),
("zh-hans", "Simplified Chinese"),
("sk", "Slovak"),
("sl", "Slovenian"),
("es", "Spanish"),
("sw", "Swahili"),
("sv", "Swedish"),
("tg", "Tajik"),
("ta", "Tamil"),
("tt", "Tatar"),
("te", "Telugu"),
("th", "Thai"),
("zh-hant", "Traditional Chinese"),
("tr", "Turkish"),
("tk", "Turkmen"),
("udm", "Udmurt"),
("uk", "Ukrainian"),
("hsb", "Upper Sorbian"),
("ur", "Urdu"),
("ug", "Uyghur"),
("uz", "Uzbek"),
("es-ve", "Venezuelan Spanish"),
("vi", "Vietnamese"),
("cy", "Welsh"),
],
default="en",
max_length=7,
),
),
migrations.AlterField(
model_name="magazine",
name="image",
field=models.ImageField(
blank=True,
storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.magazine_image_upload,
),
),
migrations.AlterField(
model_name="magazine",
name="language",
field=models.CharField(
choices=[
("af", "Afrikaans"),
("sq", "Albanian"),
("ar-dz", "Algerian Arabic"),
("ar", "Arabic"),
("es-ar", "Argentinian Spanish"),
("hy", "Armenian"),
("ast", "Asturian"),
("en-au", "Australian English"),
("az", "Azerbaijani"),
("eu", "Basque"),
("be", "Belarusian"),
("bn", "Bengali"),
("bs", "Bosnian"),
("pt-br", "Brazilian Portuguese"),
("br", "Breton"),
("en-gb", "British English"),
("bg", "Bulgarian"),
("my", "Burmese"),
("ca", "Catalan"),
("ckb", "Central Kurdish (Sorani)"),
("es-co", "Colombian Spanish"),
("hr", "Croatian"),
("cs", "Czech"),
("da", "Danish"),
("nl", "Dutch"),
("en", "English"),
("eo", "Esperanto"),
("et", "Estonian"),
("fi", "Finnish"),
("fr", "French"),
("fy", "Frisian"),
("gl", "Galician"),
("ka", "Georgian"),
("de", "German"),
("el", "Greek"),
("ht", "Haitian Creole"),
("he", "Hebrew"),
("hi", "Hindi"),
("hu", "Hungarian"),
("is", "Icelandic"),
("io", "Ido"),
("ig", "Igbo"),
("id", "Indonesian"),
("ia", "Interlingua"),
("ga", "Irish"),
("it", "Italian"),
("ja", "Japanese"),
("kab", "Kabyle"),
("kn", "Kannada"),
("kk", "Kazakh"),
("km", "Khmer"),
("ko", "Korean"),
("ky", "Kyrgyz"),
("lv", "Latvian"),
("lt", "Lithuanian"),
("dsb", "Lower Sorbian"),
("lb", "Luxembourgish"),
("mk", "Macedonian"),
("ms", "Malay"),
("ml", "Malayalam"),
("mr", "Marathi"),
("es-mx", "Mexican Spanish"),
("mn", "Mongolian"),
("ne", "Nepali"),
("es-ni", "Nicaraguan Spanish"),
("nb", "Norwegian Bokmål"),
("nn", "Norwegian Nynorsk"),
("os", "Ossetic"),
("fa", "Persian"),
("pl", "Polish"),
("pt", "Portuguese"),
("pa", "Punjabi"),
("ro", "Romanian"),
("ru", "Russian"),
("gd", "Scottish Gaelic"),
("sr", "Serbian"),
("sr-latn", "Serbian Latin"),
("zh-hans", "Simplified Chinese"),
("sk", "Slovak"),
("sl", "Slovenian"),
("es", "Spanish"),
("sw", "Swahili"),
("sv", "Swedish"),
("tg", "Tajik"),
("ta", "Tamil"),
("tt", "Tatar"),
("te", "Telugu"),
("th", "Thai"),
("zh-hant", "Traditional Chinese"),
("tr", "Turkish"),
("tk", "Turkmen"),
("udm", "Udmurt"),
("uk", "Ukrainian"),
("hsb", "Upper Sorbian"),
("ur", "Urdu"),
("ug", "Uyghur"),
("uz", "Uzbek"),
("es-ve", "Venezuelan Spanish"),
("vi", "Vietnamese"),
("cy", "Welsh"),
],
default="en",
max_length=7,
),
),
]

View File

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

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

View File

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

View File

@@ -1,8 +1,12 @@
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.db.models.functions import Lower
from django.core.exceptions import ValidationError
from django_countries.fields import CountryField from django_countries.fields import CountryField
from ram.utils import DeduplicatedStorage from ram.utils import DeduplicatedStorage
@@ -41,8 +45,8 @@ class BaseBook(BaseModel):
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
language = models.CharField( language = models.CharField(
max_length=7, max_length=7,
choices=settings.LANGUAGES, choices=sorted(settings.LANGUAGES, key=lambda s: s[1]),
default='en' default="en",
) )
number_of_pages = models.SmallIntegerField(null=True, blank=True) number_of_pages = models.SmallIntegerField(null=True, blank=True)
publication_year = models.SmallIntegerField(null=True, blank=True) publication_year = models.SmallIntegerField(null=True, blank=True)
@@ -56,27 +60,24 @@ class BaseBook(BaseModel):
blank=True, blank=True,
) )
purchase_date = models.DateField(null=True, blank=True) purchase_date = models.DateField(null=True, blank=True)
tags = models.ManyToManyField( tags = models.ManyToManyField(Tag, related_name="bookshelf", blank=True)
Tag, related_name="bookshelf", blank=True
)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
shutil.rmtree( shutil.rmtree(
os.path.join( os.path.join(
settings.MEDIA_ROOT, "images", "books", str(self.uuid) settings.MEDIA_ROOT, "images", "books", str(self.uuid)
), ),
ignore_errors=True ignore_errors=True,
) )
super(BaseBook, self).delete(*args, **kwargs) super(BaseBook, self).delete(*args, **kwargs)
def book_image_upload(instance, filename): def book_image_upload(instance, filename):
return os.path.join( return os.path.join("images", "books", str(instance.book.uuid), filename)
"images",
"books",
str(instance.book.uuid), def magazine_image_upload(instance, filename):
filename return os.path.join("images", "magazines", str(instance.uuid), filename)
)
class BaseBookImage(Image): class BaseBookImage(Image):
@@ -120,8 +121,7 @@ class Book(BaseBook):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"bookshelf_item", "bookshelf_item", kwargs={"selector": "book", "uuid": self.uuid}
kwargs={"selector": "book", "uuid": self.uuid}
) )
@@ -129,9 +129,10 @@ class Catalog(BaseBook):
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="catalogs",
) )
years = models.CharField(max_length=12) years = models.CharField(max_length=12)
scales = models.ManyToManyField(Scale) scales = models.ManyToManyField(Scale, related_name="catalogs")
class Meta: class Meta:
ordering = ["manufacturer", "publication_year"] ordering = ["manufacturer", "publication_year"]
@@ -146,10 +147,95 @@ class Catalog(BaseBook):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"bookshelf_item", "bookshelf_item", kwargs={"selector": "catalog", "uuid": self.uuid}
kwargs={"selector": "catalog", "uuid": self.uuid}
) )
def get_scales(self): def get_scales(self):
return "/".join([s.scale for s in self.scales.all()]) return "/".join([s.scale for s in self.scales.all()])
get_scales.short_description = "Scales" get_scales.short_description = "Scales"
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,
upload_to=magazine_image_upload,
storage=DeduplicatedStorage,
)
language = models.CharField(
max_length=7,
choices=sorted(settings.LANGUAGES, key=lambda s: s[1]),
default="en",
)
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,
)
super(Magazine, self).delete(*args, **kwargs)
class Meta:
ordering = [Lower("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.", "")
class MagazineIssue(BaseBook):
magazine = models.ForeignKey(
Magazine, on_delete=models.CASCADE, related_name="issue"
)
issue_number = models.CharField(max_length=100)
publication_month = models.SmallIntegerField(
null=True, blank=True, choices=MONTHS.items()
)
class Meta:
unique_together = ("magazine", "issue_number")
ordering = [
"magazine",
"publication_year",
"publication_month",
"issue_number",
]
def __str__(self):
return f"{self.magazine.name} - {self.issue_number}"
def clean(self):
if self.magazine.published is False and self.published is True:
raise ValidationError(
"Cannot set an issue as published if the magazine is not "
"published."
)
@property
def obj_label(self):
return "Magazine Issue"
def preview(self):
return self.image.first().image_thumbnail(100)
@property
def publisher(self):
return self.magazine.publisher
def get_absolute_url(self):
return reverse(
"issue", kwargs={"uuid": self.uuid, "magazine": self.magazine.uuid}
)

View File

@@ -49,3 +49,5 @@ 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

@@ -0,0 +1,16 @@
document.addEventListener('formset:added', function(event) {
const newForm = event.target; // the new inline form element
const defaultLanguage = document.querySelector('#id_language').value;
const defaultStatus = document.querySelector('#id_published').checked;
const languageInput = newForm.querySelector('select[name$="language"]');
const statusInput = newForm.querySelector('input[name$="published"]');
if (languageInput) {
languageInput.value = defaultLanguage;
}
if (statusInput) {
statusInput.checked = defaultStatus;
}
});

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
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
@@ -6,11 +7,12 @@ from django.dispatch.dispatcher import receiver
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django_countries.fields import CountryField from django_countries.fields import CountryField
from ram.models import SimpleBaseModel
from ram.utils import DeduplicatedStorage, get_image_preview, slugify from ram.utils import DeduplicatedStorage, get_image_preview, slugify
from ram.managers import PublicManager from ram.managers import PublicManager
class Property(models.Model): class Property(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
private = models.BooleanField( private = models.BooleanField(
default=False, default=False,
@@ -27,7 +29,7 @@ class Property(models.Model):
objects = PublicManager() objects = PublicManager()
class Manufacturer(models.Model): class Manufacturer(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True, editable=False) slug = models.CharField(max_length=128, unique=True, editable=False)
category = models.CharField( category = models.CharField(
@@ -57,13 +59,17 @@ class Manufacturer(models.Model):
}, },
) )
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(models.Model): class Company(SimpleBaseModel):
name = models.CharField(max_length=64, unique=True) name = models.CharField(max_length=64, unique=True)
slug = models.CharField(max_length=64, unique=True, editable=False) slug = models.CharField(max_length=64, unique=True, editable=False)
extended_name = models.CharField(max_length=128, blank=True) extended_name = models.CharField(max_length=128, blank=True)
@@ -101,7 +107,7 @@ class Company(models.Model):
logo_thumbnail.short_description = "Preview" logo_thumbnail.short_description = "Preview"
class Decoder(models.Model): class Decoder(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
manufacturer = models.ForeignKey( manufacturer = models.ForeignKey(
Manufacturer, Manufacturer,
@@ -137,7 +143,7 @@ def calculate_ratio(ratio):
raise ValidationError("Invalid ratio format") raise ValidationError("Invalid ratio format")
class Scale(models.Model): class Scale(SimpleBaseModel):
scale = models.CharField(max_length=32, unique=True) scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False) slug = models.CharField(max_length=32, unique=True, editable=False)
ratio = models.CharField(max_length=16, validators=[calculate_ratio]) ratio = models.CharField(max_length=16, validators=[calculate_ratio])
@@ -172,7 +178,7 @@ def scale_save(sender, instance, **kwargs):
instance.ratio_int = calculate_ratio(instance.ratio) instance.ratio_int = calculate_ratio(instance.ratio)
class RollingStockType(models.Model): class RollingStockType(SimpleBaseModel):
type = models.CharField(max_length=64) type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField() order = models.PositiveSmallIntegerField()
category = models.CharField( category = models.CharField(
@@ -202,7 +208,7 @@ class RollingStockType(models.Model):
return "{0} {1}".format(self.type, self.category) return "{0} {1}".format(self.type, self.category)
class Tag(models.Model): class Tag(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True) slug = models.CharField(max_length=128, unique=True)
@@ -222,7 +228,7 @@ class Tag(models.Model):
) )
class Shop(models.Model): class Shop(SimpleBaseModel):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
country = CountryField(blank=True) country = CountryField(blank=True)
website = models.URLField(blank=True) website = models.URLField(blank=True)

View File

@@ -2,23 +2,23 @@
{% load dynamic_url %} {% load dynamic_url %}
{% block header %} {% block header %}
{% if book.tags.all %} {% if data.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
{% for t in book.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in data.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
{% endif %} {% endif %}
{% if not book.published %} {% if not data.published %}
<span class="badge text-bg-warning">Unpublished</span> | <span class="badge text-bg-warning">Unpublished</span> |
{% endif %} {% endif %}
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small> <small class="text-body-secondary">Updated {{ data.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %} {% endblock %}
{% block carousel %} {% block carousel %}
<div class="row"> <div class="row">
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000"> <div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000">
<div class="carousel-inner"> <div class="carousel-inner">
{% for t in book.image.all %} {% for t in data.image.all %}
{% if forloop.first %} {% if forloop.first %}
<div class="carousel-item active"> <div class="carousel-item active">
{% else %} {% else %}
@@ -28,7 +28,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% if book.image.count > 1 %} {% if data.image.count > 1 %}
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev"> <button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden"><i class="bi bi-chevron-left"></i></span> <span class="visually-hidden"><i class="bi bi-chevron-left"></i></span>
@@ -61,57 +61,86 @@
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row"> <th colspan="2" scope="row">
{% if type == "catalog" %}Catalog {{ data.obj_label|capfirst }}
{% elif type == "book" %}Book{% endif %}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% if type == "catalog" %} {% if data.obj_type == "catalog" %}
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td>{{ book.manufacturer }}</td> <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>{{ book.get_scales }}</td> <td>{{ data.get_scales }}</td>
</tr> </tr>
{% elif type == "book" %} {% elif data.obj_type == "book" %}
<tr> <tr>
<th class="w-33" scope="row">Title</th> <th class="w-33" scope="row">Title</th>
<td>{{ book.title }}</td> <td>{{ data.title }}</td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Authors</th> <th class="w-33" scope="row">Authors</th>
<td> <td>
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul> <ul class="mb-0 list-unstyled">{% for a in data.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Publisher</th> <th class="w-33" scope="row">Publisher</th>
<td>{{ book.publisher }}</td> <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 %}
</td>
</tr>
{% elif data.obj_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>
</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 %}
</td>
</tr>
<tr>
<th class="w-33" scope="row">Issue</th>
<td>{{ data.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>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th scope="row">ISBN</th> <th scope="row">ISBN</th>
<td>{{ book.ISBN|default:"-" }}</td> <td>{{ data.ISBN|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Language</th> <th scope="row">Language</th>
<td>{{ book.get_language_display }}</td> <td>{{ data.get_language_display }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Number of pages</th> <th scope="row">Number of pages</th>
<td>{{ book.number_of_pages|default:"-" }}</td> <td>{{ data.number_of_pages|default:"-" }}</td>
</tr> </tr>
{% if data.obj_type == "book" or data.obj_type == "catalog" %}
<tr> <tr>
<th scope="row">Publication year</th> <th scope="row">Publication year</th>
<td>{{ book.publication_year|default:"-" }}</td> <td>{{ data.publication_year|default:"-" }}</td>
</tr> </tr>
{% if book.description %} {% endif %}
{% if data.description %}
<tr> <tr>
<th class="w-33" scope="row">Description</th> <th class="w-33" scope="row">Description</th>
<td>{{ book.description | safe }}</td> <td>{{ data.description | safe }}</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
@@ -127,17 +156,17 @@
<tr> <tr>
<th class="w-33" scope="row">Shop</th> <th class="w-33" scope="row">Shop</th>
<td> <td>
{{ book.shop|default:"-" }} {{ data.shop|default:"-" }}
{% if book.shop.website %} <a href="{{ book.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %} {% if data.shop.website %} <a href="{{ data.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<th class="w-33" scope="row">Purchase date</th> <th class="w-33" scope="row">Purchase date</th>
<td>{{ book.purchase_date|default:"-" }}</td> <td>{{ data.purchase_date|default:"-" }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Price ({{ site_conf.currency }})</th> <th scope="row">Price ({{ site_conf.currency }})</th>
<td>{{ book.price|default:"-" }}</td> <td>{{ data.price|default:"-" }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -180,7 +209,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' type book.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' data.obj_type data.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,6 +10,9 @@
{% if catalogs_menu %} {% if catalogs_menu %}
<li><a class="dropdown-item" href="{% url 'catalogs' %}">Catalogs</a></li> <li><a class="dropdown-item" href="{% url 'catalogs' %}">Catalogs</a></li>
{% endif %} {% endif %}
{% if magazines_menu %}
<li><a class="dropdown-item" href="{% url 'magazines' %}">Magazines</a></li>
{% endif %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,104 @@
{% 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 }}">
{% 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 }}">
{% 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 }}">
{% 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 }}">
{% 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>
</p>
<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">
{{ t.name }}</a>{# new line is required #}
{% empty %}
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
{% endfor %}
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">
{{ d.obj_label|capfirst }}
<div class="float-end">
{% if not d.published %}
<span class="badge text-bg-warning">Unpublished</span>
{% endif %}
</div>
</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.obj_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>
</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 %}
</td>
</tr>
{% if d.obj_type == "magazineissue" %}
<tr>
<th class="w-33" scope="row">Issue</th>
<td>{{ d.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>
</tr>
<tr>
<th class="w-33" scope="row">Pages</th>
<td>{{ d.number_of_pages|default:"-" }}</td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Language</th>
<td>{{ d.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>
{% else %}
<a class="btn btn-sm btn-outline-primary" href="{{ d.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 %}
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,9 +11,10 @@
<ul class="dropdown-menu" aria-labelledby="dropdownLogin"> <ul class="dropdown-menu" aria-labelledby="dropdownLogin">
<li><a class="dropdown-item" href="{% url 'admin:roster_rollingstock_changelist' %}">Rolling stock</a></li> <li><a class="dropdown-item" href="{% url 'admin:roster_rollingstock_changelist' %}">Rolling stock</a></li>
<li><a class="dropdown-item" href="{% url 'admin:consist_consist_changelist' %}">Consists</a></li> <li><a class="dropdown-item" href="{% url 'admin:consist_consist_changelist' %}">Consists</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'metadata' %}">Metadata</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_flatpage_changelist' %}">Pages</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'bookshelf' %}">Bookshelf</a></li> <li><a class="dropdown-item" href="{% url 'admin:app_list' 'bookshelf' %}">Bookshelf</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_flatpage_changelist' %}">Pages</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'repository' %}">Repository</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'metadata' %}">Metadata</a></li>
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li> <li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a></li> <li><a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a></li>

View File

@@ -0,0 +1,128 @@
{% extends "cards.html" %}
{% block header %}
{{ block.super }}
{% if magazine.tags.all %}
<p><small>Tags:</small>
{% for t in magazine.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>
{% if not magazine.published %}
<span class="badge text-bg-warning">Unpublished</span> |
{% endif %}
<small class="text-body-secondary">Updated {{ magazine.updated_time | date:"M d, Y H:i" }}</small>
{% endif %}
{% endblock %}
{% block carousel %}
{% if magazine.image %}
<div class="row pb-4">
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="{{ magazine.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="magazine cover">
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block pagination %}
{% if data.has_other_pages %}
<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 'magazine' 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">
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span>
</li>
{% else %}
{% 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>
{% 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>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}
{% block extra_content %}
<section class="py-4 text-start container">
<div class="row">
<div class="mx-auto">
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
</nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option>
</select>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">
Magazine
</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ magazine }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>
<img src="{{ magazine.publisher.country.flag }}" alt="{{ magazine.publisher.country }}"> {{ magazine.publisher }}
{% 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>
</tr>
{% if magazine.description %}
<tr>
<th scope="row">Description</th>
<td>{{ magazine.description | safe }}</td>
</tr>
{% endif %}
</tbody>
</table>
</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="{% url 'admin:bookshelf_magazine_change' magazine.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</section>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
from django import template from django import template
from portal.models import Flatpage from portal.models import Flatpage
from bookshelf.models import Book, Catalog from bookshelf.models import Book, Catalog, Magazine
register = template.Library() register = template.Library()
@@ -8,10 +8,14 @@ register = template.Library()
@register.inclusion_tag('bookshelf/bookshelf_menu.html') @register.inclusion_tag('bookshelf/bookshelf_menu.html')
def show_bookshelf_menu(): def show_bookshelf_menu():
# FIXME: Filter out unpublished books and catalogs? # FIXME: Filter out unpublished books and catalogs?
books = Book.objects.exists()
catalogs = Catalog.objects.exists()
magazines = Magazine.objects.exists()
return { return {
"bookshelf_menu": (Book.objects.exists() or Catalog.objects.exists()), "bookshelf_menu": (books or catalogs or magazines),
"books_menu": Book.objects.exists(), "books_menu": books,
"catalogs_menu": Catalog.objects.exists(), "catalogs_menu": catalogs,
"magazines_menu": magazines,
} }

View File

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

View File

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

View File

@@ -4,10 +4,12 @@ from itertools import chain
from functools import reduce from functools import reduce
from urllib.parse import unquote from urllib.parse import unquote
from django.conf import settings
from django.views import View from django.views import View
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError from django.db.utils import OperationalError, ProgrammingError
from django.db.models import F, Q, Count from django.db.models import F, Q, Count
from django.db.models.functions import Lower
from django.shortcuts import render, get_object_or_404, get_list_or_404 from django.shortcuts import render, get_object_or_404, get_list_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator from django.core.paginator import Paginator
@@ -16,7 +18,7 @@ from portal.utils import get_site_conf
from portal.models import Flatpage from portal.models import Flatpage
from roster.models import RollingStock from roster.models import RollingStock
from consist.models import Consist from consist.models import Consist
from bookshelf.models import Book, Catalog from bookshelf.models import Book, Catalog, Magazine, MagazineIssue
from metadata.models import ( from metadata.models import (
Company, Company,
Manufacturer, Manufacturer,
@@ -31,7 +33,7 @@ def get_items_per_page():
items_per_page = get_site_conf().items_per_page items_per_page = get_site_conf().items_per_page
except (OperationalError, ProgrammingError): except (OperationalError, ProgrammingError):
items_per_page = 6 items_per_page = 6
return items_per_page return int(items_per_page)
def get_order_by_field(): def get_order_by_field():
@@ -61,9 +63,8 @@ class Render404(View):
class GetData(View): class GetData(View):
title = "Home" title = None
template = "pagination.html" template = "pagination.html"
item_type = "roster"
filter = Q() # empty filter by default filter = Q() # empty filter by default
def get_data(self, request): def get_data(self, request):
@@ -74,9 +75,10 @@ class GetData(View):
) )
def get(self, request, page=1): def get(self, request, page=1):
data = [] if self.title is None or self.template is None:
for item in self.get_data(request): raise Exception("title and template must be defined")
data.append({"type": self.item_type, "item": item})
data = list(self.get_data(request))
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
@@ -89,7 +91,6 @@ class GetData(View):
self.template, self.template,
{ {
"title": self.title, "title": self.title,
"type": self.item_type,
"data": data, "data": data,
"matches": paginator.count, "matches": paginator.count,
"page_range": page_range, "page_range": page_range,
@@ -97,18 +98,36 @@ class GetData(View):
) )
class GetRoster(GetData): class GetHome(GetData):
title = "The Roster" title = "Home"
item_type = "roster" template = "home.html"
def get_data(self, request): def get_data(self, request):
return RollingStock.objects.get_published(request.user).order_by( max_items = min(settings.FEATURED_ITEMS_MAX, get_items_per_page())
*get_order_by_field() return (
) RollingStock.objects.get_published(request.user)
.filter(featured=True)
.order_by(*get_order_by_field())[:max_items]
) or super().get_data(request)
class GetRoster(GetData):
title = "The Roster"
class SearchObjects(View): class SearchObjects(View):
def run_search(self, request, search, _filter, page=1): def run_search(self, request, search, _filter, page=1):
"""
Run the search query on the database and return the results.
param request: HTTP request
param search: search string
param _filter: filter to apply (type, company, manufacturer, scale)
param page: page number for pagination
return: tuple (data, matches, page_range)
1. data: list of dicts with keys "type" and "item"
2. matches: total number of matches
3. page_range: elided page range for pagination
"""
if _filter is None: if _filter is None:
query = reduce( query = reduce(
operator.or_, operator.or_,
@@ -151,15 +170,13 @@ class SearchObjects(View):
# FIXME duplicated code! # FIXME duplicated code!
# FIXME see if it makes sense to filter calatogs and books by scale # FIXME see if it makes sense to filter calatogs and books by scale
# and manufacturer as well # and manufacturer as well
data = []
roster = ( roster = (
RollingStock.objects.get_published(request.user) RollingStock.objects.get_published(request.user)
.filter(query) .filter(query)
.distinct() .distinct()
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) )
for item in roster: data = list(roster)
data.append({"type": "roster", "item": item})
if _filter is None: if _filter is None:
consists = ( consists = (
@@ -172,20 +189,39 @@ class SearchObjects(View):
) )
.distinct() .distinct()
) )
for item in consists: data = list(chain(data, consists))
data.append({"type": "consist", "item": item})
books = ( books = (
Book.objects.get_published(request.user) Book.objects.get_published(request.user)
.filter(title__icontains=search) .filter(
Q(
Q(title__icontains=search)
| Q(description__icontains=search)
)
)
.distinct() .distinct()
) )
catalogs = ( catalogs = (
Catalog.objects.get_published(request.user) Catalog.objects.get_published(request.user)
.filter(manufacturer__name__icontains=search) .filter(
Q(
Q(manufacturer__name__icontains=search)
| Q(description__icontains=search)
)
)
.distinct() .distinct()
) )
for item in list(chain(books, catalogs)): data = list(chain(data, books, catalogs))
data.append({"type": "book", "item": item}) magazine_issues = (
MagazineIssue.objects.get_published(request.user)
.filter(
Q(
Q(magazine__name__icontains=search)
| Q(description__icontains=search)
)
)
.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)
@@ -241,10 +277,25 @@ class SearchObjects(View):
class GetManufacturerItem(View): class GetManufacturerItem(View):
def get(self, request, manufacturer, search="all", page=1): def get(self, request, manufacturer, search="all", page=1):
"""
Get all items from a specific manufacturer. If `search` is not "all",
filter by item number as well, for example to get all itmes from the
same set.
The view returns both rolling stock and catalogs.
param request: HTTP request
param manufacturer: Manufacturer slug
param search: item number slug or "all"
param page: page number for pagination
return: rendered template
1. manufacturer: Manufacturer object
2. search: item number slug or "all"
3. data: list of dicts with keys "type" and "item"
4. matches: total number of matches
5. page_range: elided page range for pagination
"""
manufacturer = get_object_or_404( manufacturer = get_object_or_404(
Manufacturer, slug__iexact=manufacturer Manufacturer, slug__iexact=manufacturer
) )
if search != "all": if search != "all":
roster = get_list_or_404( roster = get_list_or_404(
RollingStock.objects.get_published(request.user).order_by( RollingStock.objects.get_published(request.user).order_by(
@@ -255,6 +306,7 @@ class GetManufacturerItem(View):
& Q(item_number_slug__exact=search) & Q(item_number_slug__exact=search)
), ),
) )
catalogs = [] # no catalogs when searching for a specific item
title = "{0}: {1}".format( title = "{0}: {1}".format(
manufacturer, manufacturer,
# all returned records must have the same `item_number``; # all returned records must have the same `item_number``;
@@ -271,12 +323,12 @@ class GetManufacturerItem(View):
.distinct() .distinct()
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) )
catalogs = Catalog.objects.get_published(request.user).filter(
manufacturer=manufacturer
)
title = "Manufacturer: {0}".format(manufacturer) title = "Manufacturer: {0}".format(manufacturer)
data = [] data = list(chain(roster, catalogs))
for item in roster:
data.append({"type": "roster", "item": item})
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
@@ -325,9 +377,15 @@ class GetObjectsFiltered(View):
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
) )
data = [] data = list(roster)
for item in roster:
data.append({"type": "roster", "item": item}) if _filter == "scale":
catalogs = (
Catalog.objects.get_published(request.user)
.filter(scales__slug=search)
.distinct()
)
data = list(chain(data, catalogs))
try: # Execute only if query_2nd is defined try: # Execute only if query_2nd is defined
consists = ( consists = (
@@ -335,23 +393,24 @@ class GetObjectsFiltered(View):
.filter(query_2nd) .filter(query_2nd)
.distinct() .distinct()
) )
for item in consists: data = list(chain(data, consists))
data.append({"type": "consist", "item": item})
if _filter == "tag": # Books can be filtered only by tag if _filter == "tag": # Books can be filtered only by tag
books = ( books = (
Book.objects.get_published(request.user) Book.objects.get_published(request.user)
.filter(query_2nd) .filter(query_2nd)
.distinct() .distinct()
) )
for item in books:
data.append({"type": "book", "item": item})
catalogs = ( catalogs = (
Catalog.objects.get_published(request.user) Catalog.objects.get_published(request.user)
.filter(query_2nd) .filter(query_2nd)
.distinct() .distinct()
) )
for item in catalogs: magazine_issues = (
data.append({"type": "catalog", "item": item}) MagazineIssue.objects.get_published(request.user)
.filter(query_2nd)
.distinct()
)
data = list(chain(data, books, catalogs, magazine_issues))
except NameError: except NameError:
pass pass
@@ -405,16 +464,14 @@ class GetRollingStock(View):
request.user request.user
) )
consists = [ consists = list(
{"type": "consist", "item": c} Consist.objects.get_published(request.user).filter(
for c in Consist.objects.get_published(request.user).filter(
consist_item__rolling_stock=rolling_stock consist_item__rolling_stock=rolling_stock
) )
] # A dict with "item" is required by the consists card )
set = [ trainset = list(
{"type": "set", "item": s} RollingStock.objects.get_published(request.user)
for s in RollingStock.objects.get_published(request.user)
.filter( .filter(
Q( Q(
Q(item_number__exact=rolling_stock.item_number) Q(item_number__exact=rolling_stock.item_number)
@@ -422,7 +479,7 @@ class GetRollingStock(View):
) )
) )
.order_by(*get_order_by_field()) .order_by(*get_order_by_field())
] )
return render( return render(
request, request,
@@ -435,7 +492,7 @@ class GetRollingStock(View):
"decoder_documents": decoder_documents, "decoder_documents": decoder_documents,
"documents": documents, "documents": documents,
"journal": journal, "journal": journal,
"set": set, "set": trainset,
"consists": consists, "consists": consists,
}, },
) )
@@ -443,7 +500,6 @@ class GetRollingStock(View):
class Consists(GetData): class Consists(GetData):
title = "Consists" title = "Consists"
item_type = "consist"
def get_data(self, request): def get_data(self, request):
return Consist.objects.get_published(request.user).all() return Consist.objects.get_published(request.user).all()
@@ -457,16 +513,13 @@ class GetConsist(View):
) )
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
data = [
{
"type": "roster",
"item": RollingStock.objects.get_published(request.user).get(
uuid=r.rolling_stock_id
),
}
for r in consist.consist_item.all()
]
data = list(
RollingStock.objects.get_published(request.user).get(
uuid=r.rolling_stock_id
)
for r in consist.consist_item.all()
)
paginator = Paginator(data, get_items_per_page()) paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
@@ -487,11 +540,11 @@ 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 (
Manufacturer.objects.filter(self.filter).annotate( Manufacturer.objects.filter(self.filter)
.annotate(
num_rollingstock=( num_rollingstock=(
Count( Count(
"rollingstock", "rollingstock",
@@ -521,7 +574,26 @@ class Manufacturers(GetData):
) )
) )
) )
.annotate(num_items=F("num_rollingstock") + F("num_rollingclass")) .annotate(
num_catalogs=(
Count(
"catalogs",
filter=Q(
catalogs__in=(
Catalog.objects.get_published(request.user)
),
),
distinct=True,
)
)
)
.annotate(
num_items=(
F("num_rollingstock")
+ F("num_rollingclass")
+ F("num_catalogs")
)
)
.order_by("name") .order_by("name")
) )
@@ -536,7 +608,6 @@ class Manufacturers(GetData):
class Companies(GetData): class Companies(GetData):
title = "Companies" title = "Companies"
item_type = "company"
def get_data(self, request): def get_data(self, request):
return ( return (
@@ -575,7 +646,6 @@ class Companies(GetData):
class Scales(GetData): class Scales(GetData):
title = "Scales" title = "Scales"
item_type = "scale"
def get_data(self, request): def get_data(self, request):
return ( return (
@@ -592,21 +662,25 @@ class Scales(GetData):
num_consists=Count( num_consists=Count(
"consist", "consist",
filter=Q( filter=Q(
consist__in=Consist.objects.get_published( consist__in=Consist.objects.get_published(request.user)
request.user
)
), ),
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(
@@ -623,7 +697,6 @@ class Types(GetData):
class Books(GetData): class Books(GetData):
title = "Books" title = "Books"
item_type = "book"
def get_data(self, request): def get_data(self, request):
return Book.objects.get_published(request.user).all() return Book.objects.get_published(request.user).all()
@@ -631,12 +704,82 @@ 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()
class Magazines(GetData):
title = "Magazines"
def get_data(self, request):
return (
Magazine.objects.get_published(request.user)
.order_by(Lower("name"))
.annotate(
issues=Count(
"issue",
filter=Q(
issue__in=(
MagazineIssue.objects.get_published(request.user)
)
),
)
)
)
class GetMagazine(View):
def get(self, request, uuid, page=1):
try:
magazine = Magazine.objects.get_published(request.user).get(
uuid=uuid
)
except ObjectDoesNotExist:
raise Http404
data = list(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(
data.number, on_each_side=1, on_ends=1
)
return render(
request,
"magazine.html",
{
"title": magazine,
"magazine": magazine,
"data": data,
"matches": paginator.count,
"page_range": page_range,
},
)
class GetMagazineIssue(View):
def get(self, request, uuid, magazine, page=1):
try:
issue = MagazineIssue.objects.get_published(request.user).get(
uuid=uuid,
magazine__uuid=magazine,
)
except ObjectDoesNotExist:
raise Http404
properties = issue.property.get_public(request.user)
documents = issue.document.get_public(request.user)
return render(
request,
"bookshelf/book.html",
{
"title": issue,
"data": issue,
"documents": documents,
"properties": properties,
},
)
class GetBookCatalog(View): class GetBookCatalog(View):
def get_object(self, request, uuid, selector): def get_object(self, request, uuid, selector):
if selector == "book": if selector == "book":
@@ -659,10 +802,9 @@ class GetBookCatalog(View):
"bookshelf/book.html", "bookshelf/book.html",
{ {
"title": book, "title": book,
"book": book, "data": book,
"documents": documents, "documents": documents,
"properties": properties, "properties": properties,
"type": selector,
}, },
) )

View File

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

View File

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

View File

@@ -150,7 +150,7 @@ REST_FRAMEWORK = {
} }
TINYMCE_DEFAULT_CONFIG = { TINYMCE_DEFAULT_CONFIG = {
"height": "500px", "height": "300px",
"menubar": False, "menubar": False,
"plugins": "autolink lists link image charmap preview anchor " "plugins": "autolink lists link image charmap preview anchor "
"searchreplace visualblocks code fullscreen insertdatetime media " "searchreplace visualblocks code fullscreen insertdatetime media "
@@ -204,6 +204,8 @@ ROLLING_STOCK_TYPES = [
("other", "Other"), ("other", "Other"),
] ]
FEATURED_ITEMS_MAX = 6
try: try:
from ram.local_settings import * from ram.local_settings import *
except ImportError: except ImportError:

View File

@@ -0,0 +1,65 @@
# Generated by Django 6.0 on 2025-12-08 17:47
import django.db.models.deletion
import ram.utils
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0025_magazine_magazineissue"),
(
"repository",
"0003_alter_bookdocument_file_alter_catalogdocument_file_and_more",
),
]
operations = [
migrations.CreateModel(
name="MagazineIssueDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.CharField(blank=True, max_length=128)),
(
"file",
models.FileField(
storage=ram.utils.DeduplicatedStorage, upload_to="files/"
),
),
("creation_time", models.DateTimeField(auto_now_add=True)),
("updated_time", models.DateTimeField(auto_now=True)),
(
"private",
models.BooleanField(
default=False,
help_text="Document will be visible only to logged users",
),
),
(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="document",
to="bookshelf.magazineissue",
),
),
],
options={
"verbose_name_plural": "Magazines documents",
"constraints": [
models.UniqueConstraint(
fields=("issue", "file"), name="unique_issue_file"
)
],
},
),
]

View File

@@ -1,12 +1,11 @@
from django.db import models from django.db import models
from django.core.exceptions import ValidationError
from tinymce import models as tinymce from tinymce import models as tinymce
from ram.models import PrivateDocument from ram.models import PrivateDocument
from metadata.models import Decoder, Shop, Tag from metadata.models import Decoder, Shop, Tag
from roster.models import RollingStock from roster.models import RollingStock
from bookshelf.models import Book, Catalog from bookshelf.models import Book, Catalog, MagazineIssue
class GenericDocument(PrivateDocument): class GenericDocument(PrivateDocument):
@@ -77,6 +76,20 @@ class CatalogDocument(PrivateDocument):
] ]
class MagazineIssueDocument(PrivateDocument):
issue = models.ForeignKey(
MagazineIssue, on_delete=models.CASCADE, related_name="document"
)
class Meta:
verbose_name_plural = "Magazines documents"
constraints = [
models.UniqueConstraint(
fields=["issue", "file"], name="unique_issue_file"
)
]
class RollingStockDocument(PrivateDocument): class RollingStockDocument(PrivateDocument):
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="document" RollingStock, on_delete=models.CASCADE, related_name="document"

View File

@@ -44,7 +44,9 @@ class RollingClass(admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name '<img src="{}" title="{}" />',
obj.company.country.flag,
obj.company.country.name,
) )
@@ -128,9 +130,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"item_number", "item_number",
"company", "company",
"country_flag", "country_flag",
"featured",
"published", "published",
) )
list_filter = ( list_filter = (
"featured",
"published",
"rolling_class__type__category", "rolling_class__type__category",
"rolling_class__type", "rolling_class__type",
"rolling_class__company__name", "rolling_class__company__name",
@@ -152,7 +157,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}', obj.country.flag, obj.country.name '<img src="{}" title="{}" />', obj.country.flag, obj.country.name
) )
fieldsets = ( fieldsets = (
@@ -162,6 +167,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"fields": ( "fields": (
"preview", "preview",
"published", "published",
"featured",
"rolling_class", "rolling_class",
"road_number", "road_number",
"scale", "scale",
@@ -224,8 +230,8 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
if obj.invoice.exists(): if obj.invoice.exists():
html = format_html_join( html = format_html_join(
"<br>", "<br>",
"<a href=\"{}\" target=\"_blank\">{}</a>", '<a href="{}" target="_blank">{}</a>',
((i.file.url, i) for i in obj.invoice.all()) ((i.file.url, i) for i in obj.invoice.all()),
) )
else: else:
html = "-" html = "-"
@@ -296,4 +302,38 @@ 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

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

View File

@@ -5,6 +5,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.core.exceptions import ValidationError
from tinymce import models as tinymce from tinymce import models as tinymce
@@ -82,9 +83,7 @@ class RollingStock(BaseModel):
help_text="Catalog item number or code", help_text="Catalog item number or code",
) )
item_number_slug = models.CharField( item_number_slug = models.CharField(
max_length=32, max_length=32, blank=True, editable=False
blank=True,
editable=False
) )
set = models.BooleanField( set = models.BooleanField(
default=False, default=False,
@@ -113,6 +112,10 @@ class RollingStock(BaseModel):
null=True, null=True,
blank=True, blank=True,
) )
featured = models.BooleanField(
default=False,
help_text="Featured rolling stock will appear on the homepage",
)
tags = models.ManyToManyField( tags = models.ManyToManyField(
Tag, related_name="rolling_stock", blank=True Tag, related_name="rolling_stock", blank=True
) )
@@ -165,10 +168,18 @@ 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):
@@ -185,10 +196,7 @@ def pre_save_internal_fields(sender, instance, *args, **kwargs):
def rolling_stock_image_upload(instance, filename): def rolling_stock_image_upload(instance, filename):
return os.path.join( return os.path.join(
"images", "images", "rollingstock", str(instance.rolling_stock.uuid), filename
"rollingstock",
str(instance.rolling_stock.uuid),
filename
) )

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

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