mirror of
https://github.com/daniviga/django-ram.git
synced 2025-12-26 23:38:32 +01:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
49da8f58fe
|
|||
|
0688725326
|
|||
|
cd1369e9c9
|
|||
| 6b10051bc4 | |||
|
02ed907f12
|
|||
|
8a3101364e
|
|||
|
7f456890dd
|
|||
|
5e6366e10c
|
|||
|
361a05e3a8
|
|||
|
3804c3379b
|
|||
|
1b769da553
|
|||
|
f655900411
|
|||
|
3e69b9ae6e
|
|||
|
66c3c3f51c
|
|||
|
935c439084
|
|||
|
1d683b8519
|
|||
|
d757388ca8
|
|||
| 536101d2ff | |||
|
955397acd5
|
|||
|
672cadd7e1
|
|||
|
464fe57536
|
|||
|
bd16c7eee7
|
|||
|
cc2e374558
|
|||
|
1c25ac9b14
|
|||
|
de126a735d
|
|||
|
18b5ab8053
|
|||
|
3acc80e2ad
|
|||
|
552ba39970
|
|||
|
222e2075ec
|
|||
|
b5c57dcd94
|
|||
|
b81c63898f
|
|||
|
76b266b1f9
|
|||
|
86657a3b9f
|
|||
|
d0d25424fb
|
|||
|
292b95b8ed
|
|||
|
dea7a594bc
|
|||
|
60195bc99f
|
|||
|
7673f0514a
|
|||
|
40f42a9ee9
|
|||
|
2e06e94fde
|
|||
|
ece8d1ad94
|
2
.github/workflows/django.yml
vendored
2
.github/workflows/django.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
max-parallel: 2
|
max-parallel: 2
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.12', '3.13']
|
python-version: ['3.13', '3.14']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
Submodule arduino/CommandStation-EX updated: 911bbd63be...313d2cd3e0
Submodule arduino/WebThrottle-EX updated: eb43d7906f...eeec7d4af6
Submodule arduino/arduino-cli updated: fa6eafcbbe...08ff7e2b76
Submodule arduino/dcc-ex.github.io updated: a0f886b69f...190d3adfa1
@@ -2,13 +2,17 @@ import html
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html, strip_tags
|
from django.utils.html import format_html, format_html_join, strip_tags
|
||||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||||
|
|
||||||
from ram.admin import publish, unpublish
|
from ram.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 = (
|
||||||
@@ -66,7 +76,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
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 = (
|
||||||
(
|
(
|
||||||
@@ -123,13 +133,14 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
@admin.display(description="Invoices")
|
@admin.display(description="Invoices")
|
||||||
def invoices(self, obj):
|
def invoices(self, obj):
|
||||||
if obj.invoice.exists():
|
if obj.invoice.exists():
|
||||||
html = "<br>".join(
|
html = format_html_join(
|
||||||
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
|
"<br>",
|
||||||
i.file.url, i
|
"<a href=\"{}\" target=\"_blank\">{}</a>",
|
||||||
) for i in obj.invoice.all())
|
((i.file.url, i) for i in obj.invoice.all())
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
html = "-"
|
html = "-"
|
||||||
return format_html(html)
|
return html
|
||||||
|
|
||||||
@admin.display(description="Publisher")
|
@admin.display(description="Publisher")
|
||||||
def get_publisher(self, obj):
|
def get_publisher(self, obj):
|
||||||
@@ -207,7 +218,7 @@ class PublisherAdmin(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="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -228,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 = (
|
||||||
|
"manufacturer__name",
|
||||||
|
"publication_year",
|
||||||
|
"scales__scale",
|
||||||
|
"published",
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
@@ -285,13 +301,14 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
@admin.display(description="Invoices")
|
@admin.display(description="Invoices")
|
||||||
def invoices(self, obj):
|
def invoices(self, obj):
|
||||||
if obj.invoice.exists():
|
if obj.invoice.exists():
|
||||||
html = "<br>".join(
|
html = format_html_join(
|
||||||
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
|
"<br>",
|
||||||
i.file.url, i
|
"<a href=\"{}\" target=\"_blank\">{}</a>",
|
||||||
) for i in obj.invoice.all())
|
((i.file.url, i) for i in obj.invoice.all())
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
html = "-"
|
html = "-"
|
||||||
return format_html(html)
|
return html
|
||||||
|
|
||||||
def download_csv(modeladmin, request, queryset):
|
def download_csv(modeladmin, request, queryset):
|
||||||
header = [
|
header = [
|
||||||
@@ -344,3 +361,142 @@ 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 = ("publisher__name", "published")
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"published",
|
||||||
|
"name",
|
||||||
|
"publisher",
|
||||||
|
"ISBN",
|
||||||
|
"language",
|
||||||
|
"description",
|
||||||
|
"image",
|
||||||
|
"tags",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{"classes": ("collapse",), "fields": ("notes",)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Audit",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"creation_time",
|
||||||
|
"updated_time",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# Generated by Django 5.1.2 on 2024-11-27 16:35
|
# Generated by Django 5.1.2 on 2024-11-27 16:35
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models, connection
|
||||||
|
from django.db.utils import ProgrammingError, OperationalError
|
||||||
|
|
||||||
|
|
||||||
def basebook_to_book(apps, schema_editor):
|
def basebook_to_book(apps, schema_editor):
|
||||||
@@ -16,6 +17,19 @@ def basebook_to_book(apps, schema_editor):
|
|||||||
b.authors.set(row.old_authors.all())
|
b.authors.set(row.old_authors.all())
|
||||||
|
|
||||||
|
|
||||||
|
def drop_temporary_tables(apps, schema_editor):
|
||||||
|
try:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
'DROP TABLE IF EXISTS bookshelf_basebook_old_authors'
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
'DROP TABLE IF EXISTS bookshelf_basebook_authors'
|
||||||
|
)
|
||||||
|
except (ProgrammingError, OperationalError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -101,10 +115,6 @@ class Migration(migrations.Migration):
|
|||||||
model_name="basebook",
|
model_name="basebook",
|
||||||
name="old_title",
|
name="old_title",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
|
||||||
model_name="basebook",
|
|
||||||
name="old_authors",
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name="basebook",
|
model_name="basebook",
|
||||||
name="old_publisher",
|
name="old_publisher",
|
||||||
@@ -138,4 +148,16 @@ class Migration(migrations.Migration):
|
|||||||
},
|
},
|
||||||
bases=("bookshelf.basebook",),
|
bases=("bookshelf.basebook",),
|
||||||
),
|
),
|
||||||
|
# Required by Dajngo 6.0 on SQLite
|
||||||
|
migrations.SeparateDatabaseAndState(
|
||||||
|
state_operations=[
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="basebook",
|
||||||
|
name="old_authors",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
database_operations=[
|
||||||
|
migrations.RunPython(drop_temporary_tables)
|
||||||
|
]
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="basebookdocument",
|
||||||
|
name="unique_book_file",
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="basebook",
|
model_name="basebook",
|
||||||
name="shop",
|
name="shop",
|
||||||
|
|||||||
123
ram/bookshelf/migrations/0024_alter_basebook_language.py
Normal file
123
ram/bookshelf/migrations/0024_alter_basebook_language.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-03 22:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0023_delete_basebookdocument"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="basebook",
|
||||||
|
name="language",
|
||||||
|
field=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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
224
ram/bookshelf/migrations/0025_magazine_magazineissue.py
Normal file
224
ram/bookshelf/migrations/0025_magazine_magazineissue.py
Normal 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",),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -3,6 +3,8 @@ import shutil
|
|||||||
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.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 +43,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)
|
||||||
@@ -79,6 +81,15 @@ def book_image_upload(instance, filename):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def magazine_image_upload(instance, filename):
|
||||||
|
return os.path.join(
|
||||||
|
"images",
|
||||||
|
"magazines",
|
||||||
|
str(instance.uuid),
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseBookImage(Image):
|
class BaseBookImage(Image):
|
||||||
book = models.ForeignKey(
|
book = models.ForeignKey(
|
||||||
BaseBook, on_delete=models.CASCADE, related_name="image"
|
BaseBook, on_delete=models.CASCADE, related_name="image"
|
||||||
@@ -153,3 +164,85 @@ class Catalog(BaseBook):
|
|||||||
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)
|
||||||
|
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 = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"magazine",
|
||||||
|
kwargs={"uuid": self.uuid}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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", "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."
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
16
ram/bookshelf/static/admin/js/magazine_issue_defaults.js
Normal file
16
ram/bookshelf/static/admin/js/magazine_issue_defaults.js
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
|
import html
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
# from django.forms import BaseInlineFormSet # for future reference
|
||||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
from django.utils.html import format_html, strip_tags
|
||||||
|
from adminsortable2.admin import (
|
||||||
|
SortableAdminBase,
|
||||||
|
SortableInlineAdminMixin,
|
||||||
|
# CustomInlineFormSetMixin, # for future reference
|
||||||
|
)
|
||||||
|
|
||||||
from ram.admin import publish, unpublish
|
from ram.admin import publish, unpublish
|
||||||
|
from ram.utils import generate_csv
|
||||||
from consist.models import Consist, ConsistItem
|
from consist.models import Consist, ConsistItem
|
||||||
|
|
||||||
|
|
||||||
|
# for future reference
|
||||||
|
# class ConsistItemInlineFormSet(CustomInlineFormSetMixin, BaseInlineFormSet):
|
||||||
|
# def clean(self):
|
||||||
|
# super().clean()
|
||||||
|
|
||||||
|
|
||||||
class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||||
model = ConsistItem
|
model = ConsistItem
|
||||||
min_num = 1
|
min_num = 1
|
||||||
@@ -14,10 +29,13 @@ class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
|||||||
readonly_fields = (
|
readonly_fields = (
|
||||||
"preview",
|
"preview",
|
||||||
"published",
|
"published",
|
||||||
"address",
|
"scale",
|
||||||
"type",
|
"manufacturer",
|
||||||
|
"item_number",
|
||||||
"company",
|
"company",
|
||||||
|
"type",
|
||||||
"era",
|
"era",
|
||||||
|
"address",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +46,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"creation_time",
|
"creation_time",
|
||||||
"updated_time",
|
"updated_time",
|
||||||
)
|
)
|
||||||
list_filter = ("company__name", "era", "published")
|
list_filter = ("company__name", "era", "scale", "published")
|
||||||
list_display = ("__str__",) + list_filter + ("country_flag",)
|
list_display = ("__str__",) + list_filter + ("country_flag",)
|
||||||
search_fields = ("identifier",) + list_filter
|
search_fields = ("identifier",) + list_filter
|
||||||
save_as = True
|
save_as = True
|
||||||
@@ -36,7 +54,7 @@ class ConsistAdmin(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="{}" /> {}'.format(obj.country.flag, obj.country)
|
'<img src="{}" /> {}', obj.country.flag, obj.country
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -46,9 +64,10 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"fields": (
|
"fields": (
|
||||||
"published",
|
"published",
|
||||||
"identifier",
|
"identifier",
|
||||||
"consist_address",
|
|
||||||
"company",
|
"company",
|
||||||
|
"scale",
|
||||||
"era",
|
"era",
|
||||||
|
"consist_address",
|
||||||
"description",
|
"description",
|
||||||
"image",
|
"image",
|
||||||
"tags",
|
"tags",
|
||||||
@@ -70,4 +89,55 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
actions = [publish, unpublish]
|
|
||||||
|
def download_csv(modeladmin, request, queryset):
|
||||||
|
header = [
|
||||||
|
"ID",
|
||||||
|
"Name",
|
||||||
|
"Published",
|
||||||
|
"Company",
|
||||||
|
"Country",
|
||||||
|
"Address",
|
||||||
|
"Scale",
|
||||||
|
"Era",
|
||||||
|
"Description",
|
||||||
|
"Tags",
|
||||||
|
"Length",
|
||||||
|
"Composition",
|
||||||
|
"Item name",
|
||||||
|
"Item type",
|
||||||
|
"Item ID",
|
||||||
|
]
|
||||||
|
data = []
|
||||||
|
for obj in queryset:
|
||||||
|
for item in obj.consist_item.all():
|
||||||
|
types = " + ".join(
|
||||||
|
"{}x {}".format(t["count"], t["type"])
|
||||||
|
for t in obj.get_type_count()
|
||||||
|
)
|
||||||
|
data.append(
|
||||||
|
[
|
||||||
|
obj.uuid,
|
||||||
|
obj.__str__(),
|
||||||
|
"X" if obj.published else "",
|
||||||
|
obj.company.name,
|
||||||
|
obj.company.country,
|
||||||
|
obj.consist_address,
|
||||||
|
obj.scale.scale,
|
||||||
|
obj.era,
|
||||||
|
html.unescape(strip_tags(obj.description)),
|
||||||
|
settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
t.name for t in obj.tags.all()
|
||||||
|
),
|
||||||
|
obj.length,
|
||||||
|
types,
|
||||||
|
item.rolling_stock.__str__(),
|
||||||
|
item.type,
|
||||||
|
item.rolling_stock.uuid,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return generate_csv(header, data, "consists.csv")
|
||||||
|
download_csv.short_description = "Download selected items as CSV"
|
||||||
|
|
||||||
|
actions = [publish, unpublish, download_csv]
|
||||||
|
|||||||
42
ram/consist/migrations/0017_consist_scale.py
Normal file
42
ram/consist/migrations/0017_consist_scale.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-05-01 09:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def set_scale(apps, schema_editor):
|
||||||
|
Consist = apps.get_model("consist", "Consist")
|
||||||
|
|
||||||
|
for consist in Consist.objects.all():
|
||||||
|
try:
|
||||||
|
consist.scale = consist.consist_item.first().rolling_stock.scale
|
||||||
|
consist.save()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0016_alter_consistitem_order"),
|
||||||
|
(
|
||||||
|
"metadata",
|
||||||
|
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="consist",
|
||||||
|
name="scale",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="metadata.scale",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
set_scale,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
||||||
25
ram/consist/migrations/0018_alter_consist_scale.py
Normal file
25
ram/consist/migrations/0018_alter_consist_scale.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-05-02 11:33
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0017_consist_scale"),
|
||||||
|
(
|
||||||
|
"metadata",
|
||||||
|
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="consist",
|
||||||
|
name="scale",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="metadata.scale"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,12 +2,13 @@ import os
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.text import Truncator
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from ram.models import BaseModel
|
from ram.models import BaseModel
|
||||||
from ram.utils import DeduplicatedStorage
|
from ram.utils import DeduplicatedStorage
|
||||||
from metadata.models import Company, Tag
|
from metadata.models import Company, Scale, Tag
|
||||||
from roster.models import RollingStock
|
from roster.models import RollingStock
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ class Consist(BaseModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
help_text="Era or epoch of the consist",
|
help_text="Era or epoch of the consist",
|
||||||
)
|
)
|
||||||
|
scale = models.ForeignKey(Scale, on_delete=models.CASCADE)
|
||||||
image = models.ImageField(
|
image = models.ImageField(
|
||||||
upload_to=os.path.join("images", "consists"),
|
upload_to=os.path.join("images", "consists"),
|
||||||
storage=DeduplicatedStorage,
|
storage=DeduplicatedStorage,
|
||||||
@@ -82,7 +84,19 @@ class ConsistItem(models.Model):
|
|||||||
return "{0}".format(self.rolling_stock)
|
return "{0}".format(self.rolling_stock)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.consist.published and not self.rolling_stock.published:
|
rolling_stock = getattr(self, "rolling_stock", False)
|
||||||
|
if not rolling_stock:
|
||||||
|
return # exit if no inline are present
|
||||||
|
|
||||||
|
# FIXME this does not work when creating a new consist,
|
||||||
|
# because the consist is not saved yet and it must be moved
|
||||||
|
# to the admin form validation via InlineFormSet.clean()
|
||||||
|
consist = self.consist
|
||||||
|
if rolling_stock.scale != consist.scale:
|
||||||
|
raise ValidationError(
|
||||||
|
"The rolling stock and consist must be of the same scale."
|
||||||
|
)
|
||||||
|
if self.consist.published and not rolling_stock.published:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
"You must unpublish the the consist before using this item."
|
"You must unpublish the the consist before using this item."
|
||||||
)
|
)
|
||||||
@@ -94,9 +108,21 @@ class ConsistItem(models.Model):
|
|||||||
def preview(self):
|
def preview(self):
|
||||||
return self.rolling_stock.image.first().image_thumbnail(100)
|
return self.rolling_stock.image.first().image_thumbnail(100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manufacturer(self):
|
||||||
|
return Truncator(self.rolling_stock.manufacturer).chars(10)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def item_number(self):
|
||||||
|
return self.rolling_stock.item_number
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scale(self):
|
||||||
|
return self.rolling_stock.scale
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return self.rolling_stock.rolling_class.type
|
return self.rolling_stock.rolling_class.type.type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address(self):
|
def address(self):
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class CompanyAdmin(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="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class ManufacturerAdmin(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="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-05-04 20:45
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"metadata",
|
||||||
|
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="company",
|
||||||
|
options={"ordering": ["slug"], "verbose_name_plural": "Companies"},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="manufacturer",
|
||||||
|
options={"ordering": ["category", "slug"]},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="tag",
|
||||||
|
options={"ordering": ["slug"]},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -43,7 +43,7 @@ class Manufacturer(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["category", "name"]
|
ordering = ["category", "slug"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -78,7 +78,7 @@ class Company(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "Companies"
|
verbose_name_plural = "Companies"
|
||||||
ordering = ["name"]
|
ordering = ["slug"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -207,7 +207,7 @@ class Tag(models.Model):
|
|||||||
slug = models.CharField(max_length=128, unique=True)
|
slug = models.CharField(max_length=128, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
ordering = ["slug"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
/*!
|
/*!
|
||||||
* Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/)
|
* Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/)
|
||||||
* Copyright 2019-2024 The Bootstrap Authors
|
* Copyright 2019-2024 The Bootstrap Authors
|
||||||
* Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
|
||||||
*/
|
*/
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-display: block;
|
font-display: block;
|
||||||
font-family: "bootstrap-icons";
|
font-family: "bootstrap-icons";
|
||||||
src: url("./fonts/bootstrap-icons.woff2?dd67030699838ea613ee6dbda90effa6") format("woff2"),
|
src: url("./fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a") format("woff2"),
|
||||||
url("./fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("woff");
|
url("./fonts/bootstrap-icons.woff?e34853135f9e39acf64315236852cd5a") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
.bi::before,
|
.bi::before,
|
||||||
@@ -2076,3 +2076,31 @@ url("./fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("wof
|
|||||||
.bi-suitcase2-fill::before { content: "\f901"; }
|
.bi-suitcase2-fill::before { content: "\f901"; }
|
||||||
.bi-suitcase2::before { content: "\f902"; }
|
.bi-suitcase2::before { content: "\f902"; }
|
||||||
.bi-vignette::before { content: "\f903"; }
|
.bi-vignette::before { content: "\f903"; }
|
||||||
|
.bi-bluesky::before { content: "\f7f9"; }
|
||||||
|
.bi-tux::before { content: "\f904"; }
|
||||||
|
.bi-beaker-fill::before { content: "\f905"; }
|
||||||
|
.bi-beaker::before { content: "\f906"; }
|
||||||
|
.bi-flask-fill::before { content: "\f907"; }
|
||||||
|
.bi-flask-florence-fill::before { content: "\f908"; }
|
||||||
|
.bi-flask-florence::before { content: "\f909"; }
|
||||||
|
.bi-flask::before { content: "\f90a"; }
|
||||||
|
.bi-leaf-fill::before { content: "\f90b"; }
|
||||||
|
.bi-leaf::before { content: "\f90c"; }
|
||||||
|
.bi-measuring-cup-fill::before { content: "\f90d"; }
|
||||||
|
.bi-measuring-cup::before { content: "\f90e"; }
|
||||||
|
.bi-unlock2-fill::before { content: "\f90f"; }
|
||||||
|
.bi-unlock2::before { content: "\f910"; }
|
||||||
|
.bi-battery-low::before { content: "\f911"; }
|
||||||
|
.bi-anthropic::before { content: "\f912"; }
|
||||||
|
.bi-apple-music::before { content: "\f913"; }
|
||||||
|
.bi-claude::before { content: "\f914"; }
|
||||||
|
.bi-openai::before { content: "\f915"; }
|
||||||
|
.bi-perplexity::before { content: "\f916"; }
|
||||||
|
.bi-css::before { content: "\f917"; }
|
||||||
|
.bi-javascript::before { content: "\f918"; }
|
||||||
|
.bi-typescript::before { content: "\f919"; }
|
||||||
|
.bi-fork-knife::before { content: "\f91a"; }
|
||||||
|
.bi-globe-americas-fill::before { content: "\f91b"; }
|
||||||
|
.bi-globe-asia-australia-fill::before { content: "\f91c"; }
|
||||||
|
.bi-globe-central-south-asia-fill::before { content: "\f91d"; }
|
||||||
|
.bi-globe-europe-africa-fill::before { content: "\f91e"; }
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
ram/portal/static/bootstrap@5.3.8/dist/css/bootstrap.min.css
vendored
Normal file
6
ram/portal/static/bootstrap@5.3.8/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ram/portal/static/bootstrap@5.3.8/dist/css/bootstrap.min.css.map
vendored
Normal file
1
ram/portal/static/bootstrap@5.3.8/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
ram/portal/static/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js
vendored
Normal file
7
ram/portal/static/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ram/portal/static/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
1
ram/portal/static/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,9 +1,7 @@
|
|||||||
<svg width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" enable-background="accumulate" overflow="visible" stroke-width="2" />
|
<svg width="32" height="16" preserveAspectRatio="xMidYMid" version="1.0" viewBox="0 0 24 12" xmlns="http://www.w3.org/2000/svg">
|
||||||
<style>
|
<metadata>Created by potrace 1.15, written by Peter Selinger 2001-2017</metadata>
|
||||||
path {
|
<g transform="matrix(.0039261 0 0 -.0039261 -1.4249 18.53)">
|
||||||
text-indent:0;
|
<path d="m813 4723-103-4v-309h-355l14-330h369l6-42c39-273 39-1414 0-1659l-7-39h-368l-14-330h355v-318l41-7c23-4 126-7 229-7s206 3 229 7l41 7v318h670v-318l41-7c23-4 126-7 229-7s206 3 229 7l41 7v318h670v-318l37-7c48-9 432-9 472 0l31 7v318h680v-318l31-7c39-9 423-9 469 0l35 6v314l338 3 337 2v-318l38-7c48-9 416-9 465 0l37 7v318h335v2400h-335v307l-135 6c-74 3-196 3-270 0l-135-6v-307l-337 2-338 3v302l-132 6c-73 3-194 3-268 0l-135-6v-307h-680v307l-135 6c-74 3-196 3-270 0l-135-6v-307h-670v307l-135 6c-74 3-196 3-270 0l-135-6v-307h-670v310h-63c-35 0-111 2-168 4s-150 1-206-1zm1141-666c3-12 11-97 18-187 24-309 11-1402-18-1507l-6-23h-725l-7 32c-39 197-39 1454 0 1676l6 32h726zm1218-42c20-182 30-569 25-940-6-371-21-707-33-727-3-4-169-8-368-8h-363l-7 48c-38 277-38 1365 1 1647l6 45 366-2 366-3zm1203 53c39-103 45-1264 9-1660l-7-68h-735l-6 68c-35 381-35 1263 0 1610l6 62h364c283 0 366-3 369-12zm1219-42c37-316 37-1287 0-1628l-7-58h-734l-6 73c-37 424-31 1544 8 1655 3 9 86 12 368 12h364zm841-1686c-336 0-363 1-370 18-3 9-13 152-22 317-21 431-7 1292 23 1388 5 16 31 17 369 17z"/>
|
||||||
text-transform:none;
|
</g>
|
||||||
}
|
</svg>
|
||||||
</style>
|
|
||||||
</svg>
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -16,11 +16,11 @@
|
|||||||
<link rel="icon" href="{% static "favicon.png" %}" sizes="any">
|
<link rel="icon" href="{% static "favicon.png" %}" sizes="any">
|
||||||
<link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml">
|
<link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml">
|
||||||
{% if site_conf.use_cdn %}
|
{% if site_conf.use_cdn %}
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
{% else %}
|
{% else %}
|
||||||
<link href="{% static "bootstrap@5.3.3/dist/css/bootstrap.min.css" %}" rel="stylesheet">
|
<link href="{% static "bootstrap@5.3.8/dist/css/bootstrap.min.css" %}" rel="stylesheet">
|
||||||
<link href="{% static "bootstrap-icons@1.11.3/font/bootstrap-icons.css" %}" rel="stylesheet">
|
<link href="{% static "bootstrap-icons@1.13.1/font/bootstrap-icons.css" %}" rel="stylesheet">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
|
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@@ -140,14 +140,10 @@
|
|||||||
<div class="container d-flex">
|
<div class="container d-flex">
|
||||||
<div class="me-auto">
|
<div class="me-auto">
|
||||||
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
|
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
|
||||||
<svg class="me-2" width="26" height="16" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
|
<svg class="me-2" width="32" height="16" version="1.0" viewBox="0 0 24 12" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" stroke-width="2" />
|
<g transform="matrix(.0039261 0 0 -.0039261 -1.4249 18.53)">
|
||||||
<style>
|
<path d="m813 4723-103-4v-309h-355l14-330h369l6-42c39-273 39-1414 0-1659l-7-39h-368l-14-330h355v-318l41-7c23-4 126-7 229-7s206 3 229 7l41 7v318h670v-318l41-7c23-4 126-7 229-7s206 3 229 7l41 7v318h670v-318l37-7c48-9 432-9 472 0l31 7v318h680v-318l31-7c39-9 423-9 469 0l35 6v314l338 3 337 2v-318l38-7c48-9 416-9 465 0l37 7v318h335v2400h-335v307l-135 6c-74 3-196 3-270 0l-135-6v-307l-337 2-338 3v302l-132 6c-73 3-194 3-268 0l-135-6v-307h-680v307l-135 6c-74 3-196 3-270 0l-135-6v-307h-670v307l-135 6c-74 3-196 3-270 0l-135-6v-307h-670v310h-63c-35 0-111 2-168 4s-150 1-206-1zm1141-666c3-12 11-97 18-187 24-309 11-1402-18-1507l-6-23h-725l-7 32c-39 197-39 1454 0 1676l6 32h726zm1218-42c20-182 30-569 25-940-6-371-21-707-33-727-3-4-169-8-368-8h-363l-7 48c-38 277-38 1365 1 1647l6 45 366-2 366-3zm1203 53c39-103 45-1264 9-1660l-7-68h-735l-6 68c-35 381-35 1263 0 1610l6 62h364c283 0 366-3 369-12zm1219-42c37-316 37-1287 0-1628l-7-58h-734l-6 73c-37 424-31 1544 8 1655 3 9 86 12 368 12h364zm841-1686c-336 0-363 1-370 18-3 9-13 152-22 317-21 431-7 1292 23 1388 5 16 31 17 369 17z"/>
|
||||||
path {
|
</g>
|
||||||
text-indent:0;
|
|
||||||
text-transform:none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</svg>
|
</svg>
|
||||||
<strong>{{ site_conf.site_name }}</strong>
|
<strong>{{ site_conf.site_name }}</strong>
|
||||||
</a>
|
</a>
|
||||||
@@ -215,12 +211,13 @@
|
|||||||
<div class="container">{% block pagination %}{% endblock %}</div>
|
<div class="container">{% block pagination %}{% endblock %}</div>
|
||||||
</div>
|
</div>
|
||||||
{% block extra_content %}{% endblock %}
|
{% block extra_content %}{% endblock %}
|
||||||
|
{% include 'includes/symbols.html' %}
|
||||||
</main>
|
</main>
|
||||||
{% include 'includes/footer.html' %}
|
{% include 'includes/footer.html' %}
|
||||||
{% if site_conf.use_cdn %}
|
{% if site_conf.use_cdn %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<script src="{% static "bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" %}"></script>
|
<script src="{% static "bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" %}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not book.published %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span> |
|
||||||
|
{% endif %}
|
||||||
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
@@ -60,11 +63,6 @@
|
|||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">
|
||||||
{% if type == "catalog" %}Catalog
|
{% if type == "catalog" %}Catalog
|
||||||
{% elif type == "book" %}Book{% endif %}
|
{% elif type == "book" %}Book{% endif %}
|
||||||
<div class="float-end">
|
|
||||||
{% if not book.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -91,7 +89,30 @@
|
|||||||
</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="{{ book.publisher.country.flag }}" alt="{{ book.publisher.country }}"> {{ book.publisher }}
|
||||||
|
{% if book.publisher.website %} <a href="{{ book.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% elif type == "magazineissue" %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Magazine</th>
|
||||||
|
<td><a href="{% url 'magazine' book.magazine.pk %}">{{ book.magazine }}</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Publisher</th>
|
||||||
|
<td>
|
||||||
|
<img src="{{ book.publisher.country.flag }}" alt="{{ book.publisher.country }}"> {{ book.publisher }}
|
||||||
|
{% if book.publisher.website %} <a href="{{ book.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Issue</th>
|
||||||
|
<td>{{ book.issue_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Date</th>
|
||||||
|
<td>{{ book.publication_year|default:"-" }} / {{ book.publication_month|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -106,10 +127,12 @@
|
|||||||
<th scope="row">Number of pages</th>
|
<th scope="row">Number of pages</th>
|
||||||
<td>{{ book.number_of_pages|default:"-" }}</td>
|
<td>{{ book.number_of_pages|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if type == "boook" or type == "catalog" %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Publication year</th>
|
<th scope="row">Publication year</th>
|
||||||
<td>{{ book.publication_year|default:"-" }}</td>
|
<td>{{ book.publication_year|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% if book.description %}
|
{% if book.description %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Description</th>
|
<th class="w-33" scope="row">Description</th>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
{% include "cards/consist.html" %}
|
{% include "cards/consist.html" %}
|
||||||
{% elif d.type == "manufacturer" %}
|
{% elif d.type == "manufacturer" %}
|
||||||
{% include "cards/manufacturer.html" %}
|
{% include "cards/manufacturer.html" %}
|
||||||
|
{% elif d.type == "magazine" or d.type == "magazineissue" %}
|
||||||
|
{% include "cards/magazine.html" %}
|
||||||
{% elif d.type == "book" or d.type == "catalog" %}
|
{% elif d.type == "book" or d.type == "catalog" %}
|
||||||
{% include "cards/book.html" %}
|
{% include "cards/book.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -24,11 +24,10 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">
|
||||||
{% if d.type == "catalog" %}Catalog
|
{{ d.type | capfirst }}
|
||||||
{% elif d.type == "book" %}Book{% endif %}
|
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
{% if not d.item.published %}
|
{% if not d.item.published %}
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
<span class="badge text-bg-warning">Unpublished</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
@@ -53,7 +52,7 @@
|
|||||||
</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.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -39,8 +39,10 @@
|
|||||||
</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="{% url 'filtered' _filter="company" search=d.item.slug %}">Show all rolling stock</a>
|
{% with items=d.item.num_items %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="company" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,12 +27,12 @@
|
|||||||
<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 %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span>
|
||||||
|
{% endif %}
|
||||||
{% if d.item.company.freelance %}
|
{% if d.item.company.freelance %}
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
<span class="badge text-bg-secondary">Freelance</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not d.item.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</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.item.get_absolute_url }}">Show all data</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
97
ram/portal/templates/cards/magazine.html
Normal file
97
ram/portal/templates/cards/magazine.html
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{% load static %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
{% if d.type == "magazine" %}
|
||||||
|
<a href="{{ d.item.get_absolute_url }}">
|
||||||
|
{% if d.item.image and d.type == "magazine" %}
|
||||||
|
<img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}">
|
||||||
|
{% elif d.item.issue.first.image.exists %}
|
||||||
|
{% with d.item.issue.first as i %}
|
||||||
|
<img class="card-img-top" src="{{ i.image.first.image.url }}" alt="{{ d.item }}">
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
||||||
|
<img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% elif d.type == "magazineissue" %}
|
||||||
|
<a href="{{ d.item.get_absolute_url }}">
|
||||||
|
{% if d.item.image.exists %}
|
||||||
|
<img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}">
|
||||||
|
{% else %}
|
||||||
|
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
||||||
|
<img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text" style="position: relative;">
|
||||||
|
<strong>{{ d.item }}</strong>
|
||||||
|
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
|
||||||
|
</p>
|
||||||
|
{% if d.item.tags.all %}
|
||||||
|
<p class="card-text"><small>Tags:</small>
|
||||||
|
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
||||||
|
{{ t.name }}</a>{# new line is required #}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" scope="row">
|
||||||
|
{{ d.type | capfirst }}
|
||||||
|
<div class="float-end">
|
||||||
|
{% if not d.item.published %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
{% if d.type == "magazineissue" %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Magazine</th>
|
||||||
|
<td>{{ d.item.magazine }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Publisher</th>
|
||||||
|
<td>
|
||||||
|
<img src="{{ d.item.publisher.country.flag }}" alt="{{ d.item.publisher.country }}"> {{ d.item.publisher }}
|
||||||
|
{% if d.item.publisher.website %} <a href="{{ d.item.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if d.type == "magazineissue" %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Issue</th>
|
||||||
|
<td>{{ d.item.issue_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Date</th>
|
||||||
|
<td>{{ d.item.publication_year|default:"-" }} / {{ d.item.publication_month|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Pages</th>
|
||||||
|
<td>{{ d.item.number_of_pages|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Language</th>
|
||||||
|
<td>{{ d.item.get_language_display }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
|
{% if d.type == "magazine" %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary{% if d.item.issues == 0 %} disabled{% endif %}" href="{{ d.item.get_absolute_url }}">Show {{ d.item.issues }} issue{{ d.item.issues|pluralize }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:bookshelf_magazineissue_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -30,8 +30,10 @@
|
|||||||
</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="{% url 'filtered' _filter="manufacturer" search=d.item.slug %}">Show all rolling stock</a>
|
{% with items=d.item.num_items %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="manufacturer" search=d.item.slug %}">Show {{ items }} item{{ items|pluralize }}</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
</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="{% url 'filtered' _filter="type" search=d.item.slug %}">Show all rolling stock</a>
|
{% with items=d.item.num_items %}
|
||||||
{% 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 %}
|
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="type" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
||||||
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_rollingstocktype_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load dcc %}
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
{% if d.item.image.exists %}
|
{% if d.item.image.exists %}
|
||||||
@@ -25,12 +27,12 @@
|
|||||||
<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 %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span>
|
||||||
|
{% endif %}
|
||||||
{% if d.item.company.freelance %}
|
{% if d.item.company.freelance %}
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
<span class="badge text-bg-secondary">Freelance</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not d.item.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -73,33 +75,12 @@
|
|||||||
<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.item_number }}{%if d.item.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=d.item.manufacturer.slug search=d.item.item_number_slug %}">SET</a>{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if d.item.decoder or d.item.decoder_interface %}
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2" scope="row">DCC data</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">Interface</th>
|
|
||||||
<td>{{ d.item.get_decoder_interface }}</td>
|
|
||||||
</tr>
|
|
||||||
{% if d.item.decoder %}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Decoder</th>
|
|
||||||
<td>{{ d.item.decoder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Address</th>
|
|
||||||
<td>{{ d.item.address }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
<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.item.get_absolute_url}}">Show all data</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.item.pk %}">Edit</a>{% endif %}
|
{% 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 %}
|
||||||
|
|||||||
@@ -28,8 +28,10 @@
|
|||||||
</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{% if d.item.num_items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show all rolling stock</a>
|
{% with items=d.item.num_items %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
|
{% if not consist.published %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span> |
|
||||||
|
{% endif %}
|
||||||
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -83,9 +86,6 @@
|
|||||||
{% if consist.company.freelance %}
|
{% if consist.company.freelance %}
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
<span class="badge text-bg-secondary">Freelance</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not consist.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -109,7 +109,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Length</th>
|
<th scope="row">Length</th>
|
||||||
<td>{{ consist.length }}: {% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} + {% endif %}{% endfor %}</td>
|
<td>{{ consist.length }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Composition</th>
|
||||||
|
<td>{% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} » {% endif %}{% endfor %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
|
{% if not flatpage.published %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span> |
|
||||||
|
{% endif %}
|
||||||
<small class="text-body-secondary">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-body-secondary">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
@@ -9,11 +12,6 @@
|
|||||||
<section class="py-4 text-start container">
|
<section class="py-4 text-start container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
{% if not flatpage.published %}
|
|
||||||
<div class="alert alert-warning" role="alert">
|
|
||||||
⚠️ This page is a <strong>draft</strong> and is not published.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div>{{ flatpage.content | safe }} </div>
|
<div>{{ flatpage.content | safe }} </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="{% url 'admin:portal_flatpage_change' flatpage.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:portal_flatpage_change' flatpage.pk %}">Edit</a>{% endif %}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div class="modal fade" id="disclaimerModal" tabindex="-1" aria-labelledby="disclaimerLabel" aria-hidden="true">
|
<div class="modal fade" id="disclaimerModal" tabindex="-1" aria-labelledby="disclaimerLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h1 class="modal-title fs-5" id="disclaimerLabel">Disclaimer</h1>
|
<h1 class="modal-title fs-5" id="disclaimerLabel">Disclaimer</h1>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
39
ram/portal/templates/includes/symbols.html
Normal file
39
ram/portal/templates/includes/symbols.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!-- Modal -->
|
||||||
|
<div class="modal fade" id="symbolsModal" tabindex="-1" aria-labelledby="symbolsLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="symbolsLabel">Symbols</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" scope="row">DCC symbols</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-center"><i class="bi bi-ban small"></i></th>
|
||||||
|
<td>No socket</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-center"><i class="bi bi-dice-6 small"></i></th>
|
||||||
|
<td>Socket available</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-center"><i class="bi bi-arrow-bar-left"></i><i class="bi bi-cpu-fill small"></i></th>
|
||||||
|
<td>Decoder installed</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-center"><i class="bi bi-arrow-bar-left"></i><i class="bi bi-volume-up-fill small"></i></th>
|
||||||
|
<td>Sound decoder installed</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
120
ram/portal/templates/magazine.html
Normal file
120
ram/portal/templates/magazine.html
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
{% 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_pagination' uuid=magazine.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<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_pagination' uuid=magazine.uuid page=i %}#main-content">{{ i }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if data.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'magazine_pagination' uuid=magazine.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<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 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 %}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load dcc %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if rolling_stock.tags.all %}
|
{% if rolling_stock.tags.all %}
|
||||||
@@ -8,6 +9,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not rolling_stock.published %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span> |
|
||||||
|
{% endif %}
|
||||||
<small class="text-body-secondary">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-body-secondary">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
@@ -48,7 +52,7 @@
|
|||||||
<button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model</button>
|
<button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model</button>
|
||||||
<button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class</button>
|
<button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class</button>
|
||||||
<button class="nav-link" id="nav-company-tab" data-bs-toggle="tab" data-bs-target="#nav-company" type="button" role="tab" aria-controls="nav-company" aria-selected="false">Company</button>
|
<button class="nav-link" id="nav-company-tab" data-bs-toggle="tab" data-bs-target="#nav-company" type="button" role="tab" aria-controls="nav-company" aria-selected="false">Company</button>
|
||||||
{% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %}
|
{% if rolling_stock.decoder or rolling_stock.decoder_interface %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %}
|
||||||
{% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
|
{% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
|
||||||
{% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
|
{% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
|
||||||
{% if set %}<button class="nav-link" id="nav-set-tab" data-bs-toggle="tab" data-bs-target="#nav-set" type="button" role="tab" aria-controls="nav-set" aria-selected="false">Set</button>{% endif %}
|
{% if set %}<button class="nav-link" id="nav-set-tab" data-bs-toggle="tab" data-bs-target="#nav-set" type="button" role="tab" aria-controls="nav-set" aria-selected="false">Set</button>{% endif %}
|
||||||
@@ -59,7 +63,7 @@
|
|||||||
<option value="nav-model">Model</option>
|
<option value="nav-model">Model</option>
|
||||||
<option value="nav-class">Class</option>
|
<option value="nav-class">Class</option>
|
||||||
<option value="nav-company">Company</option>
|
<option value="nav-company">Company</option>
|
||||||
{% if rolling_stock.decoder %}<option value="nav-dcc">DCC</option>{% endif %}
|
{% if rolling_stock.decoder or rolling_stock.decoder_interface %}<option value="nav-dcc">DCC</option>{% endif %}
|
||||||
{% if documents or decoder_documents %}<option value="nav-documents">Documents</option>{% endif %}
|
{% if documents or decoder_documents %}<option value="nav-documents">Documents</option>{% endif %}
|
||||||
{% if journal %}<option value="nav-journal">Journal</option>{% endif %}
|
{% if journal %}<option value="nav-journal">Journal</option>{% endif %}
|
||||||
{% if set %}<option value="nav-set">Set</option>{% endif %}
|
{% if set %}<option value="nav-set">Set</option>{% endif %}
|
||||||
@@ -77,9 +81,6 @@
|
|||||||
{% if company.freelance %}
|
{% if company.freelance %}
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
<span class="badge text-bg-secondary">Freelance</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not rolling_stock.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -142,7 +143,9 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">DCC data</th>
|
<th colspan="2" scope="row">DCC data
|
||||||
|
<a class="mt-1 float-end text-reset text-decoration-none" title="Symbols" href="" data-bs-toggle="modal" data-bs-target="#symbolsModal">{% dcc rolling_stock %}</a>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -155,6 +158,16 @@
|
|||||||
<th scope="row">Decoder</th>
|
<th scope="row">Decoder</th>
|
||||||
<td>{{ rolling_stock.decoder }}</td>
|
<td>{{ rolling_stock.decoder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Sound</th>
|
||||||
|
<td>
|
||||||
|
{% if rolling_stock.decoder.sound %}
|
||||||
|
<i class="bi bi-check-circle-fill text-success"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-x-circle-fill text-secondary"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Address</th>
|
<th scope="row">Address</th>
|
||||||
<td>{{ rolling_stock.address }}</td>
|
<td>{{ rolling_stock.address }}</td>
|
||||||
@@ -339,36 +352,55 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">Decoder data</th>
|
<th colspan="2" scope="row">Decoder data
|
||||||
|
<a class="mt-1 float-end text-reset text-decoration-none" title="Symbols" href="" data-bs-toggle="modal" data-bs-target="#symbolsModal">{% dcc rolling_stock %}</a>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Interface</th>
|
<th class="w-33" scope="row">Interface</th>
|
||||||
<td>{{ rolling_stock.get_decoder_interface }}</td>
|
<td>{{ rolling_stock.get_decoder_interface }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if rolling_stock.decoder %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Address</th>
|
<th scope="row">Manufacturer</th>
|
||||||
<td>{{ rolling_stock.address }}</td>
|
<td>{{ rolling_stock.decoder.manufacturer | default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Name</th>
|
<th scope="row">Name</th>
|
||||||
<td>{{ rolling_stock.decoder.name }}</td>
|
<td>{{ rolling_stock.decoder.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">Manufacturer</th>
|
|
||||||
<td>{{ rolling_stock.decoder.manufacturer | default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Version</th>
|
<th scope="row">Version</th>
|
||||||
<td>{{ rolling_stock.decoder.version | default:"-"}}</td>
|
<td>{{ rolling_stock.decoder.version | default:"-"}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Sound</th>
|
<th scope="row">Sound</th>
|
||||||
<td>{{ rolling_stock.decoder.sound | yesno:"Yes,No" }}</td>
|
<td>
|
||||||
|
{% if rolling_stock.decoder.sound %}
|
||||||
|
<i class="bi bi-check-circle-fill text-success"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-x-circle-fill text-secondary"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" scope="row">Decoder configuration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Address</th>
|
||||||
|
<td>{{ rolling_stock.address }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
|
<div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
|
||||||
{% if documents %}
|
{% if documents %}
|
||||||
|
|||||||
38
ram/portal/templatetags/dcc.py
Normal file
38
ram/portal/templatetags/dcc.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from django import template
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def dcc(object):
|
||||||
|
socket = mark_safe(
|
||||||
|
'<i class="bi bi-ban small"></i>'
|
||||||
|
)
|
||||||
|
decoder = ''
|
||||||
|
if object.decoder_interface is not None:
|
||||||
|
socket = mark_safe(
|
||||||
|
f'<abbr title="{object.get_decoder_interface()}">'
|
||||||
|
f'<i class="bi bi-dice-6"></i></abbr>'
|
||||||
|
)
|
||||||
|
if object.decoder:
|
||||||
|
if object.decoder.sound:
|
||||||
|
decoder = mark_safe(
|
||||||
|
f'<abbr title="{object.decoder}">'
|
||||||
|
'<i class="bi bi-volume-up-fill"></i></abbr>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
decoder = mark_safe(
|
||||||
|
f'<abbr title="{object.decoder}'
|
||||||
|
f'({object.get_decoder_interface()})">'
|
||||||
|
'<i class="bi bi-cpu-fill"></i></abbr>'
|
||||||
|
)
|
||||||
|
if decoder:
|
||||||
|
return format_html(
|
||||||
|
'{} <i class="bi bi-arrow-bar-left"></i> {}',
|
||||||
|
socket,
|
||||||
|
decoder,
|
||||||
|
)
|
||||||
|
|
||||||
|
return socket
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ from portal.views import (
|
|||||||
Types,
|
Types,
|
||||||
Books,
|
Books,
|
||||||
Catalogs,
|
Catalogs,
|
||||||
|
Magazines,
|
||||||
|
GetMagazine,
|
||||||
|
GetMagazineIssue,
|
||||||
GetBookCatalog,
|
GetBookCatalog,
|
||||||
SearchObjects,
|
SearchObjects,
|
||||||
)
|
)
|
||||||
@@ -98,6 +101,31 @@ urlpatterns = [
|
|||||||
Books.as_view(),
|
Books.as_view(),
|
||||||
name="books_pagination"
|
name="books_pagination"
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"bookshelf/magazine/<uuid:uuid>",
|
||||||
|
GetMagazine.as_view(),
|
||||||
|
name="magazine"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"bookshelf/magazine/<uuid:uuid>/page/<int:page>",
|
||||||
|
GetMagazine.as_view(),
|
||||||
|
name="magazine_pagination",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"bookshelf/magazine/<uuid:magazine>/issue/<uuid:uuid>",
|
||||||
|
GetMagazineIssue.as_view(),
|
||||||
|
name="issue",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"bookshelf/magazines",
|
||||||
|
Magazines.as_view(),
|
||||||
|
name="magazines"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"bookshelf/magazines/page/<int:page>",
|
||||||
|
Magazines.as_view(),
|
||||||
|
name="magazines_pagination"
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"bookshelf/<str:selector>/<uuid:uuid>",
|
"bookshelf/<str:selector>/<uuid:uuid>",
|
||||||
GetBookCatalog.as_view(),
|
GetBookCatalog.as_view(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from urllib.parse import unquote
|
|||||||
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 Q, Count
|
from django.db.models import F, Q, Count
|
||||||
from django.shortcuts import render, get_object_or_404, get_list_or_404
|
from django.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 +16,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,
|
||||||
@@ -73,7 +73,8 @@ class GetData(View):
|
|||||||
.filter(self.filter)
|
.filter(self.filter)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, page=1):
|
def get(self, request, filter=Q(), page=1):
|
||||||
|
self.filter = filter
|
||||||
data = []
|
data = []
|
||||||
for item in self.get_data(request):
|
for item in self.get_data(request):
|
||||||
data.append({"type": self.item_type, "item": item})
|
data.append({"type": self.item_type, "item": item})
|
||||||
@@ -490,7 +491,41 @@ class Manufacturers(GetData):
|
|||||||
item_type = "manufacturer"
|
item_type = "manufacturer"
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return Manufacturer.objects.filter(self.filter)
|
return (
|
||||||
|
Manufacturer.objects.filter(self.filter)
|
||||||
|
.annotate(
|
||||||
|
num_rollingstock=(
|
||||||
|
Count(
|
||||||
|
"rollingstock",
|
||||||
|
filter=Q(
|
||||||
|
rollingstock__in=(
|
||||||
|
RollingStock.objects.get_published(
|
||||||
|
request.user
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
num_rollingclass=(
|
||||||
|
Count(
|
||||||
|
"rollingclass__rolling_stock",
|
||||||
|
filter=Q(
|
||||||
|
rollingclass__rolling_stock__in=(
|
||||||
|
RollingStock.objects.get_published(
|
||||||
|
request.user
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(num_items=F("num_rollingstock") + F("num_rollingclass"))
|
||||||
|
.order_by("name")
|
||||||
|
)
|
||||||
|
|
||||||
# overload get method to filter by category
|
# overload get method to filter by category
|
||||||
def get(self, request, category, page=1):
|
def get(self, request, category, page=1):
|
||||||
@@ -506,18 +541,67 @@ class Companies(GetData):
|
|||||||
item_type = "company"
|
item_type = "company"
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return Company.objects.all()
|
return (
|
||||||
|
Company.objects.annotate(
|
||||||
|
num_rollingstock=(
|
||||||
|
Count(
|
||||||
|
"rollingclass__rolling_stock",
|
||||||
|
filter=Q(
|
||||||
|
rollingclass__rolling_stock__in=(
|
||||||
|
RollingStock.objects.get_published(
|
||||||
|
request.user
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
num_consists=(
|
||||||
|
Count(
|
||||||
|
"consist",
|
||||||
|
filter=Q(
|
||||||
|
consist__in=(
|
||||||
|
Consist.objects.get_published(request.user)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(num_items=F("num_rollingstock") + F("num_consists"))
|
||||||
|
.order_by("name")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Scales(GetData):
|
class Scales(GetData):
|
||||||
title = "Scales"
|
title = "Scales"
|
||||||
item_type = "scale"
|
item_type = "scale"
|
||||||
queryset = Scale.objects.all()
|
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return Scale.objects.annotate(
|
return (
|
||||||
num_items=Count("rollingstock")
|
Scale.objects.annotate(
|
||||||
) # .filter(num_items__gt=0) to filter data with no items
|
num_rollingstock=Count(
|
||||||
|
"rollingstock",
|
||||||
|
filter=Q(
|
||||||
|
rollingstock__in=RollingStock.objects.get_published(
|
||||||
|
request.user
|
||||||
|
)
|
||||||
|
),
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
num_consists=Count(
|
||||||
|
"consist",
|
||||||
|
filter=Q(
|
||||||
|
consist__in=Consist.objects.get_published(request.user)
|
||||||
|
),
|
||||||
|
distinct=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(num_items=F("num_rollingstock") + F("num_consists"))
|
||||||
|
.order_by("-ratio_int", "-tracks", "scale")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Types(GetData):
|
class Types(GetData):
|
||||||
@@ -525,7 +609,16 @@ class Types(GetData):
|
|||||||
item_type = "rolling_stock_type"
|
item_type = "rolling_stock_type"
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return RollingStockType.objects.all()
|
return RollingStockType.objects.annotate(
|
||||||
|
num_items=Count(
|
||||||
|
"rollingclass__rolling_stock",
|
||||||
|
filter=Q(
|
||||||
|
rollingclass__rolling_stock__in=(
|
||||||
|
RollingStock.objects.get_published(request.user)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).order_by("order")
|
||||||
|
|
||||||
|
|
||||||
class Books(GetData):
|
class Books(GetData):
|
||||||
@@ -544,6 +637,85 @@ class Catalogs(GetData):
|
|||||||
return Catalog.objects.get_published(request.user).all()
|
return Catalog.objects.get_published(request.user).all()
|
||||||
|
|
||||||
|
|
||||||
|
class Magazines(GetData):
|
||||||
|
title = "Magazines"
|
||||||
|
item_type = "magazine"
|
||||||
|
|
||||||
|
def get_data(self, request):
|
||||||
|
return (
|
||||||
|
Magazine.objects.get_published(request.user)
|
||||||
|
.all()
|
||||||
|
.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 = [
|
||||||
|
{
|
||||||
|
"type": "magazineissue",
|
||||||
|
"item": i,
|
||||||
|
}
|
||||||
|
for i in magazine.issue.get_published(request.user).all()
|
||||||
|
]
|
||||||
|
paginator = Paginator(data, get_items_per_page())
|
||||||
|
data = paginator.get_page(page)
|
||||||
|
page_range = paginator.get_elided_page_range(
|
||||||
|
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,
|
||||||
|
"book": issue,
|
||||||
|
"documents": documents,
|
||||||
|
"properties": properties,
|
||||||
|
"type": "magazineissue",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
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":
|
||||||
@@ -569,7 +741,7 @@ class GetBookCatalog(View):
|
|||||||
"book": book,
|
"book": book,
|
||||||
"documents": documents,
|
"documents": documents,
|
||||||
"properties": properties,
|
"properties": properties,
|
||||||
"type": selector
|
"type": selector,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from ram.utils import git_suffix
|
from ram.utils import git_suffix
|
||||||
|
|
||||||
__version__ = "0.17.5"
|
__version__ = "0.18.1"
|
||||||
__version__ += git_suffix(__file__)
|
__version__ += git_suffix(__file__)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class Document(models.Model):
|
|||||||
description = models.CharField(max_length=128, blank=True)
|
description = models.CharField(max_length=128, blank=True)
|
||||||
file = models.FileField(
|
file = models.FileField(
|
||||||
upload_to="files/",
|
upload_to="files/",
|
||||||
|
storage=DeduplicatedStorage,
|
||||||
)
|
)
|
||||||
creation_time = models.DateTimeField(auto_now_add=True)
|
creation_time = models.DateTimeField(auto_now_add=True)
|
||||||
updated_time = models.DateTimeField(auto_now=True)
|
updated_time = models.DateTimeField(auto_now=True)
|
||||||
|
|||||||
@@ -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 "
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class DeduplicatedStorage(FileSystemStorage):
|
|||||||
"""
|
"""
|
||||||
A derived FileSystemStorage class that compares already existing files
|
A derived FileSystemStorage class that compares already existing files
|
||||||
(with the same name) with new uploaded ones and stores new file only if
|
(with the same name) with new uploaded ones and stores new file only if
|
||||||
sha256 hash on is content is different
|
sha256 hash on its content is different
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def save(self, name, content, max_length=None):
|
def save(self, name, content, max_length=None):
|
||||||
@@ -48,8 +48,9 @@ def git_suffix(fname):
|
|||||||
|
|
||||||
def get_image_preview(url, max_size=150):
|
def get_image_preview(url, max_size=150):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<img src="{src}" style="max-width: {size}px; max-height: {size}px;'
|
'<img src="{src}" style="max-width: {size}px; max-height: {size}px; background-color: #eee;" />', # noqa: E501
|
||||||
'background-color: #eee;" />'.format(src=url, size=max_size)
|
src=url,
|
||||||
|
size=max_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-10-28 21:17
|
||||||
|
|
||||||
|
import ram.utils
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("repository", "0002_invoicedocument_remove_basebookdocument_book_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="bookdocument",
|
||||||
|
name="file",
|
||||||
|
field=models.FileField(
|
||||||
|
storage=ram.utils.DeduplicatedStorage, upload_to="files/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="catalogdocument",
|
||||||
|
name="file",
|
||||||
|
field=models.FileField(
|
||||||
|
storage=ram.utils.DeduplicatedStorage, upload_to="files/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="decoderdocument",
|
||||||
|
name="file",
|
||||||
|
field=models.FileField(
|
||||||
|
storage=ram.utils.DeduplicatedStorage, upload_to="files/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="genericdocument",
|
||||||
|
name="file",
|
||||||
|
field=models.FileField(
|
||||||
|
storage=ram.utils.DeduplicatedStorage, upload_to="files/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rollingstockdocument",
|
||||||
|
name="file",
|
||||||
|
field=models.FileField(
|
||||||
|
storage=ram.utils.DeduplicatedStorage, upload_to="files/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
65
ram/repository/migrations/0004_magazineissuedocument.py
Normal file
65
ram/repository/migrations/0004_magazineissuedocument.py
Normal 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"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -2,7 +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.utils.html import format_html, strip_tags
|
from django.utils.html import format_html, format_html_join, strip_tags
|
||||||
|
|
||||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ class RollingClass(admin.ModelAdmin):
|
|||||||
search_fields = (
|
search_fields = (
|
||||||
"identifier",
|
"identifier",
|
||||||
"company__name",
|
"company__name",
|
||||||
|
"company__slug",
|
||||||
"type__type",
|
"type__type",
|
||||||
)
|
)
|
||||||
save_as = True
|
save_as = True
|
||||||
@@ -43,7 +44,7 @@ class RollingClass(admin.ModelAdmin):
|
|||||||
@admin.display(description="Country")
|
@admin.display(description="Country")
|
||||||
def country_flag(self, obj):
|
def country_flag(self, obj):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country)
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -139,7 +140,9 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
search_fields = (
|
search_fields = (
|
||||||
"rolling_class__identifier",
|
"rolling_class__identifier",
|
||||||
"rolling_class__company__name",
|
"rolling_class__company__name",
|
||||||
|
"rolling_class__company__slug",
|
||||||
"manufacturer__name",
|
"manufacturer__name",
|
||||||
|
"scale__scale",
|
||||||
"road_number",
|
"road_number",
|
||||||
"address",
|
"address",
|
||||||
"item_number",
|
"item_number",
|
||||||
@@ -149,7 +152,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
@admin.display(description="Country")
|
@admin.display(description="Country")
|
||||||
def country_flag(self, obj):
|
def country_flag(self, obj):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country)
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -219,19 +222,23 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
@admin.display(description="Invoices")
|
@admin.display(description="Invoices")
|
||||||
def invoices(self, obj):
|
def invoices(self, obj):
|
||||||
if obj.invoice.exists():
|
if obj.invoice.exists():
|
||||||
html = "<br>".join(
|
html = format_html_join(
|
||||||
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
|
"<br>",
|
||||||
i.file.url, i
|
"<a href=\"{}\" target=\"_blank\">{}</a>",
|
||||||
) for i in obj.invoice.all())
|
((i.file.url, i) for i in obj.invoice.all())
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
html = "-"
|
html = "-"
|
||||||
return format_html(html)
|
return html
|
||||||
|
|
||||||
def download_csv(modeladmin, request, queryset):
|
def download_csv(modeladmin, request, queryset):
|
||||||
header = [
|
header = [
|
||||||
|
"ID",
|
||||||
"Name",
|
"Name",
|
||||||
|
"Class",
|
||||||
|
"Type",
|
||||||
"Company",
|
"Company",
|
||||||
"Identifier",
|
"Country",
|
||||||
"Road Number",
|
"Road Number",
|
||||||
"Manufacturer",
|
"Manufacturer",
|
||||||
"Scale",
|
"Scale",
|
||||||
@@ -258,9 +265,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
data.append(
|
data.append(
|
||||||
[
|
[
|
||||||
|
obj.uuid,
|
||||||
obj.__str__(),
|
obj.__str__(),
|
||||||
obj.rolling_class.company.name,
|
|
||||||
obj.rolling_class.identifier,
|
obj.rolling_class.identifier,
|
||||||
|
obj.rolling_class.type,
|
||||||
|
obj.rolling_class.company.name,
|
||||||
|
obj.rolling_class.company.country,
|
||||||
obj.road_number,
|
obj.road_number,
|
||||||
obj.manufacturer.name,
|
obj.manufacturer.name,
|
||||||
obj.scale.scale,
|
obj.scale.scale,
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-05-04 17:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("roster", "0036_delete_rollingstockdocument"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rollingstock",
|
||||||
|
name="road_number_int",
|
||||||
|
field=models.PositiveIntegerField(default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-05-24 12:56
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("roster", "0037_alter_rollingstock_road_number_int"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rollingstock",
|
||||||
|
name="rolling_class",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="rolling_stock",
|
||||||
|
to="roster.rollingclass",
|
||||||
|
verbose_name="Class",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -63,11 +63,11 @@ class RollingStock(BaseModel):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
related_name="rolling_class",
|
related_name="rolling_stock",
|
||||||
verbose_name="Class",
|
verbose_name="Class",
|
||||||
)
|
)
|
||||||
road_number = models.CharField(max_length=128, unique=False)
|
road_number = models.CharField(max_length=128, unique=False)
|
||||||
road_number_int = models.PositiveSmallIntegerField(default=0, unique=False)
|
road_number_int = models.PositiveIntegerField(default=0, unique=False)
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@@ -135,9 +135,23 @@ class RollingStock(BaseModel):
|
|||||||
def get_decoder_interface(self):
|
def get_decoder_interface(self):
|
||||||
return str(
|
return str(
|
||||||
dict(settings.DECODER_INTERFACES).get(self.decoder_interface)
|
dict(settings.DECODER_INTERFACES).get(self.decoder_interface)
|
||||||
or "-"
|
or "No interface"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def dcc(self):
|
||||||
|
if self.decoder:
|
||||||
|
dcc = (
|
||||||
|
'<i class="bi bi-volume-up-fill"></i>'
|
||||||
|
if self.decoder.sound
|
||||||
|
else '<i class="bi bi-cpu-fill"></i>'
|
||||||
|
)
|
||||||
|
dcc = f'<abbr title="{self.decoder} ({self.get_decoder_interface()})">{dcc}</abbr>' # noqa: E501
|
||||||
|
elif self.decoder_interface:
|
||||||
|
dcc = f'<abbr title="{self.get_decoder_interface()}"><i class="bi bi-cpu"></i></abbr>' # noqa: E501
|
||||||
|
else:
|
||||||
|
dcc = f'<abbr title="{self.get_decoder_interface()}"><i class="bi bi-ban"></i></abbr>' # noqa: E501
|
||||||
|
return dcc
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def country(self):
|
def country(self):
|
||||||
return self.rolling_class.company.country
|
return self.rolling_class.company.country
|
||||||
|
|||||||
Reference in New Issue
Block a user