mirror of
https://github.com/daniviga/django-ram.git
synced 2026-02-03 17:40:39 +01:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
564416b3d5
|
|||
|
967ea5d495
|
|||
|
7656aa8b68
|
|||
| 1be102b9d4 | |||
| 4ec7b8fc18 | |||
| 9a469378df | |||
| ede8741473 | |||
|
49c8d804d6
|
|||
|
2ab2d00585
|
|||
|
c95064ddec
|
|||
|
16bd82de39
|
|||
|
2ae7f2685d
|
|||
|
29f9a213b4
|
|||
|
884661d4e1
|
|||
|
c7cace96f7
|
|||
|
d3c099c05b
|
|||
|
903633b5a7
|
|||
|
ee775d737e
|
|||
|
8087ab5997
|
|||
|
1899747909
|
|||
|
0880bd0817
|
|||
|
74d7df2c8b
|
|||
|
c81508bbd5
|
|||
|
b4f69d8a34
|
|||
| 676418cb67 | |||
|
98d2e7beab
|
|||
|
fb17dc2a7c
|
|||
|
5a71dc36fa
|
|||
|
c539255bf9
|
|||
|
fc527d5cd1
|
|||
|
f45d754c91
|
|||
|
e9c9ede357
|
|||
| 39b0a9378b | |||
| 6b10051bc4 | |||
|
3804c3379b
|
|||
|
1b769da553
|
|||
|
f655900411
|
|||
|
3e69b9ae6e
|
|||
|
66c3c3f51c
|
|||
|
935c439084
|
|||
|
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
|
|||
|
e9ec126ada
|
|||
|
1222116874
|
|||
|
85741f090c
|
|||
|
88d718fa94
|
|||
|
a2c857a3cd
|
|||
|
647894bca7
|
|||
|
c8cc8c5ed0
|
|||
| e80dc604a7 |
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: 13488e1e93...313d2cd3e0
Submodule arduino/WebThrottle-EX updated: eb43d7906f...eeec7d4af6
Submodule arduino/arduino-cli updated: fa6eafcbbe...08ff7e2b76
Submodule arduino/dcc-ex.github.io updated: 9acc446358...190d3adfa1
Submodule arduino/vim-arduino updated: 111db616db...2ded67cdf0
43
docs/nginx/nginx.conf
Normal file
43
docs/nginx/nginx.conf
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
server {
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name myhost;
|
||||||
|
|
||||||
|
# ssl_certificate ...;
|
||||||
|
|
||||||
|
add_header X-Xss-Protection "1; mode=block";
|
||||||
|
add_header Strict-Transport-Security "max-age=15768000";
|
||||||
|
add_header Permissions-Policy "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()";
|
||||||
|
add_header Content-Security-Policy "child-src 'none'; object-src 'none'";
|
||||||
|
|
||||||
|
client_max_body_size 250M;
|
||||||
|
error_page 403 404 https://$server_name/404;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_redirect http:// https://;
|
||||||
|
proxy_connect_timeout 1800;
|
||||||
|
proxy_read_timeout 1800;
|
||||||
|
proxy_max_temp_file_size 8192m;
|
||||||
|
}
|
||||||
|
|
||||||
|
# static files
|
||||||
|
location /static {
|
||||||
|
root /myroot/ram/storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
# media files
|
||||||
|
location ~ ^/media/(images|uploads) {
|
||||||
|
root /myroot/ram/storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
# protected filed to be served via X-Accel-Redirect
|
||||||
|
location /private {
|
||||||
|
internal;
|
||||||
|
alias /myroot/ram/storage/media;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,20 +2,27 @@ 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,
|
||||||
|
MagazineIssueDocument,
|
||||||
|
)
|
||||||
from bookshelf.models import (
|
from bookshelf.models import (
|
||||||
BaseBookProperty,
|
BaseBookProperty,
|
||||||
BaseBookImage,
|
BaseBookImage,
|
||||||
BaseBookDocument,
|
|
||||||
Book,
|
Book,
|
||||||
Author,
|
Author,
|
||||||
Publisher,
|
Publisher,
|
||||||
Catalog,
|
Catalog,
|
||||||
|
Magazine,
|
||||||
|
MagazineIssue,
|
||||||
|
TocEntry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -28,13 +35,6 @@ class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
|
|||||||
verbose_name = "Image"
|
verbose_name = "Image"
|
||||||
|
|
||||||
|
|
||||||
class BookDocInline(admin.TabularInline):
|
|
||||||
model = BaseBookDocument
|
|
||||||
min_num = 0
|
|
||||||
extra = 0
|
|
||||||
classes = ["collapse"]
|
|
||||||
|
|
||||||
|
|
||||||
class BookPropertyInline(admin.TabularInline):
|
class BookPropertyInline(admin.TabularInline):
|
||||||
model = BaseBookProperty
|
model = BaseBookProperty
|
||||||
min_num = 0
|
min_num = 0
|
||||||
@@ -44,9 +44,38 @@ class BookPropertyInline(admin.TabularInline):
|
|||||||
verbose_name_plural = "Properties"
|
verbose_name_plural = "Properties"
|
||||||
|
|
||||||
|
|
||||||
|
class BookDocInline(admin.TabularInline):
|
||||||
|
model = BookDocument
|
||||||
|
min_num = 0
|
||||||
|
extra = 0
|
||||||
|
classes = ["collapse"]
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogDocInline(BookDocInline):
|
||||||
|
model = CatalogDocument
|
||||||
|
|
||||||
|
|
||||||
|
class MagazineIssueDocInline(BookDocInline):
|
||||||
|
model = MagazineIssueDocument
|
||||||
|
|
||||||
|
|
||||||
|
class BookTocInline(admin.TabularInline):
|
||||||
|
model = TocEntry
|
||||||
|
min_num = 0
|
||||||
|
extra = 0
|
||||||
|
fields = (
|
||||||
|
"title",
|
||||||
|
"subtitle",
|
||||||
|
"authors",
|
||||||
|
"page",
|
||||||
|
"featured",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Book)
|
@admin.register(Book)
|
||||||
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
inlines = (
|
inlines = (
|
||||||
|
BookTocInline,
|
||||||
BookPropertyInline,
|
BookPropertyInline,
|
||||||
BookImageInline,
|
BookImageInline,
|
||||||
BookDocInline,
|
BookDocInline,
|
||||||
@@ -60,9 +89,9 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"published",
|
"published",
|
||||||
)
|
)
|
||||||
autocomplete_fields = ("authors", "publisher", "shop")
|
autocomplete_fields = ("authors", "publisher", "shop")
|
||||||
readonly_fields = ("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 = (
|
||||||
(
|
(
|
||||||
@@ -89,6 +118,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"shop",
|
"shop",
|
||||||
"purchase_date",
|
"purchase_date",
|
||||||
"price",
|
"price",
|
||||||
|
"invoices",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -115,6 +145,18 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
@admin.display(description="Invoices")
|
||||||
|
def invoices(self, obj):
|
||||||
|
if obj.invoice.exists():
|
||||||
|
html = format_html_join(
|
||||||
|
"<br>",
|
||||||
|
'<a href="{}" target="_blank">{}</a>',
|
||||||
|
((i.file.url, i) for i in obj.invoice.all()),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
html = "-"
|
||||||
|
return html
|
||||||
|
|
||||||
@admin.display(description="Publisher")
|
@admin.display(description="Publisher")
|
||||||
def get_publisher(self, obj):
|
def get_publisher(self, obj):
|
||||||
return obj.publisher.name
|
return obj.publisher.name
|
||||||
@@ -185,13 +227,13 @@ class AuthorAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Publisher)
|
@admin.register(Publisher)
|
||||||
class PublisherAdmin(admin.ModelAdmin):
|
class PublisherAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "country_flag")
|
list_display = ("name", "country_flag_name")
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
|
|
||||||
@admin.display(description="Country")
|
@admin.display(description="Country")
|
||||||
def country_flag(self, obj):
|
def country_flag_name(self, obj):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -200,7 +242,7 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
inlines = (
|
inlines = (
|
||||||
BookPropertyInline,
|
BookPropertyInline,
|
||||||
BookImageInline,
|
BookImageInline,
|
||||||
BookDocInline,
|
CatalogDocInline,
|
||||||
)
|
)
|
||||||
list_display = (
|
list_display = (
|
||||||
"__str__",
|
"__str__",
|
||||||
@@ -210,9 +252,14 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"published",
|
"published",
|
||||||
)
|
)
|
||||||
autocomplete_fields = ("manufacturer",)
|
autocomplete_fields = ("manufacturer",)
|
||||||
readonly_fields = ("creation_time", "updated_time")
|
readonly_fields = ("invoices", "creation_time", "updated_time")
|
||||||
search_fields = ("manufacturer__name", "years", "scales__scale")
|
search_fields = ("manufacturer__name", "years", "scales__scale")
|
||||||
list_filter = ("manufacturer__name", "publication_year", "scales__scale")
|
list_filter = (
|
||||||
|
"published",
|
||||||
|
"manufacturer__name",
|
||||||
|
"publication_year",
|
||||||
|
"scales__scale",
|
||||||
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
@@ -236,8 +283,10 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"Purchase data",
|
"Purchase data",
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
|
"shop",
|
||||||
"purchase_date",
|
"purchase_date",
|
||||||
"price",
|
"price",
|
||||||
|
"invoices",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -264,6 +313,18 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
@admin.display(description="Invoices")
|
||||||
|
def invoices(self, obj):
|
||||||
|
if obj.invoice.exists():
|
||||||
|
html = format_html_join(
|
||||||
|
"<br>",
|
||||||
|
'<a href="{}" target="_blank">{}</a>',
|
||||||
|
((i.file.url, i) for i in obj.invoice.all()),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
html = "-"
|
||||||
|
return html
|
||||||
|
|
||||||
def download_csv(modeladmin, request, queryset):
|
def download_csv(modeladmin, request, queryset):
|
||||||
header = [
|
header = [
|
||||||
"Catalog",
|
"Catalog",
|
||||||
@@ -315,3 +376,145 @@ 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 = (
|
||||||
|
BookTocInline,
|
||||||
|
BookPropertyInline,
|
||||||
|
BookImageInline,
|
||||||
|
MagazineIssueDocInline,
|
||||||
|
)
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"issue_number",
|
||||||
|
"published",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("shop",)
|
||||||
|
readonly_fields = ("magazine", "creation_time", "updated_time")
|
||||||
|
|
||||||
|
def get_model_perms(self, request):
|
||||||
|
"""
|
||||||
|
Return empty perms dict thus hiding the model from admin index.
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"published",
|
||||||
|
"magazine",
|
||||||
|
"issue_number",
|
||||||
|
"publication_year",
|
||||||
|
"publication_month",
|
||||||
|
"ISBN",
|
||||||
|
"language",
|
||||||
|
"number_of_pages",
|
||||||
|
"description",
|
||||||
|
"tags",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Purchase data",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"shop",
|
||||||
|
"purchase_date",
|
||||||
|
"price",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{"classes": ("collapse",), "fields": ("notes",)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Audit",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"creation_time",
|
||||||
|
"updated_time",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
|
|
||||||
|
|
||||||
|
class MagazineIssueInline(admin.TabularInline):
|
||||||
|
model = MagazineIssue
|
||||||
|
min_num = 0
|
||||||
|
extra = 0
|
||||||
|
autocomplete_fields = ("shop",)
|
||||||
|
show_change_link = True
|
||||||
|
fields = (
|
||||||
|
"preview",
|
||||||
|
"published",
|
||||||
|
"issue_number",
|
||||||
|
"publication_year",
|
||||||
|
"publication_month",
|
||||||
|
"number_of_pages",
|
||||||
|
"language",
|
||||||
|
)
|
||||||
|
readonly_fields = ("preview",)
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ("admin/js/magazine_issue_defaults.js",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Magazine)
|
||||||
|
class MagazineAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
|
inlines = (MagazineIssueInline,)
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"publisher",
|
||||||
|
"published",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("publisher",)
|
||||||
|
readonly_fields = ("creation_time", "updated_time")
|
||||||
|
search_fields = ("name", "publisher__name")
|
||||||
|
list_filter = (
|
||||||
|
"published",
|
||||||
|
"publisher__name",
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"published",
|
||||||
|
"name",
|
||||||
|
"website",
|
||||||
|
"publisher",
|
||||||
|
"ISBN",
|
||||||
|
"language",
|
||||||
|
"description",
|
||||||
|
"image",
|
||||||
|
"tags",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{"classes": ("collapse",), "fields": ("notes",)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Audit",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"creation_time",
|
||||||
|
"updated_time",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
17
ram/bookshelf/migrations/0023_delete_basebookdocument.py
Normal file
17
ram/bookshelf/migrations/0023_delete_basebookdocument.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-09 13:47
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0022_basebook_shop"),
|
||||||
|
("repository", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="BaseBookDocument",
|
||||||
|
),
|
||||||
|
]
|
||||||
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
ram/bookshelf/migrations/0027_magazine_website.py
Normal file
18
ram/bookshelf/migrations/0027_magazine_website.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-12 14:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0026_alter_basebook_language_alter_magazine_image_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="magazine",
|
||||||
|
name="website",
|
||||||
|
field=models.URLField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-21 21:56
|
||||||
|
|
||||||
|
import django.db.models.functions.text
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0027_magazine_website"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="magazine",
|
||||||
|
options={"ordering": [django.db.models.functions.text.Lower("name")]},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="magazineissue",
|
||||||
|
options={
|
||||||
|
"ordering": [
|
||||||
|
"magazine",
|
||||||
|
"publication_year",
|
||||||
|
"publication_month",
|
||||||
|
"issue_number",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-23 11:18
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0028_alter_magazine_options_alter_magazineissue_options"),
|
||||||
|
("metadata", "0025_alter_company_options_alter_manufacturer_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="catalog",
|
||||||
|
name="manufacturer",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="catalogs",
|
||||||
|
to="metadata.manufacturer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="catalog",
|
||||||
|
name="scales",
|
||||||
|
field=models.ManyToManyField(related_name="catalogs", to="metadata.scale"),
|
||||||
|
),
|
||||||
|
]
|
||||||
53
ram/bookshelf/migrations/0030_tocentry.py
Normal file
53
ram/bookshelf/migrations/0030_tocentry.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-29 11:02
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import tinymce.models
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0029_alter_catalog_manufacturer_alter_catalog_scales"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="TocEntry",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("description", tinymce.models.HTMLField(blank=True)),
|
||||||
|
("notes", tinymce.models.HTMLField(blank=True)),
|
||||||
|
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_time", models.DateTimeField(auto_now=True)),
|
||||||
|
("published", models.BooleanField(default=True)),
|
||||||
|
("title", models.CharField(max_length=200)),
|
||||||
|
("subtitle", models.CharField(blank=True, max_length=200)),
|
||||||
|
("authors", models.CharField(blank=True, max_length=256)),
|
||||||
|
("page", models.SmallIntegerField()),
|
||||||
|
("featured", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="toc",
|
||||||
|
to="bookshelf.basebook",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Table of Contents Entry",
|
||||||
|
"verbose_name_plural": "Table of Contents Entries",
|
||||||
|
"ordering": ["page"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 6.0 on 2025-12-31 13:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0030_tocentry"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tocentry",
|
||||||
|
name="authors",
|
||||||
|
field=models.CharField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tocentry",
|
||||||
|
name="subtitle",
|
||||||
|
field=models.CharField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="tocentry",
|
||||||
|
name="title",
|
||||||
|
field=models.CharField(),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
from urllib.parse import urlparse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.dates import MONTHS
|
||||||
|
from django.db.models.functions import Lower
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
|
||||||
from ram.utils import DeduplicatedStorage
|
from ram.utils import DeduplicatedStorage
|
||||||
from ram.models import BaseModel, Image, Document, PropertyInstance
|
from ram.models import BaseModel, Image, PropertyInstance
|
||||||
from metadata.models import Scale, Manufacturer, Shop, Tag
|
from metadata.models import Scale, Manufacturer, Shop, Tag
|
||||||
|
|
||||||
|
|
||||||
@@ -41,8 +45,8 @@ class BaseBook(BaseModel):
|
|||||||
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
|
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
|
||||||
language = models.CharField(
|
language = models.CharField(
|
||||||
max_length=7,
|
max_length=7,
|
||||||
choices=settings.LANGUAGES,
|
choices=sorted(settings.LANGUAGES, key=lambda s: s[1]),
|
||||||
default='en'
|
default="en",
|
||||||
)
|
)
|
||||||
number_of_pages = models.SmallIntegerField(null=True, blank=True)
|
number_of_pages = models.SmallIntegerField(null=True, blank=True)
|
||||||
publication_year = models.SmallIntegerField(null=True, blank=True)
|
publication_year = models.SmallIntegerField(null=True, blank=True)
|
||||||
@@ -56,27 +60,24 @@ class BaseBook(BaseModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
purchase_date = models.DateField(null=True, blank=True)
|
purchase_date = models.DateField(null=True, blank=True)
|
||||||
tags = models.ManyToManyField(
|
tags = models.ManyToManyField(Tag, related_name="bookshelf", blank=True)
|
||||||
Tag, related_name="bookshelf", blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
shutil.rmtree(
|
shutil.rmtree(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
settings.MEDIA_ROOT, "images", "books", str(self.uuid)
|
settings.MEDIA_ROOT, "images", "books", str(self.uuid)
|
||||||
),
|
),
|
||||||
ignore_errors=True
|
ignore_errors=True,
|
||||||
)
|
)
|
||||||
super(BaseBook, self).delete(*args, **kwargs)
|
super(BaseBook, self).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def book_image_upload(instance, filename):
|
def book_image_upload(instance, filename):
|
||||||
return os.path.join(
|
return os.path.join("images", "books", str(instance.book.uuid), filename)
|
||||||
"images",
|
|
||||||
"books",
|
|
||||||
str(instance.book.uuid),
|
def magazine_image_upload(instance, filename):
|
||||||
filename
|
return os.path.join("images", "magazines", str(instance.uuid), filename)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseBookImage(Image):
|
class BaseBookImage(Image):
|
||||||
@@ -89,21 +90,6 @@ class BaseBookImage(Image):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseBookDocument(Document):
|
|
||||||
book = models.ForeignKey(
|
|
||||||
BaseBook, on_delete=models.CASCADE, related_name="document"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "Documents"
|
|
||||||
constraints = [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["book", "file"],
|
|
||||||
name="unique_book_file"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BaseBookProperty(PropertyInstance):
|
class BaseBookProperty(PropertyInstance):
|
||||||
book = models.ForeignKey(
|
book = models.ForeignKey(
|
||||||
BaseBook,
|
BaseBook,
|
||||||
@@ -135,8 +121,7 @@ class Book(BaseBook):
|
|||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"bookshelf_item",
|
"bookshelf_item", kwargs={"selector": "book", "uuid": self.uuid}
|
||||||
kwargs={"selector": "book", "uuid": self.uuid}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -144,23 +129,160 @@ class Catalog(BaseBook):
|
|||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
related_name="catalogs",
|
||||||
)
|
)
|
||||||
years = models.CharField(max_length=12)
|
years = models.CharField(max_length=12)
|
||||||
scales = models.ManyToManyField(Scale)
|
scales = models.ManyToManyField(Scale, related_name="catalogs")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["manufacturer", "publication_year"]
|
ordering = ["manufacturer", "publication_year"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
# if the object is new, return an empty string to avoid
|
||||||
|
# calling self.scales.all() which would raise a infinite recursion
|
||||||
|
if self.pk is None:
|
||||||
|
return str() # empty string
|
||||||
scales = self.get_scales()
|
scales = self.get_scales()
|
||||||
return "%s %s %s" % (self.manufacturer.name, self.years, scales)
|
return "%s %s %s" % (self.manufacturer.name, self.years, scales)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"bookshelf_item",
|
"bookshelf_item", kwargs={"selector": "catalog", "uuid": self.uuid}
|
||||||
kwargs={"selector": "catalog", "uuid": self.uuid}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_scales(self):
|
def get_scales(self):
|
||||||
return "/".join([s.scale for s in self.scales.all()])
|
return "/".join([s.scale for s in self.scales.all()])
|
||||||
|
|
||||||
get_scales.short_description = "Scales"
|
get_scales.short_description = "Scales"
|
||||||
|
|
||||||
|
|
||||||
|
class Magazine(BaseModel):
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
|
||||||
|
website = models.URLField(blank=True)
|
||||||
|
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
|
||||||
|
image = models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
upload_to=magazine_image_upload,
|
||||||
|
storage=DeduplicatedStorage,
|
||||||
|
)
|
||||||
|
language = models.CharField(
|
||||||
|
max_length=7,
|
||||||
|
choices=sorted(settings.LANGUAGES, key=lambda s: s[1]),
|
||||||
|
default="en",
|
||||||
|
)
|
||||||
|
tags = models.ManyToManyField(Tag, related_name="magazine", blank=True)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
shutil.rmtree(
|
||||||
|
os.path.join(
|
||||||
|
settings.MEDIA_ROOT, "images", "magazines", str(self.uuid)
|
||||||
|
),
|
||||||
|
ignore_errors=True,
|
||||||
|
)
|
||||||
|
super(Magazine, self).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = [Lower("name")]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse("magazine", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
|
def get_cover(self):
|
||||||
|
if self.image:
|
||||||
|
return self.image
|
||||||
|
else:
|
||||||
|
cover_issue = self.issue.filter(published=True).first()
|
||||||
|
if cover_issue and cover_issue.image.exists():
|
||||||
|
return cover_issue.image.first().image
|
||||||
|
return None
|
||||||
|
|
||||||
|
def website_short(self):
|
||||||
|
if self.website:
|
||||||
|
return urlparse(self.website).netloc.replace("www.", "")
|
||||||
|
|
||||||
|
|
||||||
|
class MagazineIssue(BaseBook):
|
||||||
|
magazine = models.ForeignKey(
|
||||||
|
Magazine, on_delete=models.CASCADE, related_name="issue"
|
||||||
|
)
|
||||||
|
issue_number = models.CharField(max_length=100)
|
||||||
|
publication_month = models.SmallIntegerField(
|
||||||
|
null=True, blank=True, choices=MONTHS.items()
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("magazine", "issue_number")
|
||||||
|
ordering = [
|
||||||
|
"magazine",
|
||||||
|
"publication_year",
|
||||||
|
"publication_month",
|
||||||
|
"issue_number",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.magazine.name} - {self.issue_number}"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.magazine.published is False and self.published is True:
|
||||||
|
raise ValidationError(
|
||||||
|
"Cannot set an issue as published if the magazine is not "
|
||||||
|
"published."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def obj_label(self):
|
||||||
|
return "Magazine Issue"
|
||||||
|
|
||||||
|
def preview(self):
|
||||||
|
return self.image.first().image_thumbnail(100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def publisher(self):
|
||||||
|
return self.magazine.publisher
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"issue", kwargs={"uuid": self.uuid, "magazine": self.magazine.uuid}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TocEntry(BaseModel):
|
||||||
|
book = models.ForeignKey(
|
||||||
|
BaseBook, on_delete=models.CASCADE, related_name="toc"
|
||||||
|
)
|
||||||
|
title = models.CharField()
|
||||||
|
subtitle = models.CharField(blank=True)
|
||||||
|
authors = models.CharField(blank=True)
|
||||||
|
page = models.SmallIntegerField()
|
||||||
|
featured = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["page"]
|
||||||
|
verbose_name = "Table of Contents Entry"
|
||||||
|
verbose_name_plural = "Table of Contents Entries"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.subtitle:
|
||||||
|
title = f"{self.title}: {self.subtitle}"
|
||||||
|
else:
|
||||||
|
title = self.title
|
||||||
|
return f"{title} (p. {self.page})"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.page is None:
|
||||||
|
raise ValidationError("Page number is required.")
|
||||||
|
if self.page < 1:
|
||||||
|
raise ValidationError("Page number is invalid.")
|
||||||
|
try:
|
||||||
|
if self.page > self.book.number_of_pages:
|
||||||
|
raise ValidationError(
|
||||||
|
"Page number exceeds the publication's number of pages."
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
pass # number_of_pages is None
|
||||||
|
|||||||
@@ -49,3 +49,5 @@ class CatalogSerializer(serializers.ModelSerializer):
|
|||||||
"price",
|
"price",
|
||||||
)
|
)
|
||||||
read_only_fields = ("creation_time", "updated_time")
|
read_only_fields = ("creation_time", "updated_time")
|
||||||
|
|
||||||
|
# FIXME: add Magazine and MagazineIssue serializers
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -38,3 +38,5 @@ class CatalogGet(RetrieveAPIView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Book.objects.get_published(self.request.user)
|
return Book.objects.get_published(self.request.user)
|
||||||
|
|
||||||
|
# FIXME: add Magazine and MagazineIssue views
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
|
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 adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
# from django.forms import BaseInlineFormSet # for future reference
|
||||||
|
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 +30,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,15 +47,22 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"creation_time",
|
"creation_time",
|
||||||
"updated_time",
|
"updated_time",
|
||||||
)
|
)
|
||||||
list_filter = ("company", "era", "published")
|
list_filter = ("published", "company__name", "era", "scale")
|
||||||
list_display = ("__str__",) + list_filter + ("country_flag",)
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"company__name",
|
||||||
|
"era",
|
||||||
|
"scale",
|
||||||
|
"country_flag",
|
||||||
|
"published",
|
||||||
|
)
|
||||||
search_fields = ("identifier",) + list_filter
|
search_fields = ("identifier",) + list_filter
|
||||||
save_as = True
|
save_as = True
|
||||||
|
|
||||||
@admin.display(description="Country")
|
@admin.display(description="Country")
|
||||||
def country_flag(self, obj):
|
def country_flag(self, obj):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country)
|
'<img src="{}" title="{}" />', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
@@ -46,9 +72,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 +97,56 @@ 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]
|
||||||
|
|||||||
18
ram/consist/migrations/0016_alter_consistitem_order.py
Normal file
18
ram/consist/migrations/0016_alter_consistitem_order.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-04-27 19:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0015_consist_description"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="consistitem",
|
||||||
|
name="order",
|
||||||
|
field=models.PositiveIntegerField(),
|
||||||
|
),
|
||||||
|
]
|
||||||
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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
ram/consist/migrations/0019_consistitem_load.py
Normal file
18
ram/consist/migrations/0019_consistitem_load.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-01-03 12:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0018_alter_consist_scale"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="consistitem",
|
||||||
|
name="load",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
@@ -39,16 +41,34 @@ class Consist(BaseModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("consist", kwargs={"uuid": self.uuid})
|
return reverse("consist", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
return self.consist_item.filter(load=False).count()
|
||||||
|
|
||||||
|
def get_type_count(self):
|
||||||
|
return self.consist_item.filter(load=False).annotate(
|
||||||
|
type=models.F("rolling_stock__rolling_class__type__type")
|
||||||
|
).values(
|
||||||
|
"type"
|
||||||
|
).annotate(
|
||||||
|
count=models.Count("rolling_stock"),
|
||||||
|
category=models.F("rolling_stock__rolling_class__type__category"),
|
||||||
|
order=models.Max("order"),
|
||||||
|
).order_by("order")
|
||||||
|
|
||||||
|
def get_cover(self):
|
||||||
|
if self.image:
|
||||||
|
return self.image
|
||||||
|
else:
|
||||||
|
consist_item = self.consist_item.first()
|
||||||
|
if consist_item and consist_item.rolling_stock.image.exists():
|
||||||
|
return consist_item.rolling_stock.image.first().image
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def country(self):
|
def country(self):
|
||||||
return self.company.country
|
return self.company.country
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.consist_item.filter(rolling_stock__published=False).exists():
|
|
||||||
raise ValidationError(
|
|
||||||
"You must publish all items in the consist before publishing the consist." # noqa: E501
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["company", "-creation_time"]
|
ordering = ["company", "-creation_time"]
|
||||||
|
|
||||||
@@ -58,11 +78,8 @@ class ConsistItem(models.Model):
|
|||||||
Consist, on_delete=models.CASCADE, related_name="consist_item"
|
Consist, on_delete=models.CASCADE, related_name="consist_item"
|
||||||
)
|
)
|
||||||
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE)
|
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE)
|
||||||
order = models.PositiveIntegerField(
|
load = models.BooleanField(default=False)
|
||||||
default=1000, # make sure it is always added at the end
|
order = models.PositiveIntegerField(blank=False, null=False)
|
||||||
blank=False,
|
|
||||||
null=False
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["order"]
|
ordering = ["order"]
|
||||||
@@ -76,6 +93,29 @@ class ConsistItem(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0}".format(self.rolling_stock)
|
return "{0}".format(self.rolling_stock)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
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
|
||||||
|
# Scale must match, but allow loads of any scale
|
||||||
|
if rolling_stock.scale != consist.scale and not self.load:
|
||||||
|
raise ValidationError(
|
||||||
|
"The rolling stock and consist must be of the same scale."
|
||||||
|
)
|
||||||
|
if self.load and rolling_stock.scale.ratio != consist.scale.ratio:
|
||||||
|
raise ValidationError(
|
||||||
|
"The load and consist must be of the same scale ratio."
|
||||||
|
)
|
||||||
|
if self.consist.published and not rolling_stock.published:
|
||||||
|
raise ValidationError(
|
||||||
|
"You must unpublish the the consist before using this item."
|
||||||
|
)
|
||||||
|
|
||||||
def published(self):
|
def published(self):
|
||||||
return self.rolling_stock.published
|
return self.rolling_stock.published
|
||||||
published.boolean = True
|
published.boolean = True
|
||||||
@@ -83,9 +123,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):
|
||||||
|
|||||||
@@ -2,18 +2,16 @@ from django.contrib import admin
|
|||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from adminsortable2.admin import SortableAdminMixin
|
from adminsortable2.admin import SortableAdminMixin
|
||||||
|
|
||||||
from ram.admin import publish, unpublish
|
from repository.models import DecoderDocument
|
||||||
from metadata.models import (
|
from metadata.models import (
|
||||||
Property,
|
Property,
|
||||||
Decoder,
|
Decoder,
|
||||||
DecoderDocument,
|
|
||||||
Scale,
|
Scale,
|
||||||
Shop,
|
Shop,
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
Company,
|
Company,
|
||||||
Tag,
|
Tag,
|
||||||
RollingStockType,
|
RollingStockType,
|
||||||
GenericDocument,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -49,28 +47,28 @@ class ScaleAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(Company)
|
@admin.register(Company)
|
||||||
class CompanyAdmin(admin.ModelAdmin):
|
class CompanyAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ("logo_thumbnail",)
|
readonly_fields = ("logo_thumbnail",)
|
||||||
list_display = ("name", "country_flag")
|
list_display = ("name", "country_flag_name")
|
||||||
list_filter = ("name", "country")
|
list_filter = ("name", "country")
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
|
|
||||||
@admin.display(description="Country")
|
@admin.display(description="Country")
|
||||||
def country_flag(self, obj):
|
def country_flag_name(self, obj):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Manufacturer)
|
@admin.register(Manufacturer)
|
||||||
class ManufacturerAdmin(admin.ModelAdmin):
|
class ManufacturerAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ("logo_thumbnail",)
|
readonly_fields = ("logo_thumbnail",)
|
||||||
list_display = ("name", "category", "country_flag")
|
list_display = ("name", "category", "country_flag_name")
|
||||||
list_filter = ("category",)
|
list_filter = ("category",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
|
|
||||||
@admin.display(description="Country")
|
@admin.display(description="Country")
|
||||||
def country_flag(self, obj):
|
def country_flag_name(self, obj):
|
||||||
return format_html(
|
return format_html(
|
||||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,53 +86,14 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
|
|||||||
search_fields = ("type", "category")
|
search_fields = ("type", "category")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(GenericDocument)
|
|
||||||
class GenericDocumentAdmin(admin.ModelAdmin):
|
|
||||||
readonly_fields = ("size", "creation_time", "updated_time")
|
|
||||||
list_display = (
|
|
||||||
"__str__",
|
|
||||||
"description",
|
|
||||||
"private",
|
|
||||||
"size",
|
|
||||||
"download",
|
|
||||||
)
|
|
||||||
search_fields = (
|
|
||||||
"description",
|
|
||||||
"file",
|
|
||||||
)
|
|
||||||
fieldsets = (
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"private",
|
|
||||||
"description",
|
|
||||||
"file",
|
|
||||||
"size",
|
|
||||||
"tags",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Notes",
|
|
||||||
{"classes": ("collapse",), "fields": ("notes",)},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Audit",
|
|
||||||
{
|
|
||||||
"classes": ("collapse",),
|
|
||||||
"fields": (
|
|
||||||
"creation_time",
|
|
||||||
"updated_time",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
actions = [publish, unpublish]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Shop)
|
@admin.register(Shop)
|
||||||
class ShopAdmin(admin.ModelAdmin):
|
class ShopAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "on_line", "active")
|
list_display = ("name", "on_line", "active", "country_flag_name")
|
||||||
list_filter = ("on_line", "active")
|
list_filter = ("on_line", "active")
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
@admin.display(description="Country")
|
||||||
|
def country_flag_name(self, obj):
|
||||||
|
return format_html(
|
||||||
|
'<img src="{}" /> {}', obj.country.flag, obj.country.name
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-09 13:47
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0023_shop"),
|
||||||
|
("repository", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="genericdocument",
|
||||||
|
name="tags",
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="DecoderDocument",
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="GenericDocument",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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"]},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -6,14 +7,12 @@ from django.dispatch.dispatcher import receiver
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
|
||||||
from tinymce import models as tinymce
|
from ram.models import SimpleBaseModel
|
||||||
|
|
||||||
from ram.models import Document
|
|
||||||
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
|
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
|
||||||
from ram.managers import PublicManager
|
from ram.managers import PublicManager
|
||||||
|
|
||||||
|
|
||||||
class Property(models.Model):
|
class Property(SimpleBaseModel):
|
||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(max_length=128, unique=True)
|
||||||
private = models.BooleanField(
|
private = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
@@ -30,7 +29,7 @@ class Property(models.Model):
|
|||||||
objects = PublicManager()
|
objects = PublicManager()
|
||||||
|
|
||||||
|
|
||||||
class Manufacturer(models.Model):
|
class Manufacturer(SimpleBaseModel):
|
||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(max_length=128, unique=True)
|
||||||
slug = models.CharField(max_length=128, unique=True, editable=False)
|
slug = models.CharField(max_length=128, unique=True, editable=False)
|
||||||
category = models.CharField(
|
category = models.CharField(
|
||||||
@@ -46,7 +45,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
|
||||||
@@ -60,13 +59,17 @@ class Manufacturer(models.Model):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def website_short(self):
|
||||||
|
if self.website:
|
||||||
|
return urlparse(self.website).netloc.replace("www.", "")
|
||||||
|
|
||||||
def logo_thumbnail(self):
|
def logo_thumbnail(self):
|
||||||
return get_image_preview(self.logo.url)
|
return get_image_preview(self.logo.url)
|
||||||
|
|
||||||
logo_thumbnail.short_description = "Preview"
|
logo_thumbnail.short_description = "Preview"
|
||||||
|
|
||||||
|
|
||||||
class Company(models.Model):
|
class Company(SimpleBaseModel):
|
||||||
name = models.CharField(max_length=64, unique=True)
|
name = models.CharField(max_length=64, unique=True)
|
||||||
slug = models.CharField(max_length=64, unique=True, editable=False)
|
slug = models.CharField(max_length=64, unique=True, editable=False)
|
||||||
extended_name = models.CharField(max_length=128, blank=True)
|
extended_name = models.CharField(max_length=128, blank=True)
|
||||||
@@ -81,7 +84,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
|
||||||
@@ -104,7 +107,7 @@ class Company(models.Model):
|
|||||||
logo_thumbnail.short_description = "Preview"
|
logo_thumbnail.short_description = "Preview"
|
||||||
|
|
||||||
|
|
||||||
class Decoder(models.Model):
|
class Decoder(SimpleBaseModel):
|
||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(max_length=128, unique=True)
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
@@ -132,20 +135,6 @@ class Decoder(models.Model):
|
|||||||
image_thumbnail.short_description = "Preview"
|
image_thumbnail.short_description = "Preview"
|
||||||
|
|
||||||
|
|
||||||
class DecoderDocument(Document):
|
|
||||||
decoder = models.ForeignKey(
|
|
||||||
Decoder, on_delete=models.CASCADE, related_name="document"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
constraints = [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["decoder", "file"],
|
|
||||||
name="unique_decoder_file"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_ratio(ratio):
|
def calculate_ratio(ratio):
|
||||||
try:
|
try:
|
||||||
num, den = ratio.split(":")
|
num, den = ratio.split(":")
|
||||||
@@ -154,7 +143,7 @@ def calculate_ratio(ratio):
|
|||||||
raise ValidationError("Invalid ratio format")
|
raise ValidationError("Invalid ratio format")
|
||||||
|
|
||||||
|
|
||||||
class Scale(models.Model):
|
class Scale(SimpleBaseModel):
|
||||||
scale = models.CharField(max_length=32, unique=True)
|
scale = models.CharField(max_length=32, unique=True)
|
||||||
slug = models.CharField(max_length=32, unique=True, editable=False)
|
slug = models.CharField(max_length=32, unique=True, editable=False)
|
||||||
ratio = models.CharField(max_length=16, validators=[calculate_ratio])
|
ratio = models.CharField(max_length=16, validators=[calculate_ratio])
|
||||||
@@ -189,7 +178,7 @@ def scale_save(sender, instance, **kwargs):
|
|||||||
instance.ratio_int = calculate_ratio(instance.ratio)
|
instance.ratio_int = calculate_ratio(instance.ratio)
|
||||||
|
|
||||||
|
|
||||||
class RollingStockType(models.Model):
|
class RollingStockType(SimpleBaseModel):
|
||||||
type = models.CharField(max_length=64)
|
type = models.CharField(max_length=64)
|
||||||
order = models.PositiveSmallIntegerField()
|
order = models.PositiveSmallIntegerField()
|
||||||
category = models.CharField(
|
category = models.CharField(
|
||||||
@@ -219,12 +208,12 @@ class RollingStockType(models.Model):
|
|||||||
return "{0} {1}".format(self.type, self.category)
|
return "{0} {1}".format(self.type, self.category)
|
||||||
|
|
||||||
|
|
||||||
class Tag(models.Model):
|
class Tag(SimpleBaseModel):
|
||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(max_length=128, unique=True)
|
||||||
slug = models.CharField(max_length=128, unique=True)
|
slug = models.CharField(max_length=128, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
ordering = ["slug"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -239,15 +228,7 @@ class Tag(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GenericDocument(Document):
|
class Shop(SimpleBaseModel):
|
||||||
notes = tinymce.HTMLField(blank=True)
|
|
||||||
tags = models.ManyToManyField(Tag, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "Generic Documents"
|
|
||||||
|
|
||||||
|
|
||||||
class Shop(models.Model):
|
|
||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(max_length=128, unique=True)
|
||||||
country = CountryField(blank=True)
|
country = CountryField(blank=True)
|
||||||
website = models.URLField(blank=True)
|
website = models.URLField(blank=True)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class SiteConfigurationAdmin(SingletonModelAdmin):
|
|||||||
"about",
|
"about",
|
||||||
"items_per_page",
|
"items_per_page",
|
||||||
"items_ordering",
|
"items_ordering",
|
||||||
|
"featured_items_ordering",
|
||||||
"currency",
|
"currency",
|
||||||
"footer",
|
"footer",
|
||||||
"footer_extended",
|
"footer_extended",
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-01-02 23:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("portal", "0020_alter_flatpage_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="siteconfiguration",
|
||||||
|
name="featured_items_ordering",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("type", "By rolling stock type and company"),
|
||||||
|
("class", "By rolling stock type and class"),
|
||||||
|
("company", "By company and type"),
|
||||||
|
("country", "By country and type"),
|
||||||
|
("cou+com", "By country and company"),
|
||||||
|
],
|
||||||
|
default="type",
|
||||||
|
max_length=11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="siteconfiguration",
|
||||||
|
name="items_ordering",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("type", "By rolling stock type and company"),
|
||||||
|
("class", "By rolling stock type and class"),
|
||||||
|
("company", "By company and type"),
|
||||||
|
("country", "By country and type"),
|
||||||
|
("cou+com", "By country and company"),
|
||||||
|
],
|
||||||
|
default="type",
|
||||||
|
max_length=11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -22,14 +22,17 @@ class SiteConfiguration(SingletonModel):
|
|||||||
default="6",
|
default="6",
|
||||||
)
|
)
|
||||||
items_ordering = models.CharField(
|
items_ordering = models.CharField(
|
||||||
max_length=10,
|
max_length=11,
|
||||||
choices=[
|
choices=[
|
||||||
("type", "By rolling stock type"),
|
("type", "By rolling stock type and company"),
|
||||||
("company", "By company name"),
|
("class", "By rolling stock type and class"),
|
||||||
("identifier", "By rolling stock class"),
|
("company", "By company and type"),
|
||||||
|
("country", "By country and type"),
|
||||||
|
("cou+com", "By country and company"),
|
||||||
],
|
],
|
||||||
default="type",
|
default="type",
|
||||||
)
|
)
|
||||||
|
featured_items_ordering = items_ordering.clone()
|
||||||
currency = models.CharField(max_length=3, default="EUR")
|
currency = models.CharField(max_length=3, default="EUR")
|
||||||
footer = tinymce.HTMLField(blank=True)
|
footer = tinymce.HTMLField(blank=True)
|
||||||
footer_extended = tinymce.HTMLField(blank=True)
|
footer_extended = tinymce.HTMLField(blank=True)
|
||||||
|
|||||||
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
@@ -43,13 +43,15 @@ a.badge, a.badge:hover {
|
|||||||
border-top: calc(var(--bs-border-width) * 3) solid var(--bs-border-color);
|
border-top: calc(var(--bs-border-width) * 3) solid var(--bs-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav-journal ul, #nav-journal ol {
|
#nav-journal ul,
|
||||||
margin: 0;
|
#nav-journal ol {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav-journal p {
|
#nav-journal p:last-child,
|
||||||
margin: 0;
|
#nav-journal ul:last-child,
|
||||||
|
#nav-journal ol:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#footer > p {
|
#footer > p {
|
||||||
|
|||||||
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 |
26
ram/portal/templates/_includes/documents.html
Normal file
26
ram/portal/templates/_includes/documents.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% if documents %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="3" scope="row">{{ header|default:"Documents" }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
{% for d in documents.all %}
|
||||||
|
<tr>
|
||||||
|
<td class="w-33">{{ d.description }}</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{% if d.private %}
|
||||||
|
<i class="bi bi-file-earmark-lock2"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-file-earmark-text"></i>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
@@ -12,14 +12,16 @@
|
|||||||
<div class="container d-flex text-body-secondary">
|
<div class="container d-flex text-body-secondary">
|
||||||
<p class="flex-fill small">Made with ❤️ for 🚂 and <i class="bi bi-github"></i> <a href="https://github.com/daniviga/django-ram">django-ram</a>
|
<p class="flex-fill small">Made with ❤️ for 🚂 and <i class="bi bi-github"></i> <a href="https://github.com/daniviga/django-ram">django-ram</a>
|
||||||
{% if site_conf.show_version %}<br>Version {{ site_conf.version }}{% endif %}</p>
|
{% if site_conf.show_version %}<br>Version {{ site_conf.version }}{% endif %}</p>
|
||||||
<p class="text-end fs-5">
|
<p class="text-end">
|
||||||
{% if site_conf.disclaimer %}<a class="text-reset" title="Disclaimer" href="" data-bs-toggle="modal" data-bs-target="#disclaimerModal"><i class="bi bi-info-square-fill"></i></a> {% endif %}
|
{% if site_conf.disclaimer %}
|
||||||
<a class="text-reset" title="Back to top" href="#"><i class="bi bi-arrow-up-left-square-fill"></i></a>
|
<a title="Disclaimer" href="" data-bs-toggle="modal" data-bs-target="#disclaimerModal"><i class="text-muted d-lg-none fs-5 bi bi-info-square-fill"></i><span class="d-none d-lg-inline small">Disclaimer</span></a><span class="d-none d-lg-inline small"> | </span>
|
||||||
|
{% endif %}
|
||||||
|
<a title="Back to top" href="#"><i class="text-muted d-lg-none fs-5 bi bi-arrow-up-left-square-fill"></i><span class="d-none d-lg-inline small">Back to top</span></a>
|
||||||
</p>
|
</p>
|
||||||
</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>
|
||||||
|
|
||||||
26
ram/portal/templates/_modules/documents.html
Normal file
26
ram/portal/templates/_modules/documents.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% if documents %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="3" scope="row">{{ header|default:"Documents" }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
{% for d in documents.all %}
|
||||||
|
<tr>
|
||||||
|
<td class="w-33">{{ d.description }}</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{% if d.private %}
|
||||||
|
<i class="bi bi-file-earmark-lock2"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-file-earmark-text"></i>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
18
ram/portal/templates/_modules/properties.html
Normal file
18
ram/portal/templates/_modules/properties.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{% if properties %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" scope="row">Properties</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
{% for p in properties %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">{{ p.property }}</th>
|
||||||
|
<td>{{ p.value }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
29
ram/portal/templates/_modules/purchase_data.html
Normal file
29
ram/portal/templates/_modules/purchase_data.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{% if request.user.is_staff %}
|
||||||
|
{% if data.shop or data.purchase_date or data.price %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" scope="row">Purchase</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Shop</th>
|
||||||
|
<td>
|
||||||
|
{{ data.shop|default:"-" }}
|
||||||
|
{% if data.shop.website %} <a href="{{ data.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Purchase date</th>
|
||||||
|
<td>{{ data.purchase_date|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Price ({{ site_conf.currency }})</th>
|
||||||
|
<td>{{ data.price|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
@@ -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,19 +140,15 @@
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
{% include 'includes/login.html' %}
|
{% include '_includes/login.html' %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -190,7 +186,7 @@
|
|||||||
{% show_bookshelf_menu %}
|
{% show_bookshelf_menu %}
|
||||||
{% show_flatpages_menu user %}
|
{% show_flatpages_menu user %}
|
||||||
</ul>
|
</ul>
|
||||||
{% include 'includes/search.html' %}
|
{% include '_includes/search.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -2,20 +2,23 @@
|
|||||||
{% load dynamic_url %}
|
{% load dynamic_url %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if book.tags.all %}
|
{% if data.tags.all %}
|
||||||
<p><small>Tags:</small>
|
<p><small>Tags:</small>
|
||||||
{% for t in book.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
{% for t in data.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
{% if not data.published %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span> |
|
||||||
|
{% endif %}
|
||||||
|
<small class="text-body-secondary">Updated {{ data.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000">
|
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000">
|
||||||
<div class="carousel-inner">
|
<div class="carousel-inner">
|
||||||
{% for t in book.image.all %}
|
{% for t in data.image.all %}
|
||||||
{% if forloop.first %}
|
{% if forloop.first %}
|
||||||
<div class="carousel-item active">
|
<div class="carousel-item active">
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -25,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% if book.image.count > 1 %}
|
{% if data.image.count > 1 %}
|
||||||
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
|
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
|
||||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||||
<span class="visually-hidden"><i class="bi bi-chevron-left"></i></span>
|
<span class="visually-hidden"><i class="bi bi-chevron-left"></i></span>
|
||||||
@@ -46,10 +49,12 @@
|
|||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
|
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
|
||||||
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
|
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
|
||||||
|
{% if data.toc.all %}<button class="nav-link" id="nav-toc-tab" data-bs-toggle="tab" data-bs-target="#nav-toc" type="button" role="tab" aria-controls="nav-toc" aria-selected="true">Table of contents</button>{% endif %}
|
||||||
{% if documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
|
{% if documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
||||||
<option value="nav-summary" selected>Summary</option>
|
<option value="nav-summary" selected>Summary</option>
|
||||||
|
{% if data.toc.all %}<option value="nav-toc">Table of contents</option>{% endif %}
|
||||||
{% if documents %}<option value="nav-documents">Documents</option>{% endif %}
|
{% if documents %}<option value="nav-documents">Documents</option>{% endif %}
|
||||||
</select>
|
</select>
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
@@ -58,131 +63,123 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">
|
||||||
{% if type == "catalog" %}Catalog
|
{{ data.obj_label|capfirst }}
|
||||||
{% elif type == "book" %}Book{% endif %}
|
|
||||||
<div class="float-end">
|
|
||||||
{% if not book.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
{% if type == "catalog" %}
|
{% if data.obj_type == "catalog" %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Manufacturer</th>
|
<th class="w-33" scope="row">Manufacturer</th>
|
||||||
<td>{{ book.manufacturer }}</td>
|
<td>
|
||||||
|
<a href="{% url 'filtered' _filter="manufacturer" search=data.manufacturer.slug %}">{{ data.manufacturer }}{% if data.manufacturer.website %}</a> <a href="{{ data.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Scales</th>
|
<th class="w-33" scope="row">Scales</th>
|
||||||
<td>{{ book.get_scales }}</td>
|
<td>{{ data.get_scales }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% elif type == "book" %}
|
{% elif data.obj_type == "book" %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Title</th>
|
<th class="w-33" scope="row">Title</th>
|
||||||
<td>{{ book.title }}</td>
|
<td>{{ data.title }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Authors</th>
|
<th class="w-33" scope="row">Authors</th>
|
||||||
<td>
|
<td>
|
||||||
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
|
<ul class="mb-0 list-unstyled">{% for a in data.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Publisher</th>
|
<th class="w-33" scope="row">Publisher</th>
|
||||||
<td>{{ book.publisher }}</td>
|
<td>
|
||||||
|
<img src="{{ data.publisher.country.flag }}" alt="{{ data.publisher.country }}"> {{ data.publisher }}
|
||||||
|
{% if data.publisher.website %} <a href="{{ data.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% elif data.obj_type == "magazineissue" %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Magazine</th>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'magazine' data.magazine.pk %}">{{ data.magazine }}</a>
|
||||||
|
{% if data.magazine.website %} <a href="{{ data.magazine.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Publisher</th>
|
||||||
|
<td>
|
||||||
|
<img src="{{ data.publisher.country.flag }}" alt="{{ data.publisher.country }}"> {{ data.publisher }}
|
||||||
|
{% if data.publisher.website %} <a href="{{ data.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Issue</th>
|
||||||
|
<td>{{ data.issue_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Date</th>
|
||||||
|
<td>{{ data.publication_year|default:"-" }} / {{ data.get_publication_month_display|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">ISBN</th>
|
<th scope="row">ISBN</th>
|
||||||
<td>{{ book.ISBN|default:"-" }}</td>
|
<td>{{ data.ISBN|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Language</th>
|
<th scope="row">Language</th>
|
||||||
<td>{{ book.get_language_display }}</td>
|
<td>{{ data.get_language_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Number of pages</th>
|
<th scope="row">Number of pages</th>
|
||||||
<td>{{ book.number_of_pages|default:"-" }}</td>
|
<td>{{ data.number_of_pages|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if data.obj_type == "book" or data.obj_type == "catalog" %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Publication year</th>
|
<th scope="row">Publication year</th>
|
||||||
<td>{{ book.publication_year|default:"-" }}</td>
|
<td>{{ data.publication_year|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if book.description %}
|
{% endif %}
|
||||||
|
{% if data.description %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Description</th>
|
<th class="w-33" scope="row">Description</th>
|
||||||
<td>{{ book.description | safe }}</td>
|
<td>{{ data.description | safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if request.user.is_staff %}
|
{% include "_modules/purchase_data.html" %}
|
||||||
|
{% include "_modules/properties.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane table-responsive" id="nav-toc" role="tabpanel" aria-labelledby="nav-toc-tab">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">Purchase</th>
|
<th scope="row">Title</th>
|
||||||
|
<th scope="row">Subtitle</th>
|
||||||
|
<th scope="row">Authors</th>
|
||||||
|
<th scope="row">Page</th>
|
||||||
|
<th scope="row"><abbr title="Featured article"><i class="bi bi-star-fill"></i></abbr></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
|
{% for toc in data.toc.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Shop</th>
|
<td class="w-33">{{ toc.title }}</td>
|
||||||
<td>
|
<td class="w-33">{{ toc.subtitle }}</td>
|
||||||
{{ book.shop|default:"-" }}
|
<td>{{ toc.authors }}</td>
|
||||||
{% if book.shop.website %} <a href="{{ book.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
<td>{{ toc.page }}</td>
|
||||||
</td>
|
<td>{% if toc.featured %}<abbr title="Featured article"><i class="bi bi-star-fill text-warning"></i></abbr>{% endif %}</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">Purchase date</th>
|
|
||||||
<td>{{ book.purchase_date|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Price ({{ site_conf.currency }})</th>
|
|
||||||
<td>{{ book.price|default:"-" }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
|
||||||
{% if properties %}
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2" scope="row">Properties</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
{% for p in properties %}
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">{{ p.property }}</th>
|
|
||||||
<td>{{ p.value }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
</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">
|
||||||
<table class="table table-striped">
|
{% include "_modules/documents.html" %}
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="3" scope="row">Documents</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
{% for d in documents.all %}
|
|
||||||
<tr>
|
|
||||||
<td class="w-33">{{ d.description }}</td>
|
|
||||||
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
|
|
||||||
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' type book.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' data.obj_type data.pk %}">Edit</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -6,19 +6,21 @@
|
|||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
||||||
{% block cards %}
|
{% block cards %}
|
||||||
{% for d in data %}
|
{% for d in data %}
|
||||||
{% if d.type == "roster" %}
|
{% if d.obj_type == "rollingstock" %}
|
||||||
{% include "cards/roster.html" %}
|
{% include "cards/roster.html" %}
|
||||||
{% elif d.type == "company" %}
|
{% elif d.obj_type == "company" %}
|
||||||
{% include "cards/company.html" %}
|
{% include "cards/company.html" %}
|
||||||
{% elif d.type == "rolling_stock_type" %}
|
{% elif d.obj_type == "rollingstocktype" %}
|
||||||
{% include "cards/rolling_stock_type.html" %}
|
{% include "cards/rolling_stock_type.html" %}
|
||||||
{% elif d.type == "scale" %}
|
{% elif d.obj_type == "scale" %}
|
||||||
{% include "cards/scale.html" %}
|
{% include "cards/scale.html" %}
|
||||||
{% elif d.type == "consist" %}
|
{% elif d.obj_type == "consist" %}
|
||||||
{% include "cards/consist.html" %}
|
{% include "cards/consist.html" %}
|
||||||
{% elif d.type == "manufacturer" %}
|
{% elif d.obj_type == "manufacturer" %}
|
||||||
{% include "cards/manufacturer.html" %}
|
{% include "cards/manufacturer.html" %}
|
||||||
{% elif d.type == "book" or d.type == "catalog" %}
|
{% elif d.obj_type == "magazine" or d.obj_type == "magazineissue" %}
|
||||||
|
{% include "cards/magazine.html" %}
|
||||||
|
{% elif d.obj_type == "book" or d.obj_type == "catalog" %}
|
||||||
{% include "cards/book.html" %}
|
{% include "cards/book.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -2,77 +2,78 @@
|
|||||||
{% load dynamic_url %}
|
{% load dynamic_url %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
{% if d.item.image.exists %}
|
{% if d.image.exists %}
|
||||||
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a>
|
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
||||||
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}"></a>
|
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text" style="position: relative;">
|
<p class="card-text" style="position: relative;">
|
||||||
<strong>{{ d.item }}</strong>
|
<strong>{{ d }}</strong>
|
||||||
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
|
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
|
||||||
</p>
|
</p>
|
||||||
{% if d.item.tags.all %}
|
|
||||||
<p class="card-text"><small>Tags:</small>
|
<p class="card-text"><small>Tags:</small>
|
||||||
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
|
{% empty %}
|
||||||
|
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">
|
||||||
{% if d.type == "catalog" %}Catalog
|
{{ d.obj_label|capfirst }}
|
||||||
{% elif d.type == "book" %}Book{% endif %}
|
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
{% if not d.item.published %}
|
{% if not d.published %}
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
<span class="badge text-bg-warning">Unpublished</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
{% if d.type == "catalog" %}
|
{% if d.obj_type == "catalog" %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Manufacturer</th>
|
<th class="w-33" scope="row">Manufacturer</th>
|
||||||
<td>{{ d.item.manufacturer }}</td>
|
<td>
|
||||||
|
<a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.slug %}">{{ d.manufacturer }}{% if d.manufacturer.website %}</a> <a href="{{ d.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Scales</th>
|
<th class="w-33" scope="row">Scales</th>
|
||||||
<td>{{ d.item.get_scales }}</td>
|
<td>{{ d.get_scales }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% elif d.type == "book" %}
|
{% elif d.obj_type == "book" %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Authors</th>
|
<th class="w-33" scope="row">Authors</th>
|
||||||
<td>
|
<td>
|
||||||
<ul class="mb-0 list-unstyled">{% for a in d.item.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
|
<ul class="mb-0 list-unstyled">{% for a in d.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Publisher</th>
|
<th class="w-33" scope="row">Publisher</th>
|
||||||
<td>{{ d.item.publisher }}</td>
|
<td><img src="{{ d.publisher.country.flag }}" alt="{{ d.publisher.country }}"> {{ d.publisher }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Language</th>
|
<th scope="row">Language</th>
|
||||||
<td>{{ d.item.get_language_display }}</td>
|
<td>{{ d.get_language_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Pages</th>
|
<th scope="row">Pages</th>
|
||||||
<td>{{ d.item.number_of_pages|default:"-" }}</td>
|
<td>{{ d.number_of_pages|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Year</th>
|
<th scope="row">Year</th>
|
||||||
<td>{{ d.item.publication_year|default:"-" }}</td>
|
<td>{{ d.publication_year|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a>
|
<a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' d.type d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' d.obj_type d.pk %}">Edit</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text" style="position: relative;">
|
<p class="card-text" style="position: relative;">
|
||||||
<strong>{{ d.item.name }}</strong>
|
<strong>{{ d.name }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">
|
||||||
Company
|
Company
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
{% if d.item.freelance %}
|
{% if d.freelance %}
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
<span class="badge text-bg-secondary">Freelance</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -18,29 +18,31 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
{% if d.item.logo %}
|
{% if d.logo %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Logo</th>
|
<th class="w-33" scope="row">Logo</th>
|
||||||
<td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td>
|
<td><img class="logo" src="{{ d.logo.url }}" alt="{{ d.name }} logo"></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Name</th>
|
<th class="w-33" scope="row">Name</th>
|
||||||
<td>{{ d.item.extended_name }}</td>
|
<td>{{ d.extended_name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Abbreviation</th>
|
<th class="w-33" scope="row">Abbreviation</th>
|
||||||
<td>{{ d.item.name }}</td>
|
<td>{{ d.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Country</th>
|
<th class="w-33" scope="row">Country</th>
|
||||||
<td><img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}"> {{ d.item.country.name }}</td>
|
<td><img src="{{ d.country.flag }}" alt="{{ d.country }}"> {{ d.country.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
<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.num_items %}
|
||||||
{% 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 %}
|
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="company" search=d.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
||||||
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.pk %}">Edit</a>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,66 +1,70 @@
|
|||||||
|
{% load static %}
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<a href="{{ d.item.get_absolute_url }}">
|
<a href="{{ d.get_absolute_url }}">
|
||||||
{% if d.item.image %}
|
{% if d.get_cover %}
|
||||||
<img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}">
|
<img class="card-img-top" src="{{ d.get_cover.url }}" alt="{{ d }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with d.item.consist_item.first.rolling_stock as r %}
|
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
||||||
<img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d.item }}">
|
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text" style="position: relative;">
|
<p class="card-text" style="position: relative;">
|
||||||
<strong>{{ d.item }}</strong>
|
<strong>{{ d }}</strong>
|
||||||
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
|
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
|
||||||
</p>
|
</p>
|
||||||
{% if d.item.tags.all %}
|
|
||||||
<p class="card-text"><small>Tags:</small>
|
<p class="card-text"><small>Tags:</small>
|
||||||
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
|
{% empty %}
|
||||||
|
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">
|
||||||
Consist
|
Consist
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
{% if d.item.company.freelance %}
|
{% if not d.published %}
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
<span class="badge text-bg-warning">Unpublished</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not d.item.published %}
|
{% if d.company.freelance %}
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
<span class="badge text-bg-secondary">Freelance</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
{% if d.item.address %}
|
{% if d.address %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Address</th>
|
<th class="w-33" scope="row">Address</th>
|
||||||
<td>{{ d.item.address }}</td>
|
<td>{{ d.address }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Company</th>
|
<th class="w-33" scope="row">Company</th>
|
||||||
<td><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></td>
|
<td>
|
||||||
|
<img src="{{ d.company.country.flag }}" alt="{{ d.company.country }}">
|
||||||
|
<abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Era</th>
|
<th scope="row">Era</th>
|
||||||
<td>{{ d.item.era }}</td>
|
<td>{{ d.era }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Length</th>
|
<th scope="row">Length</th>
|
||||||
<td>{{ d.item.consist_item.count }}</td>
|
<td>{{ d.length }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a>
|
<a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.pk %}">Edit</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
100
ram/portal/templates/cards/magazine.html
Normal file
100
ram/portal/templates/cards/magazine.html
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
{% load static %}
|
||||||
|
{% load dynamic_url %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
{% if d.obj_type == "magazine" %}
|
||||||
|
<a href="{{ d.get_absolute_url }}">
|
||||||
|
{% if d.get_cover %}
|
||||||
|
<img class="card-img-top" src="{{ d.get_cover.url }}" alt="{{ d }}">
|
||||||
|
{% else %}
|
||||||
|
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
||||||
|
<img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% elif d.obj_type == "magazineissue" %}
|
||||||
|
<a href="{{ d.get_absolute_url }}">
|
||||||
|
{% if d.image.exists %}
|
||||||
|
<img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}">
|
||||||
|
{% else %}
|
||||||
|
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
||||||
|
<img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}">
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text" style="position: relative;">
|
||||||
|
<strong>{{ d }}</strong>
|
||||||
|
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
|
||||||
|
</p>
|
||||||
|
<p class="card-text"><small>Tags:</small>
|
||||||
|
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
||||||
|
{{ t.name }}</a>{# new line is required #}
|
||||||
|
{% empty %}
|
||||||
|
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" scope="row">
|
||||||
|
{{ d.obj_label|capfirst }}
|
||||||
|
|
||||||
|
<div class="float-end">
|
||||||
|
{% if not d.published %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
{% if d.obj_type == "magazineissue" %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Magazine</th>
|
||||||
|
<td>{{ d.magazine }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Website</th>
|
||||||
|
<td>{% if d.website %}<a href="{{ d.website }}" target="_blank">{{ d.website_short }}</td>{% else %}-{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Publisher</th>
|
||||||
|
<td>
|
||||||
|
<img src="{{ d.publisher.country.flag }}" alt="{{ d.publisher.country }}"> {{ d.publisher }}
|
||||||
|
{% if d.publisher.website %} <a href="{{ d.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% if d.obj_type == "magazineissue" %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Issue</th>
|
||||||
|
<td>{{ d.issue_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Date</th>
|
||||||
|
<td>{{ d.publication_year|default:"-" }} / {{ d.get_publication_month_display|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Pages</th>
|
||||||
|
<td>{{ d.number_of_pages|default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Language</th>
|
||||||
|
<td>{{ d.get_language_display }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
|
{% if d.obj_type == "magazine" %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary{% if d.issues == 0 %} disabled{% endif %}" href="{{ d.get_absolute_url }}">Show {{ d.issues }} issue{{ d.issues|pluralize }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' d.obj_type d.pk %}">Edit</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text" style="position: relative;">
|
<p class="card-text" style="position: relative;">
|
||||||
<strong>{{ d.item.name }}</strong>
|
<strong>{{ d.name }}</strong>
|
||||||
</p>
|
</p>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -11,27 +11,27 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
{% if d.item.logo %}
|
{% if d.logo %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Logo</th>
|
<th class="w-33" scope="row">Logo</th>
|
||||||
<td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td>
|
<td><img class="logo" src="{{ d.logo.url }}" alt="{{ d.name }} logo"></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if d.item.website %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Website</th>
|
<th class="w-33" scope="row">Website</th>
|
||||||
<td><a href="{{ d.item.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a></td>
|
<td>{% if d.website %}<a href="{{ d.website }}" target="_blank">{{ d.website_short }}</td>{% else %}-{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Category</th>
|
<th class="w-33" scope="row">Category</th>
|
||||||
<td>{{ d.item.category | title }}</td>
|
<td>{{ d.category | title }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
<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.num_items %}
|
||||||
{% 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 %}
|
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="manufacturer" search=d.slug %}">Show {{ items }} item{{ items|pluralize }}</a>
|
||||||
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.pk %}">Edit</a>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text"><strong>{{ d.item }}</strong></p>
|
<p class="card-text"><strong>{{ d }}</strong></p>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -11,17 +11,19 @@
|
|||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Type</th>
|
<th class="w-33" scope="row">Type</th>
|
||||||
<td>{{ d.item.type }}</td>
|
<td>{{ d.type }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Category</th>
|
<th class="w-33" scope="row">Category</th>
|
||||||
<td>{{ d.item.category | title}}</td>
|
<td>{{ d.category | title}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
<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.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.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
||||||
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_rollingstocktype_change' d.pk %}">Edit</a>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
{% 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 %}
|
<div id="card-img-container" class="position-relative">
|
||||||
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a>
|
{% if d.featured %}
|
||||||
{% else %}
|
<span class="position-absolute translate-middle top-0 start-0 m-3 text-danger">
|
||||||
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
<abbr title="Featured item"><i class="bi bi-heart-fill"></i></abbr>
|
||||||
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}"></a>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if d.image.exists %}
|
||||||
|
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a>
|
||||||
|
{% else %}
|
||||||
|
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
|
||||||
|
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text" style="position: relative;">
|
<p class="card-text" style="position: relative;">
|
||||||
<strong>{{ d.item }}</strong>
|
<strong>{{ d }}</strong>
|
||||||
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
|
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
|
||||||
</p>
|
</p>
|
||||||
{% if d.item.tags.all %}
|
|
||||||
<p class="card-text"><small>Tags:</small>
|
<p class="card-text"><small>Tags:</small>
|
||||||
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
|
{% empty %}
|
||||||
|
<span class="badge rounded-pill bg-secondary"><i class="bi bi-ban"></i></span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">
|
||||||
Rolling stock
|
Rolling stock
|
||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
{% if d.item.company.freelance %}
|
{% if not d.published %}
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
<span class="badge text-bg-warning">Unpublished</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not d.item.published %}
|
{% if d.company.freelance %}
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
<span class="badge text-bg-secondary">Freelance</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
@@ -38,70 +47,50 @@
|
|||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Type</th>
|
<th class="w-33" scope="row">Type</th>
|
||||||
<td>{{ d.item.rolling_class.type }}</td>
|
<td>{{ d.rolling_class.type }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Company</th>
|
<th scope="row">Company</th>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'filtered' _filter="company" search=d.item.company.slug %}"><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></a>
|
<img src="{{ d.company.country.flag }}" alt="{{ d.company.country }}">
|
||||||
|
<a href="{% url 'filtered' _filter="company" search=d.company.slug %}"><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Class</th>
|
<th scope="row">Class</th>
|
||||||
<td>{{ d.item.rolling_class.identifier }}</td>
|
<td>{{ d.rolling_class.identifier }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Road number</th>
|
<th scope="row">Road number</th>
|
||||||
<td>{{ d.item.road_number }}</td>
|
<td>{{ d.road_number }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Era</th>
|
<th scope="row">Era</th>
|
||||||
<td>{{ d.item.era }}</td>
|
<td>{{ d.era }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Manufacturer</th>
|
<th class="w-33" scope="row">Manufacturer</th>
|
||||||
<td>{%if d.item.manufacturer %}
|
<td>{%if d.manufacturer %}
|
||||||
<a href="{% url 'filtered' _filter="manufacturer" search=d.item.manufacturer.slug %}">{{ d.item.manufacturer }}{% if d.item.manufacturer.website %}</a> <a href="{{ d.item.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
<a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.slug %}">{{ d.manufacturer }}{% if d.manufacturer.website %}</a> <a href="{{ d.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
{% endif %}</td>
|
{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Scale</th>
|
<th scope="row">Scale</th>
|
||||||
<td><a href="{% url 'filtered' _filter="scale" search=d.item.scale.slug %}"><abbr title="{{ d.item.scale.ratio }} - {{ d.item.scale.tracks }} mm">{{ d.item.scale }}</abbr></a></td>
|
<td><a href="{% url 'filtered' _filter="scale" search=d.scale.slug %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }} mm">{{ d.scale }}</abbr></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Item number</th>
|
<th scope="row">Item number</th>
|
||||||
<td>{{ d.item.item_number }}{%if d.item.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=d.item.manufacturer.slug search=d.item.item_number_slug %}">SET</a>{% endif %}</td>
|
<td>{{ d.item_number }}{%if d.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=d.manufacturer.slug search=d.item_number_slug %}">SET</a>{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">DCC</th>
|
||||||
|
<td><a class="text-reset text-decoration-none" title="Symbols" href="" data-bs-toggle="modal" data-bs-target="#symbolsModal">{% dcc d %}</a></td>
|
||||||
</tr>
|
</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.get_absolute_url}}">Show all data</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.pk %}">Edit</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text"><strong>{{ d.item }}</strong></p>
|
<p class="card-text"><strong>{{ d }}</strong></p>
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -11,25 +11,27 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Name</th>
|
<th class="w-33" scope="row">Name</th>
|
||||||
<td>{{ d.item.scale }}</td>
|
<td>{{ d.scale }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Ratio</th>
|
<th class="w-33" scope="row">Ratio</th>
|
||||||
<td>{{ d.item.ratio }}</td>
|
<td>{{ d.ratio }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Tracks</th>
|
<th class="w-33" scope="row">Tracks</th>
|
||||||
<td>{{ d.item.tracks }} mm</td>
|
<td>{{ d.tracks }} mm</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Gauge</th>
|
<th class="w-33" scope="row">Gauge</th>
|
||||||
<td>{{ d.item.gauge }}</td>
|
<td>{{ d.gauge }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
<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.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="scale" search=d.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
||||||
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,8 +7,11 @@
|
|||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% 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>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
{% if consist.image %}
|
{% if consist.image %}
|
||||||
@@ -23,13 +26,42 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block cards_layout %}
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
||||||
|
{% block cards %}
|
||||||
|
{% for d in data %}
|
||||||
|
{% include "cards/roster.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% if loads %}
|
||||||
|
<div class="accordion shadow-sm mt-4" id="accordionLoads">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLoads" aria-expanded="false" aria-controls="collapseLoads">
|
||||||
|
<i class="bi bi-download"></i> Rolling Stock loaded on freight cars
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="collapseLoads" class="accordion-collapse collapse" data-bs-parent="#accordionLoads">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
||||||
|
{% for l in loads %}
|
||||||
|
{% include "cards/roster.html" with d=l %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
{% block pagination %}
|
{% block pagination %}
|
||||||
{% if data.has_other_pages %}
|
{% if data.has_other_pages %}
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url 'consist' uuid=consist.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -45,13 +77,13 @@
|
|||||||
{% if i == data.paginator.ELLIPSIS %}
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item"><a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=i %}#main-content">{{ i }}</a></li>
|
<li class="page-item"><a class="page-link" href="{% url 'consist' uuid=consist.uuid page=i %}#main-content">{{ i }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url 'consist' uuid=consist.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -73,7 +105,7 @@
|
|||||||
<option value="nav-summary" selected>Summary</option>
|
<option value="nav-summary" selected>Summary</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
<div class="tab-pane show active table-responsive" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -83,9 +115,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 +138,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Length</th>
|
<th scope="row">Length</th>
|
||||||
<td>{{ data | length }}</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 %}{% if loads %} | <i class="bi bi-download"></i> {{ loads|length }}x Load{{ loads|pluralize }}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url 'filtered' _filter=filter search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
{% if i == data.paginator.ELLIPSIS %}
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item"><a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=i %}#main-content">{{ i }}</a></li>
|
<li class="page-item"><a class="page-link" href="{% url 'filtered' _filter=filter search=search page=i %}#main-content">{{ i }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url 'filtered' _filter=filter search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -3,3 +3,18 @@
|
|||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="text-body-secondary">{{ site_conf.about | safe }}</div>
|
<div class="text-body-secondary">{{ site_conf.about | safe }}</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block cards %}
|
||||||
|
{% for d in data %}
|
||||||
|
{% include "cards/roster.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pagination %}
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url "roster" %}#main-content" tabindex="-1">Go to the roster <i class="bi bi-chevron-right"></i></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
128
ram/portal/templates/magazine.html
Normal file
128
ram/portal/templates/magazine.html
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
{% extends "cards.html" %}
|
||||||
|
{% block header %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% if magazine.tags.all %}
|
||||||
|
<p><small>Tags:</small>
|
||||||
|
{% for t in magazine.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
|
||||||
|
{{ t.name }}</a>{# new line is required #}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
{% if not magazine.published %}
|
||||||
|
<span class="badge text-bg-warning">Unpublished</span> |
|
||||||
|
{% endif %}
|
||||||
|
<small class="text-body-secondary">Updated {{ magazine.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block carousel %}
|
||||||
|
{% if magazine.image %}
|
||||||
|
<div class="row pb-4">
|
||||||
|
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel">
|
||||||
|
<div class="carousel-inner">
|
||||||
|
<div class="carousel-item active">
|
||||||
|
<img src="{{ magazine.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="magazine cover">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block pagination %}
|
||||||
|
{% if data.has_other_pages %}
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||||
|
{% if data.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'magazine' uuid=magazine.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in page_range %}
|
||||||
|
{% if data.number == i %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ i }}</span>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item"><a class="page-link" href="{% url 'magazine' uuid=magazine.uuid page=i %}#main-content">{{ i }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if data.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'magazine' uuid=magazine.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block extra_content %}
|
||||||
|
<section class="py-4 text-start container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="mx-auto">
|
||||||
|
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
|
||||||
|
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
|
||||||
|
</nav>
|
||||||
|
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
||||||
|
<option value="nav-summary" selected>Summary</option>
|
||||||
|
</select>
|
||||||
|
<div class="tab-content" id="nav-tabContent">
|
||||||
|
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" scope="row">
|
||||||
|
Magazine
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Name</th>
|
||||||
|
<td>{{ magazine }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Publisher</th>
|
||||||
|
<td>
|
||||||
|
<img src="{{ magazine.publisher.country.flag }}" alt="{{ magazine.publisher.country }}"> {{ magazine.publisher }}
|
||||||
|
{% if magazine.publisher.website %} <a href="{{ magazine.publisher.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Website</th>
|
||||||
|
<td>{% if magazine.website %}<a href="{{ magazine.website }}" target="_blank">{{ magazine.website_short }}</td>{% else %}-{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Language</th>
|
||||||
|
<td>{{ magazine.get_language_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">ISBN</th>
|
||||||
|
<td>{{ magazine.ISBN | default:"-" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% if magazine.description %}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Description</th>
|
||||||
|
<td>{{ magazine.description | safe }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:bookshelf_magazine_change' magazine.pk %}">Edit</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url 'manufacturer' manufacturer=manufacturer.slug search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -21,13 +21,13 @@
|
|||||||
{% if i == data.paginator.ELLIPSIS %}
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item"><a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=i %}#main-content">{{ i }}</a></li>
|
<li class="page-item"><a class="page-link" href="{% url 'manufacturer' manufacturer=manufacturer.slug search=search page=i %}#main-content">{{ i }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url 'manufacturer' manufacturer=manufacturer.slug search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% dynamic_pagination type page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url request.resolver_match.url_name page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -23,13 +23,13 @@
|
|||||||
{% if i == data.paginator.ELLIPSIS %}
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item"><a class="page-link" href="{% dynamic_pagination type page=i %}#main-content">{{ i }}</a></li>
|
<li class="page-item"><a class="page-link" href="{% url request.resolver_match.url_name page=i %}#main-content">{{ i }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% dynamic_pagination type page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url request.resolver_match.url_name page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{% extends "cards.html" %}
|
{% extends "cards.html" %}
|
||||||
{% block pagination %}
|
{% block pagination %}
|
||||||
{% if data.has_other_pages %}
|
{% if data.has_other_pages %}
|
||||||
{% with data.0.item.category as c %}
|
{% with data.0.category as c %}
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation">
|
||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url 'manufacturers' category=c page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -22,13 +22,13 @@
|
|||||||
{% if i == data.paginator.ELLIPSIS %}
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item"><a class="page-link" href="{% url 'manufacturers_pagination' category=c page=i %}#main-content">{{ i }}</a></li>
|
<li class="page-item"><a class="page-link" href="{% url 'manufacturers' category=c page=i %}#main-content">{{ i }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url 'manufacturers' category=c page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -204,49 +217,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if request.user.is_staff %}
|
{% include "_modules/purchase_data.html" with data=rolling_stock %}
|
||||||
<table class="table table-striped">
|
{% include "_modules/properties.html" %}
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2" scope="row">Purchase</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">Shop</th>
|
|
||||||
<td>
|
|
||||||
{{ rolling_stock.shop | default:"-" }}
|
|
||||||
{% if rolling_stock.shop.website %} <a href="{{ rolling_stock.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">Purchase date</th>
|
|
||||||
<td>{{ rolling_stock.purchase_date | default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Price ({{ site_conf.currency }})</th>
|
|
||||||
<td>{{ rolling_stock.price | default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
{% if properties %}
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2" scope="row">Properties</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
{% for p in properties %}
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">{{ p.property }}</th>
|
|
||||||
<td>{{ p.value }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab">
|
<div class="tab-pane" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@@ -283,23 +255,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if class_properties %}
|
{% include "_modules/properties.html" with properties=class_properties %}
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2" scope="row">Properties</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
{% for p in class_properties %}
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">{{ p.property }}</th>
|
|
||||||
<td>{{ p.value }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="nav-company" role="tabpanel" aria-labelledby="nav-company-tab">
|
<div class="tab-pane" id="nav-company" role="tabpanel" aria-labelledby="nav-company-tab">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
@@ -339,74 +295,59 @@
|
|||||||
<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 table-responsive" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
|
||||||
{% if documents %}
|
{% include "_modules/documents.html" %}
|
||||||
<table class="table table-striped">
|
{% include "_modules/documents.html" with documents=decoder_documents header="Decoder documents" %}
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="3" scope="row">Documents</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
{% for d in documents.all %}
|
|
||||||
<tr>
|
|
||||||
<td class="w-33">{{ d.description }}</td>
|
|
||||||
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
|
|
||||||
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
{% if decoder_documents %}
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="3" scope="row">Decoder documents</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
{% for d in decoder_documents.all %}
|
|
||||||
<tr>
|
|
||||||
<td class="w-33">{{ d.description }}</td>
|
|
||||||
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
|
|
||||||
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
|
<div class="tab-pane" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url 'search' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -22,13 +22,13 @@
|
|||||||
{% if i == data.paginator.ELLIPSIS %}
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item"><a class="page-link" href="{% url 'search_pagination' search=encoded_search page=i %}#main-content">{{ i }}</a></li>
|
<li class="page-item"><a class="page-link" href="{% url 'search' search=encoded_search page=i %}#main-content">{{ i }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url 'search' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
|
|||||||
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
|
||||||
@@ -12,10 +12,3 @@ def dynamic_admin_url(app_name, model_name, object_id=None):
|
|||||||
args=[object_id]
|
args=[object_id]
|
||||||
)
|
)
|
||||||
return reverse(f'admin:{app_name}_{model_name}_changelist')
|
return reverse(f'admin:{app_name}_{model_name}_changelist')
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
|
||||||
def dynamic_pagination(reverse_name, page):
|
|
||||||
if reverse_name.endswith('y'):
|
|
||||||
return reverse(f'{reverse_name[:-1]}ies_pagination', args=[page])
|
|
||||||
return reverse(f'{reverse_name}s_pagination', args=[page])
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
11
ram/portal/templatetags/shuffle.py
Normal file
11
ram/portal/templatetags/shuffle.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import random
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def shuffle(items):
|
||||||
|
shuffled_items = list(items)
|
||||||
|
random.shuffle(shuffled_items)
|
||||||
|
return shuffled_items
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from portal.views import (
|
from portal.views import (
|
||||||
GetData,
|
GetHome,
|
||||||
GetRoster,
|
GetRoster,
|
||||||
GetObjectsFiltered,
|
GetObjectsFiltered,
|
||||||
GetManufacturerItem,
|
GetManufacturerItem,
|
||||||
@@ -15,103 +15,83 @@ from portal.views import (
|
|||||||
Types,
|
Types,
|
||||||
Books,
|
Books,
|
||||||
Catalogs,
|
Catalogs,
|
||||||
|
Magazines,
|
||||||
|
GetMagazine,
|
||||||
|
GetMagazineIssue,
|
||||||
GetBookCatalog,
|
GetBookCatalog,
|
||||||
SearchObjects,
|
SearchObjects,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", GetData.as_view(template="home.html"), name="index"),
|
path("", GetHome.as_view(), name="index"),
|
||||||
path("roster", GetRoster.as_view(), name="roster"),
|
path("roster", GetRoster.as_view(), name="roster"),
|
||||||
path(
|
path("roster/page/<int:page>", GetRoster.as_view(), name="roster"),
|
||||||
"roster/page/<int:page>",
|
|
||||||
GetRoster.as_view(),
|
|
||||||
name="rosters_pagination"
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"page/<str:flatpage>",
|
"page/<str:flatpage>",
|
||||||
GetFlatpage.as_view(),
|
GetFlatpage.as_view(),
|
||||||
name="flatpage",
|
name="flatpage",
|
||||||
),
|
),
|
||||||
path(
|
path("consists", Consists.as_view(), name="consists"),
|
||||||
"consists",
|
path("consists/page/<int:page>", Consists.as_view(), name="consists"),
|
||||||
Consists.as_view(),
|
|
||||||
name="consists"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"consists/page/<int:page>",
|
|
||||||
Consists.as_view(),
|
|
||||||
name="consists_pagination"
|
|
||||||
),
|
|
||||||
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
|
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
|
||||||
path(
|
path(
|
||||||
"consist/<uuid:uuid>/page/<int:page>",
|
"consist/<uuid:uuid>/page/<int:page>",
|
||||||
GetConsist.as_view(),
|
GetConsist.as_view(),
|
||||||
name="consist_pagination",
|
name="consist",
|
||||||
),
|
|
||||||
path(
|
|
||||||
"companies",
|
|
||||||
Companies.as_view(),
|
|
||||||
name="companies"
|
|
||||||
),
|
),
|
||||||
|
path("companies", Companies.as_view(), name="companies"),
|
||||||
path(
|
path(
|
||||||
"companies/page/<int:page>",
|
"companies/page/<int:page>",
|
||||||
Companies.as_view(),
|
Companies.as_view(),
|
||||||
name="companies_pagination",
|
name="companies",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"manufacturers/<str:category>",
|
"manufacturers/<str:category>",
|
||||||
Manufacturers.as_view(template="pagination_manufacturers.html"),
|
Manufacturers.as_view(template="pagination_manufacturers.html"),
|
||||||
name="manufacturers"
|
name="manufacturers",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"manufacturers/<str:category>/page/<int:page>",
|
"manufacturers/<str:category>/page/<int:page>",
|
||||||
Manufacturers.as_view(template="pagination_manufacturers.html"),
|
Manufacturers.as_view(template="pagination_manufacturers.html"),
|
||||||
name="manufacturers_pagination",
|
name="manufacturers",
|
||||||
|
),
|
||||||
|
path("scales", Scales.as_view(), name="scales"),
|
||||||
|
path("scales/page/<int:page>", Scales.as_view(), name="scales"),
|
||||||
|
path("types", Types.as_view(), name="rolling_stock_types"),
|
||||||
|
path("types/page/<int:page>", Types.as_view(), name="rolling_stock_types"),
|
||||||
|
path("bookshelf/books", Books.as_view(), name="books"),
|
||||||
|
path("bookshelf/books/page/<int:page>", Books.as_view(), name="books"),
|
||||||
|
path(
|
||||||
|
"bookshelf/magazine/<uuid:uuid>",
|
||||||
|
GetMagazine.as_view(),
|
||||||
|
name="magazine",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"scales",
|
"bookshelf/magazine/<uuid:uuid>/page/<int:page>",
|
||||||
Scales.as_view(),
|
GetMagazine.as_view(),
|
||||||
name="scales"
|
name="magazine",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"scales/page/<int:page>",
|
"bookshelf/magazine/<uuid:magazine>/issue/<uuid:uuid>",
|
||||||
Scales.as_view(),
|
GetMagazineIssue.as_view(),
|
||||||
name="scales_pagination"
|
name="issue",
|
||||||
),
|
),
|
||||||
|
path("bookshelf/magazines", Magazines.as_view(), name="magazines"),
|
||||||
path(
|
path(
|
||||||
"types",
|
"bookshelf/magazines/page/<int:page>",
|
||||||
Types.as_view(),
|
Magazines.as_view(),
|
||||||
name="rolling_stock_types"
|
name="magazines",
|
||||||
),
|
|
||||||
path(
|
|
||||||
"types/page/<int:page>",
|
|
||||||
Types.as_view(),
|
|
||||||
name="rolling_stock_types_pagination"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"bookshelf/books",
|
|
||||||
Books.as_view(),
|
|
||||||
name="books"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"bookshelf/books/page/<int:page>",
|
|
||||||
Books.as_view(),
|
|
||||||
name="books_pagination"
|
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"bookshelf/<str:selector>/<uuid:uuid>",
|
"bookshelf/<str:selector>/<uuid:uuid>",
|
||||||
GetBookCatalog.as_view(),
|
GetBookCatalog.as_view(),
|
||||||
name="bookshelf_item"
|
name="bookshelf_item",
|
||||||
),
|
|
||||||
path(
|
|
||||||
"bookshelf/catalogs",
|
|
||||||
Catalogs.as_view(),
|
|
||||||
name="catalogs"
|
|
||||||
),
|
),
|
||||||
|
path("bookshelf/catalogs", Catalogs.as_view(), name="catalogs"),
|
||||||
path(
|
path(
|
||||||
"bookshelf/catalogs/page/<int:page>",
|
"bookshelf/catalogs/page/<int:page>",
|
||||||
Catalogs.as_view(),
|
Catalogs.as_view(),
|
||||||
name="catalogs_pagination"
|
name="catalogs",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"search",
|
"search",
|
||||||
@@ -121,7 +101,7 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"search/<str:search>/page/<int:page>",
|
"search/<str:search>/page/<int:page>",
|
||||||
SearchObjects.as_view(),
|
SearchObjects.as_view(),
|
||||||
name="search_pagination",
|
name="search",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"manufacturer/<str:manufacturer>",
|
"manufacturer/<str:manufacturer>",
|
||||||
@@ -131,7 +111,7 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"manufacturer/<str:manufacturer>/page/<int:page>",
|
"manufacturer/<str:manufacturer>/page/<int:page>",
|
||||||
GetManufacturerItem.as_view(),
|
GetManufacturerItem.as_view(),
|
||||||
name="manufacturer_pagination",
|
name="manufacturer",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"manufacturer/<str:manufacturer>/<str:search>",
|
"manufacturer/<str:manufacturer>/<str:search>",
|
||||||
@@ -141,7 +121,7 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"manufacturer/<str:manufacturer>/<str:search>/page/<int:page>",
|
"manufacturer/<str:manufacturer>/<str:search>/page/<int:page>",
|
||||||
GetManufacturerItem.as_view(),
|
GetManufacturerItem.as_view(),
|
||||||
name="manufacturer_pagination",
|
name="manufacturer",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<str:_filter>/<str:search>",
|
"<str:_filter>/<str:search>",
|
||||||
@@ -151,7 +131,7 @@ urlpatterns = [
|
|||||||
path(
|
path(
|
||||||
"<str:_filter>/<str:search>/page/<int:page>",
|
"<str:_filter>/<str:search>/page/<int:page>",
|
||||||
GetObjectsFiltered.as_view(),
|
GetObjectsFiltered.as_view(),
|
||||||
name="filtered_pagination",
|
name="filtered",
|
||||||
),
|
),
|
||||||
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),
|
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ from itertools import chain
|
|||||||
from functools import reduce
|
from functools import reduce
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from django.urls import Resolver404
|
||||||
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.db.models.functions import Lower
|
||||||
from django.shortcuts import render, get_object_or_404, get_list_or_404
|
from django.shortcuts import render, get_object_or_404, get_list_or_404
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
@@ -16,7 +19,7 @@ from portal.utils import get_site_conf
|
|||||||
from portal.models import Flatpage
|
from portal.models import Flatpage
|
||||||
from roster.models import RollingStock
|
from roster.models import RollingStock
|
||||||
from consist.models import Consist
|
from consist.models import Consist
|
||||||
from bookshelf.models import Book, Catalog
|
from bookshelf.models import Book, Catalog, Magazine, MagazineIssue
|
||||||
from metadata.models import (
|
from metadata.models import (
|
||||||
Company,
|
Company,
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
@@ -31,52 +34,67 @@ def get_items_per_page():
|
|||||||
items_per_page = get_site_conf().items_per_page
|
items_per_page = get_site_conf().items_per_page
|
||||||
except (OperationalError, ProgrammingError):
|
except (OperationalError, ProgrammingError):
|
||||||
items_per_page = 6
|
items_per_page = 6
|
||||||
return items_per_page
|
return int(items_per_page)
|
||||||
|
|
||||||
|
|
||||||
def get_order_by_field():
|
def get_items_ordering(config="items_ordering"):
|
||||||
try:
|
try:
|
||||||
order_by = get_site_conf().items_ordering
|
order_by = getattr(get_site_conf(), config)
|
||||||
except (OperationalError, ProgrammingError):
|
except (OperationalError, ProgrammingError):
|
||||||
order_by = "type"
|
order_by = "type"
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
"rolling_class__type",
|
"rolling_class__type", # 0
|
||||||
"rolling_class__company",
|
"rolling_class__company", # 1
|
||||||
"rolling_class__identifier",
|
"rolling_class__company__country", # 2
|
||||||
"road_number_int",
|
"rolling_class__identifier", # 3
|
||||||
|
"road_number_int", # 4
|
||||||
]
|
]
|
||||||
|
|
||||||
if order_by == "type":
|
order_map = {
|
||||||
return (fields[0], fields[1], fields[2], fields[3])
|
"type": (0, 1, 3, 4),
|
||||||
elif order_by == "company":
|
"company": (1, 0, 3, 4),
|
||||||
return (fields[1], fields[0], fields[2], fields[3])
|
"country": (2, 0, 1, 3, 4),
|
||||||
elif order_by == "identifier":
|
"cou+com": (2, 1, 0, 3, 4),
|
||||||
return (fields[2], fields[0], fields[1], fields[3])
|
"class": (0, 3, 1, 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
return tuple(fields[i] for i in order_map.get(order_by, "type"))
|
||||||
|
|
||||||
|
|
||||||
class Render404(View):
|
class Render404(View):
|
||||||
def get(self, request, exception):
|
def get(self, request, exception):
|
||||||
return render(request, "base.html", {"title": "404 page not found"})
|
generic_message = "Page not found"
|
||||||
|
if isinstance(exception, Resolver404):
|
||||||
|
message = generic_message
|
||||||
|
else:
|
||||||
|
message = str(exception) if exception else generic_message
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"base.html",
|
||||||
|
{"title": message},
|
||||||
|
status=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GetData(View):
|
class GetData(View):
|
||||||
title = "Home"
|
title = None
|
||||||
template = "pagination.html"
|
template = "pagination.html"
|
||||||
item_type = "roster"
|
|
||||||
filter = Q() # empty filter by default
|
filter = Q() # empty filter by default
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return (
|
return (
|
||||||
RollingStock.objects.get_published(request.user)
|
RollingStock.objects.get_published(request.user)
|
||||||
.order_by(*get_order_by_field())
|
.order_by(*get_items_ordering())
|
||||||
.filter(self.filter)
|
.filter(self.filter)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, page=1):
|
def get(self, request, page=1):
|
||||||
data = []
|
if self.title is None or self.template is None:
|
||||||
for item in self.get_data(request):
|
raise Exception("title and template must be defined")
|
||||||
data.append({"type": self.item_type, "item": item})
|
|
||||||
|
data = list(self.get_data(request))
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, get_items_per_page())
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
@@ -89,7 +107,6 @@ class GetData(View):
|
|||||||
self.template,
|
self.template,
|
||||||
{
|
{
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"type": self.item_type,
|
|
||||||
"data": data,
|
"data": data,
|
||||||
"matches": paginator.count,
|
"matches": paginator.count,
|
||||||
"page_range": page_range,
|
"page_range": page_range,
|
||||||
@@ -97,18 +114,38 @@ class GetData(View):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GetRoster(GetData):
|
class GetHome(GetData):
|
||||||
title = "The Roster"
|
title = "Home"
|
||||||
item_type = "roster"
|
template = "home.html"
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return RollingStock.objects.get_published(request.user).order_by(
|
max_items = min(settings.FEATURED_ITEMS_MAX, get_items_per_page())
|
||||||
*get_order_by_field()
|
return (
|
||||||
)
|
RollingStock.objects.get_published(request.user)
|
||||||
|
.filter(featured=True)
|
||||||
|
.order_by(*get_items_ordering(config="featured_items_ordering"))[
|
||||||
|
:max_items
|
||||||
|
]
|
||||||
|
) or super().get_data(request)
|
||||||
|
|
||||||
|
|
||||||
|
class GetRoster(GetData):
|
||||||
|
title = "The Roster"
|
||||||
|
|
||||||
|
|
||||||
class SearchObjects(View):
|
class SearchObjects(View):
|
||||||
def run_search(self, request, search, _filter, page=1):
|
def run_search(self, request, search, _filter, page=1):
|
||||||
|
"""
|
||||||
|
Run the search query on the database and return the results.
|
||||||
|
param request: HTTP request
|
||||||
|
param search: search string
|
||||||
|
param _filter: filter to apply (type, company, manufacturer, scale)
|
||||||
|
param page: page number for pagination
|
||||||
|
return: tuple (data, matches, page_range)
|
||||||
|
1. data: list of dicts with keys "type" and "item"
|
||||||
|
2. matches: total number of matches
|
||||||
|
3. page_range: elided page range for pagination
|
||||||
|
"""
|
||||||
if _filter is None:
|
if _filter is None:
|
||||||
query = reduce(
|
query = reduce(
|
||||||
operator.or_,
|
operator.or_,
|
||||||
@@ -151,15 +188,13 @@ class SearchObjects(View):
|
|||||||
# FIXME duplicated code!
|
# FIXME duplicated code!
|
||||||
# FIXME see if it makes sense to filter calatogs and books by scale
|
# FIXME see if it makes sense to filter calatogs and books by scale
|
||||||
# and manufacturer as well
|
# and manufacturer as well
|
||||||
data = []
|
|
||||||
roster = (
|
roster = (
|
||||||
RollingStock.objects.get_published(request.user)
|
RollingStock.objects.get_published(request.user)
|
||||||
.filter(query)
|
.filter(query)
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by(*get_order_by_field())
|
.order_by(*get_items_ordering())
|
||||||
)
|
)
|
||||||
for item in roster:
|
data = list(roster)
|
||||||
data.append({"type": "roster", "item": item})
|
|
||||||
|
|
||||||
if _filter is None:
|
if _filter is None:
|
||||||
consists = (
|
consists = (
|
||||||
@@ -172,20 +207,41 @@ class SearchObjects(View):
|
|||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
for item in consists:
|
data = list(chain(data, consists))
|
||||||
data.append({"type": "consist", "item": item})
|
|
||||||
books = (
|
books = (
|
||||||
Book.objects.get_published(request.user)
|
Book.objects.get_published(request.user)
|
||||||
.filter(title__icontains=search)
|
.filter(
|
||||||
|
Q(
|
||||||
|
Q(title__icontains=search)
|
||||||
|
| Q(description__icontains=search)
|
||||||
|
| Q(toc__title__icontains=search)
|
||||||
|
)
|
||||||
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
catalogs = (
|
catalogs = (
|
||||||
Catalog.objects.get_published(request.user)
|
Catalog.objects.get_published(request.user)
|
||||||
.filter(manufacturer__name__icontains=search)
|
.filter(
|
||||||
|
Q(
|
||||||
|
Q(manufacturer__name__icontains=search)
|
||||||
|
| Q(description__icontains=search)
|
||||||
|
)
|
||||||
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
for item in list(chain(books, catalogs)):
|
data = list(chain(data, books, catalogs))
|
||||||
data.append({"type": "book", "item": item})
|
magazine_issues = (
|
||||||
|
MagazineIssue.objects.get_published(request.user)
|
||||||
|
.filter(
|
||||||
|
Q(
|
||||||
|
Q(magazine__name__icontains=search)
|
||||||
|
| Q(description__icontains=search)
|
||||||
|
| Q(toc__title__icontains=search)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
data = list(chain(data, magazine_issues))
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, get_items_per_page())
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
@@ -241,20 +297,36 @@ class SearchObjects(View):
|
|||||||
|
|
||||||
class GetManufacturerItem(View):
|
class GetManufacturerItem(View):
|
||||||
def get(self, request, manufacturer, search="all", page=1):
|
def get(self, request, manufacturer, search="all", page=1):
|
||||||
|
"""
|
||||||
|
Get all items from a specific manufacturer. If `search` is not "all",
|
||||||
|
filter by item number as well, for example to get all itmes from the
|
||||||
|
same set.
|
||||||
|
The view returns both rolling stock and catalogs.
|
||||||
|
param request: HTTP request
|
||||||
|
param manufacturer: Manufacturer slug
|
||||||
|
param search: item number slug or "all"
|
||||||
|
param page: page number for pagination
|
||||||
|
return: rendered template
|
||||||
|
1. manufacturer: Manufacturer object
|
||||||
|
2. search: item number slug or "all"
|
||||||
|
3. data: list of dicts with keys "type" and "item"
|
||||||
|
4. matches: total number of matches
|
||||||
|
5. page_range: elided page range for pagination
|
||||||
|
"""
|
||||||
manufacturer = get_object_or_404(
|
manufacturer = get_object_or_404(
|
||||||
Manufacturer, slug__iexact=manufacturer
|
Manufacturer, slug__iexact=manufacturer
|
||||||
)
|
)
|
||||||
|
|
||||||
if search != "all":
|
if search != "all":
|
||||||
roster = get_list_or_404(
|
roster = get_list_or_404(
|
||||||
RollingStock.objects.get_published(request.user).order_by(
|
RollingStock.objects.get_published(request.user).order_by(
|
||||||
*get_order_by_field()
|
*get_items_ordering()
|
||||||
),
|
),
|
||||||
Q(
|
Q(
|
||||||
Q(manufacturer=manufacturer)
|
Q(manufacturer=manufacturer)
|
||||||
& Q(item_number_slug__exact=search)
|
& Q(item_number_slug__exact=search)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
catalogs = [] # no catalogs when searching for a specific item
|
||||||
title = "{0}: {1}".format(
|
title = "{0}: {1}".format(
|
||||||
manufacturer,
|
manufacturer,
|
||||||
# all returned records must have the same `item_number``;
|
# all returned records must have the same `item_number``;
|
||||||
@@ -269,14 +341,14 @@ class GetManufacturerItem(View):
|
|||||||
| Q(rolling_class__manufacturer=manufacturer)
|
| Q(rolling_class__manufacturer=manufacturer)
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by(*get_order_by_field())
|
.order_by(*get_items_ordering())
|
||||||
|
)
|
||||||
|
catalogs = Catalog.objects.get_published(request.user).filter(
|
||||||
|
manufacturer=manufacturer
|
||||||
)
|
)
|
||||||
title = "Manufacturer: {0}".format(manufacturer)
|
title = "Manufacturer: {0}".format(manufacturer)
|
||||||
|
|
||||||
data = []
|
data = list(chain(roster, catalogs))
|
||||||
for item in roster:
|
|
||||||
data.append({"type": "roster", "item": item})
|
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, get_items_per_page())
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
page_range = paginator.get_elided_page_range(
|
page_range = paginator.get_elided_page_range(
|
||||||
@@ -322,12 +394,18 @@ class GetObjectsFiltered(View):
|
|||||||
RollingStock.objects.get_published(request.user)
|
RollingStock.objects.get_published(request.user)
|
||||||
.filter(query)
|
.filter(query)
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by(*get_order_by_field())
|
.order_by(*get_items_ordering())
|
||||||
)
|
)
|
||||||
|
|
||||||
data = []
|
data = list(roster)
|
||||||
for item in roster:
|
|
||||||
data.append({"type": "roster", "item": item})
|
if _filter == "scale":
|
||||||
|
catalogs = (
|
||||||
|
Catalog.objects.get_published(request.user)
|
||||||
|
.filter(scales__slug=search)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
data = list(chain(data, catalogs))
|
||||||
|
|
||||||
try: # Execute only if query_2nd is defined
|
try: # Execute only if query_2nd is defined
|
||||||
consists = (
|
consists = (
|
||||||
@@ -335,23 +413,24 @@ class GetObjectsFiltered(View):
|
|||||||
.filter(query_2nd)
|
.filter(query_2nd)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
for item in consists:
|
data = list(chain(data, consists))
|
||||||
data.append({"type": "consist", "item": item})
|
|
||||||
if _filter == "tag": # Books can be filtered only by tag
|
if _filter == "tag": # Books can be filtered only by tag
|
||||||
books = (
|
books = (
|
||||||
Book.objects.get_published(request.user)
|
Book.objects.get_published(request.user)
|
||||||
.filter(query_2nd)
|
.filter(query_2nd)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
for item in books:
|
|
||||||
data.append({"type": "book", "item": item})
|
|
||||||
catalogs = (
|
catalogs = (
|
||||||
Catalog.objects.get_published(request.user)
|
Catalog.objects.get_published(request.user)
|
||||||
.filter(query_2nd)
|
.filter(query_2nd)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
for item in catalogs:
|
magazine_issues = (
|
||||||
data.append({"type": "catalog", "item": item})
|
MagazineIssue.objects.get_published(request.user)
|
||||||
|
.filter(query_2nd)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
data = list(chain(data, books, catalogs, magazine_issues))
|
||||||
except NameError:
|
except NameError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -405,24 +484,22 @@ class GetRollingStock(View):
|
|||||||
request.user
|
request.user
|
||||||
)
|
)
|
||||||
|
|
||||||
consists = [
|
consists = list(
|
||||||
{"type": "consist", "item": c}
|
Consist.objects.get_published(request.user).filter(
|
||||||
for c in Consist.objects.get_published(request.user).filter(
|
|
||||||
consist_item__rolling_stock=rolling_stock
|
consist_item__rolling_stock=rolling_stock
|
||||||
)
|
)
|
||||||
] # A dict with "item" is required by the consists card
|
)
|
||||||
|
|
||||||
set = [
|
trainset = list(
|
||||||
{"type": "set", "item": s}
|
RollingStock.objects.get_published(request.user)
|
||||||
for s in RollingStock.objects.get_published(request.user)
|
|
||||||
.filter(
|
.filter(
|
||||||
Q(
|
Q(
|
||||||
Q(item_number__exact=rolling_stock.item_number)
|
Q(item_number__exact=rolling_stock.item_number)
|
||||||
& Q(set=True)
|
& Q(set=True)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.order_by(*get_order_by_field())
|
.order_by(*get_items_ordering())
|
||||||
]
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -435,7 +512,7 @@ class GetRollingStock(View):
|
|||||||
"decoder_documents": decoder_documents,
|
"decoder_documents": decoder_documents,
|
||||||
"documents": documents,
|
"documents": documents,
|
||||||
"journal": journal,
|
"journal": journal,
|
||||||
"set": set,
|
"set": trainset,
|
||||||
"consists": consists,
|
"consists": consists,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -443,7 +520,6 @@ class GetRollingStock(View):
|
|||||||
|
|
||||||
class Consists(GetData):
|
class Consists(GetData):
|
||||||
title = "Consists"
|
title = "Consists"
|
||||||
item_type = "consist"
|
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return Consist.objects.get_published(request.user).all()
|
return Consist.objects.get_published(request.user).all()
|
||||||
@@ -457,16 +533,19 @@ class GetConsist(View):
|
|||||||
)
|
)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
data = [
|
|
||||||
{
|
|
||||||
"type": "roster",
|
|
||||||
"item": RollingStock.objects.get_published(request.user).get(
|
|
||||||
uuid=r.rolling_stock_id
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for r in consist.consist_item.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
data = list(
|
||||||
|
RollingStock.objects.get_published(request.user).get(
|
||||||
|
uuid=r.rolling_stock_id
|
||||||
|
)
|
||||||
|
for r in consist.consist_item.filter(load=False)
|
||||||
|
)
|
||||||
|
loads = list(
|
||||||
|
RollingStock.objects.get_published(request.user).get(
|
||||||
|
uuid=r.rolling_stock_id
|
||||||
|
)
|
||||||
|
for r in consist.consist_item.filter(load=True)
|
||||||
|
)
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, get_items_per_page())
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
page_range = paginator.get_elided_page_range(
|
page_range = paginator.get_elided_page_range(
|
||||||
@@ -480,6 +559,7 @@ class GetConsist(View):
|
|||||||
"title": consist,
|
"title": consist,
|
||||||
"consist": consist,
|
"consist": consist,
|
||||||
"data": data,
|
"data": data,
|
||||||
|
"loads": loads,
|
||||||
"page_range": page_range,
|
"page_range": page_range,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -487,10 +567,62 @@ class GetConsist(View):
|
|||||||
|
|
||||||
class Manufacturers(GetData):
|
class Manufacturers(GetData):
|
||||||
title = "Manufacturers"
|
title = "Manufacturers"
|
||||||
item_type = "manufacturer"
|
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return 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_catalogs=(
|
||||||
|
Count(
|
||||||
|
"catalogs",
|
||||||
|
filter=Q(
|
||||||
|
catalogs__in=(
|
||||||
|
Catalog.objects.get_published(request.user)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
distinct=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
num_items=(
|
||||||
|
F("num_rollingstock")
|
||||||
|
+ F("num_rollingclass")
|
||||||
|
+ F("num_catalogs")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("name")
|
||||||
|
)
|
||||||
|
|
||||||
# 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):
|
||||||
@@ -503,34 +635,95 @@ class Manufacturers(GetData):
|
|||||||
|
|
||||||
class Companies(GetData):
|
class Companies(GetData):
|
||||||
title = "Companies"
|
title = "Companies"
|
||||||
item_type = "company"
|
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return 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"
|
|
||||||
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,
|
||||||
|
),
|
||||||
|
num_catalogs=Count("catalogs", distinct=True),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
num_items=(
|
||||||
|
F("num_rollingstock")
|
||||||
|
+ F("num_consists")
|
||||||
|
+ F("num_catalogs")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("-ratio_int", "-tracks", "scale")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Types(GetData):
|
class Types(GetData):
|
||||||
title = "Types"
|
title = "Types"
|
||||||
item_type = "rolling_stock_type"
|
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return RollingStockType.objects.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):
|
||||||
title = "Books"
|
title = "Books"
|
||||||
item_type = "book"
|
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return Book.objects.get_published(request.user).all()
|
return Book.objects.get_published(request.user).all()
|
||||||
@@ -538,12 +731,82 @@ class Books(GetData):
|
|||||||
|
|
||||||
class Catalogs(GetData):
|
class Catalogs(GetData):
|
||||||
title = "Catalogs"
|
title = "Catalogs"
|
||||||
item_type = "catalog"
|
|
||||||
|
|
||||||
def get_data(self, request):
|
def get_data(self, request):
|
||||||
return Catalog.objects.get_published(request.user).all()
|
return Catalog.objects.get_published(request.user).all()
|
||||||
|
|
||||||
|
|
||||||
|
class Magazines(GetData):
|
||||||
|
title = "Magazines"
|
||||||
|
|
||||||
|
def get_data(self, request):
|
||||||
|
return (
|
||||||
|
Magazine.objects.get_published(request.user)
|
||||||
|
.order_by(Lower("name"))
|
||||||
|
.annotate(
|
||||||
|
issues=Count(
|
||||||
|
"issue",
|
||||||
|
filter=Q(
|
||||||
|
issue__in=(
|
||||||
|
MagazineIssue.objects.get_published(request.user)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GetMagazine(View):
|
||||||
|
def get(self, request, uuid, page=1):
|
||||||
|
try:
|
||||||
|
magazine = Magazine.objects.get_published(request.user).get(
|
||||||
|
uuid=uuid
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
data = list(magazine.issue.get_published(request.user).all())
|
||||||
|
paginator = Paginator(data, get_items_per_page())
|
||||||
|
data = paginator.get_page(page)
|
||||||
|
page_range = paginator.get_elided_page_range(
|
||||||
|
data.number, on_each_side=1, on_ends=1
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"magazine.html",
|
||||||
|
{
|
||||||
|
"title": magazine,
|
||||||
|
"magazine": magazine,
|
||||||
|
"data": data,
|
||||||
|
"matches": paginator.count,
|
||||||
|
"page_range": page_range,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GetMagazineIssue(View):
|
||||||
|
def get(self, request, uuid, magazine, page=1):
|
||||||
|
try:
|
||||||
|
issue = MagazineIssue.objects.get_published(request.user).get(
|
||||||
|
uuid=uuid,
|
||||||
|
magazine__uuid=magazine,
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
properties = issue.property.get_public(request.user)
|
||||||
|
documents = issue.document.get_public(request.user)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"bookshelf/book.html",
|
||||||
|
{
|
||||||
|
"title": issue,
|
||||||
|
"data": issue,
|
||||||
|
"documents": documents,
|
||||||
|
"properties": properties,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GetBookCatalog(View):
|
class GetBookCatalog(View):
|
||||||
def get_object(self, request, uuid, selector):
|
def get_object(self, request, uuid, selector):
|
||||||
if selector == "book":
|
if selector == "book":
|
||||||
@@ -566,10 +829,9 @@ class GetBookCatalog(View):
|
|||||||
"bookshelf/book.html",
|
"bookshelf/book.html",
|
||||||
{
|
{
|
||||||
"title": book,
|
"title": book,
|
||||||
"book": book,
|
"data": book,
|
||||||
"documents": documents,
|
"documents": documents,
|
||||||
"properties": properties,
|
"properties": properties,
|
||||||
"type": selector
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from ram.utils import git_suffix
|
from ram.utils import git_suffix
|
||||||
|
|
||||||
__version__ = "0.16.9"
|
__version__ = "0.19.8"
|
||||||
__version__ += git_suffix(__file__)
|
__version__ += git_suffix(__file__)
|
||||||
|
|||||||
@@ -1,22 +1,60 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
admin.site.site_header = settings.SITE_NAME
|
admin.site.site_header = settings.SITE_NAME
|
||||||
|
|
||||||
|
|
||||||
def publish(modeladmin, request, queryset):
|
def publish(modeladmin, request, queryset):
|
||||||
for obj in queryset:
|
queryset.update(published=True)
|
||||||
obj.published = True
|
cache.clear()
|
||||||
obj.save()
|
|
||||||
|
|
||||||
|
|
||||||
publish.short_description = "Publish selected items"
|
publish.short_description = "Publish selected items"
|
||||||
|
|
||||||
|
|
||||||
def unpublish(modeladmin, request, queryset):
|
def unpublish(modeladmin, request, queryset):
|
||||||
for obj in queryset:
|
queryset.update(published=False)
|
||||||
obj.published = False
|
cache.clear()
|
||||||
obj.save()
|
|
||||||
|
|
||||||
|
|
||||||
unpublish.short_description = "Unpublish selected items"
|
unpublish.short_description = "Unpublish selected items"
|
||||||
|
|
||||||
|
|
||||||
|
def set_featured(modeladmin, request, queryset):
|
||||||
|
count = queryset.count()
|
||||||
|
if count > settings.FEATURED_ITEMS_MAX:
|
||||||
|
modeladmin.message_user(
|
||||||
|
request,
|
||||||
|
"You can only mark up to {} items as featured.".format(
|
||||||
|
settings.FEATURED_ITEMS_MAX
|
||||||
|
),
|
||||||
|
level="error",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
featured = modeladmin.model.objects.filter(featured=True).count()
|
||||||
|
if featured + count > settings.FEATURED_ITEMS_MAX:
|
||||||
|
modeladmin.message_user(
|
||||||
|
request,
|
||||||
|
"There are already {} featured items. You can only mark {} more items as featured.".format( # noqa: E501
|
||||||
|
featured,
|
||||||
|
settings.FEATURED_ITEMS_MAX - featured,
|
||||||
|
),
|
||||||
|
level="error",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
queryset.update(featured=True)
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
set_featured.short_description = "Mark selected items as featured"
|
||||||
|
|
||||||
|
|
||||||
|
def unset_featured(modeladmin, request, queryset):
|
||||||
|
queryset.update(featured=False)
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
unset_featured.short_description = (
|
||||||
|
"Unmark selected items as featured"
|
||||||
|
)
|
||||||
|
|||||||
@@ -34,3 +34,4 @@ ALLOWED_HOSTS = ["127.0.0.1", "myhost"]
|
|||||||
CSRF_TRUSTED_ORIGINS = ["https://myhost"]
|
CSRF_TRUSTED_ORIGINS = ["https://myhost"]
|
||||||
STATIC_URL = "static/"
|
STATIC_URL = "static/"
|
||||||
MEDIA_URL = "media/"
|
MEDIA_URL = "media/"
|
||||||
|
USE_X_ACCEL_REDIRECT = True
|
||||||
|
|||||||
@@ -9,6 +9,19 @@ from ram.utils import DeduplicatedStorage, get_image_preview
|
|||||||
from ram.managers import PublicManager
|
from ram.managers import PublicManager
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleBaseModel(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def obj_type(self):
|
||||||
|
return self._meta.model_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def obj_label(self):
|
||||||
|
return self._meta.object_name
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
class BaseModel(models.Model):
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
description = tinymce.HTMLField(blank=True)
|
description = tinymce.HTMLField(blank=True)
|
||||||
@@ -20,6 +33,14 @@ class BaseModel(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def obj_type(self):
|
||||||
|
return self._meta.model_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def obj_label(self):
|
||||||
|
return self._meta.object_name
|
||||||
|
|
||||||
objects = PublicManager()
|
objects = PublicManager()
|
||||||
|
|
||||||
|
|
||||||
@@ -27,11 +48,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(),
|
storage=DeduplicatedStorage,
|
||||||
)
|
|
||||||
private = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="Document will be visible only to logged users",
|
|
||||||
)
|
)
|
||||||
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)
|
||||||
@@ -61,8 +78,17 @@ class Document(models.Model):
|
|||||||
'<a href="{0}" target="_blank">Link</a>'.format(self.file.url)
|
'<a href="{0}" target="_blank">Link</a>'.format(self.file.url)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateDocument(Document):
|
||||||
|
private = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Document will be visible only to logged users",
|
||||||
|
)
|
||||||
objects = PublicManager()
|
objects = PublicManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Image(models.Model):
|
class Image(models.Model):
|
||||||
order = models.PositiveIntegerField(default=0, blank=False, null=False)
|
order = models.PositiveIntegerField(default=0, blank=False, null=False)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ INSTALLED_APPS = [
|
|||||||
"portal",
|
"portal",
|
||||||
# "driver", # uncomment this to enable the "driver" API
|
# "driver", # uncomment this to enable the "driver" API
|
||||||
"metadata",
|
"metadata",
|
||||||
|
"repository",
|
||||||
"roster",
|
"roster",
|
||||||
"consist",
|
"consist",
|
||||||
"bookshelf",
|
"bookshelf",
|
||||||
@@ -149,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 "
|
||||||
@@ -203,6 +204,21 @@ ROLLING_STOCK_TYPES = [
|
|||||||
("other", "Other"),
|
("other", "Other"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
FEATURED_ITEMS_MAX = 6
|
||||||
|
|
||||||
|
# If True, use X-Accel-Redirect (Nginx)
|
||||||
|
# when using X-Accel-Redirect, we don't serve the file
|
||||||
|
# directly from Django, but let Nginx handle it
|
||||||
|
# in Nginx config, we need to map /private/ to
|
||||||
|
# the actual media files location with internal directive
|
||||||
|
# eg:
|
||||||
|
# location /private {
|
||||||
|
# internal;
|
||||||
|
# alias /path/to/media;
|
||||||
|
# }
|
||||||
|
# make also sure that the entire /media is _not_ mapped directly in Nginx
|
||||||
|
USE_X_ACCEL_REDIRECT = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ram.local_settings import *
|
from ram.local_settings import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -21,17 +21,22 @@ from django.conf.urls.static import static
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from ram.views import UploadImage
|
from ram.views import UploadImage, DownloadFile
|
||||||
from portal.views import Render404
|
from portal.views import Render404
|
||||||
|
|
||||||
handler404 = Render404.as_view()
|
handler404 = Render404.as_view()
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", lambda r: redirect("portal/")),
|
path("", lambda r: redirect("portal/")),
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
path("tinymce/", include("tinymce.urls")),
|
path("tinymce/", include("tinymce.urls")),
|
||||||
path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"),
|
path("tinymce/upload_image", UploadImage.as_view(), name="upload_image"),
|
||||||
|
path(
|
||||||
|
"media/files/<path:filename>",
|
||||||
|
DownloadFile.as_view(),
|
||||||
|
name="download_file",
|
||||||
|
),
|
||||||
path("portal/", include("portal.urls")),
|
path("portal/", include("portal.urls")),
|
||||||
path("admin/", admin.site.urls),
|
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
# Enable the "/dcc" routing only if the "driver" app is active
|
# Enable the "/dcc" routing only if the "driver" app is active
|
||||||
@@ -55,6 +60,7 @@ if settings.DEBUG:
|
|||||||
if settings.REST_ENABLED:
|
if settings.REST_ENABLED:
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from rest_framework.schemas import get_schema_view
|
from rest_framework.schemas import get_schema_view
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path(
|
path(
|
||||||
"swagger/",
|
"swagger/",
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,26 @@ import posixpath
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from PIL import Image, UnidentifiedImageError
|
from PIL import Image, UnidentifiedImageError
|
||||||
|
|
||||||
from django.views import View
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import (
|
from django.http import (
|
||||||
|
Http404,
|
||||||
|
HttpResponse,
|
||||||
HttpResponseBadRequest,
|
HttpResponseBadRequest,
|
||||||
HttpResponseForbidden,
|
HttpResponseForbidden,
|
||||||
|
FileResponse,
|
||||||
JsonResponse,
|
JsonResponse,
|
||||||
)
|
)
|
||||||
|
from django.views import View
|
||||||
from django.utils.text import slugify as slugify
|
from django.utils.text import slugify as slugify
|
||||||
|
from django.utils.encoding import smart_str
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from rest_framework.pagination import LimitOffsetPagination
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
|
|
||||||
|
from ram.models import PrivateDocument
|
||||||
|
|
||||||
|
|
||||||
class CustomLimitOffsetPagination(LimitOffsetPagination):
|
class CustomLimitOffsetPagination(LimitOffsetPagination):
|
||||||
default_limit = 10
|
default_limit = 10
|
||||||
@@ -67,3 +74,50 @@ class UploadImage(View):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadFile(View):
|
||||||
|
def get(self, request, filename, disposition="inline"):
|
||||||
|
# Clean up the filename to prevent directory traversal attacks
|
||||||
|
filename = os.path.basename(filename)
|
||||||
|
|
||||||
|
# Find a document where the stored file name matches
|
||||||
|
# Find all models inheriting from PublishableFile
|
||||||
|
for model in apps.get_models():
|
||||||
|
if issubclass(model, PrivateDocument) and not model._meta.abstract:
|
||||||
|
try:
|
||||||
|
doc = model.objects.get(file__endswith=filename)
|
||||||
|
if doc.private and not request.user.is_staff:
|
||||||
|
break
|
||||||
|
|
||||||
|
file = doc.file
|
||||||
|
if not os.path.exists(file.path):
|
||||||
|
break
|
||||||
|
|
||||||
|
# in Nginx config, we need to map /private/ to
|
||||||
|
# the actual media files location with internal directive
|
||||||
|
# eg:
|
||||||
|
# location /private {
|
||||||
|
# internal;
|
||||||
|
# alias /path/to/media;
|
||||||
|
# }
|
||||||
|
if getattr(settings, "USE_X_ACCEL_REDIRECT", False):
|
||||||
|
response = HttpResponse()
|
||||||
|
response["Content-Type"] = ""
|
||||||
|
response["X-Accel-Redirect"] = f"/private/{file.name}"
|
||||||
|
else:
|
||||||
|
response = FileResponse(
|
||||||
|
open(file.path, "rb"), as_attachment=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response["Content-Disposition"] = (
|
||||||
|
'{}; filename="{}"'.format(
|
||||||
|
disposition,
|
||||||
|
smart_str(os.path.basename(file.path))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
except model.DoesNotExist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise Http404("File not found")
|
||||||
|
|||||||
0
ram/repository/__init__.py
Normal file
0
ram/repository/__init__.py
Normal file
248
ram/repository/admin.py
Normal file
248
ram/repository/admin.py
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from ram.admin import publish, unpublish
|
||||||
|
from repository.models import (
|
||||||
|
GenericDocument,
|
||||||
|
InvoiceDocument,
|
||||||
|
BookDocument,
|
||||||
|
CatalogDocument,
|
||||||
|
DecoderDocument,
|
||||||
|
RollingStockDocument
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(GenericDocument)
|
||||||
|
class GenericDocumentAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ("size", "creation_time", "updated_time")
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"description",
|
||||||
|
"private",
|
||||||
|
"size",
|
||||||
|
"download",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
)
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"private",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
"size",
|
||||||
|
"tags",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{"classes": ("collapse",), "fields": ("notes",)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Audit",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"creation_time",
|
||||||
|
"updated_time",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(InvoiceDocument)
|
||||||
|
class InvoiceDocumentAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ("size", "creation_time", "updated_time")
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"description",
|
||||||
|
"date",
|
||||||
|
"shop",
|
||||||
|
"size",
|
||||||
|
"download",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"rolling_stock__manufacturer__name",
|
||||||
|
"rolling_stock__item_number",
|
||||||
|
"book__title",
|
||||||
|
"catalog__manufacturer__name",
|
||||||
|
"shop__name",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("rolling_stock", "book", "catalog", "shop")
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"rolling_stock",
|
||||||
|
"book",
|
||||||
|
"catalog",
|
||||||
|
"description",
|
||||||
|
"date",
|
||||||
|
"shop",
|
||||||
|
"file",
|
||||||
|
"size",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{"classes": ("collapse",), "fields": ("notes",)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Audit",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"creation_time",
|
||||||
|
"updated_time",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(BookDocument)
|
||||||
|
class BookDocumentAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ("size",)
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"book",
|
||||||
|
"description",
|
||||||
|
"private",
|
||||||
|
"size",
|
||||||
|
"download",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"book__title",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("book",)
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"private",
|
||||||
|
"book",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
"size",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CatalogDocument)
|
||||||
|
class CatalogDocumentAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ("size",)
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"catalog",
|
||||||
|
"description",
|
||||||
|
"private",
|
||||||
|
"size",
|
||||||
|
"download",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"catalog__title",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("catalog",)
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"private",
|
||||||
|
"catalog",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
"size",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(DecoderDocument)
|
||||||
|
class DecoderDocumentAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ("size",)
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"decoder",
|
||||||
|
"description",
|
||||||
|
"private",
|
||||||
|
"size",
|
||||||
|
"download",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"decoder__name",
|
||||||
|
"decoder__manufacturer__name",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("decoder",)
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"private",
|
||||||
|
"decoder",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
"size",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(RollingStockDocument)
|
||||||
|
class RollingStockDocumentAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ("size",)
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"rolling_stock",
|
||||||
|
"description",
|
||||||
|
"private",
|
||||||
|
"size",
|
||||||
|
"download",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"rolling_stock__rolling_class__identifier",
|
||||||
|
"rolling_stock__item_number",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("rolling_stock",)
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"private",
|
||||||
|
"rolling_stock",
|
||||||
|
"description",
|
||||||
|
"file",
|
||||||
|
"size",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
6
ram/repository/apps.py
Normal file
6
ram/repository/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "repository"
|
||||||
361
ram/repository/migrations/0001_initial.py
Normal file
361
ram/repository/migrations/0001_initial.py
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-09 13:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ram.utils
|
||||||
|
import tinymce.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_document(apps, schema_editor):
|
||||||
|
document = apps.get_model("metadata", "GenericDocument")
|
||||||
|
document_new = apps.get_model("repository", "GenericDocument")
|
||||||
|
for d in document.objects.all():
|
||||||
|
n = document_new.objects.create(
|
||||||
|
notes=d.notes,
|
||||||
|
description=d.description,
|
||||||
|
file=d.file,
|
||||||
|
private=d.private,
|
||||||
|
creation_time=d.creation_time,
|
||||||
|
updated_time=d.updated_time,
|
||||||
|
)
|
||||||
|
for t in d.tags.all():
|
||||||
|
n.tags.add(t)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_decoder(apps, schema_editor):
|
||||||
|
dcc_document = apps.get_model("metadata", "DecoderDocument")
|
||||||
|
dcc_document_new = apps.get_model("repository", "DecoderDocument")
|
||||||
|
for d in dcc_document.objects.all():
|
||||||
|
dcc_document_new.objects.create(
|
||||||
|
decoder=d.decoder,
|
||||||
|
description=d.description,
|
||||||
|
file=d.file,
|
||||||
|
private=d.private,
|
||||||
|
creation_time=d.creation_time,
|
||||||
|
updated_time=d.updated_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_rollingstock(apps, schema_editor):
|
||||||
|
rs_document = apps.get_model("roster", "RollingStockDocument")
|
||||||
|
rs_document_new = apps.get_model("repository", "RollingStockDocument")
|
||||||
|
for d in rs_document.objects.all():
|
||||||
|
rs_document_new.objects.create(
|
||||||
|
rolling_stock=d.rolling_stock,
|
||||||
|
description=d.description,
|
||||||
|
file=d.file,
|
||||||
|
private=d.private,
|
||||||
|
creation_time=d.creation_time,
|
||||||
|
updated_time=d.updated_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_book(apps, schema_editor):
|
||||||
|
book_document = apps.get_model("bookshelf", "BaseBookDocument")
|
||||||
|
book_document_new = apps.get_model("repository", "BaseBookDocument")
|
||||||
|
catalog_document_new = apps.get_model("repository", "CatalogDocument")
|
||||||
|
for d in book_document.objects.all():
|
||||||
|
if hasattr(d.book, "book"):
|
||||||
|
book_document_new.objects.create(
|
||||||
|
book=d.book.book,
|
||||||
|
description=d.description,
|
||||||
|
file=d.file,
|
||||||
|
private=d.private,
|
||||||
|
creation_time=d.creation_time,
|
||||||
|
updated_time=d.updated_time,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
catalog_document_new.objects.create(
|
||||||
|
catalog=d.book.catalog,
|
||||||
|
description=d.description,
|
||||||
|
file=d.file,
|
||||||
|
private=d.private,
|
||||||
|
creation_time=d.creation_time,
|
||||||
|
updated_time=d.updated_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0022_basebook_shop"),
|
||||||
|
("metadata", "0023_shop"),
|
||||||
|
("roster", "0035_alter_rollingstock_shop"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BaseBookDocument",
|
||||||
|
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/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"private",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Document will be visible only to logged users",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_time", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="new_document",
|
||||||
|
to="bookshelf.basebook",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name_plural": "Documents",
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BookDocument",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="document",
|
||||||
|
to="bookshelf.book",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name_plural": "Book documents",
|
||||||
|
"constraints": [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("book", "file"), name="unique_book_file"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CatalogDocument",
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"catalog",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="document",
|
||||||
|
to="bookshelf.catalog",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name_plural": "Catalog documents",
|
||||||
|
"constraints": [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=("catalog", "file"), name="unique_catalog_file"
|
||||||
|
)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="GenericDocument",
|
||||||
|
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/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"private",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Document will be visible only to logged users",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_time", models.DateTimeField(auto_now=True)),
|
||||||
|
("notes", tinymce.models.HTMLField(blank=True)),
|
||||||
|
(
|
||||||
|
"tags",
|
||||||
|
models.ManyToManyField(
|
||||||
|
blank=True, related_name="new_document", to="metadata.tag"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name_plural": "Generic Documents",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="RollingStockDocument",
|
||||||
|
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/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"private",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Document will be visible only to logged users",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_time", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"rolling_stock",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="new_document",
|
||||||
|
to="roster.rollingstock",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name_plural": "Documents",
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="DecoderDocument",
|
||||||
|
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/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"private",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Document will be visible only to logged users",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_time", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"decoder",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="new_document",
|
||||||
|
to="metadata.decoder",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name_plural": "Documents",
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_document,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_decoder,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_rollingstock,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_book,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user