mirror of
https://github.com/daniviga/django-ram.git
synced 2025-08-06 14:17:49 +02:00
Compare commits
29 Commits
asset-mqtt
...
master
Author | SHA1 | Date | |
---|---|---|---|
955397acd5
|
|||
672cadd7e1
|
|||
464fe57536
|
|||
bd16c7eee7
|
|||
cc2e374558
|
|||
1c25ac9b14
|
|||
de126a735d
|
|||
18b5ab8053
|
|||
3acc80e2ad
|
|||
552ba39970
|
|||
222e2075ec
|
|||
b5c57dcd94
|
|||
b81c63898f
|
|||
76b266b1f9
|
|||
86657a3b9f
|
|||
d0d25424fb
|
|||
292b95b8ed
|
|||
dea7a594bc
|
|||
60195bc99f
|
|||
7673f0514a
|
|||
40f42a9ee9
|
|||
2e06e94fde
|
|||
ece8d1ad94
|
|||
e9ec126ada
|
|||
1222116874
|
|||
85741f090c
|
|||
88d718fa94
|
|||
a2c857a3cd
|
|||
647894bca7
|
Submodule arduino/CommandStation-EX updated: 911bbd63be...3b15491608
@@ -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",
|
||||
|
@@ -137,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)
|
||||
|
||||
|
@@ -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]
|
||||
|
18
ram/consist/migrations/0016_alter_consistitem_order.py
Normal file
18
ram/consist/migrations/0016_alter_consistitem_order.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-27 19:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("consist", "0015_consist_description"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="consistitem",
|
||||
name="order",
|
||||
field=models.PositiveIntegerField(),
|
||||
),
|
||||
]
|
42
ram/consist/migrations/0017_consist_scale.py
Normal file
42
ram/consist/migrations/0017_consist_scale.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 5.1.4 on 2025-05-01 09:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_scale(apps, schema_editor):
|
||||
Consist = apps.get_model("consist", "Consist")
|
||||
|
||||
for consist in Consist.objects.all():
|
||||
try:
|
||||
consist.scale = consist.consist_item.first().rolling_stock.scale
|
||||
consist.save()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("consist", "0016_alter_consistitem_order"),
|
||||
(
|
||||
"metadata",
|
||||
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="consist",
|
||||
name="scale",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="metadata.scale",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
set_scale,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
25
ram/consist/migrations/0018_alter_consist_scale.py
Normal file
25
ram/consist/migrations/0018_alter_consist_scale.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.4 on 2025-05-02 11:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("consist", "0017_consist_scale"),
|
||||
(
|
||||
"metadata",
|
||||
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="consist",
|
||||
name="scale",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="metadata.scale"
|
||||
),
|
||||
),
|
||||
]
|
@@ -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):
|
||||
|
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.4 on 2025-05-04 20:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"metadata",
|
||||
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="company",
|
||||
options={"ordering": ["slug"], "verbose_name_plural": "Companies"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="manufacturer",
|
||||
options={"ordering": ["category", "slug"]},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="tag",
|
||||
options={"ordering": ["slug"]},
|
||||
),
|
||||
]
|
@@ -43,7 +43,7 @@ class Manufacturer(models.Model):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ["category", "name"]
|
||||
ordering = ["category", "slug"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -78,7 +78,7 @@ class Company(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Companies"
|
||||
ordering = ["name"]
|
||||
ordering = ["slug"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -207,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
|
||||
|
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
/*!
|
||||
* Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/)
|
||||
* Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/)
|
||||
* Copyright 2019-2024 The Bootstrap Authors
|
||||
* 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"; }
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
ram/portal/static/bootstrap@5.3.6/dist/css/bootstrap.min.css
vendored
Normal file
6
ram/portal/static/bootstrap@5.3.6/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ram/portal/static/bootstrap@5.3.6/dist/css/bootstrap.min.css.map
vendored
Normal file
1
ram/portal/static/bootstrap@5.3.6/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
ram/portal/static/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js
vendored
Normal file
7
ram/portal/static/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
ram/portal/static/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
1
ram/portal/static/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.2 KiB |
@@ -1,9 +1,7 @@
|
||||
<svg width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<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 |
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
||||
|
@@ -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>
|
||||
|
@@ -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 %} » {% endif %}{% endfor %}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@@ -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 %}
|
||||
|
@@ -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>
|
||||
|
39
ram/portal/templates/includes/symbols.html
Normal file
39
ram/portal/templates/includes/symbols.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="symbolsModal" tabindex="-1" aria-labelledby="symbolsLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="symbolsLabel">Symbols</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">DCC symbols</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
<tr>
|
||||
<th scope="row" class="text-center"><i class="bi bi-ban small"></i></th>
|
||||
<td>No socket</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-center"><i class="bi bi-dice-6 small"></i></th>
|
||||
<td>Socket available</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-center"><i class="bi bi-arrow-bar-left"></i><i class="bi bi-cpu-fill small"></i></th>
|
||||
<td>Decoder installed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-center"><i class="bi bi-arrow-bar-left"></i><i class="bi bi-volume-up-fill small"></i></th>
|
||||
<td>Sound decoder installed</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -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 %}
|
||||
|
36
ram/portal/templatetags/dcc.py
Normal file
36
ram/portal/templatetags/dcc.py
Normal 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)
|
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from ram.utils import git_suffix
|
||||
|
||||
__version__ = "0.17.1"
|
||||
__version__ = "0.17.13"
|
||||
__version__ += git_suffix(__file__)
|
||||
|
@@ -36,6 +36,7 @@ class RollingClass(admin.ModelAdmin):
|
||||
search_fields = (
|
||||
"identifier",
|
||||
"company__name",
|
||||
"company__slug",
|
||||
"type__type",
|
||||
)
|
||||
save_as = True
|
||||
@@ -139,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",
|
||||
@@ -229,9 +232,12 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||
|
||||
def download_csv(modeladmin, request, queryset):
|
||||
header = [
|
||||
"ID",
|
||||
"Name",
|
||||
"Class",
|
||||
"Type",
|
||||
"Company",
|
||||
"Identifier",
|
||||
"Country",
|
||||
"Road Number",
|
||||
"Manufacturer",
|
||||
"Scale",
|
||||
@@ -258,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,
|
||||
@@ -273,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,
|
||||
]
|
||||
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-05-04 17:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("roster", "0036_delete_rollingstockdocument"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="rollingstock",
|
||||
name="road_number_int",
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
]
|
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.4 on 2025-05-24 12:56
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("roster", "0037_alter_rollingstock_road_number_int"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="rollingstock",
|
||||
name="rolling_class",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="rolling_stock",
|
||||
to="roster.rollingclass",
|
||||
verbose_name="Class",
|
||||
),
|
||||
),
|
||||
]
|
@@ -63,11 +63,11 @@ class RollingStock(BaseModel):
|
||||
on_delete=models.CASCADE,
|
||||
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
|
||||
|
Reference in New Issue
Block a user