31 Commits

Author SHA1 Message Date
955397acd5 Update site icon 2025-06-01 21:51:51 +02:00
672cadd7e1 Introduce symbols legend 2025-05-28 23:38:02 +02:00
464fe57536 Fix an abbr in DCC 2025-05-27 23:57:21 +02:00
bd16c7eee7 Still improve the DCC section 2025-05-27 23:49:22 +02:00
cc2e374558 Simplify cards, use icons for DCC 2025-05-26 00:04:07 +02:00
1c25ac9b14 Minor UI improvements 2025-05-25 19:13:28 +02:00
de126a735d Reverse field renaming 2025-05-24 15:00:52 +02:00
18b5ab8053 Bump DCC-EX submodule 2025-05-24 14:52:36 +02:00
3acc80e2ad Fix another counting issue 2025-05-24 14:44:42 +02:00
552ba39970 Upgrade bootstrap to 5.3.6 and icons to 1.13.1 2025-05-12 14:06:37 +02:00
222e2075ec Change the behavior of the modal 2025-05-12 13:51:31 +02:00
b5c57dcd94 Rely on slugs to have a more natural sorting 2025-05-04 22:46:23 +02:00
b81c63898f Add more information in consist_item rows 2025-05-04 22:05:47 +02:00
76b266b1f9 Extend search on company to slug field to better manage accented and utf names 2025-05-04 19:13:43 +02:00
86657a3b9f Manually fix a migration to correctly bootsrap a new DB 2025-05-04 19:12:50 +02:00
d0d25424fb Fix road_number_int field sizing 2025-05-04 19:12:05 +02:00
292b95b8ed Fix a bug in roster search via scale 2025-05-03 21:02:44 +02:00
dea7a594bc Implement CSV export for cosists 2025-05-02 22:25:59 +02:00
60195bc99f Simplify the logic about scales in the consist and remove async updates 2025-05-02 13:40:09 +02:00
7673f0514a Fix filter by scale counters and add consist constrains
Still to be improved, see FIXME
2025-05-01 23:49:22 +02:00
40f42a9ee9 Reformat portal/views.py 2025-04-30 22:52:51 +02:00
2e06e94fde Add counters to cards 2025-04-30 22:50:43 +02:00
ece8d1ad94 Minor UI improvement 2025-04-29 22:34:46 +02:00
e9ec126ada Fix a bug in the consist admin search 2025-04-27 22:22:47 +02:00
1222116874 Improve consist counter, fix a bug with unpublished stock 2025-04-27 22:12:43 +02:00
85741f090c Provide consist composition 2025-04-27 18:22:13 +02:00
88d718fa94 Minor footer improvement on large screens 2025-04-26 00:16:09 +02:00
a2c857a3cd Fix a couple of bugs 2025-04-25 23:14:10 +02:00
647894bca7 Add country flag to cards 2025-03-20 22:07:12 +01:00
c8cc8c5ed0 Minor improvement in the CSS
Update the CommandStation-EX tag as well
2025-03-02 22:31:07 +01:00
e80dc604a7 Improve docs management and add invoices repo (#51)
* Create a repository app for documents, first step

* Step two (broken)

* Complete the implementation of document repository and add invoices

* Add support for invoices

* Update submodules
2025-02-17 23:25:19 +01:00
64 changed files with 1667 additions and 317 deletions

View File

@@ -8,10 +8,10 @@ from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from ram.admin import publish, unpublish
from ram.utils import generate_csv
from portal.utils import get_site_conf
from repository.models import BookDocument, CatalogDocument
from bookshelf.models import (
BaseBookProperty,
BaseBookImage,
BaseBookDocument,
Book,
Author,
Publisher,
@@ -28,13 +28,6 @@ class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
verbose_name = "Image"
class BookDocInline(admin.TabularInline):
model = BaseBookDocument
min_num = 0
extra = 0
classes = ["collapse"]
class BookPropertyInline(admin.TabularInline):
model = BaseBookProperty
min_num = 0
@@ -44,6 +37,17 @@ class BookPropertyInline(admin.TabularInline):
verbose_name_plural = "Properties"
class BookDocInline(admin.TabularInline):
model = BookDocument
min_num = 0
extra = 0
classes = ["collapse"]
class CatalogDocInline(BookDocInline):
model = CatalogDocument
@admin.register(Book)
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (
@@ -60,7 +64,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
"published",
)
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")
list_filter = ("publisher__name", "authors")
@@ -89,6 +93,7 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
"shop",
"purchase_date",
"price",
"invoices",
)
},
),
@@ -115,6 +120,17 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
)
return form
@admin.display(description="Invoices")
def invoices(self, obj):
if obj.invoice.exists():
html = "<br>".join(
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
i.file.url, i
) for i in obj.invoice.all())
else:
html = "-"
return format_html(html)
@admin.display(description="Publisher")
def get_publisher(self, obj):
return obj.publisher.name
@@ -200,7 +216,7 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = (
BookPropertyInline,
BookImageInline,
BookDocInline,
CatalogDocInline,
)
list_display = (
"__str__",
@@ -210,7 +226,7 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
"published",
)
autocomplete_fields = ("manufacturer",)
readonly_fields = ("creation_time", "updated_time")
readonly_fields = ("invoices", "creation_time", "updated_time")
search_fields = ("manufacturer__name", "years", "scales__scale")
list_filter = ("manufacturer__name", "publication_year", "scales__scale")
@@ -236,8 +252,10 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
"Purchase data",
{
"fields": (
"shop",
"purchase_date",
"price",
"invoices",
)
},
),
@@ -264,6 +282,17 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
)
return form
@admin.display(description="Invoices")
def invoices(self, obj):
if obj.invoice.exists():
html = "<br>".join(
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
i.file.url, i
) for i in obj.invoice.all())
else:
html = "-"
return format_html(html)
def download_csv(modeladmin, request, queryset):
header = [
"Catalog",

View File

@@ -29,6 +29,10 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RemoveConstraint(
model_name="basebookdocument",
name="unique_book_file",
),
migrations.AddField(
model_name="basebook",
name="shop",

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

View File

@@ -6,7 +6,7 @@ from django.urls import reverse
from django_countries.fields import CountryField
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
@@ -89,21 +89,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):
book = models.ForeignKey(
BaseBook,
@@ -152,6 +137,10 @@ class Catalog(BaseBook):
ordering = ["manufacturer", "publication_year"]
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()
return "%s %s %s" % (self.manufacturer.name, self.years, scales)

View File

@@ -1,11 +1,26 @@
import html
from django.conf import settings
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.utils import generate_csv
from consist.models import Consist, ConsistItem
# for future reference
# class ConsistItemInlineFormSet(CustomInlineFormSetMixin, BaseInlineFormSet):
# def clean(self):
# super().clean()
class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
model = ConsistItem
min_num = 1
@@ -14,10 +29,13 @@ class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
readonly_fields = (
"preview",
"published",
"address",
"type",
"scale",
"manufacturer",
"item_number",
"company",
"type",
"era",
"address",
)
@@ -28,7 +46,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"creation_time",
"updated_time",
)
list_filter = ("company", "era", "published")
list_filter = ("company__name", "era", "scale", "published")
list_display = ("__str__",) + list_filter + ("country_flag",)
search_fields = ("identifier",) + list_filter
save_as = True
@@ -46,9 +64,10 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"fields": (
"published",
"identifier",
"consist_address",
"company",
"scale",
"era",
"consist_address",
"description",
"image",
"tags",
@@ -70,4 +89,55 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
},
),
)
actions = [publish, unpublish]
def download_csv(modeladmin, request, queryset):
header = [
"ID",
"Name",
"Published",
"Company",
"Country",
"Address",
"Scale",
"Era",
"Description",
"Tags",
"Length",
"Composition",
"Item name",
"Item type",
"Item ID",
]
data = []
for obj in queryset:
for item in obj.consist_item.all():
types = " + ".join(
"{}x {}".format(t["count"], t["type"])
for t in obj.get_type_count()
)
data.append(
[
obj.uuid,
obj.__str__(),
"X" if obj.published else "",
obj.company.name,
obj.company.country,
obj.consist_address,
obj.scale.scale,
obj.era,
html.unescape(strip_tags(obj.description)),
settings.CSV_SEPARATOR_ALT.join(
t.name for t in obj.tags.all()
),
obj.length,
types,
item.rolling_stock.__str__(),
item.type,
item.rolling_stock.uuid,
]
)
return generate_csv(header, data, "consists.csv")
download_csv.short_description = "Download selected items as CSV"
actions = [publish, unpublish, download_csv]

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

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

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

View File

@@ -2,12 +2,13 @@ import os
from django.db import models
from django.urls import reverse
from django.utils.text import Truncator
from django.dispatch import receiver
from django.core.exceptions import ValidationError
from ram.models import BaseModel
from ram.utils import DeduplicatedStorage
from metadata.models import Company, Tag
from metadata.models import Company, Scale, Tag
from roster.models import RollingStock
@@ -26,6 +27,7 @@ class Consist(BaseModel):
blank=True,
help_text="Era or epoch of the consist",
)
scale = models.ForeignKey(Scale, on_delete=models.CASCADE)
image = models.ImageField(
upload_to=os.path.join("images", "consists"),
storage=DeduplicatedStorage,
@@ -39,16 +41,25 @@ class Consist(BaseModel):
def get_absolute_url(self):
return reverse("consist", kwargs={"uuid": self.uuid})
@property
def length(self):
return self.consist_item.count()
def get_type_count(self):
return self.consist_item.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")
@property
def country(self):
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:
ordering = ["company", "-creation_time"]
@@ -58,11 +69,7 @@ class ConsistItem(models.Model):
Consist, on_delete=models.CASCADE, related_name="consist_item"
)
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE)
order = models.PositiveIntegerField(
default=1000, # make sure it is always added at the end
blank=False,
null=False
)
order = models.PositiveIntegerField(blank=False, null=False)
class Meta:
ordering = ["order"]
@@ -76,6 +83,24 @@ class ConsistItem(models.Model):
def __str__(self):
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
if rolling_stock.scale != consist.scale:
raise ValidationError(
"The rolling stock and consist must be of the same scale."
)
if self.consist.published and not rolling_stock.published:
raise ValidationError(
"You must unpublish the the consist before using this item."
)
def published(self):
return self.rolling_stock.published
published.boolean = True
@@ -83,9 +108,21 @@ class ConsistItem(models.Model):
def preview(self):
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
def type(self):
return self.rolling_stock.rolling_class.type
return self.rolling_stock.rolling_class.type.type
@property
def address(self):

View File

@@ -2,18 +2,16 @@ from django.contrib import admin
from django.utils.html import format_html
from adminsortable2.admin import SortableAdminMixin
from ram.admin import publish, unpublish
from repository.models import DecoderDocument
from metadata.models import (
Property,
Decoder,
DecoderDocument,
Scale,
Shop,
Manufacturer,
Company,
Tag,
RollingStockType,
GenericDocument,
)
@@ -88,51 +86,6 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
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)
class ShopAdmin(admin.ModelAdmin):
list_display = ("name", "on_line", "active")

View File

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

View File

@@ -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"]},
),
]

View File

@@ -6,9 +6,6 @@ from django.dispatch.dispatcher import receiver
from django.core.exceptions import ValidationError
from django_countries.fields import CountryField
from tinymce import models as tinymce
from ram.models import Document
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
from ram.managers import PublicManager
@@ -46,7 +43,7 @@ class Manufacturer(models.Model):
)
class Meta:
ordering = ["category", "name"]
ordering = ["category", "slug"]
def __str__(self):
return self.name
@@ -81,7 +78,7 @@ class Company(models.Model):
class Meta:
verbose_name_plural = "Companies"
ordering = ["name"]
ordering = ["slug"]
def __str__(self):
return self.name
@@ -132,20 +129,6 @@ class Decoder(models.Model):
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):
try:
num, den = ratio.split(":")
@@ -224,7 +207,7 @@ class Tag(models.Model):
slug = models.CharField(max_length=128, unique=True)
class Meta:
ordering = ["name"]
ordering = ["slug"]
def __str__(self):
return self.name
@@ -239,14 +222,6 @@ class Tag(models.Model):
)
class GenericDocument(Document):
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)
country = CountryField(blank=True)

View File

@@ -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
* Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
*/
@@ -7,8 +7,8 @@
@font-face {
font-display: block;
font-family: "bootstrap-icons";
src: url("./fonts/bootstrap-icons.woff2?dd67030699838ea613ee6dbda90effa6") format("woff2"),
url("./fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("woff");
src: url("./fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a") format("woff2"),
url("./fonts/bootstrap-icons.woff?e34853135f9e39acf64315236852cd5a") format("woff");
}
.bi::before,
@@ -2076,3 +2076,31 @@ url("./fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("wof
.bi-suitcase2-fill::before { content: "\f901"; }
.bi-suitcase2::before { content: "\f902"; }
.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"; }

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

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

View File

@@ -43,13 +43,15 @@ a.badge, a.badge:hover {
border-top: calc(var(--bs-border-width) * 3) solid var(--bs-border-color);
}
#nav-journal ul, #nav-journal ol {
margin: 0;
#nav-journal ul,
#nav-journal ol {
padding-left: 1rem;
}
#nav-journal p {
margin: 0;
#nav-journal p:last-child,
#nav-journal ul:last-child,
#nav-journal ol:last-child {
margin-bottom: 0;
}
#footer > p {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -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">
<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" />
<style>
path {
text-indent:0;
text-transform:none;
}
</style>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="16" preserveAspectRatio="xMidYMid" version="1.0" viewBox="0 0 24 12" xmlns="http://www.w3.org/2000/svg">
<metadata>Created by potrace 1.15, written by Peter Selinger 2001-2017</metadata>
<g transform="matrix(.0039261 0 0 -.0039261 -1.4249 18.53)">
<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"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -16,11 +16,11 @@
<link rel="icon" href="{% static "favicon.png" %}" sizes="any">
<link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml">
{% 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-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" rel="stylesheet">
{% else %}
<link href="{% static "bootstrap@5.3.3/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@5.3.6/dist/css/bootstrap.min.css" %}" rel="stylesheet">
<link href="{% static "bootstrap-icons@1.13.1/font/bootstrap-icons.css" %}" rel="stylesheet">
{% endif %}
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
<style>
@@ -140,14 +140,10 @@
<div class="container d-flex">
<div class="me-auto">
<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">
<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" />
<style>
path {
text-indent:0;
text-transform:none;
}
</style>
<svg class="me-2" width="32" height="16" version="1.0" viewBox="0 0 24 12" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(.0039261 0 0 -.0039261 -1.4249 18.53)">
<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"/>
</g>
</svg>
<strong>{{ site_conf.site_name }}</strong>
</a>
@@ -215,12 +211,13 @@
<div class="container">{% block pagination %}{% endblock %}</div>
</div>
{% block extra_content %}{% endblock %}
{% include 'includes/symbols.html' %}
</main>
{% include 'includes/footer.html' %}
{% 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.6/dist/js/bootstrap.bundle.min.js"></script>
{% else %}
<script src="{% static "bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" %}"></script>
<script src="{% static "bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js" %}"></script>
{% endif %}
</body>
</html>

View File

@@ -9,6 +9,9 @@
{% endfor %}
</p>
{% endif %}
{% if not book.published %}
<span class="badge text-bg-warning">Unpublished</span> |
{% endif %}
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %}
{% block carousel %}
@@ -60,11 +63,6 @@
<th colspan="2" scope="row">
{% if type == "catalog" %}Catalog
{% elif type == "book" %}Book{% endif %}
<div class="float-end">
{% if not book.published %}
<span class="badge text-bg-warning">Draft</span>
{% endif %}
</div>
</th>
</tr>
</thead>

View File

@@ -28,7 +28,7 @@
{% elif d.type == "book" %}Book{% endif %}
<div class="float-end">
{% if not d.item.published %}
<span class="badge text-bg-warning">Draft</span>
<span class="badge text-bg-warning">Unpublished</span>
{% endif %}
</div>
</th>

View File

@@ -39,8 +39,10 @@
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.item.slug %}">Show all rolling stock</a>
{% with items=d.item.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="company" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.item.pk %}">Edit</a>{% endif %}
{% endwith %}
</div>
</div>
</div>

View File

@@ -27,12 +27,12 @@
<th colspan="2" scope="row">
Consist
<div class="float-end">
{% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span>
{% endif %}
{% if d.item.company.freelance %}
<span class="badge text-bg-secondary">Freelance</span>
{% endif %}
{% if not d.item.published %}
<span class="badge text-bg-warning">Draft</span>
{% endif %}
</div>
</th>
</tr>
@@ -46,7 +46,10 @@
{% endif %}
<tr>
<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.item.company.country.flag }}" alt="{{ d.item.company.country }}">
<abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr>
</td>
</tr>
<tr>
<th scope="row">Era</th>
@@ -54,7 +57,7 @@
</tr>
<tr>
<th scope="row">Length</th>
<td>{{ d.item.consist_item.count }}</td>
<td>{{ d.item.length }}</td>
</tr>
</tbody>
</table>

View File

@@ -30,8 +30,10 @@
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="manufacturer" search=d.item.slug %}">Show all rolling stock</a>
{% with items=d.item.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="manufacturer" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.item.pk %}">Edit</a>{% endif %}
{% endwith %}
</div>
</div>
</div>

View File

@@ -20,8 +20,10 @@
</tbody>
</table>
<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>
{% 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 %}
{% with items=d.item.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="type" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_rollingstocktype_change' d.item.pk %}">Edit</a>{% endif %}
{% endwith %}
</div>
</div>
</div>

View File

@@ -1,4 +1,6 @@
{% load static %}
{% load dcc %}
<div class="col">
<div class="card shadow-sm">
{% if d.item.image.exists %}
@@ -25,12 +27,12 @@
<th colspan="2" scope="row">
Rolling stock
<div class="float-end">
{% if not d.item.published %}
<span class="badge text-bg-warning">Unpublished</span>
{% endif %}
{% if d.item.company.freelance %}
<span class="badge text-bg-secondary">Freelance</span>
{% endif %}
{% if not d.item.published %}
<span class="badge text-bg-warning">Draft</span>
{% endif %}
</div>
</th>
</tr>
@@ -43,6 +45,7 @@
<tr>
<th scope="row">Company</th>
<td>
<img src="{{ d.item.company.country.flag }}" alt="{{ d.item.company.country }}">
<a href="{% url 'filtered' _filter="company" search=d.item.company.slug %}"><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></a>
</td>
</tr>
@@ -72,33 +75,12 @@
<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>
</tr>
<tr>
<th scope="row">DCC</th>
<td><a class="text-reset text-decoration-none" title="Symbols" href="" data-bs-toggle="modal" data-bs-target="#symbolsModal">{% dcc d.item %}</a></td>
</tr>
</tbody>
</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">
<a class="btn btn-sm btn-outline-primary" href="{{d.item.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.item.pk %}">Edit</a>{% endif %}

View File

@@ -28,8 +28,10 @@
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary{% if d.item.num_items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show all rolling stock</a>
{% with items=d.item.num_items %}
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
{% endwith %}
</div>
</div>
</div>

View File

@@ -7,6 +7,9 @@
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% if not consist.published %}
<span class="badge text-bg-warning">Unpublished</span> |
{% endif %}
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
{% endif %}
{% endblock %}
@@ -83,9 +86,6 @@
{% if consist.company.freelance %}
<span class="badge text-bg-secondary">Freelance</span>
{% endif %}
{% if not consist.published %}
<span class="badge text-bg-warning">Draft</span>
{% endif %}
</div>
</th>
</tr>
@@ -109,7 +109,11 @@
{% endif %}
<tr>
<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 %} &raquo; {% endif %}{% endfor %}</td>
</tr>
</tbody>
</table>

View File

@@ -1,6 +1,9 @@
{% extends 'base.html' %}
{% 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>
{% endblock %}
{% block carousel %}
@@ -9,11 +12,6 @@
<section class="py-4 text-start container">
<div class="row">
<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 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 %}

View File

@@ -12,14 +12,16 @@
<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>
{% if site_conf.show_version %}<br>Version {{ site_conf.version }}{% endif %}</p>
<p class="text-end fs-5">
{% 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 %}
<a class="text-reset" title="Back to top" href="#"><i class="bi bi-arrow-up-left-square-fill"></i></a>
<p class="text-end">
{% if site_conf.disclaimer %}
<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>
</div>
<!-- Modal -->
<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-header">
<h1 class="modal-title fs-5" id="disclaimerLabel">Disclaimer</h1>

View 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>

View File

@@ -1,4 +1,5 @@
{% extends 'base.html' %}
{% load dcc %}
{% block header %}
{% if rolling_stock.tags.all %}
@@ -8,6 +9,9 @@
{% endfor %}
</p>
{% 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>
{% endblock %}
{% 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-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>
{% 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 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 %}
@@ -59,7 +63,7 @@
<option value="nav-model">Model</option>
<option value="nav-class">Class</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 journal %}<option value="nav-journal">Journal</option>{% endif %}
{% if set %}<option value="nav-set">Set</option>{% endif %}
@@ -77,9 +81,6 @@
{% if company.freelance %}
<span class="badge text-bg-secondary">Freelance</span>
{% endif %}
{% if not rolling_stock.published %}
<span class="badge text-bg-warning">Draft</span>
{% endif %}
</div>
</th>
</tr>
@@ -142,7 +143,9 @@
<table class="table table-striped">
<thead>
<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>
</thead>
<tbody class="table-group-divider">
@@ -155,6 +158,16 @@
<th scope="row">Decoder</th>
<td>{{ rolling_stock.decoder }}</td>
</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>
<th scope="row">Address</th>
<td>{{ rolling_stock.address }}</td>
@@ -339,36 +352,55 @@
<table class="table table-striped">
<thead>
<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>
</thead>
<tbody class="table-group-divider">
<tr>
<th scope="row">Interface</th>
<th class="w-33" scope="row">Interface</th>
<td>{{ rolling_stock.get_decoder_interface }}</td>
</tr>
{% if rolling_stock.decoder %}
<tr>
<th class="w-33" scope="row">Address</th>
<td>{{ rolling_stock.address }}</td>
<th scope="row">Manufacturer</th>
<td>{{ rolling_stock.decoder.manufacturer | default:"-" }}</td>
</tr>
<tr>
<th scope="row">Name</th>
<td>{{ rolling_stock.decoder.name }}</td>
</tr>
<tr>
<th scope="row">Manufacturer</th>
<td>{{ rolling_stock.decoder.manufacturer | default:"-" }}</td>
</tr>
<tr>
<th scope="row">Version</th>
<td>{{ rolling_stock.decoder.version | default:"-"}}</td>
</tr>
<tr>
<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>
</tbody>
</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 class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
{% if documents %}

View File

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

View File

@@ -7,7 +7,7 @@ from urllib.parse import unquote
from django.views import View
from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError
from django.db.models import Q, Count
from django.db.models import F, Q, Count
from django.shortcuts import render, get_object_or_404, get_list_or_404
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
@@ -490,7 +490,40 @@ class Manufacturers(GetData):
item_type = "manufacturer"
def get_data(self, request):
return Manufacturer.objects.filter(self.filter)
return (
Manufacturer.objects.filter(self.filter).annotate(
num_rollingstock=(
Count(
"rollingstock",
filter=Q(
rollingstock__in=(
RollingStock.objects.get_published(
request.user
)
)
),
distinct=True,
)
)
)
.annotate(
num_rollingclass=(
Count(
"rollingclass__rolling_stock",
filter=Q(
rollingclass__rolling_stock__in=(
RollingStock.objects.get_published(
request.user
)
),
),
distinct=True,
)
)
)
.annotate(num_items=F("num_rollingstock") + F("num_rollingclass"))
.order_by("name")
)
# overload get method to filter by category
def get(self, request, category, page=1):
@@ -506,18 +539,69 @@ class Companies(GetData):
item_type = "company"
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):
title = "Scales"
item_type = "scale"
queryset = Scale.objects.all()
def get_data(self, request):
return Scale.objects.annotate(
num_items=Count("rollingstock")
) # .filter(num_items__gt=0) to filter data with no items
return (
Scale.objects.annotate(
num_rollingstock=Count(
"rollingstock",
filter=Q(
rollingstock__in=RollingStock.objects.get_published(
request.user
)
),
distinct=True,
),
num_consists=Count(
"consist",
filter=Q(
consist__in=Consist.objects.get_published(
request.user
)
),
distinct=True,
),
)
.annotate(num_items=F("num_rollingstock") + F("num_consists"))
.order_by("-ratio_int", "-tracks", "scale")
)
class Types(GetData):
@@ -525,7 +609,16 @@ class Types(GetData):
item_type = "rolling_stock_type"
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):
@@ -569,7 +662,7 @@ class GetBookCatalog(View):
"book": book,
"documents": documents,
"properties": properties,
"type": selector
"type": selector,
},
)

View File

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

View File

@@ -27,11 +27,6 @@ class Document(models.Model):
description = models.CharField(max_length=128, blank=True)
file = models.FileField(
upload_to="files/",
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)
updated_time = models.DateTimeField(auto_now=True)
@@ -61,8 +56,17 @@ class Document(models.Model):
'<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()
class Meta:
abstract = True
class Image(models.Model):
order = models.PositiveIntegerField(default=0, blank=False, null=False)

View File

@@ -50,6 +50,7 @@ INSTALLED_APPS = [
"portal",
# "driver", # uncomment this to enable the "driver" API
"metadata",
"repository",
"roster",
"consist",
"bookshelf",

View File

248
ram/repository/admin.py Normal file
View 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
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class RepositoryConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "repository"

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

View File

@@ -0,0 +1,157 @@
# Generated by Django 5.1.4 on 2025-02-09 23:10
import django.db.models.deletion
import tinymce.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0023_delete_basebookdocument"),
(
"metadata",
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
),
("repository", "0001_initial"),
("roster", "0036_delete_rollingstockdocument"),
]
operations = [
migrations.CreateModel(
name="InvoiceDocument",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("description", models.CharField(blank=True, max_length=128)),
("creation_time", models.DateTimeField(auto_now_add=True)),
("updated_time", models.DateTimeField(auto_now=True)),
("private", models.BooleanField(default=True, editable=False)),
("date", models.DateField()),
("file", models.FileField(upload_to="files/invoices/")),
("notes", tinymce.models.HTMLField(blank=True)),
],
options={
"abstract": False,
},
),
migrations.RemoveField(
model_name="basebookdocument",
name="book",
),
migrations.AlterModelOptions(
name="decoderdocument",
options={},
),
migrations.AlterModelOptions(
name="genericdocument",
options={"verbose_name_plural": "Generic documents"},
),
migrations.AlterModelOptions(
name="rollingstockdocument",
options={},
),
migrations.AlterField(
model_name="bookdocument",
name="file",
field=models.FileField(upload_to="files/"),
),
migrations.AlterField(
model_name="catalogdocument",
name="file",
field=models.FileField(upload_to="files/"),
),
migrations.AlterField(
model_name="decoderdocument",
name="decoder",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="document",
to="metadata.decoder",
),
),
migrations.AlterField(
model_name="decoderdocument",
name="file",
field=models.FileField(upload_to="files/"),
),
migrations.AlterField(
model_name="genericdocument",
name="file",
field=models.FileField(upload_to="files/"),
),
migrations.AlterField(
model_name="genericdocument",
name="tags",
field=models.ManyToManyField(
blank=True, related_name="document", to="metadata.tag"
),
),
migrations.AlterField(
model_name="rollingstockdocument",
name="file",
field=models.FileField(upload_to="files/"),
),
migrations.AlterField(
model_name="rollingstockdocument",
name="rolling_stock",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="document",
to="roster.rollingstock",
),
),
migrations.AddConstraint(
model_name="decoderdocument",
constraint=models.UniqueConstraint(
fields=("decoder", "file"), name="unique_decoder_file"
),
),
migrations.AddConstraint(
model_name="rollingstockdocument",
constraint=models.UniqueConstraint(
fields=("rolling_stock", "file"), name="unique_stock_file"
),
),
migrations.AddField(
model_name="invoicedocument",
name="book",
field=models.ManyToManyField(
blank=True, related_name="invoice", to="bookshelf.book"
),
),
migrations.AddField(
model_name="invoicedocument",
name="catalog",
field=models.ManyToManyField(
blank=True, related_name="invoice", to="bookshelf.catalog"
),
),
migrations.AddField(
model_name="invoicedocument",
name="rolling_stock",
field=models.ManyToManyField(
blank=True, related_name="invoice", to="roster.rollingstock"
),
),
migrations.AddField(
model_name="invoicedocument",
name="shop",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="metadata.shop",
),
),
migrations.DeleteModel(
name="BaseBookDocument",
),
]

View File

90
ram/repository/models.py Normal file
View File

@@ -0,0 +1,90 @@
from django.db import models
from django.core.exceptions import ValidationError
from tinymce import models as tinymce
from ram.models import PrivateDocument
from metadata.models import Decoder, Shop, Tag
from roster.models import RollingStock
from bookshelf.models import Book, Catalog
class GenericDocument(PrivateDocument):
notes = tinymce.HTMLField(blank=True)
tags = models.ManyToManyField(Tag, blank=True, related_name="document")
class Meta:
verbose_name_plural = "Generic documents"
class InvoiceDocument(PrivateDocument):
private = models.BooleanField(default=True, editable=False)
rolling_stock = models.ManyToManyField(
RollingStock, related_name="invoice", blank=True
)
book = models.ManyToManyField(Book, related_name="invoice", blank=True)
catalog = models.ManyToManyField(
Catalog, related_name="invoice", blank=True
)
date = models.DateField()
shop = models.ForeignKey(
Shop, on_delete=models.SET_NULL, null=True, blank=True
)
file = models.FileField(
upload_to="files/invoices/",
)
notes = tinymce.HTMLField(blank=True)
class DecoderDocument(PrivateDocument):
decoder = models.ForeignKey(
Decoder, on_delete=models.CASCADE, related_name="document"
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["decoder", "file"], name="unique_decoder_file"
)
]
class BookDocument(PrivateDocument):
book = models.ForeignKey(
Book, on_delete=models.CASCADE, related_name="document"
)
class Meta:
verbose_name_plural = "Book documents"
constraints = [
models.UniqueConstraint(
fields=["book", "file"], name="unique_book_file"
)
]
class CatalogDocument(PrivateDocument):
catalog = models.ForeignKey(
Catalog, on_delete=models.CASCADE, related_name="document"
)
class Meta:
verbose_name_plural = "Catalog documents"
constraints = [
models.UniqueConstraint(
fields=["catalog", "file"], name="unique_catalog_file"
)
]
class RollingStockDocument(PrivateDocument):
rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="document"
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["rolling_stock", "file"], name="unique_stock_file"
)
]

3
ram/repository/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
ram/repository/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -8,13 +8,13 @@ from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from ram.admin import publish, unpublish
from ram.utils import generate_csv
from repository.models import RollingStockDocument
from portal.utils import get_site_conf
from roster.models import (
RollingClass,
RollingClassProperty,
RollingStock,
RollingStockImage,
RollingStockDocument,
RollingStockProperty,
RollingStockJournal,
)
@@ -36,6 +36,7 @@ class RollingClass(admin.ModelAdmin):
search_fields = (
"identifier",
"company__name",
"company__slug",
"type__type",
)
save_as = True
@@ -76,42 +77,8 @@ class RollingStockJournalInline(admin.TabularInline):
classes = ["collapse"]
@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",
)
},
),
)
@admin.register(RollingStockJournal)
class RollingJournalDocumentAdmin(admin.ModelAdmin):
class RollingJournalAdmin(admin.ModelAdmin):
list_display = (
"__str__",
"date",
@@ -152,7 +119,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
RollingStockJournalInline,
)
autocomplete_fields = ("rolling_class", "shop")
readonly_fields = ("preview", "creation_time", "updated_time")
readonly_fields = ("preview", "invoices", "creation_time", "updated_time")
list_display = (
"__str__",
"address",
@@ -173,7 +140,9 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
search_fields = (
"rolling_class__identifier",
"rolling_class__company__name",
"rolling_class__company__slug",
"manufacturer__name",
"scale__scale",
"road_number",
"address",
"item_number",
@@ -223,6 +192,7 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"shop",
"purchase_date",
"price",
"invoices",
)
},
),
@@ -249,11 +219,25 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
)
return form
@admin.display(description="Invoices")
def invoices(self, obj):
if obj.invoice.exists():
html = "<br>".join(
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
i.file.url, i
) for i in obj.invoice.all())
else:
html = "-"
return format_html(html)
def download_csv(modeladmin, request, queryset):
header = [
"ID",
"Name",
"Class",
"Type",
"Company",
"Identifier",
"Country",
"Road Number",
"Manufacturer",
"Scale",
@@ -280,9 +264,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
)
data.append(
[
obj.uuid,
obj.__str__(),
obj.rolling_class.company.name,
obj.rolling_class.identifier,
obj.rolling_class.type,
obj.rolling_class.company.name,
obj.rolling_class.company.country,
obj.road_number,
obj.manufacturer.name,
obj.scale.scale,
@@ -295,11 +282,11 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
settings.CSV_SEPARATOR_ALT.join(
t.name for t in obj.tags.all()
),
obj.decoder_interface,
obj.get_decoder_interface_display(),
obj.decoder,
obj.address,
obj.purchase_date,
obj.shop,
obj.purchase_date,
obj.price,
properties,
]

View 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 = [
("roster", "0035_alter_rollingstock_shop"),
("repository", "0001_initial"),
]
operations = [
migrations.DeleteModel(
name="RollingStockDocument",
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-05-04 17:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0036_delete_rollingstockdocument"),
]
operations = [
migrations.AlterField(
model_name="rollingstock",
name="road_number_int",
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.1.4 on 2025-05-24 12:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0037_alter_rollingstock_road_number_int"),
]
operations = [
migrations.AlterField(
model_name="rollingstock",
name="rolling_class",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="rolling_stock",
to="roster.rollingclass",
verbose_name="Class",
),
),
]

View File

@@ -8,7 +8,7 @@ from django.dispatch import receiver
from tinymce import models as tinymce
from ram.models import BaseModel, Document, Image, PropertyInstance
from ram.models import BaseModel, Image, PropertyInstance
from ram.utils import DeduplicatedStorage, slugify
from ram.managers import PublicManager
from metadata.models import (
@@ -63,11 +63,11 @@ class RollingStock(BaseModel):
on_delete=models.CASCADE,
null=False,
blank=False,
related_name="rolling_class",
related_name="rolling_stock",
verbose_name="Class",
)
road_number = models.CharField(max_length=128, unique=False)
road_number_int = models.PositiveSmallIntegerField(default=0, unique=False)
road_number_int = models.PositiveIntegerField(default=0, unique=False)
manufacturer = models.ForeignKey(
Manufacturer,
on_delete=models.CASCADE,
@@ -135,9 +135,23 @@ class RollingStock(BaseModel):
def get_decoder_interface(self):
return str(
dict(settings.DECODER_INTERFACES).get(self.decoder_interface)
or "-"
or "No interface"
)
def dcc(self):
if self.decoder:
dcc = (
'<i class="bi bi-volume-up-fill"></i>'
if self.decoder.sound
else '<i class="bi bi-cpu-fill"></i>'
)
dcc = f'<abbr title="{self.decoder} ({self.get_decoder_interface()})">{dcc}</abbr>' # noqa: E501
elif self.decoder_interface:
dcc = f'<abbr title="{self.get_decoder_interface()}"><i class="bi bi-cpu"></i></abbr>' # noqa: E501
else:
dcc = f'<abbr title="{self.get_decoder_interface()}"><i class="bi bi-ban"></i></abbr>' # noqa: E501
return dcc
@property
def country(self):
return self.rolling_class.company.country
@@ -169,20 +183,6 @@ def pre_save_internal_fields(sender, instance, *args, **kwargs):
instance.item_number_slug = slugify(instance.item_number)
class RollingStockDocument(Document):
rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="document"
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["rolling_stock", "file"],
name="unique_stock_file"
)
]
def rolling_stock_image_upload(instance, filename):
return os.path.join(
"images",