36 Commits

Author SHA1 Message Date
2c5f0dcd6f Use slugs to filter 2023-01-09 22:54:15 +01:00
3545824016 Merge pull request #19 from daniviga/more-filters
Add more filters
2023-01-09 00:12:55 +01:00
78f9faee5e Switch back from pk filtering to safe name 2023-01-09 00:10:57 +01:00
6fbea294da Add more filters and search refactoring 2023-01-08 19:06:38 +01:00
35bdffdb3f Hotfix for 0.1.0 2023-01-08 01:26:14 +01:00
dccf467d38 Merge pull request #18 from daniviga/manufacturers
Add support for manufacturer filters
2023-01-08 00:44:01 +01:00
9dfa9172f4 Major templates and views refactoring 2023-01-08 00:40:13 +01:00
9279142a41 Add more manufacturers categories 2023-01-06 01:54:14 +01:00
aff1d20260 Add support for manufacturer filters 2023-01-06 01:47:07 +01:00
9b8ec6ba6b Add a favicon 2023-01-05 11:44:02 +01:00
c0b1b0b37b Hotfix some templates 2023-01-05 02:24:00 +01:00
169763e237 Merge pull request #17 from daniviga/ext-link
Support external links and replace font-awesome with bootstrap icons
2023-01-04 18:20:12 +01:00
bbe0758c6b Fix local copy of bootstrap icons 2023-01-04 18:18:47 +01:00
c73305fd85 Add support for external links 2023-01-04 18:17:20 +01:00
4a3fbda3dc Replace font-awesome with bootstrap icons 2023-01-04 18:15:04 +01:00
295965710f Merge pull request #16 from daniviga/cdn
Add cover to consist page and cdn option
2023-01-04 15:21:20 +01:00
c152f43aa6 Fix template indentation 2023-01-04 15:19:43 +01:00
8ed92dc5f0 Bump version 2023-01-04 15:16:02 +01:00
b70aa27a13 Add cover to consist page 2023-01-04 15:14:30 +01:00
3860ed70fd Allow the use of local copies of cdn files 2023-01-04 14:49:00 +01:00
68a18fcf58 Replace thumbnails with carousels in rolling stock pages (#15)
* Replace thumbnails with carousels in rolling stock pages

* Add consist data and notes in page
2023-01-03 01:32:16 +01:00
e45d11d4b1 Raise minimum python version to 3.9 2023-01-02 16:10:15 +01:00
32b5522a1e Change how images and consists are sorted (#14) 2023-01-02 16:08:25 +01:00
89b666dab2 Update README.md 2022-12-30 09:28:08 +01:00
ffad964373 Add possibility to inject js in head (analytics) 2022-12-28 23:54:49 +01:00
538dc0bd80 Add page title in html 2022-12-28 23:36:46 +01:00
8bd2635c28 Change image sort, thumbnails first 2022-12-28 22:14:10 +01:00
feda1f6cb4 [auto update] sync submodules 2022-11-28 18:22:05 +01:00
2c851b2822 Add migrations 2022-11-27 01:11:43 +01:00
e5ba2cfaec Merge pull request #13 from daniviga/dedup
Reuse existing file if content is the same
2022-11-27 01:09:50 +01:00
091f426242 Bump version 2022-11-27 01:09:34 +01:00
f603fd3e2d Reuse existing file if content is the same 2022-11-27 01:07:38 +01:00
a3b2112e03 Fix search query 2022-11-24 16:38:07 +01:00
055b0bab59 Enable "Save as" in roster and consist 2022-11-01 12:30:38 +01:00
3aea2ae340 Merge pull request #12 from daniviga/move-dcc-interface
Move decoder interface def into rolling stock
2022-11-01 00:07:18 +01:00
242fe6814d Move decoder interface def into rolling stock 2022-11-01 00:06:30 +01:00
55 changed files with 3412 additions and 524 deletions

View File

@@ -13,7 +13,7 @@ jobs:
strategy: strategy:
max-parallel: 2 max-parallel: 2
matrix: matrix:
python-version: ['3.9', '3.10'] python-version: ['3.9', '3.10', '3.11']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@@ -21,7 +21,7 @@ it has been developed with a commitment of few minutes a day;
it lacks any kind of documentation, code review, architectural review, it lacks any kind of documentation, code review, architectural review,
security assesment, pentest, ISO certification, etc. security assesment, pentest, ISO certification, etc.
This project probably doesn't match you needs nor expectations. Be aware. This project probably doesn't match your needs nor expectations. Be aware.
Your model train may also catch fire while using this software. Your model train may also catch fire while using this software.
@@ -49,7 +49,7 @@ It has been developed with:
## Requirements ## Requirements
- Python 3.8+ - Python 3.9+
- A USB port when running Arduino hardware (and adaptors if you have a Mac) - A USB port when running Arduino hardware (and adaptors if you have a Mac)
## Web portal installation ## Web portal installation

View File

@@ -21,6 +21,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
list_display = ("identifier", "company", "era") list_display = ("identifier", "company", "era")
list_filter = list_display list_filter = list_display
search_fields = list_display search_fields = list_display
save_as = True
fieldsets = ( fieldsets = (
( (

View File

@@ -0,0 +1,24 @@
# Generated by Django 4.1.2 on 2022-11-27 00:10
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("consist", "0006_md_to_html"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.3 on 2023-01-02 15:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("consist", "0007_alter_consist_image"),
]
operations = [
migrations.AlterModelOptions(
name="consist",
options={"ordering": ["company", "-creation_time"]},
),
]

View File

@@ -4,6 +4,7 @@ from django.urls import reverse
from ckeditor_uploader.fields import RichTextUploadingField from ckeditor_uploader.fields import RichTextUploadingField
from ram.utils import DeduplicatedStorage
from metadata.models import Company, Tag from metadata.models import Company, Tag
from roster.models import RollingStock from roster.models import RollingStock
@@ -17,7 +18,9 @@ class Consist(models.Model):
) )
company = models.ForeignKey(Company, on_delete=models.CASCADE) company = models.ForeignKey(Company, on_delete=models.CASCADE)
era = models.CharField(max_length=32, blank=True) era = models.CharField(max_length=32, blank=True)
image = models.ImageField(upload_to="images/", null=True, blank=True) image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
notes = RichTextUploadingField(blank=True) notes = RichTextUploadingField(blank=True)
creation_time = models.DateTimeField(auto_now_add=True) creation_time = models.DateTimeField(auto_now_add=True)
updated_time = models.DateTimeField(auto_now=True) updated_time = models.DateTimeField(auto_now=True)
@@ -29,7 +32,7 @@ class Consist(models.Model):
return reverse("consist", kwargs={"uuid": self.uuid}) return reverse("consist", kwargs={"uuid": self.uuid})
class Meta: class Meta:
ordering = ["creation_time"] ordering = ["company", "-creation_time"]
class ConsistItem(models.Model): class ConsistItem(models.Model):

View File

@@ -20,8 +20,8 @@ class PropertyAdmin(admin.ModelAdmin):
@admin.register(Decoder) @admin.register(Decoder)
class DecoderAdmin(admin.ModelAdmin): class DecoderAdmin(admin.ModelAdmin):
readonly_fields = ("image_thumbnail",) readonly_fields = ("image_thumbnail",)
list_display = ("__str__", "interface") list_display = ("__str__", "sound")
list_filter = ("manufacturer", "interface") list_filter = ("manufacturer", "sound")
search_fields = ("name", "manufacturer__name") search_fields = ("name", "manufacturer__name")

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.2 on 2022-10-31 23:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("metadata", "0007_rename_track_scale_tracks"),
("roster", "0013_rollingstock_decoder_interface"),
]
operations = [
migrations.RemoveField(
model_name="decoder",
name="interface",
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 4.1.2 on 2022-11-27 00:10
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0008_remove_decoder_interface"),
]
operations = [
migrations.AlterField(
model_name="company",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
migrations.AlterField(
model_name="decoder",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
migrations.AlterField(
model_name="manufacturer",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
]

View File

@@ -0,0 +1,26 @@
# Generated by Django 4.1.5 on 2023-01-06 00:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0009_alter_company_logo_alter_decoder_image_and_more"),
]
operations = [
migrations.AlterField(
model_name="manufacturer",
name="category",
field=models.CharField(
choices=[
("model", "Model"),
("real", "Real"),
("accessory", "Accessory"),
("other", "Other"),
],
max_length=64,
),
),
]

View File

@@ -0,0 +1,93 @@
# Generated by Django 4.1.5 on 2023-01-09 11:22
from django.db import migrations, models
from ram.utils import slugify
def create_slug(apps, schema_editor):
fields = ["Company", "Manufacturer", "RollingStockType", "Scale"]
for m in fields:
model = apps.get_model("metadata", m)
for row in model.objects.all():
if hasattr(row, "type"):
row.slug = slugify("{} {}".format(row.type, row.category))
elif hasattr(row, "scale"):
row.slug = slugify(row.scale)
else:
row.slug = slugify(row.name)
row.save(update_fields=["slug"])
class Migration(migrations.Migration):
dependencies = [
("metadata", "0010_alter_manufacturer_category"),
]
operations = [
migrations.AddField(
model_name="company",
name="slug",
field=models.CharField(
editable=False, max_length=64, blank=True
),
),
migrations.AddField(
model_name="manufacturer",
name="slug",
field=models.CharField(
editable=False, max_length=128, blank=True
),
),
migrations.AddField(
model_name="rollingstocktype",
name="slug",
field=models.CharField(
editable=False, max_length=128, blank=True
),
),
migrations.AddField(
model_name="scale",
name="slug",
field=models.CharField(
editable=False, max_length=32, blank=True
),
),
migrations.RunPython(
create_slug,
reverse_code=migrations.RunPython.noop
),
migrations.AlterField(
model_name="company",
name="slug",
field=models.CharField(
editable=False, max_length=64, unique=True
),
),
migrations.AlterField(
model_name="manufacturer",
name="slug",
field=models.CharField(
editable=False, max_length=128, unique=True
),
),
migrations.AlterField(
model_name="rollingstocktype",
name="slug",
field=models.CharField(
editable=False, max_length=128, unique=True
),
),
migrations.AlterField(
model_name="scale",
name="slug",
field=models.CharField(
editable=False, max_length=32, unique=True
),
),
]

View File

@@ -1,9 +1,12 @@
from urllib.parse import quote
from django.db import models from django.db import models
from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.dispatch.dispatcher import receiver from django.dispatch.dispatcher import receiver
from django_countries.fields import CountryField from django_countries.fields import CountryField
from ram.utils import get_image_preview, slugify from ram.utils import DeduplicatedStorage, get_image_preview, slugify
class Property(models.Model): class Property(models.Model):
@@ -20,11 +23,14 @@ class Property(models.Model):
class Manufacturer(models.Model): class Manufacturer(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True, editable=False)
category = models.CharField( category = models.CharField(
max_length=64, choices=settings.MANUFACTURER_TYPES max_length=64, choices=settings.MANUFACTURER_TYPES
) )
website = models.URLField(blank=True) website = models.URLField(blank=True)
logo = models.ImageField(upload_to="images/", null=True, blank=True) logo = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
class Meta: class Meta:
ordering = ["category", "name"] ordering = ["category", "name"]
@@ -32,6 +38,14 @@ class Manufacturer(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "manufacturer",
"search": self.slug,
}
)
def logo_thumbnail(self): def logo_thumbnail(self):
return get_image_preview(self.logo.url) return get_image_preview(self.logo.url)
@@ -40,10 +54,13 @@ class Manufacturer(models.Model):
class Company(models.Model): class Company(models.Model):
name = models.CharField(max_length=64, unique=True) name = models.CharField(max_length=64, unique=True)
slug = models.CharField(max_length=64, unique=True, editable=False)
extended_name = models.CharField(max_length=128, blank=True) extended_name = models.CharField(max_length=128, blank=True)
country = CountryField() country = CountryField()
freelance = models.BooleanField(default=False) freelance = models.BooleanField(default=False)
logo = models.ImageField(upload_to="images/", null=True, blank=True) logo = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
class Meta: class Meta:
verbose_name_plural = "Companies" verbose_name_plural = "Companies"
@@ -52,6 +69,14 @@ class Company(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "company",
"search": self.slug,
}
)
def logo_thumbnail(self): def logo_thumbnail(self):
return get_image_preview(self.logo.url) return get_image_preview(self.logo.url)
@@ -66,11 +91,10 @@ class Decoder(models.Model):
limit_choices_to={"category": "model"}, limit_choices_to={"category": "model"},
) )
version = models.CharField(max_length=64, blank=True) version = models.CharField(max_length=64, blank=True)
interface = models.PositiveSmallIntegerField(
choices=settings.DECODER_INTERFACES, null=True, blank=True
)
sound = models.BooleanField(default=False) sound = models.BooleanField(default=False)
image = models.ImageField(upload_to="images/", null=True, blank=True) image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
def __str__(self): def __str__(self):
return "{0} - {1}".format(self.manufacturer, self.name) return "{0} - {1}".format(self.manufacturer, self.name)
@@ -83,6 +107,7 @@ class Decoder(models.Model):
class Scale(models.Model): class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True) scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False)
ratio = models.CharField(max_length=16, blank=True) ratio = models.CharField(max_length=16, blank=True)
gauge = models.CharField(max_length=16, blank=True) gauge = models.CharField(max_length=16, blank=True)
tracks = models.CharField(max_length=16, blank=True) tracks = models.CharField(max_length=16, blank=True)
@@ -90,10 +115,42 @@ class Scale(models.Model):
class Meta: class Meta:
ordering = ["scale"] ordering = ["scale"]
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "scale",
"search": self.slug,
}
)
def __str__(self): def __str__(self):
return str(self.scale) return str(self.scale)
class RollingStockType(models.Model):
type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField()
category = models.CharField(
max_length=64, choices=settings.ROLLING_STOCK_TYPES
)
slug = models.CharField(max_length=128, unique=True, editable=False)
class Meta(object):
unique_together = ("category", "type")
ordering = ["order"]
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "type",
"search": self.slug,
}
)
def __str__(self):
return "{0} {1}".format(self.type, self.category)
class Tag(models.Model): class Tag(models.Model):
name = models.CharField(max_length=128, unique=True) name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True) slug = models.CharField(max_length=128, unique=True)
@@ -101,22 +158,20 @@ class Tag(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "tag",
"search": self.slug,
}
)
@receiver(models.signals.pre_save, sender=Manufacturer)
@receiver(models.signals.pre_save, sender=Company)
@receiver(models.signals.pre_save, sender=Scale)
@receiver(models.signals.pre_save, sender=RollingStockType)
@receiver(models.signals.pre_save, sender=Tag) @receiver(models.signals.pre_save, sender=Tag)
def tag_pre_save(sender, instance, **kwargs): def slug_pre_save(sender, instance, **kwargs):
instance.slug = slugify(instance.name) instance.slug = slugify(instance.__str__())
class RollingStockType(models.Model):
type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField()
category = models.CharField(
max_length=64, choices=settings.ROLLING_STOCK_TYPES
)
class Meta(object):
unique_together = ("category", "type")
ordering = ["order"]
def __str__(self):
return "{0} {1}".format(self.type, self.category)

View File

@@ -3,7 +3,35 @@ from solo.admin import SingletonModelAdmin
from portal.models import SiteConfiguration, Flatpage from portal.models import SiteConfiguration, Flatpage
admin.site.register(SiteConfiguration, SingletonModelAdmin) @admin.register(SiteConfiguration)
class SiteConfigurationAdmin(SingletonModelAdmin):
fieldsets = (
(
None,
{
"fields": (
"site_name",
"site_author",
"about",
"items_per_page",
"items_ordering",
"footer",
"footer_extended",
)
},
),
(
"Advanced",
{
"classes": ("collapse",),
"fields": (
"show_version",
"use_cdn",
"extra_head",
),
},
),
)
@admin.register(Flatpage) @admin.register(Flatpage)

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.3 on 2022-12-28 22:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("portal", "0013_remove_flatpage_draft_flatpage_published"),
]
operations = [
migrations.AddField(
model_name="siteconfiguration",
name="extra_head",
field=models.TextField(blank=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.1.5 on 2023-01-03 15:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("portal", "0014_siteconfiguration_extra_head"),
]
operations = [
migrations.AddField(
model_name="siteconfiguration",
name="use_cdn",
field=models.BooleanField(default=True),
),
]

View File

@@ -35,6 +35,8 @@ class SiteConfiguration(SingletonModel):
footer = RichTextField(blank=True) footer = RichTextField(blank=True)
footer_extended = RichTextField(blank=True) footer_extended = RichTextField(blank=True)
show_version = models.BooleanField(default=True) show_version = models.BooleanField(default=True)
use_cdn = models.BooleanField(default=True)
extra_head = models.TextField(blank=True)
class Meta: class Meta:
verbose_name = "Site Configuration" verbose_name = "Site Configuration"

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,8 @@
/* Switch SVG logo to white on dark mode */
html.dark .navbar-light svg {
fill: #fff;
}
.card > a > img { .card > a > img {
width: 100%; width: 100%;
} }
@@ -11,10 +16,6 @@ a.badge, a.badge:hover {
color: #fff; color: #fff;
} }
.tab-pane {
min-height: 300px;
}
.img-thumbnail { .img-thumbnail {
padding: 0; padding: 0;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,9 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -12,10 +12,17 @@
<meta name="description" content="{{ site_conf.about}}"> <meta name="description" content="{{ site_conf.about}}">
<meta name="author" content="{{ site_conf.site_author }}"> <meta name="author" content="{{ site_conf.site_author }}">
<meta name="generator" content="Django Framework"> <meta name="generator" content="Django Framework">
<title>{{ site_conf.site_name }}</title> <title>{% block title %}{{ title }}{% endblock %} - {{ site_conf.site_name }}</title>
<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-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" rel="stylesheet">
<link href="{% static "css/main.css" %}" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet"> {% else %}
<link href="{% static "bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" %}" rel="stylesheet">
<link href="{% static "bootstrap-icons@1.10.3/font/bootstrap-icons.css" %}" rel="stylesheet">
{% endif %}
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
<style> <style>
.bd-placeholder-img { .bd-placeholder-img {
font-size: 1.125rem; font-size: 1.125rem;
@@ -34,6 +41,10 @@
html.dark .d-light-inline { display: none !important; } html.dark .d-light-inline { display: none !important; }
html.dark .d-dark-inline { display: inline !important; } html.dark .d-dark-inline { display: inline !important; }
</style> </style>
{% block extra_head %}
{{ site_conf.extra_head | safe }}
{% endblock %}
</head> </head>
<body> <body>
<header> <header>
@@ -41,13 +52,19 @@
<div class="container"> <div class="container">
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center"> <a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
<svg class="me-2" width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg"> <svg class="me-2" 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" fill="#000" overflow="visible" stroke-width="2" style="text-indent:0;text-transform:none"/> <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> </svg>
<strong>{{ site_conf.site_name }}</strong> <strong>{{ site_conf.site_name }}</strong>
</a> </a>
<div class="btn-group" role="group" aria-label="Basic example"> <div class="btn-group" role="group" aria-label="Login menu">
{% include 'includes/login.html' %} {% include 'includes/login.html' %}
<a id="darkmode-button" class="btn btn-sm btn-outline-dark"><i class="fa fa-moon-o fa-fw d-none d-light-inline" title="Switch to dark mode"></i><i class="fa fa-sun-o fa-fw d-none d-dark-inline" title="Switch to light mode"></i></a> <a id="darkmode-button" class="btn btn-sm btn-outline-dark"><i class="bi bi-moon d-none d-light-inline" title="Switch to dark mode"></i><i class="bi bi-sun d-none d-dark-inline" title="Switch to light mode"></i></a>
</div> </div>
</div> </div>
</div> </div>
@@ -59,17 +76,28 @@
<a class="navbar-brand" href="{% url 'index' %}">Home</a> <a class="navbar-brand" href="{% url 'index' %}">Home</a>
<div class="navbar-collapse" id="navbarSupportedContent"> <div class="navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"> <li class="nav-item dropdown">
<a class="nav-link" href="{% url 'index' %}">Roster</a> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Roster
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><a class="dropdown-item" href="{% url 'roster' %}">Rolling stock</a></li>
<li><a class="dropdown-item" href="{% url 'companies' %}">Companies</a></li>
<li><a class="dropdown-item" href="{% url 'types' %}">Types</a></li>
<li><a class="dropdown-item" href="{% url 'scales' %}">Scales</a></li>
</ul>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'consists' %}">Consists</a> <a class="nav-link" href="{% url 'consists' %}">Consists</a>
</li> </li>
<li class="nav-item"> <li class="nav-item dropdown">
<a class="nav-link" href="{% url 'companies' %}">Companies</a> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
</li> Manufacturers
<li class="nav-item"> </a>
<a class="nav-link" href="{% url 'scales' %}">Scales</a> <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Models</a></li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Real</a></li>
</ul>
</li> </li>
{% show_menu %} {% show_menu %}
</ul> </ul>
@@ -81,111 +109,32 @@
<section class="py-4 text-center container"> <section class="py-4 text-center container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
{% block header %}{% endblock %} <h1 class="fw-light">{{ title }}</h1>
{% block header %}
{% endblock %}
</div> </div>
</div> </div>
</section> </section>
<div class="album py-4 bg-light"> <div class="album py-4 bg-light">
<div class="container"> <div class="container">
{% block carousel %}
{% endblock %}
<a id="rolling-stock"></a> <a id="rolling-stock"></a>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> {% block cards_layout %}
{% block cards %} {% endblock %}
{% for r in rolling_stock %}
<div class="col">
<div class="card shadow-sm">
{% for i in r.image.all %}
{% if i.is_thumbnail %}<a href="{{r.get_absolute_url}}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %}
{% endfor %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ r }}</strong>
<a class="stretched-link" href="{{ r.get_absolute_url }}"></a>
</p>
{% if r.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in r.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ r.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td><abbr title="{{ r.rolling_class.company.extended_name }}">{{ r.rolling_class.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ r.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ r.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ r.era }}</td>
</tr>
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{% if r.manufacturer.website %}<a href="{{ r.manufacturer.website }}">{% endif %}{{ r.manufacturer }}{% if r.manufacturer.website %}</a>{% endif %}</th>
</tr>
<tr>
<th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=r.scale %}"><abbr title="{{ r.scale.ratio }} - {{ r.scale.tracks }}">{{ r.scale }}</abbr></a></td>
</tr>
<tr>
<th scope="row">SKU</th>
<td>{{ r.sku }}</td>
</tr>
</tbody>
</table>
{% if r.decoder %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Decoder</th>
<td>{{ r.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ r.address }}</td>
</tr>
</tbody>
</table>
{% endif %}
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{r.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' r.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
</div>
</div> </div>
<div class="container">{% block pagination %}{% endblock %}</div> <div class="container">{% block pagination %}{% endblock %}</div>
</div> </div>
{% block extra_content %}{% endblock %} {% block extra_content %}{% endblock %}
</main> </main>
{% include 'includes/footer.html' %} {% include 'includes/footer.html' %}
{% if site_conf.use_cdn %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"></script>
{% else %}
<script src="{% static "bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" %}"></script>
<script src="{% static "bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js" %}"></script>
{% endif %}
<!-- script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script --> <!-- script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script -->
<script> <script>
document.querySelector("#darkmode-button").onclick = function(e){ document.querySelector("#darkmode-button").onclick = function(e){

View File

@@ -0,0 +1,101 @@
{% extends "base.html" %}
{% block header %}
<p class="lead text-muted">Results found: {{ matches }}</p>
{% endblock %}
{% block cards_layout %}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
{% for i in d.image.all %}
{% if forloop.first %}<a href="{{d.get_absolute_url}}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %}
{% endfor %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p>
{% if d.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ d.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=d.rolling_class.company.slug %}"><abbr title="{{ d.rolling_class.company.extended_name }}">{{ d.rolling_class.company }}</abbr></a>
</td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ d.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ d.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.era }}</td>
</tr>
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{%if d.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.slug %}">{{ d.manufacturer }}{% if d.manufacturer.website %}</a> <a href="{{ d.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
<tr>
<th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=d.scale.slug %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }}">{{ d.scale }}</abbr></a></td>
</tr>
<tr>
<th scope="row">SKU</th>
<td>{{ d.sku }}</td>
</tr>
</tbody>
</table>
{% if d.decoder %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Decoder</th>
<td>{{ d.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ d.address }}</td>
</tr>
</tbody>
</table>
{% endif %}
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{d.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
</div>
{% endblock %}

View File

@@ -1,15 +1,12 @@
{% extends "base.html" %} {% extends "cards.html" %}
{% block header %}
<h1 class="fw-light">Companies</h1>
{% endblock %}
{% block cards %} {% block cards %}
{% for c in company %} {% for d in data %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ c.name }}</strong> <strong>{{ d.name }}</strong>
</p> </p>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@@ -18,25 +15,25 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if c.logo %} {% if d.logo %}
<tr> <tr>
<th width="35%" scope="row">Logo</th> <th width="35%" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ c.logo.url }}" /></td> <td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th width="35%" scope="row">Name</th> <th width="35%" scope="row">Name</th>
<td>{{ c.extended_name }}</td> <td>{{ d.extended_name }}</td>
</tr> </tr>
<tr> <tr>
<th width="35%" scope="row">Abbreviation</th> <th width="35%" scope="row">Abbreviation</th>
<td>{{ c }}</td> <td>{{ d.name }}</td>
</tr> </tr>
<tr> <tr>
<th width="35%" scope="row">Country</th> <th width="35%" scope="row">Country</th>
<td>{{ c.country.name }} <img src="{{ c.country.flag }}" alt="{{ c.country }}" /> <td>{{ d.country.name }} <img src="{{ d.country.flag }}" alt="{{ d.country }}" />
</tr> </tr>
{% if c.freelance %} {% if d.freelance %}
<tr> <tr>
<th width="35%" scope="row">Notes</th> <th width="35%" scope="row">Notes</th>
<td>A <em>freelance</em> company</td> <td>A <em>freelance</em> company</td>
@@ -45,8 +42,8 @@
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=c %}">Show all rolling stock</a> <a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' c.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -54,12 +51,12 @@
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% block pagination %} {% block pagination %}
{% if company.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if company.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'companies_pagination' page=company.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'companies_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -67,21 +64,21 @@
</li> </li>
{% endif %} {% endif %}
{% for i in page_range %} {% for i in page_range %}
{% if company.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span></span>
</li> </li>
{% else %} {% else %}
{% if i == company.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'companies_pagination' page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'companies_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if company.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'companies_pagination' page=company.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'companies_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,7 +1,6 @@
{% extends "base.html" %} {% extends "cards.html" %}
{% block header %} {% block header %}
<h1 class="fw-light">{{ consist }}</h1>
{% if consist.tags.all %} {% if consist.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
{% for t in consist.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in consist.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
@@ -11,101 +10,26 @@
<small class="text-muted">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small> <small class="text-muted">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block cards %} {% block carousel %}
{% for r in rolling_stock %} {% if consist.image %}
<div class="col"> <div class="row pb-4">
<div class="card shadow-sm"> <div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel">
{% for i in r.rolling_stock.image.all %} <div class="carousel-inner">
{% if i.is_thumbnail %}<a href="{{r.rolling_stock.get_absolute_url}}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %} <div class="carousel-item active">
{% endfor %} <img src="{{ consist.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="...">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ r }}</strong>
<a class="stretched-link" href="{{ r.rolling_stock.get_absolute_url }}"></a>
</p>
{% if r.rolling_stock.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in r.rolling_stock.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ r.rolling_stock.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td><abbr title="{{ r.rolling_stock.rolling_class.company.extended_name }}">{{ r.rolling_stock.rolling_class.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ r.rolling_stock.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ r.rolling_stock.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ r.rolling_stock.era }}</td>
</tr>
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{% if r.rolling_stock.manufacturer.website %}<a href="{{ r.rolling_stock.manufacturer.website }}">{% endif %}{{ r.rolling_stock.manufacturer }}{% if r.rolling_stock.manufacturer.website %}</a>{% endif %}</th>
</tr>
<tr>
<th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=r.rolling_stock.scale %}"><abbr title="{{ r.rolling_stock.scale.ratio }} - {{ r.rolling_stock.scale.tracks }}">{{ r.rolling_stock.scale }}</abbr></a></td>
</tr>
<tr>
<th scope="row">SKU</th>
<td>{{ r.rolling_stock.sku }}</td>
</tr>
</tbody>
</table>
{% if r.rolling_stock.decoder %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Decoder</th>
<td>{{ r.rolling_stock.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ r.rolling_stock.address }}</td>
</tr>
</tbody>
</table>
{% endif %}
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{r.rolling_stock.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' r.rolling_stock.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} {% endif %}
{% endblock %} {% endblock %}
{% block pagination %} {% block pagination %}
{% if rolling_stock.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if rolling_stock.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=rolling_stock.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -113,21 +37,21 @@
</li> </li>
{% endif %} {% endif %}
{% for i in page_range %} {% for i in page_range %}
{% if rolling_stock.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span></span>
</li> </li>
{% else %} {% else %}
{% if i == rolling_stock.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if rolling_stock.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=rolling_stock.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -142,6 +66,51 @@
<section class="py-4 text-start container"> <section class="py-4 text-start container">
<div class="row"> <div class="row">
<div class="mx-auto"> <div class="mx-auto">
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
{% if consist.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Data</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Company</th>
<td><abbr title="{{ consist.company.extended_name }}">{{ consist.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ consist.era }}</td>
</tr>
<tr>
<th scope="row">Length</th>
<td>{{ data | length }}</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
<table class="table">
<thead>
<tr>
<th scope="row">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ consist.notes | safe }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <div class="d-grid gap-2 d-md-flex justify-content-md-end">
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' consist.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' consist.pk %}">Edit</a>{% endif %}
</div> </div>

View File

@@ -1,31 +1,28 @@
{% extends "base.html" %} {% extends "cards.html" %}
{% block header %}
<h1 class="fw-light">Consists</h1>
{% endblock %}
{% block cards %} {% block cards %}
{% for c in consist %} {% for d in data %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
<a href="{{ c.get_absolute_url }}"> <a href="{{ d.get_absolute_url }}">
{% if c.image %} {% if d.image %}
<img src="{{ c.image.url }}" alt="Card image cap"> <img src="{{ d.image.url }}" alt="Card image cap">
{% else %} {% else %}
{% with c.consist_item.first.rolling_stock as r %} {% with d.consist_item.first.rolling_stock as r %}
{% for i in r.image.all %} {% for i in r.image.all %}
{% if i.is_thumbnail %}<img src="{{ i.image.url }}" alt="Card image cap">{% endif %} {% if forloop.first %}<img src="{{ i.image.url }}" alt="Card image cap">{% endif %}
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}
</a> </a>
<div class="card-body"> <div class="card-body">
<p class="card-text" style="position: relative;"> <p class="card-text" style="position: relative;">
<strong>{{ c }}</strong> <strong>{{ d }}</strong>
<a class="stretched-link" href="{{ c.get_absolute_url }}"></a> <a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p> </p>
{% if c.tags.all %} {% if d.tags.all %}
<p class="card-text"><small>Tags:</small> <p class="card-text"><small>Tags:</small>
{% for t in c.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in d.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #} {{ t.name }}</a>{# new line is required #}
{% endfor %} {% endfor %}
</p> </p>
@@ -37,29 +34,29 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% if c.address %} {% if d.address %}
<tr> <tr>
<th width="35%" scope="row">Address</th> <th width="35%" scope="row">Address</th>
<td>{{ c.address }}</td> <td>{{ d.address }}</td>
</tr> </tr>
{% endif %} {% endif %}
<tr> <tr>
<th width="35%" scope="row">Company</th> <th width="35%" scope="row">Company</th>
<td><abbr title="{{ c.company.extended_name }}">{{ c.company }}</abbr></td> <td><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
<td>{{ c.era }}</td> <td>{{ d.era }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Length</th> <th scope="row">Length</th>
<td>{{ c.consist_item.all | length }}</td> <td>{{ d.consist_item.all | length }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ c.get_absolute_url }}">Show all data</a> <a class="btn btn-sm btn-outline-primary" href="{{ d.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' c.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -67,12 +64,12 @@
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% block pagination %} {% block pagination %}
{% if consist.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if consist.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consists_pagination' page=consist.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'consists_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -80,21 +77,21 @@
</li> </li>
{% endif %} {% endif %}
{% for i in page_range %} {% for i in page_range %}
{% if consist.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span></span>
</li> </li>
{% else %} {% else %}
{% if i == consist.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'consists_pagination' page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'consists_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if consist.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'consists_pagination' page=consist.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'consists_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -0,0 +1,41 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -1,7 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block header %} {% block header %}
<h1 class="fw-light">{{ flatpage.name }}</h1>
<small class="text-muted">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small> <small class="text-muted">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %} {% endblock %}
{% block extra_content %} {% block extra_content %}

View File

@@ -1,7 +1,7 @@
{% if menu %} {% if menu %}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
More ... Articles
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
{% for m in menu %} {% for m in menu %}

View File

@@ -1,46 +1,5 @@
{% extends "base.html" %} {% extends "roster.html" %}
{% block header %} {% block header %}
{% if site_conf.about %}<h1 class="fw-light">About</h1>{% endif %}
<p class="lead text-muted">{{ site_conf.about | safe }}</p> <p class="lead text-muted">{{ site_conf.about | safe }}</p>
{% endblock %} {% endblock %}
{% block pagination %}
{% if rolling_stock.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if rolling_stock.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'index_pagination' page=rolling_stock.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in page_range %}
{% if rolling_stock.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == rolling_stock.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'index_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if rolling_stock.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'index_pagination' page=rolling_stock.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -1,21 +1,19 @@
{% if request.user.is_staff %} {% if request.user.is_staff %}
<div class="dropdown"> <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" id="dropdownMenu2" data-bs-toggle="dropdown" aria-expanded="false">
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" id="dropdownMenu2" data-bs-toggle="dropdown" aria-expanded="false"> Welcome back, <strong>{{ request.user }}</strong>
Welcome back, <strong>{{ request.user }}</strong> </button>
</button> <ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="dropdownMenu2"> <li><a class="dropdown-item" href="{% url 'admin:roster_rollingstock_changelist' %}">Rolling stock</a></li>
<li><a class="dropdown-item" href="{% url 'admin:roster_rollingstock_changelist' %}">Rolling stock</a></li> <li><a class="dropdown-item" href="{% url 'admin:consist_consist_changelist' %}">Consists</a></li>
<li><a class="dropdown-item" href="{% url 'admin:consist_consist_changelist' %}">Consists</a></li> <li><a class="dropdown-item" href="{% url 'admin:app_list' 'metadata' %}">Metadata</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'metadata' %}">Metadata</a></li> <li><a class="dropdown-item" href="{% url 'admin:portal_flatpage_changelist' %}">Pages</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_flatpage_changelist' %}">Pages</a></li> <li><hr class="dropdown-divider"></li>
<li><hr class="dropdown-divider"></li> <li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li> <li><a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a></li> <li><a class="dropdown-item" href="{% url 'admin:driver_driverconfiguration_changelist' %}">DCC configuration</a></li>
<li><a class="dropdown-item" href="{% url 'admin:driver_driverconfiguration_changelist' %}">DCC configuration</a></li> <li><hr class="dropdown-divider"></li>
<li><hr class="dropdown-divider"></li> <li><a class="dropdown-item text-danger" href="{% url 'admin:logout' %}?next={{ request.path }}">Logout</a></li>
<li><a class="dropdown-item text-danger" href="{% url 'admin:logout' %}?next={{ request.path }}">Logout</a></li> </ul>
</ul>
</div>
{% else %} {% else %}
<a class="btn btn-sm btn-outline-dark" href="{% url 'admin:login' %}?next={{ request.path }}">Log in</a> <a class="btn btn-sm btn-outline-dark" href="{% url 'admin:login' %}?next={{ request.path }}">Log in</a>
{% endif %} {% endif %}

View File

@@ -1,6 +1,12 @@
<form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate> <form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate>
<div class="input-group has-validation"> <div class="input-group has-validation">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required> <input class="form-control me-2" type="search" list="datalistOptions" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required>
<datalist id="datalistOptions">
<option value="company: ">
<option value="manufacturer: ">
<option value="scale: ">
<option value="type: ">
</datalist>
<button class="btn btn-outline-primary" type="submit">Search</button> <button class="btn btn-outline-primary" type="submit">Search</button>
</div> </div>
</form> </form>

View File

@@ -0,0 +1,85 @@
{% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Manufacturer</th>
</tr>
</thead>
<tbody>
{% if d.logo %}
<tr>
<th width="35%" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
{% if d.website %}
<tr>
<th width="35%" scope="row">Website</th>
<td><a href="{{ d.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a></td>
</tr>
{% endif %}
<tr>
<th width="35%" scope="row">Category</th>
<td>{{ d.category | title }}</td>
</tr>
</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.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %}
{% if data.has_other_pages %}
{% with data.0.category as c %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'manufacturers_pagination' category=c page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endwith %}
{% endif %}
{% endblock %}

View File

@@ -1,7 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block header %} {% block header %}
<h1 class="fw-light">{{ rolling_stock }}</h1>
{% if rolling_stock.tags.all %} {% if rolling_stock.tags.all %}
<p><small>Tags:</small> <p><small>Tags:</small>
{% for t in rolling_stock.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary"> {% for t in rolling_stock.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
@@ -11,29 +10,34 @@
{% endif %} {% endif %}
<small class="text-muted">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small> <small class="text-muted">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
{% endblock %} {% endblock %}
{% block cards %} {% block carousel %}
{% for t in rolling_stock.image.all %} <div class="row">
<div class="col"> <div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel" data-bs-interval="10000">
<a href="" data-bs-toggle="modal" data-bs-target="#pictureModal{{ forloop.counter }}"><img class="img-thumbnail" src="{{ t.image.url }}" alt="Rolling stock image"></a> <div class="carousel-inner">
</div> {% for t in rolling_stock.image.all %}
<!-- Modal --> {% if forloop.first %}
<div class="modal fade" id="pictureModal{{ forloop.counter }}" tabindex="-1" aria-labelledby="pictureModalLabel{{ forloop.counter }}" aria-hidden="true"> <div class="carousel-item active">
<div class="modal-dialog modal-lg"> {% else %}
<div class="modal-content"> <div class="carousel-item">
<div class="modal-header"> {% endif %}
<h5 class="modal-title" id="pictureModalLabel{{ forloop.counter }}">{{ rolling_stock }}</h5> <img src="{{ t.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="...">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img class="rounded img-fluid" src="{{ t.image.url }}" alt="Rolling stock image">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div> </div>
{% endfor %}
</div> </div>
{% if rolling_stock.image.count > 1 %}
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselControls" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
{% endif %}
</div> </div>
</div> </div>
{% endfor %} {% endblock %}
{% block cards %}
{% endblock %} {% endblock %}
{% block extra_content %} {% block extra_content %}
<section class="py-4 text-start container"> <section class="py-4 text-start container">
@@ -65,7 +69,9 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Company</th> <th scope="row">Company</th>
<td><abbr title="{{ rolling_stock.rolling_class.company.extended_name }}">{{ rolling_stock.rolling_class.company }}</abbr></td> <td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.slug %}"><abbr title="{{ rolling_stock.rolling_class.company.extended_name }}">{{ rolling_stock.rolling_class.company }}</abbr></a>
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Class</th> <th scope="row">Class</th>
@@ -90,7 +96,9 @@
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Manufacturer</th> <th width="35%" scope="row">Manufacturer</th>
<td>{% if rolling_stock.manufacturer.website %}<a href="{{ rolling_stock.manufacturer.website }}">{% endif %}{{ rolling_stock.manufacturer|default_if_none:"" }}{% if rolling_stock.manufacturer.website %}</a>{% endif %}</th> <td>{%if rolling_stock.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
@@ -102,7 +110,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if rolling_stock.decoder %} {% if rolling_stock.decoder or rolling_stock.decoder_interface %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -111,13 +119,19 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Decoder</th> <th width="35%" scope="row">Interface</th>
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr>
{% if rolling_stock.decoder %}
<tr>
<th scope="row">Decoder</th>
<td>{{ rolling_stock.decoder }}</td> <td>{{ rolling_stock.decoder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Address</th> <th scope="row">Address</th>
<td>{{ rolling_stock.address }}</td> <td>{{ rolling_stock.address }}</td>
</tr> </tr>
{% endif %}
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
@@ -132,11 +146,13 @@
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Manufacturer</th> <th width="35%" scope="row">Manufacturer</th>
<td>{% if rolling_stock.manufacturer.website %}<a href="{{ rolling_stock.manufacturer.website }}">{% endif %}{{ rolling_stock.manufacturer|default_if_none:"" }}{% if rolling_stock.manufacturer.website %}</a>{% endif %}</th> <td>{%if rolling_stock.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
<td><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></td> <td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td>
</tr> </tr>
<tr> <tr>
<th scope="row">SKU</th> <th scope="row">SKU</th>
@@ -192,7 +208,9 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Company</th> <th scope="row">Company</th>
<td>{{ rolling_stock.rolling_class.company }} ({{ rolling_stock.rolling_class.company.extended_name }})</td> <td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.slug %}">{{ rolling_stock.rolling_class.company }}</a> ({{ rolling_stock.rolling_class.company.extended_name }})
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Country</th> <th scope="row">Country</th>
@@ -200,7 +218,9 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{{ rolling_stock.rolling_class.manufacturer|default_if_none:"" }}</td> <td>{%if rolling_stock.rolling_class.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.rolling_class.manufacturer.slug %}">{{ rolling_stock.rolling_class.manufacturer }}{% if rolling_stock.rolling_class.manufacturer.website %}</a> <a href="{{ rolling_stock.rolling_class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -230,6 +250,10 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<th scope="row">Interface</th>
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
</tr>
<tr> <tr>
<th width="35%" scope="row">Address</th> <th width="35%" scope="row">Address</th>
<td>{{ rolling_stock.address }}</td> <td>{{ rolling_stock.address }}</td>
@@ -246,10 +270,6 @@
<th scope="row">Version</th> <th scope="row">Version</th>
<td>{{ rolling_stock.decoder.version }}</td> <td>{{ rolling_stock.decoder.version }}</td>
</tr> </tr>
<tr>
<th scope="row">Interface</th>
<td>{{ rolling_stock.decoder.get_interface_display }}</td>
</tr>
<tr> <tr>
<th scope="row">Sound</th> <th scope="row">Sound</th>
<td>{{ rolling_stock.decoder.sound | yesno:"Yes,No" }}</td> <td>{{ rolling_stock.decoder.sound | yesno:"Yes,No" }}</td>
@@ -258,7 +278,18 @@
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ rolling_stock.notes | safe }} <table class="table">
<thead>
<tr>
<th scope="row">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ rolling_stock.notes | safe }}</td>
</tr>
</tbody>
</table>
</div> </div>
<div class="tab-pane fade" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab"> <div class="tab-pane fade" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
<table class="table table-striped"> <table class="table table-striped">
@@ -271,7 +302,7 @@
{% for d in rolling_stock.document.all %} {% for d in rolling_stock.document.all %}
<tr> <tr>
<td>{{ d.description }}</td> <td>{{ d.description }}</td>
<td><a href="{{ d.file.url }}">{{ d.filename }}</a></td> <td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
<td class="text-end">{{ d.file.size | filesizeformat }}</td> <td class="text-end">{{ d.file.size | filesizeformat }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -0,0 +1,41 @@
{% extends "cards.html" %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'roster_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'roster_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'roster_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -1,14 +1,11 @@
{% extends "base.html" %} {% extends "cards.html" %}
{% block header %}
<h1 class="fw-light">Scales</h1>
{% endblock %}
{% block cards %} {% block cards %}
{% for s in scale %} {% for d in data %}
<div class="col"> <div class="col">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
<p class="card-text"><strong>{{ s }}</strong></p> <p class="card-text"><strong>{{ d }}</strong></p>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -18,25 +15,25 @@
<tbody> <tbody>
<tr> <tr>
<th width="35%" scope="row">Name</th> <th width="35%" scope="row">Name</th>
<td>{{ s.scale }}</td> <td>{{ d.scale }}</td>
</tr> </tr>
<tr> <tr>
<th width="35%" scope="row">Ratio</th> <th width="35%" scope="row">Ratio</th>
<td>{{ s.ratio }}</td> <td>{{ d.ratio }}</td>
</tr> </tr>
<tr> <tr>
<th width="35%" scope="row">Gauge</th> <th width="35%" scope="row">Gauge</th>
<td>{{ s.gauge }}</td> <td>{{ d.gauge }}</td>
</tr> </tr>
<tr> <tr>
<th width="35%" scope="row">Tracks</th> <th width="35%" scope="row">Tracks</th>
<td>{{ s.tracks }}</td> <td>{{ d.tracks }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="d-grid gap-2 mb-1 d-md-block"> <div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=s %}">Show all rolling stock</a> <a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=d.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' s.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -44,12 +41,12 @@
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}
{% block pagination %} {% block pagination %}
{% if scale.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if scale.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scale_pagination' page=scale.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -57,21 +54,21 @@
</li> </li>
{% endif %} {% endif %}
{% for i in page_range %} {% for i in page_range %}
{% if scale.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span></span>
</li> </li>
{% else %} {% else %}
{% if i == scale.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'scale_pagination' page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if scale.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scale_pagination' page=scale.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -1,16 +1,12 @@
{% extends "base.html" %} {% extends "cards.html" %}
{% block header %}
<h1 class="fw-light">{{ filter | default_if_none:"Search" | title }}: {{ search }}</h1>
<p class="lead text-muted">Results found: {{ matches }}</p>
{% endblock %}
{% block pagination %} {% block pagination %}
{% if rolling_stock.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0"> <ul class="pagination justify-content-center mt-4 mb-0">
{% if rolling_stock.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=rolling_stock.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -18,21 +14,21 @@
</li> </li>
{% endif %} {% endif %}
{% for i in page_range %} {% for i in page_range %}
{% if rolling_stock.number == i %} {% if data.number == i %}
<li class="page-item active"> <li class="page-item active">
<span class="page-link">{{ i }}</span></span> <span class="page-link">{{ i }}</span></span>
</li> </li>
{% else %} {% else %}
{% if i == rolling_stock.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=i %}#rolling-stock">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'search_pagination' search=encoded_search page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if rolling_stock.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=rolling_stock.next_page_number %}#rolling-stock" tabindex="-1">Next</a> <a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -0,0 +1,73 @@
{% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Type</th>
</tr>
</thead>
<tbody>
<tr>
<th width="35%" scope="row">Type</th>
<td>{{ d.type }}</td>
</tr>
<tr>
<th width="35%" scope="row">Category</th>
<td>{{ d.category | title}}</td>
</tr>
</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.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.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %}
{% if data.has_other_pages %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center mt-4 mb-0">
{% if data.has_previous %}
<li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#rolling-stock" tabindex="-1">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
{% for i in page_range %}
{% if data.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span></span>
</li>
{% else %}
{% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %}
<li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#rolling-stock">{{ i }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
{% if data.has_next %}
<li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#rolling-stock" tabindex="-1">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% endblock %}

View File

@@ -1,29 +1,29 @@
from django.urls import path from django.urls import path
from portal.views import ( from portal.views import (
GetHome, GetData,
GetHomeFiltered, GetRoster,
GetRosterFiltered,
GetFlatpage, GetFlatpage,
GetRollingStock, GetRollingStock,
GetConsist, GetConsist,
Consists, Consists,
Companies, Companies,
Manufacturers,
Scales, Scales,
Types,
SearchRoster,
) )
urlpatterns = [ urlpatterns = [
path("", GetHome.as_view(), name="index"), path("", GetData.as_view(), name="index"),
path("<int:page>", GetHome.as_view(), name="index_pagination"), path("roster", GetRoster.as_view(), name="roster"),
path("roster/<int:page>", GetRoster.as_view(), name="roster_pagination"),
path( path(
"page/<str:flatpage>", "page/<str:flatpage>",
GetFlatpage.as_view(), GetFlatpage.as_view(),
name="flatpage", name="flatpage",
), ),
path(
"search",
GetHomeFiltered.as_view(http_method_names=["post"]),
name="search",
),
path("consists", Consists.as_view(), name="consists"), path("consists", Consists.as_view(), name="consists"),
path( path(
"consists/<int:page>", Consists.as_view(), name="consists_pagination" "consists/<int:page>", Consists.as_view(), name="consists_pagination"
@@ -40,16 +40,38 @@ urlpatterns = [
Companies.as_view(), Companies.as_view(),
name="companies_pagination", name="companies_pagination",
), ),
path(
"manufacturers/<str:category>",
Manufacturers.as_view(),
name="manufacturers"
),
path(
"manufacturers/<str:category>/<int:page>",
Manufacturers.as_view(),
name="manufacturers_pagination",
),
path("scales", Scales.as_view(), name="scales"), path("scales", Scales.as_view(), name="scales"),
path("scales/<int:page>", Scales.as_view(), name="scales_pagination"), path("scales/<int:page>", Types.as_view(), name="scales_pagination"),
path("types", Types.as_view(), name="types"),
path("types/<int:page>", Types.as_view(), name="types_pagination"),
path(
"search",
SearchRoster.as_view(http_method_names=["post"]),
name="search",
),
path(
"search/<str:search>/<int:page>",
SearchRoster.as_view(),
name="search_pagination",
),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
GetHomeFiltered.as_view(), GetRosterFiltered.as_view(),
name="filtered", name="filtered",
), ),
path( path(
"<str:_filter>/<str:search>/<int:page>", "<str:_filter>/<str:search>/<int:page>",
GetHomeFiltered.as_view(), GetRosterFiltered.as_view(),
name="filtered_pagination", name="filtered_pagination",
), ),
path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"), path("<uuid:uuid>", GetRollingStock.as_view(), name="rolling_stock"),

View File

@@ -1,18 +1,20 @@
import base64
import operator import operator
from functools import reduce from functools import reduce
from urllib.parse import unquote
from django.views import View from django.views import View
from django.http import Http404 from django.http import Http404, HttpResponseBadRequest
from django.db.models import Q from django.db.models import Q
from django.shortcuts import render from django.shortcuts import render, get_object_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator, PageNotAnInteger from django.core.paginator import Paginator
from portal.utils import get_site_conf from portal.utils import get_site_conf
from portal.models import Flatpage from portal.models import Flatpage
from roster.models import RollingStock from roster.models import RollingStock
from consist.models import Consist from consist.models import Consist
from metadata.models import Company, Scale from metadata.models import Company, Manufacturer, Scale, RollingStockType, Tag
def order_by_fields(): def order_by_fields():
@@ -32,28 +34,44 @@ def order_by_fields():
return (fields[2], fields[0], fields[1], fields[3]) return (fields[2], fields[0], fields[1], fields[3])
class GetHome(View): class GetData(View):
def __init__(self):
self.title = "Home"
self.template = "home.html"
self.data = RollingStock.objects.order_by(*order_by_fields())
def get(self, request, page=1): def get(self, request, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
rolling_stock = RollingStock.objects.order_by(*order_by_fields())
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(self.data, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
) )
return render( return render(
request, request,
"home.html", self.template,
{"rolling_stock": rolling_stock, "page_range": page_range}, {
"title": self.title,
"data": data,
"matches": paginator.count,
"page_range": page_range,
},
) )
class GetHomeFiltered(View): class GetRoster(GetData):
def __init__(self):
self.title = "Rolling stock"
self.template = "roster.html"
self.data = RollingStock.objects.order_by(*order_by_fields())
class SearchRoster(View):
def run_search(self, request, search, _filter, page=1): def run_search(self, request, search, _filter, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
if _filter == "search": if _filter is None:
query = reduce( query = reduce(
operator.or_, operator.or_,
( (
@@ -72,21 +90,32 @@ class GetHomeFiltered(View):
for s in search.split() for s in search.split()
), ),
) )
elif _filter == "type":
query = Q(
Q(rolling_class__type__type__icontains=search)
| Q(rolling_class__type__category__icontains=search)
)
elif _filter == "company": elif _filter == "company":
query = Q( query = Q(
Q(rolling_class__company__name__icontains=search) Q(rolling_class__company__name__icontains=search)
| Q(rolling_class__company__extended_name__icontains=search) | Q(rolling_class__company__extended_name__icontains=search)
) )
elif _filter == "manufacturer":
query = Q(
Q(manufacturer__name__icontains=search)
| Q(rolling_class__manufacturer__name__icontains=search)
)
elif _filter == "scale": elif _filter == "scale":
query = Q(scale__scale__iexact=search) query = Q(scale__scale__icontains=search)
elif _filter == "tag":
query = Q(tags__slug__iexact=search)
else: else:
raise Http404 raise Http404
rolling_stock = RollingStock.objects.filter(query).order_by(
*order_by_fields() rolling_stock = (
RollingStock.objects.filter(query)
.distinct()
.order_by(*order_by_fields())
) )
matches = len(rolling_stock) matches = rolling_stock.count()
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) rolling_stock = paginator.get_page(page)
@@ -96,39 +125,105 @@ class GetHomeFiltered(View):
return rolling_stock, matches, page_range return rolling_stock, matches, page_range
def get(self, request, search, _filter="search", page=1): def split_search(self, search):
search = search.strip().split(":")
if not search:
raise Http404
elif len(search) == 1: # no filter
_filter = None
search = search[0].strip()
elif len(search) == 2: # filter: search
_filter = search[0].strip().lower()
search = search[1].strip()
else:
return HttpResponseBadRequest
return _filter, search
def get(self, request, search, page=1):
try:
encoded_search = search
search = base64.b64decode(search.encode()).decode()
except Exception:
encoded_search = base64.b64encode(
search.encode()).decode()
_filter, keyword = self.split_search(search)
rolling_stock, matches, page_range = self.run_search( rolling_stock, matches, page_range = self.run_search(
request, search, _filter, page request, keyword, _filter, page
) )
return render( return render(
request, request,
"search.html", "search.html",
{ {
"title": "Search: \"{}\"".format(search),
"search": search, "search": search,
"filter": _filter, "encoded_search": encoded_search,
"matches": matches, "matches": matches,
"rolling_stock": rolling_stock, "data": rolling_stock,
"page_range": page_range, "page_range": page_range,
}, },
) )
def post(self, request, _filter="search", page=1): def post(self, request, page=1):
search = request.POST.get("search") search = request.POST.get("search")
if not search: return self.get(request, search, page)
class GetRosterFiltered(View):
def run_filter(self, request, search, _filter, page=1):
site_conf = get_site_conf()
if _filter == "type":
title = RollingStockType.objects.get(slug__iexact=search)
query = Q(rolling_class__type__slug__iexact=search)
elif _filter == "company":
title = get_object_or_404(Company, slug__iexact=search)
query = Q(rolling_class__company__slug__iexact=search)
elif _filter == "manufacturer":
title = get_object_or_404(Manufacturer, slug__iexact=search)
query = Q(
Q(rolling_class__manufacturer__slug__iexact=search)
| Q(manufacturer__slug__iexact=search)
)
elif _filter == "scale":
title = get_object_or_404(Scale, slug__iexact=search)
query = Q(scale__slug__iexact=search)
elif _filter == "tag":
title = get_object_or_404(Tag, slug__iexact=search)
query = Q(tags__slug__iexact=search)
else:
raise Http404 raise Http404
rolling_stock, matches, page_range = self.run_search(
request, search, _filter, page rolling_stock = (
RollingStock.objects.filter(query)
.distinct()
.order_by(*order_by_fields())
)
matches = rolling_stock.count()
paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
rolling_stock.number, on_each_side=2, on_ends=1
)
return rolling_stock, title, matches, page_range
def get(self, request, search, _filter, page=1):
data, title, matches, page_range = self.run_filter(
request, unquote(search), _filter, page
) )
return render( return render(
request, request,
"search.html", "filter.html",
{ {
"title": "{0}: {1}".format(
_filter.capitalize(), title),
"search": search, "search": search,
"filter": _filter, "filter": _filter,
"matches": matches, "matches": matches,
"rolling_stock": rolling_stock, "data": data,
"page_range": page_range, "page_range": page_range,
}, },
) )
@@ -162,8 +257,9 @@ class GetRollingStock(View):
return render( return render(
request, request,
"page.html", "rollingstock.html",
{ {
"title": rolling_stock,
"rolling_stock": rolling_stock, "rolling_stock": rolling_stock,
"class_properties": class_properties, "class_properties": class_properties,
"rolling_stock_properties": rolling_stock_properties, "rolling_stock_properties": rolling_stock_properties,
@@ -172,22 +268,11 @@ class GetRollingStock(View):
) )
class Consists(View): class Consists(GetData):
def get(self, request, page=1): def __init__(self):
site_conf = get_site_conf() self.title = "Consists"
consist = Consist.objects.all() self.template = "consists.html"
self.data = Consist.objects.all()
paginator = Paginator(consist, site_conf.items_per_page)
consist = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
consist.number, on_each_side=2, on_ends=1
)
return render(
request,
"consists.html",
{"consist": consist, "page_range": page_range},
)
class GetConsist(View): class GetConsist(View):
@@ -197,7 +282,10 @@ class GetConsist(View):
consist = Consist.objects.get(uuid=uuid) consist = Consist.objects.get(uuid=uuid)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
rolling_stock = consist.consist_item.all() rolling_stock = [
RollingStock.objects.get(uuid=r.rolling_stock_id) for r in
consist.consist_item.all()
]
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(rolling_stock, site_conf.items_per_page)
rolling_stock = paginator.get_page(page) rolling_stock = paginator.get_page(page)
@@ -209,47 +297,47 @@ class GetConsist(View):
request, request,
"consist.html", "consist.html",
{ {
"title": consist,
"consist": consist, "consist": consist,
"rolling_stock": rolling_stock, "data": rolling_stock,
"page_range": page_range, "page_range": page_range,
}, },
) )
class Companies(View): class Manufacturers(GetData):
def get(self, request, page=1): def __init__(self):
site_conf = get_site_conf() self.title = "Manufacturers"
company = Company.objects.all() self.template = "manufacturers.html"
self.data = None # Set via method get
paginator = Paginator(company, site_conf.items_per_page) # overload get method to filter by category
company = paginator.get_page(page) def get(self, request, category, page=1):
page_range = paginator.get_elided_page_range( if category not in ("real", "model"):
company.number, on_each_side=2, on_ends=1 raise Http404
) self.data = Manufacturer.objects.filter(category=category)
return super().get(request, page)
return render(
request,
"companies.html",
{"company": company, "page_range": page_range},
)
class Scales(View): class Companies(GetData):
def get(self, request, page=1): def __init__(self):
site_conf = get_site_conf() self.title = "Companies"
scale = Scale.objects.all() self.template = "companies.html"
self.data = Company.objects.all()
paginator = Paginator(scale, site_conf.items_per_page)
scale = paginator.get_page(page)
page_range = paginator.get_elided_page_range(
scale.number, on_each_side=2, on_ends=1
)
return render( class Scales(GetData):
request, def __init__(self):
"scales.html", self.title = "Scales"
{"scale": scale, "page_range": page_range}, self.template = "scales.html"
) self.data = Scale.objects.all()
class Types(GetData):
def __init__(self):
self.title = "Types"
self.template = "types.html"
self.data = RollingStockType.objects.all()
class GetFlatpage(View): class GetFlatpage(View):
@@ -264,5 +352,5 @@ class GetFlatpage(View):
return render( return render(
request, request,
"flatpage.html", "flatpage.html",
{"flatpage": flatpage}, {"title": flatpage.name, "flatpage": flatpage},
) )

View File

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

View File

@@ -156,7 +156,12 @@ DECODER_INTERFACES = [
(5, "Next18/Next18S"), (5, "Next18/Next18S"),
] ]
MANUFACTURER_TYPES = [("model", "Model"), ("real", "Real")] MANUFACTURER_TYPES = [
("model", "Model"),
("real", "Real"),
("accessory", "Accessory"),
("other", "Other")
]
ROLLING_STOCK_TYPES = [ ROLLING_STOCK_TYPES = [
("engine", "Engine"), ("engine", "Engine"),

View File

@@ -1,8 +1,29 @@
import os import os
import hashlib
import subprocess import subprocess
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.text import slugify as django_slugify from django.utils.text import slugify as django_slugify
from django.core.files.storage import FileSystemStorage
class DeduplicatedStorage(FileSystemStorage):
"""
A derived FileSystemStorage class that compares already existing files
(with the same name) with new uploaded ones and stores new file only if
sha256 hash on is content is different
"""
def save(self, name, content, max_length=None):
if super().exists(name):
new = hashlib.sha256(content.file.getbuffer()).hexdigest()
with open(super().path(name), "rb") as file:
file_binary = file.read()
old = hashlib.sha256(file_binary).hexdigest()
if old == new:
return name
return super().save(name, content, max_length)
def git_suffix(fname): def git_suffix(fname):

View File

@@ -1,4 +1,6 @@
from django.contrib import admin from django.contrib import admin
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
from roster.models import ( from roster.models import (
RollingClass, RollingClass,
RollingClassProperty, RollingClassProperty,
@@ -35,7 +37,7 @@ class RollingStockDocInline(admin.TabularInline):
classes = ["collapse"] classes = ["collapse"]
class RollingStockImageInline(admin.TabularInline): class RollingStockImageInline(SortableInlineAdminMixin, admin.TabularInline):
model = RollingStockImage model = RollingStockImage
min_num = 0 min_num = 0
extra = 0 extra = 0
@@ -93,7 +95,7 @@ class RollingJournalDocumentAdmin(admin.ModelAdmin):
@admin.register(RollingStock) @admin.register(RollingStock)
class RollingStockAdmin(admin.ModelAdmin): class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
inlines = ( inlines = (
RollingStockPropertyInline, RollingStockPropertyInline,
RollingStockImageInline, RollingStockImageInline,
@@ -125,6 +127,7 @@ class RollingStockAdmin(admin.ModelAdmin):
"address", "address",
"sku", "sku",
) )
save_as = True
fieldsets = ( fieldsets = (
( (
@@ -148,6 +151,7 @@ class RollingStockAdmin(admin.ModelAdmin):
"DCC", "DCC",
{ {
"fields": ( "fields": (
"decoder_interface",
"decoder", "decoder",
"address", "address",
) )

View File

@@ -0,0 +1,42 @@
# Generated by Django 4.1 on 2022-10-31 22:27
from django.db import migrations, models
def meta_to_roster(apps, schema_editor):
model = apps.get_model("roster", "RollingStock")
for row in model.objects.all():
if row.decoder:
decoder_interface = row.decoder.interface
row.__dict__["decoder_interface"] = decoder_interface
row.save(update_fields=["decoder_interface"])
class Migration(migrations.Migration):
dependencies = [
("roster", "0012_rollingstockjournal"),
]
operations = [
migrations.AddField(
model_name="rollingstock",
name="decoder_interface",
field=models.PositiveSmallIntegerField(
blank=True,
choices=[
(1, "NEM651"),
(2, "NEM652"),
(3, "PluX"),
(4, "21MTC"),
(5, "Next18/Next18S"),
],
null=True,
),
),
migrations.RunPython(
meta_to_roster,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 4.1.2 on 2022-11-27 00:10
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("roster", "0013_rollingstock_decoder_interface"),
]
operations = [
migrations.AlterField(
model_name="rollingstockdocument",
name="file",
field=models.FileField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage(),
upload_to="files/",
),
),
migrations.AlterField(
model_name="rollingstockimage",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/",
),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.3 on 2022-12-28 21:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0014_alter_rollingstockdocument_file_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="rollingstockimage",
options={"ordering": ["-is_thumbnail"]},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.1.3 on 2023-01-02 12:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0015_alter_rollingstockimage_options"),
]
operations = [
migrations.AlterModelOptions(
name="rollingstockimage",
options={"ordering": ["order"]},
),
migrations.AddField(
model_name="rollingstockimage",
name="order",
field=models.PositiveIntegerField(default=0),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.1.3 on 2023-01-02 15:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("roster", "0016_alter_rollingstockimage_options_and_more"),
]
operations = [
migrations.RemoveField(
model_name="rollingstockimage",
name="is_thumbnail",
),
]

View File

@@ -3,12 +3,13 @@ import re
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from ckeditor_uploader.fields import RichTextUploadingField from ckeditor_uploader.fields import RichTextUploadingField
from ram.utils import get_image_preview from ram.utils import DeduplicatedStorage, get_image_preview
from metadata.models import ( from metadata.models import (
Property, Property,
Scale, Scale,
@@ -19,11 +20,6 @@ from metadata.models import (
RollingStockType, RollingStockType,
) )
# class OverwriteMixin(FileSystemStorage):
# def get_available_name(self, name, max_length):
# self.delete(name)
# return name
class RollingClass(models.Model): class RollingClass(models.Model):
identifier = models.CharField(max_length=128, unique=False) identifier = models.CharField(max_length=128, unique=False)
@@ -87,6 +83,9 @@ class RollingStock(models.Model):
) )
scale = models.ForeignKey(Scale, on_delete=models.CASCADE) scale = models.ForeignKey(Scale, on_delete=models.CASCADE)
sku = models.CharField(max_length=32, blank=True) sku = models.CharField(max_length=32, blank=True)
decoder_interface = models.PositiveSmallIntegerField(
choices=settings.DECODER_INTERFACES, null=True, blank=True
)
decoder = models.ForeignKey( decoder = models.ForeignKey(
Decoder, on_delete=models.CASCADE, null=True, blank=True Decoder, on_delete=models.CASCADE, null=True, blank=True
) )
@@ -133,7 +132,12 @@ class RollingStockDocument(models.Model):
RollingStock, on_delete=models.CASCADE, related_name="document" RollingStock, on_delete=models.CASCADE, related_name="document"
) )
description = models.CharField(max_length=128, blank=True) description = models.CharField(max_length=128, blank=True)
file = models.FileField(upload_to="files/", null=True, blank=True) file = models.FileField(
upload_to="files/",
storage=DeduplicatedStorage(),
null=True,
blank=True,
)
class Meta(object): class Meta(object):
unique_together = ("rolling_stock", "file") unique_together = ("rolling_stock", "file")
@@ -151,11 +155,13 @@ class RollingStockDocument(models.Model):
class RollingStockImage(models.Model): class RollingStockImage(models.Model):
order = models.PositiveIntegerField(default=0, blank=False, null=False)
rolling_stock = models.ForeignKey( rolling_stock = models.ForeignKey(
RollingStock, on_delete=models.CASCADE, related_name="image" RollingStock, on_delete=models.CASCADE, related_name="image"
) )
image = models.ImageField(upload_to="images/", null=True, blank=True) image = models.ImageField(
is_thumbnail = models.BooleanField() upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True
)
def image_thumbnail(self): def image_thumbnail(self):
return get_image_preview(self.image.url) return get_image_preview(self.image.url)
@@ -165,12 +171,8 @@ class RollingStockImage(models.Model):
def __str__(self): def __str__(self):
return "{0}".format(os.path.basename(self.image.name)) return "{0}".format(os.path.basename(self.image.name))
def save(self, **kwargs): class Meta:
if self.is_thumbnail: ordering = ["order"]
RollingStockImage.objects.filter(
rolling_stock=self.rolling_stock
).update(is_thumbnail=False)
super().save(**kwargs)
class RollingStockProperty(models.Model): class RollingStockProperty(models.Model):