3 Commits

13 changed files with 170 additions and 394 deletions

View File

@@ -2,17 +2,13 @@ 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 ( from repository.models import BookDocument, CatalogDocument
BookDocument,
CatalogDocument,
MagazineIssueDocument
)
from bookshelf.models import ( from bookshelf.models import (
BaseBookProperty, BaseBookProperty,
BaseBookImage, BaseBookImage,
@@ -20,8 +16,6 @@ from bookshelf.models import (
Author, Author,
Publisher, Publisher,
Catalog, Catalog,
Magazine,
MagazineIssue,
) )
@@ -54,10 +48,6 @@ class CatalogDocInline(BookDocInline):
model = CatalogDocument model = CatalogDocument
class MagazineIssueDocInline(BookDocInline):
model = MagazineIssueDocument
@admin.register(Book) @admin.register(Book)
class BookAdmin(SortableAdminBase, admin.ModelAdmin): class BookAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = ( inlines = (
@@ -133,13 +123,14 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Invoices") @admin.display(description="Invoices")
def invoices(self, obj): def invoices(self, obj):
if obj.invoice.exists(): if obj.invoice.exists():
html = "<br>".join( html = format_html_join(
"<a href=\"{}\" target=\"_blank\">{}</a>".format( "<br>",
i.file.url, i "<a href=\"{}\" target=\"_blank\">{}</a>",
) for i in obj.invoice.all()) ((i.file.url, i) for i in obj.invoice.all())
)
else: else:
html = "-" html = "-"
return format_html(html) return html
@admin.display(description="Publisher") @admin.display(description="Publisher")
def get_publisher(self, obj): def get_publisher(self, obj):
@@ -217,7 +208,7 @@ class PublisherAdmin(admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name) '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
@@ -295,13 +286,14 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Invoices") @admin.display(description="Invoices")
def invoices(self, obj): def invoices(self, obj):
if obj.invoice.exists(): if obj.invoice.exists():
html = "<br>".join( html = format_html_join(
"<a href=\"{}\" target=\"_blank\">{}</a>".format( "<br>",
i.file.url, i "<a href=\"{}\" target=\"_blank\">{}</a>",
) for i in obj.invoice.all()) ((i.file.url, i) for i in obj.invoice.all())
)
else: else:
html = "-" html = "-"
return format_html(html) return html
def download_csv(modeladmin, request, queryset): def download_csv(modeladmin, request, queryset):
header = [ header = [
@@ -354,47 +346,3 @@ 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(Issue)
class MagazineIssueAdmin(admin.ModelAdmin):
inlines = (
BookPropertyInline,
BookImageInline,
MagazineIssueDocInline,
)
list_display = (
"__str__",
"issue_number",
"published",
)
# autocomplete_fields = ("publisher",)
# readonly_fields = ("creation_time", "updated_time")
# search_fields = ("title", "publisher__name")
# list_filter = ("publisher__name", "language")
def get_model_perms(self, request):
"""
Return empty perms dict thus hiding the model from admin index.
"""
return {}
actions = [publish, unpublish]
@admin.register(Magazine)
class MagazineAdmin(admin.ModelAdmin):
inlines = (
MagazineIssueInline,
)
list_display = (
"__str__",
"publisher",
"published",
)
autocomplete_fields = ("publisher",)
readonly_fields = ("creation_time", "updated_time")
search_fields = ("name", "publisher__name")
list_filter = ("publisher__name", "language")
actions = [publish, unpublish]

View File

@@ -101,10 +101,10 @@ class Migration(migrations.Migration):
model_name="basebook", model_name="basebook",
name="old_title", name="old_title",
), ),
migrations.RemoveField( # migrations.RemoveField(
model_name="basebook", # model_name="basebook",
name="old_authors", # name="old_authors",
), # ),
migrations.RemoveField( migrations.RemoveField(
model_name="basebook", model_name="basebook",
name="old_publisher", name="old_publisher",

View 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,
),
),
]

View File

@@ -1,226 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-13 23:01
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", "0023_delete_basebookdocument"),
("metadata", "0025_alter_company_options_alter_manufacturer_options_and_more"),
]
operations = [
migrations.CreateModel(
name="Issue",
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)),
],
options={
"abstract": False,
},
bases=("bookshelf.basebook",),
),
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)),
(
"image",
models.ImageField(
blank=True,
storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.magazine_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"),
("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=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="magazine_issue",
to="bookshelf.issue",
),
),
(
"magazine",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="magazine_issue",
to="bookshelf.magazine",
),
),
],
options={
"ordering": ["magazine", "issue"],
"unique_together": {("magazine", "issue")},
},
),
]

View File

@@ -3,7 +3,6 @@ import shutil
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.utils.dates import MONTHS
from django_countries.fields import CountryField from django_countries.fields import CountryField
from ram.utils import DeduplicatedStorage from ram.utils import DeduplicatedStorage
@@ -154,62 +153,3 @@ class Catalog(BaseBook):
def get_scales(self): def get_scales(self):
return "/".join([s.scale for s in self.scales.all()]) return "/".join([s.scale for s in self.scales.all()])
get_scales.short_description = "Scales" get_scales.short_description = "Scales"
class Magazine(BaseModel):
name = models.CharField(max_length=200)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
image = models.ImageField(
blank=True,
upload_to=book_image_upload,
storage=DeduplicatedStorage,
)
language = models.CharField(
max_length=7,
choices=settings.LANGUAGES,
default='en'
)
tags = models.ManyToManyField(
Tag, related_name="magazine", blank=True
)
def delete(self, *args, **kwargs):
shutil.rmtree(
os.path.join(
settings.MEDIA_ROOT, "images", "magazines", str(self.uuid)
),
ignore_errors=True
)
super(Magazine, self).delete(*args, **kwargs)
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse(
"bookshelf_item",
kwargs={"selector": "magazine", "uuid": self.uuid}
)
class MagazineIssue(BaseBook):
magazine = models.ForeignKey(
Magazine, on_delete=models.CASCADE, related_name="issue"
)
issue_number = models.CharField(max_length=100)
publication_month = models.SmallIntegerField(
null=True,
blank=True,
choices=MONTHS.items()
)
class Meta:
unique_together = ("magazine", "issue_number")
ordering = ["magazine", "issue_number"]
def __str__(self):
return f"{self.magazine.name} - {self.issue.issue_number}"

View File

@@ -54,7 +54,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country) '<img src="{}" /> {}', obj.country.flag, obj.country
) )
fieldsets = ( fieldsets = (

View File

@@ -54,7 +54,7 @@ class CompanyAdmin(admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name) '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
@@ -68,7 +68,7 @@ class ManufacturerAdmin(admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name) '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )

View File

@@ -1,36 +1,38 @@
from django import template from django import template
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe
register = template.Library() register = template.Library()
@register.simple_tag @register.simple_tag
def dcc(object): def dcc(object):
socket = ( socket = mark_safe(
'<i class="bi bi-ban small"></i>' '<i class="bi bi-ban small"></i>'
) )
decoder = '' decoder = ''
if object.decoder_interface is not None: if object.decoder_interface is not None:
socket = ( socket = mark_safe(
f'<abbr title="{object.get_decoder_interface()}">' f'<abbr title="{object.get_decoder_interface()}">'
f'<i class="bi bi-dice-6"></i></abbr>' f'<i class="bi bi-dice-6"></i></abbr>'
) )
if object.decoder: if object.decoder:
if object.decoder.sound: if object.decoder.sound:
decoder = ( decoder = mark_safe(
f'<abbr title="{object.decoder}">' f'<abbr title="{object.decoder}">'
'<i class="bi bi-volume-up-fill"></i></abbr>' '<i class="bi bi-volume-up-fill"></i></abbr>'
) )
else: else:
decoder = ( decoder = mark_safe(
f'<abbr title="{object.decoder}' f'<abbr title="{object.decoder}'
f'({object.get_decoder_interface()})">' f'({object.get_decoder_interface()})">'
'<i class="bi bi-cpu-fill"></i></abbr>' '<i class="bi bi-cpu-fill"></i></abbr>'
) )
if decoder: if decoder:
return format_html( return format_html(
f'{socket} <i class="bi bi-arrow-bar-left"></i>{decoder}' '{} <i class="bi bi-arrow-bar-left"></i> {}',
socket,
decoder,
) )
return format_html(socket) return socket

View File

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

View File

@@ -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,
) )

View File

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

View File

@@ -2,7 +2,7 @@ import html
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html, strip_tags from django.utils.html import format_html, format_html_join, strip_tags
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
@@ -44,7 +44,7 @@ class RollingClass(admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country) '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
@@ -152,7 +152,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Country") @admin.display(description="Country")
def country_flag(self, obj): def country_flag(self, obj):
return format_html( return format_html(
'<img src="{}" /> {}'.format(obj.country.flag, obj.country) '<img src="{}" /> {}', obj.country.flag, obj.country.name
) )
fieldsets = ( fieldsets = (
@@ -222,13 +222,14 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
@admin.display(description="Invoices") @admin.display(description="Invoices")
def invoices(self, obj): def invoices(self, obj):
if obj.invoice.exists(): if obj.invoice.exists():
html = "<br>".join( html = format_html_join(
"<a href=\"{}\" target=\"_blank\">{}</a>".format( "<br>",
i.file.url, i "<a href=\"{}\" target=\"_blank\">{}</a>",
) for i in obj.invoice.all()) ((i.file.url, i) for i in obj.invoice.all())
)
else: else:
html = "-" html = "-"
return format_html(html) return html
def download_csv(modeladmin, request, queryset): def download_csv(modeladmin, request, queryset):
header = [ header = [