35 Commits

Author SHA1 Message Date
7dadf23f5f Make pylibmc optional in requirements-prod.txt 2023-11-04 23:58:51 +01:00
4a12201d22 Make Document and Image files not nullable 2023-11-04 23:54:56 +01:00
830da80302 Keep media folder clean (#28)
* Reorg roster, portal and bookshelf media
* Extend media reorg to consists
* Delete roster and bookshelf images on delte.
   Do not delete others data that might be dedup! 
* Bump version
2023-10-31 11:16:55 +01:00
416ca5bbc6 eu.gif is part of dajngo-countries 2023-10-28 14:00:52 +02:00
03fc82c38d Enable csrf protection 2023-10-28 13:56:43 +02:00
ec8684dbc0 Add a "None" country and "Europe" with flags 2023-10-28 13:55:21 +02:00
7ec8baf733 Replace \t with spaces in base.html 2023-10-28 09:29:11 +02:00
86589ad718 More w3c minor fixes 2023-10-27 23:20:36 +02:00
98fed02a40 Fix a table in rollingstock.html 2023-10-27 23:16:23 +02:00
9602f67e0e Remove a spurious tag 2023-10-27 23:14:09 +02:00
5bb6279095 Extend UX improvements on other pages 2023-10-27 23:11:21 +02:00
84cdee42a6 Fix html syntax in rollingstock.html 2023-10-27 22:58:24 +02:00
168b424df7 Bump version 2023-10-27 22:46:19 +02:00
e1400fe720 Remove health page 2023-10-27 22:26:24 +02:00
26dea2fb35 Improve rollingstock page UX on mobile 2023-10-27 22:26:05 +02:00
ef767ec33d Fix a pretty-print on companies 2023-10-23 18:54:57 +02:00
b23801dbf0 Clear cache on save if active 2023-10-21 21:42:03 +02:00
c7fa54e90e Rename roster methods in portal view 2023-10-17 22:46:55 +02:00
9164ba494f Update examples to implement caching 2023-10-17 22:40:31 +02:00
97989c3384 Improve UX and filtering 2023-10-17 13:44:30 +02:00
7865bf04f0 Add consists view in rolling stock and them in company filter 2023-10-16 22:48:46 +02:00
e6f1480894 Change login menu icon on mobile 2023-10-12 22:33:55 +02:00
8d8ede4c06 Improve page layout on mobile 2023-10-11 22:39:29 +02:00
87e1107156 Bugfixing (#27)
* Enforce ordering on some metadata models
* Fix a 500 error while accessing flat pages
* Clean up HTML and fix cards (missing class)
* Make the "driver" app optional and disabled by default
2023-10-10 22:17:21 +02:00
448ecae070 Add Python 3.12 flow 2023-10-09 23:17:00 +02:00
2b0fdc4487 Workaround for python 3.12 on Fedora 39 2023-10-09 23:16:06 +02:00
764240d67a Fix bookshelf default sorting 2023-10-09 23:09:05 +02:00
424b17ae58 Bug fixing for consists 2023-10-08 09:52:38 +02:00
c73efb01e4 Introduce private docs and flatpages preview (#26)
* Add support for private documents
* Fix migrations after merge
* Rebase fixtures
* Filter private decoder docs
* Enable preview of unpublished pages
2023-10-07 22:38:20 +02:00
a21baac10c Fix a dependency on solo during bootstrap 2023-10-06 21:37:24 +02:00
4b0361acc1 Fix the consists search 2023-10-05 23:21:52 +02:00
425eed3d83 Bookshelf reloaded (#25)
* Navbar refactoring
* Fix coming soon SVG fonts
* Overhaul templating and extend search to consists and books
2023-10-05 23:13:42 +02:00
2d48463474 Change model default sort for Book 2023-10-03 23:08:11 +02:00
08226247c7 Extend ISBN to include dashes 2023-10-03 22:43:20 +02:00
4f52736d97 Fix a copy-paste issue in bookshelf model 2023-10-03 22:26:51 +02:00
59 changed files with 1333 additions and 622 deletions

View File

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

View File

@@ -12,9 +12,6 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name="book", name="book",
options={ options={"ordering": ["authors__last_name", "title"]},
"ordering": ["authors__last_name", "title"],
"verbose_name_plural": "Rolling stock",
},
), ),
] ]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-03 20:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0005_alter_book_options"),
]
operations = [
migrations.AlterField(
model_name="book",
name="ISBN",
field=models.CharField(blank=True, max_length=17),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-10-03 21:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0006_alter_book_isbn"),
]
operations = [
migrations.AlterModelOptions(
name="book",
options={"ordering": ["title"]},
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-10-09 21:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0007_alter_book_options"),
]
operations = [
migrations.AlterModelOptions(
name="author",
options={"ordering": ["last_name", "first_name"]},
),
migrations.AlterModelOptions(
name="publisher",
options={"ordering": ["name"]},
),
]

View File

@@ -0,0 +1,49 @@
# Generated by Django 4.2.6 on 2023-10-30 13:16
import os
import sys
import shutil
import ram.utils
import bookshelf.models
from django.db import migrations, models
from django.conf import settings
def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...")
for r in bookshelf.models.BookImage.objects.all():
fname = os.path.basename(r.image.path)
new_image = bookshelf.models.book_image_upload(r, fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(r.image.path, new_path)
except FileNotFoundError:
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
pass
r.image.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0008_alter_author_options_alter_publisher_options"),
]
operations = [
migrations.AlterField(
model_name="bookimage",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.book_image_upload,
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 4.2.6 on 2023-11-04 22:53
import bookshelf.models
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("bookshelf", "0009_alter_bookimage_image"),
]
operations = [
migrations.AlterField(
model_name="bookimage",
name="image",
field=models.ImageField(
storage=ram.utils.DeduplicatedStorage,
upload_to=bookshelf.models.book_image_upload,
),
),
]

View File

@@ -1,3 +1,5 @@
import os
import shutil
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
@@ -16,6 +18,9 @@ class Publisher(models.Model):
country = CountryField(blank=True) country = CountryField(blank=True)
website = models.URLField(blank=True) website = models.URLField(blank=True)
class Meta:
ordering = ["name"]
def __str__(self): def __str__(self):
return self.name return self.name
@@ -24,6 +29,9 @@ class Author(models.Model):
first_name = models.CharField(max_length=100) first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100) last_name = models.CharField(max_length=100)
class Meta:
ordering = ["last_name", "first_name"]
def __str__(self): def __str__(self):
return f"{self.last_name}, {self.first_name}" return f"{self.last_name}, {self.first_name}"
@@ -36,7 +44,7 @@ class Book(models.Model):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
authors = models.ManyToManyField(Author) authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
ISBN = models.CharField(max_length=13, blank=True) ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
language = models.CharField( language = models.CharField(
max_length=7, max_length=7,
choices=settings.LANGUAGES, choices=settings.LANGUAGES,
@@ -53,8 +61,7 @@ class Book(models.Model):
updated_time = models.DateTimeField(auto_now=True) updated_time = models.DateTimeField(auto_now=True)
class Meta: class Meta:
ordering = ["authors__last_name", "title"] ordering = ["title"]
verbose_name_plural = "Rolling stock"
def __str__(self): def __str__(self):
return self.title return self.title
@@ -65,16 +72,32 @@ class Book(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("book", kwargs={"uuid": self.uuid}) return reverse("book", kwargs={"uuid": self.uuid})
def delete(self, *args, **kwargs):
shutil.rmtree(
os.path.join(
settings.MEDIA_ROOT, "images", "books", str(self.uuid)
),
ignore_errors=True
)
super(Book, self).delete(*args, **kwargs)
def book_image_upload(instance, filename):
return os.path.join(
"images",
"books",
str(instance.book.uuid),
filename
)
class BookImage(Image): class BookImage(Image):
book = models.ForeignKey( book = models.ForeignKey(
Book, on_delete=models.CASCADE, related_name="image" Book, on_delete=models.CASCADE, related_name="image"
) )
image = models.ImageField( image = models.ImageField(
upload_to="images/books/", # FIXME, find a better way to replace this upload_to=book_image_upload,
storage=DeduplicatedStorage, storage=DeduplicatedStorage,
null=True,
blank=True
) )

View File

@@ -0,0 +1,51 @@
# Generated by Django 4.2.6 on 2023-10-31 09:41
import os
import sys
import shutil
import ram.utils
from django.conf import settings
from django.db import migrations, models
def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...")
model = apps.get_model("consist", "Consist")
for r in model.objects.all():
if not r.image: # exit the loop if there's no image
continue
fname = os.path.basename(r.image.path)
new_image = os.path.join("images", "consists", fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(r.image.path, new_path)
except FileNotFoundError:
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
pass
r.image.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("consist", "0008_alter_consist_options"),
]
operations = [
migrations.AlterField(
model_name="consist",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/consists",
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -1,3 +1,5 @@
import os
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
@@ -19,7 +21,10 @@ 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( image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to=os.path.join("images", "consists"),
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)

View File

@@ -15,6 +15,7 @@ from metadata.models import (
@admin.register(Property) @admin.register(Property)
class PropertyAdmin(admin.ModelAdmin): class PropertyAdmin(admin.ModelAdmin):
list_display = ("name", "private")
search_fields = ("name",) search_fields = ("name",)

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-06 19:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("metadata", "0012_alter_decoder_manufacturer_decoderdocument"),
]
operations = [
migrations.AddField(
model_name="decoderdocument",
name="private",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-10-10 12:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("metadata", "0013_decoderdocument_private"),
]
operations = [
migrations.AlterModelOptions(
name="decoder",
options={"ordering": ["manufacturer", "name"]},
),
migrations.AlterModelOptions(
name="tag",
options={"ordering": ["name"]},
),
]

View File

@@ -0,0 +1,80 @@
# Generated by Django 4.2.6 on 2023-10-30 13:16
import os
import sys
import shutil
import ram.utils
from django.conf import settings
from django.db import migrations, models
def move_images(apps, schema_editor):
fields = {
"Company": ["companies", "logo"],
"Decoder": ["decoders", "image"],
"Manufacturer": ["manufacturers", "logo"],
}
sys.stdout.write("\n Processing files. Please await...")
for m in fields.items():
model = apps.get_model("metadata", m[0])
for r in model.objects.all():
field = getattr(r, m[1][1])
if not field: # exit the loop if there's no image
continue
fname = os.path.basename(field.path)
new_image = os.path.join("images", m[1][0], fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(field.path, new_path)
except FileNotFoundError:
sys.stderr.write(
" !! FileNotFoundError: {}\n".format(new_image)
)
pass
field.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("metadata", "0014_alter_decoder_options_alter_tag_options"),
]
operations = [
migrations.AlterField(
model_name="company",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/companies",
),
),
migrations.AlterField(
model_name="decoder",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/decoders",
),
),
migrations.AlterField(
model_name="manufacturer",
name="logo",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to="images/manufacturers",
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-11-04 22:53
from django.db import migrations, models
import ram.utils
class Migration(migrations.Migration):
dependencies = [
("metadata", "0015_alter_company_logo_alter_decoder_image_and_more"),
]
operations = [
migrations.AlterField(
model_name="decoderdocument",
name="file",
field=models.FileField(
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
),
),
]

View File

@@ -1,3 +1,4 @@
import os
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
@@ -28,7 +29,10 @@ class Manufacturer(models.Model):
) )
website = models.URLField(blank=True) website = models.URLField(blank=True)
logo = models.ImageField( logo = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to=os.path.join("images", "manufacturers"),
storage=DeduplicatedStorage,
null=True,
blank=True,
) )
class Meta: class Meta:
@@ -58,7 +62,10 @@ class Company(models.Model):
country = CountryField() country = CountryField()
freelance = models.BooleanField(default=False) freelance = models.BooleanField(default=False)
logo = models.ImageField( logo = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to=os.path.join("images", "companies"),
storage=DeduplicatedStorage,
null=True,
blank=True,
) )
class Meta: class Meta:
@@ -76,6 +83,9 @@ class Company(models.Model):
} }
) )
def extended_name_pp(self):
return "({})".format(self.extended_name) if self.extended_name else ""
def logo_thumbnail(self): def logo_thumbnail(self):
return get_image_preview(self.logo.url) return get_image_preview(self.logo.url)
@@ -92,9 +102,15 @@ class Decoder(models.Model):
version = models.CharField(max_length=64, blank=True) version = models.CharField(max_length=64, blank=True)
sound = models.BooleanField(default=False) sound = models.BooleanField(default=False)
image = models.ImageField( image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to=os.path.join("images", "decoders"),
storage=DeduplicatedStorage,
null=True,
blank=True,
) )
class Meta(object):
ordering = ["manufacturer", "name"]
def __str__(self): def __str__(self):
return "{0} - {1}".format(self.manufacturer, self.name) return "{0} - {1}".format(self.manufacturer, self.name)
@@ -163,6 +179,9 @@ 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)
class Meta(object):
ordering = ["name"]
def __str__(self): def __str__(self):
return self.name return self.name

View File

@@ -66,12 +66,11 @@ class Flatpage(models.Model):
return reverse("flatpage", kwargs={"flatpage": self.path}) return reverse("flatpage", kwargs={"flatpage": self.path})
def get_link(self): def get_link(self):
if self.published: return mark_safe(
return mark_safe( '<a href="{0}" target="_blank">Link</a>'.format(
'<a href="{0}" target="_blank">Link</a>'.format( self.get_absolute_url()
self.get_absolute_url()
)
) )
)
@receiver(models.signals.pre_save, sender=Flatpage) @receiver(models.signals.pre_save, sender=Flatpage)

View File

@@ -46,12 +46,12 @@
transform="rotate(-90)" /> transform="rotate(-90)" />
<text <text
xml:space="preserve" xml:space="preserve"
style="font-weight:bold;font-size:0.444444px;line-height:1.25;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans Bold';letter-spacing:0px;word-spacing:0px;stroke-width:0.0138889" style="font-weight:bold;font-size:0.444444px;line-height:1.25;font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue','Noto Sans','Liberation Sans',Arial,sans-serif;-inkscape-font-specification:'Noto Sans Bold';letter-spacing:0px;word-spacing:0px;stroke-width:0.0138889"
x="1.5366687" x="1.5366687"
y="1.6798887" y="1.6798887"
id="text1"><tspan id="text1"><tspan
id="tspan1" id="tspan1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:0.444444px;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';fill:#dee2e6;fill-opacity:1;stroke-width:0.0138889" style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:0.444444px;font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue','Noto Sans','Liberation Sans',Arial,sans-serif;-inkscape-font-specification:'Noto Sans';fill:#dee2e6;fill-opacity:1;stroke-width:0.0138889"
x="1.5366687" x="1.5366687"
y="1.6798887">Coming soon</tspan></text> y="1.6798887">Coming soon</tspan></text>
</g> </g>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -7,6 +7,16 @@ html[data-bs-theme='dark'] .navbar svg {
width: 100%; width: 100%;
} }
td > img.logo {
max-width: 200px;
max-height: 48px;
}
td > img.logo-xl {
max-width: 400px;
max-height: 96px;
}
.btn > span { .btn > span {
display: inline-block; display: inline-block;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 B

View File

@@ -115,13 +115,26 @@
}) })
})() })()
</script> </script>
<script>
document.addEventListener('DOMContentLoaded', function () {
var selectElement = document.getElementById('tabSelector');
selectElement.addEventListener('change', function () {
var selectedTabId = this.value;
var tabs = document.querySelectorAll('.tab-pane');
tabs.forEach(function (tab) {
tab.classList.remove('show', 'active');
});
document.getElementById(selectedTabId).classList.add('show', 'active');
});
});
</script>
{% block extra_head %} {% block extra_head %}
{{ site_conf.extra_head | safe }} {{ site_conf.extra_head | safe }}
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
<header> <header>
<nav class="navbar navbar-expand-lg bg-body-tertiary shadow-sm"> <nav class="navbar navbar-expand-sm bg-body-tertiary shadow-sm">
<div class="container d-flex"> <div class="container d-flex">
<div class="me-auto"> <div class="me-auto">
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center"> <a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
@@ -146,33 +159,34 @@
<nav class="navbar navbar-expand-lg"> <nav class="navbar navbar-expand-lg">
<div class="container-fluid g-0"> <div class="container-fluid g-0">
<a class="navbar-brand" href="{% url 'index' %}">Home</a> <a class="navbar-brand" href="{% url 'index' %}">Home</a>
<div class="navbar-collapse" id="navbarSupportedContent"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse 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 dropdown"> <li class="nav-item">
<a class="nav-link dropdown-toggle" href="#" id="rosterDropdownMenu" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link" href="{% url 'roster' %}">Roster</a>
Roster
</a>
<ul class="dropdown-menu" aria-labelledby="rosterDropdownMenu">
<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 dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="manufacturersDropdownMenu" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="filterDropdownMenu" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Manufacturers Search by
</a> </a>
<ul class="dropdown-menu" aria-labelledby="manufacturersDropdownMenu"> <ul class="dropdown-menu" aria-labelledby="filterDropdownMenu">
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Models</a></li> <li class="ps-2 text-secondary">Model</li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Real</a></li> <li><a class="dropdown-item" href="{% url 'scales' %}">Scale</a></li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Manufacturer</a></li>
<li><hr class="dropdown-divider"></li>
<li class="ps-2 text-secondary">Prototype</li>
<li><a class="dropdown-item" href="{% url 'types' %}">Type</a></li>
<li><a class="dropdown-item" href="{% url 'companies' %}">Company</a></li>
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Manufacturer</a></li>
</ul> </ul>
</li> </li>
{% show_flatpages_menu %}
{% show_bookshelf_menu %} {% show_bookshelf_menu %}
{% show_flatpages_menu %}
</ul> </ul>
{% include 'includes/search.html' %} {% include 'includes/search.html' %}
</div> </div>

View File

@@ -43,14 +43,16 @@
<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> <nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
<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>
<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 book.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 %}
{% if book.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> </nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option>
{% if book.notes %}<option value="nav-notes">Notes</option>{% endif %}
</select>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -112,7 +114,7 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ book.notes | safe }} {{ book.notes | safe }}
</div> </div>
</div> </div>

View File

@@ -1,68 +1,4 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
{% if d.image.all %}
<a href="{{ d.get_absolute_url }}">
{% for r in d.image.all %}
{% if forloop.first %}<img src="{{ r.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
</a>
{% endif %}
<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">Book</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Authors</th>
<td>
<ul class="mb-0 list-unstyled">{% for a in d.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td>
</tr>
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>{{ d.publisher }}</td>
</tr>
<tr>
<th scope="row">Language</th>
<td>{{ d.get_language_display }}</td>
</tr>
<tr>
<th scope="row">Pages</th>
<td>{{ d.number_of_pages|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Year</th>
<td>{{ d.publication_year|default:"-" }}</td>
</tr>
</tbody>
</table>
<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:bookshelf_book_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">

View File

@@ -4,7 +4,7 @@
Bookshelf Bookshelf
</a> </a>
<ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink"> <ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink">
<li><a class="nav-link" href="{% url 'books' %}">Books</a></li> <li><a class="dropdown-item" href="{% url 'books' %}">Books</a></li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}

View File

@@ -1,104 +1,26 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %}
{% block header %} {% block header %}
<p class="lead text-muted">Results found: {{ matches }}</p> <p class="lead text-muted">Results found: {{ matches }}</p>
{% endblock %} {% endblock %}
{% block cards_layout %} {% block cards_layout %}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
{% block cards %} {% block cards %}
{% for d in data %} {% for d in data %}
<div class="col"> {% if d.type == "rolling_stock" %}
<div class="card shadow-sm"> {% include "cards/roster.html" %}
{% if d.image.count > 0 %} {% elif d.type == "company" %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ d.image.first.image.url }}" alt="{{ d }}"></a> {% include "cards/company.html" %}
{% else %} {% elif d.type == "rolling_stock_type" %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) --> {% include "cards/rolling_stock_type.html" %}
<a href="{{d.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d }}"></a> {% elif d.type == "scale" %}
{% endif %} {% include "cards/scale.html" %}
<div class="card-body"> {% elif d.type == "consist" %}
<p class="card-text" style="position: relative;"> {% include "cards/consist.html" %}
<strong>{{ d }}</strong> {% elif d.type == "manufacturer" %}
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a> {% include "cards/manufacturer.html" %}
</p> {% elif d.type == "book" %}
{% if d.tags.all %} {% include "cards/book.html" %}
<p class="card-text"><small>Tags:</small> {% endif %}
{% 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 class="table-group-divider">
<tr>
<th class="w-33" 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 class="w-33" 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">Item number</th>
<td>{{ d.item_number }}</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 class="table-group-divider">
<tr>
<th class="w-33" 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 %} {% endfor %}
{% endblock %} {% endblock %}
</div> </div>

View File

@@ -0,0 +1,56 @@
<div class="col">
<div class="card shadow-sm">
{% if d.item.image.exists %}
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a>
{% endif %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Book</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Authors</th>
<td>
<ul class="mb-0 list-unstyled">{% for a in d.item.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
</td>
</tr>
<tr>
<th class="w-33" scope="row">Publisher</th>
<td>{{ d.item.publisher }}</td>
</tr>
<tr>
<th scope="row">Language</th>
<td>{{ d.item.get_language_display }}</td>
</tr>
<tr>
<th scope="row">Pages</th>
<td>{{ d.item.number_of_pages|default:"-" }}</td>
</tr>
<tr>
<th scope="row">Year</th>
<td>{{ d.item.publication_year|default:"-" }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:bookshelf_book_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Company</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.item.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.item.extended_name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Abbreviation</th>
<td>{{ d.item.name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Country</th>
<td>{{ d.item.country.name }} <img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}">
</tr>
{% if d.item.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.item.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,57 @@
<div class="col">
<div class="card shadow-sm">
<a href="{{ d.item.get_absolute_url }}">
{% if d.item.image %}
<img class="card-img-top" src="{{ d.item.image.url }}" alt="{{ d.item }}">
{% else %}
{% with d.item.consist_item.first.rolling_stock as r %}
<img class="card-img-top" src="{{ r.image.first.image.url }}" alt="{{ d.item }}">
{% endwith %}
{% endif %}
</a>
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Consist</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.item.address %}
<tr>
<th class="w-33" scope="row">Address</th>
<td>{{ d.item.address }}</td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Company</th>
<td><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.item.era }}</td>
</tr>
<tr>
<th scope="row">Length</th>
<td>{{ d.item.consist_item.count }}</td>
</tr>
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,39 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item.name }}</strong>
</p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Manufacturer</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.item.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img class="logo" src="{{ d.item.logo.url }}" alt="{{ d.item.name }} logo"></td>
</tr>
{% endif %}
{% if d.item.website %}
<tr>
<th class="w-33" scope="row">Website</th>
<td><a href="{{ d.item.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Category</th>
<td>{{ d.item.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.item.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.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d.item }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Type</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.item.type }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Category</th>
<td>{{ d.item.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.item.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,92 @@
{% load static %}
<div class="col">
<div class="card shadow-sm">
{% if d.item.image.exists %}
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top" src="{{ d.item.image.first.image.url }}" alt="{{ d.item }}"></a>
{% else %}
<!-- Do not show the "Coming soon" image when running in a single card column mode (e.g. on mobile) -->
<a href="{{d.item.get_absolute_url}}"><img class="card-img-top d-none d-sm-block" src="{% static DEFAULT_CARD_IMAGE %}" alt="{{ d.item }}"></a>
{% endif %}
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d.item }}</strong>
<a class="stretched-link" href="{{ d.item.get_absolute_url }}"></a>
</p>
{% if d.item.tags.all %}
<p class="card-text"><small>Tags:</small>
{% for t in d.item.tags.all %}<a href="{% url 'filtered' _filter="tag" search=t.slug %}" class="badge rounded-pill bg-primary">
{{ t.name }}</a>{# new line is required #}
{% endfor %}
</p>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Rolling stock</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.item.rolling_class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=d.item.rolling_class.company.slug %}"><abbr title="{{ d.item.rolling_class.company.extended_name }}">{{ d.item.rolling_class.company }}</abbr></a>
</td>
</tr>
<tr>
<th scope="row">Class</th>
<td>{{ d.item.rolling_class.identifier }}</td>
</tr>
<tr>
<th scope="row">Road number</th>
<td>{{ d.item.road_number }}</td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.item.era }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Manufacturer</th>
<td>{%if d.item.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.item.manufacturer.slug %}">{{ d.item.manufacturer }}{% if d.item.manufacturer.website %}</a> <a href="{{ d.item.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
<tr>
<th scope="row">Scale</th>
<td><a href="{% url 'filtered' _filter="scale" search=d.item.scale.slug %}"><abbr title="{{ d.item.scale.ratio }} - {{ d.item.scale.tracks }}">{{ d.item.scale }}</abbr></a></td>
</tr>
<tr>
<th scope="row">Item number</th>
<td>{{ d.item.item_number }}</td>
</tr>
</tbody>
</table>
{% if d.item.decoder %}
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">DCC data</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr>
<th class="w-33" scope="row">Decoder</th>
<td>{{ d.item.decoder }}</td>
</tr>
<tr>
<th scope="row">Address</th>
<td>{{ d.item.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.item.get_absolute_url}}">Show all data</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<div class="col">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text"><strong>{{ d.item }}</strong></p>
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Scale</th>
</tr>
</thead>
<tbody>
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.item.scale }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Ratio</th>
<td>{{ d.item.ratio }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Gauge</th>
<td>{{ d.item.gauge }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Tracks</th>
<td>{{ d.item.tracks }}</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="scale" search=d.item.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -1,55 +1,4 @@
{% extends "cards.html" %} {% 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">Company</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.extended_name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Abbreviation</th>
<td>{{ d.name }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Country</th>
<td>{{ d.country.name }} <img src="{{ d.country.flag }}" alt="{{ d.country }}" />
</tr>
{% if d.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</tbody>
</table>
<div class="d-grid gap-2 mb-1 d-md-block">
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.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' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">

View File

@@ -16,7 +16,7 @@
<div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel"> <div id="carouselControls" class="carousel carousel-dark slide" data-bs-ride="carousel">
<div class="carousel-inner"> <div class="carousel-inner">
<div class="carousel-item active"> <div class="carousel-item active">
<img src="{{ consist.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="..."> <img src="{{ consist.image.url }}" class="d-block w-100 rounded img-thumbnail" alt="Consist cover">
</div> </div>
</div> </div>
</div> </div>
@@ -66,14 +66,16 @@
<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> <nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
<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>
<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 %}
{% 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> </nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option>
{% if consist.notes %}<option value="nav-notes">Notes</option>{% endif %}
</select>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -83,7 +85,9 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Company</th> <th class="w-33" scope="row">Company</th>
<td><abbr title="{{ consist.company.extended_name }}">{{ consist.company }}</abbr></td> <td>
<a href="{% url 'filtered' _filter="company" search=consist.company.slug %}">{{ consist.company }}</a> ({{ consist.company.extended_name }})
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Era</th> <th scope="row">Era</th>
@@ -96,7 +100,7 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>

View File

@@ -1,68 +1,4 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block cards %}
{% for d in data %}
<div class="col">
<div class="card shadow-sm">
<a href="{{ d.get_absolute_url }}">
{% if d.image %}
<img src="{{ d.image.url }}" alt="Card image cap">
{% else %}
{% with d.consist_item.first.rolling_stock as r %}
{% for i in r.image.all %}
{% if forloop.first %}<img src="{{ i.image.url }}" alt="Card image cap">{% endif %}
{% endfor %}
{% endwith %}
{% endif %}
</a>
<div class="card-body">
<p class="card-text" style="position: relative;">
<strong>{{ d }}</strong>
<a class="stretched-link" href="{{ d.get_absolute_url }}"></a>
</p>
{% 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">Consist data</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if d.address %}
<tr>
<th class="w-33" scope="row">Address</th>
<td>{{ d.address }}</td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Company</th>
<td><abbr title="{{ d.company.extended_name }}">{{ d.company }}</abbr></td>
</tr>
<tr>
<th scope="row">Era</th>
<td>{{ d.era }}</td>
</tr>
<tr>
<th scope="row">Length</th>
<td>{{ d.consist_item.all | length }}</td>
</tr>
</tbody>
</table>
<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:consist_consist_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">

View File

@@ -1,5 +1,4 @@
{% extends "cards.html" %} {% extends "cards.html" %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">

View File

@@ -1,4 +1,7 @@
<div class="navbar-collapse justify-content-end" id="loginNavbar"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#loginNavbar" aria-controls="loginNavbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="bi bi-person-fill-gear"></span>
</button>
<div class="collapse navbar-collapse justify-content-end" id="loginNavbar">
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
{% if request.user.is_staff %} {% if request.user.is_staff %}
@@ -14,7 +17,6 @@
<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><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>

View File

@@ -1,4 +1,4 @@
<form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate> <form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate>{% csrf_token %}
<div class="input-group has-validation"> <div class="input-group has-validation">
<input class="form-control" type="search" list="datalistOptions" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required> <input class="form-control" type="search" list="datalistOptions" placeholder="Search" aria-label="Search" name="search" id="searchValidation" required>
<datalist id="datalistOptions"> <datalist id="datalistOptions">

View File

@@ -1,50 +1,7 @@
{% extends "cards.html" %} {% 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 class="table-group-divider">
{% if d.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img style="max-height: 48px" src="{{ d.logo.url }}" /></td>
</tr>
{% endif %}
{% if d.website %}
<tr>
<th class="w-33" 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 class="w-33" 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 %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
{% with data.0.category as c %} {% with data.0.item.category as c %}
<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 data.has_previous %} {% if data.has_previous %}

View File

@@ -43,19 +43,31 @@
<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> <nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
<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>
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button> <button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model</button>
<button class="nav-link" id="nav-model-tab" data-bs-toggle="tab" data-bs-target="#nav-model" type="button" role="tab" aria-controls="nav-model" aria-selected="false">Model data</button> <button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class</button>
<button class="nav-link" id="nav-class-tab" data-bs-toggle="tab" data-bs-target="#nav-class" type="button" role="tab" aria-controls="nav-class" aria-selected="false">Class data</button> <button class="nav-link" id="nav-company-tab" data-bs-toggle="tab" data-bs-target="#nav-company" type="button" role="tab" aria-controls="nav-company" aria-selected="false">Company</button>
{% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %} {% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %}
{% if rolling_stock.document.count > 0 or rolling_stock.decoder.document.count > 0 %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %} {% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
{% if rolling_stock_journal.count > 0 %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %} {% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
{% if rolling_stock.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 %} {% if rolling_stock.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> {% if consists %}<button class="nav-link" id="nav-consists-tab" data-bs-toggle="tab" data-bs-target="#nav-consists" type="button" role="tab" aria-controls="nav-consists" aria-selected="false">Consists</button>{% endif %}
</nav> </nav>
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
<option value="nav-summary" selected>Summary</option>
<option value="nav-model">Model</option>
<option value="nav-class">Class</option>
<option value="nav-company">Company</option>
{% if rolling_stock.decoder %}<option value="nav-dcc">DCC</option>{% endif %}
{% if documents or decoder_documents %}<option value="nav-documents">Documents</option>{% endif %}
{% if journal %}<option value="nav-journal">Journal</option>{% endif %}
{% if rolling_stock.notes %}<option value="nav-notes">Notes</option>{% endif %}
{% if consists %}<option value="nav-consists">Consists</option>{% endif %}
</select>
{% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %}
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab"> <div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -65,17 +77,17 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Type</th> <th class="w-33" scope="row">Type</th>
<td>{{ rolling_stock.rolling_class.type }}</td> <td>{{ class.type }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Company</th> <th scope="row">Company</th>
<td> <td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.slug %}"><abbr title="{{ rolling_stock.rolling_class.company.extended_name }}">{{ rolling_stock.rolling_class.company }}</abbr></a> <a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> {{ company.extended_name_pp }}
</td> </td>
</tr> </tr>
<tr> <tr>
<th scope="row">Class</th> <th scope="row">Class</th>
<td>{{ rolling_stock.rolling_class.identifier }}</td> <td>{{ class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Road number</th> <th scope="row">Road number</th>
@@ -97,7 +109,7 @@
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %} <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 %} <a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}</a>{% if rolling_stock.manufacturer.website %} <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td> {% endif %}</td>
</tr> </tr>
<tr> <tr>
@@ -136,7 +148,7 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-model" role="tabpanel" aria-labelledby="nav-model-tab"> <div class="tab-pane" id="nav-model" role="tabpanel" aria-labelledby="nav-model-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -146,9 +158,11 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Manufacturer</th> <th class="w-33" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %} <td>
<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 %} {%if rolling_stock.manufacturer %}
{% endif %}</td> <a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.slug %}">{{ rolling_stock.manufacturer }}</a>{% if rolling_stock.manufacturer.website %} <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% else %}-{% endif %}
</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Scale</th> <th scope="row">Scale</th>
@@ -172,7 +186,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if rolling_stock_properties %} {% if properties %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -180,7 +194,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for p in rolling_stock_properties %} {% for p in properties %}
<tr> <tr>
<th class="w-33" scope="row">{{ p.property }}</th> <th class="w-33" scope="row">{{ p.property }}</th>
<td>{{ p.value }}</td> <td>{{ p.value }}</td>
@@ -190,7 +204,7 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab"> <div class="tab-pane" id="nav-class" role="tabpanel" aria-labelledby="nav-class-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -200,27 +214,19 @@
<tbody class="table-group-divider"> <tbody class="table-group-divider">
<tr> <tr>
<th class="w-33" scope="row">Class</th> <th class="w-33" scope="row">Class</th>
<td>{{ rolling_stock.rolling_class.identifier }}</td> <td>{{ class.identifier }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Type</th> <th scope="row">Type</th>
<td>{{ rolling_stock.rolling_class.type }}</td> <td>{{ class.type }}</td>
</tr>
<tr>
<th scope="row">Company</th>
<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>
<th scope="row">Country</th>
<td>{{ rolling_stock.rolling_class.company.country.name }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Manufacturer</th> <th scope="row">Manufacturer</th>
<td>{%if rolling_stock.rolling_class.manufacturer %} <td>
<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 %} {%if class.manufacturer %}
{% endif %}</td> <a href="{% url 'filtered' _filter="manufacturer" search=class.manufacturer.slug %}">{{ class.manufacturer }}</a>{% if class.manufacturer.website %} <a href="{{ class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% else %}-{% endif %}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -242,11 +248,42 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab"> <div class="tab-pane" id="nav-company" role="tabpanel" aria-labelledby="nav-company-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="2" scope="row">DCC data</th> <th colspan="2" scope="row">Company data</th>
</tr>
</thead>
<tbody class="table-group-divider">
{% if company.logo %}
<tr>
<th class="w-33" scope="row">Logo</th>
<td><img class="logo-xl" src="{{ company.logo.url }}" alt="{{ company }} logo"></td>
</tr>
{% endif %}
<tr>
<th class="w-33" scope="row">Name</th>
<td><a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> {{ company.extended_name_pp }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Country</th>
<td>{{ company.country.name }} <img src="{{ company.country.flag }}" alt="{{ company.country }}">
</tr>
{% if company.freelance %}
<tr>
<th class="w-33" scope="row">Notes</th>
<td>A <em>freelance</em> company</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="tab-pane" id="nav-dcc" role="tabpanel" aria-labelledby="nav-dcc-tab">
<table class="table table-striped">
<thead>
<tr>
<th colspan="2" scope="row">Decoder data</th>
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
@@ -277,8 +314,8 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab"> <div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
{% if rolling_stock.document.count > 0 %} {% if documents %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -286,7 +323,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for d in rolling_stock.document.all %} {% for d in documents.all %}
<tr> <tr>
<td class="w-33">{{ d.description }}</td> <td class="w-33">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td> <td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
@@ -296,7 +333,7 @@
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
{% if rolling_stock.decoder.document.count > 0 %} {% if decoder_documents %}
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
@@ -304,7 +341,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for d in rolling_stock.decoder.document.all %} {% for d in decoder_documents.all %}
<tr> <tr>
<td class="w-33">{{ d.description }}</td> <td class="w-33">{{ d.description }}</td>
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td> <td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
@@ -315,27 +352,35 @@
</table> </table>
{% endif %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab"> <div class="tab-pane" id="nav-journal" role="tabpanel" aria-labelledby="nav-journal-tab">
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th colspan="3" scope="row">Journal</th> <th colspan="2" scope="row">Journal</th>
</tr> </tr>
</thead> </thead>
<tbody class="table-group-divider"> <tbody class="table-group-divider">
{% for j in rolling_stock_journal %} {% for j in journal %}
<tr> <tr>
<th class="w-33" scope="row">{{ j.date }}</th> <th class="w-33" scope="row">{{ j.date }}</th>
<td>{{ j.log | safe }}</a></td> <td>{{ j.log | safe }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab"> <div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
{{ rolling_stock.notes | safe }} {{ rolling_stock.notes | safe }}
</div> </div>
<div class="tab-pane" id="nav-consists" role="tabpanel" aria-labelledby="nav-cosists-tab">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3">
{% for d in consists %}
{% include "cards/consist.html" %}
{% endfor %}
</div>
</div>
</div> </div>
{% endwith %}
<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:roster_rollingstock_change' rolling_stock.pk %}">Edit</a>{% endif %} {% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' rolling_stock.pk %}">Edit</a>{% endif %}
</div> </div>

View File

@@ -1,45 +1,4 @@
{% extends "cards.html" %} {% 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">Scale</th>
</tr>
</thead>
<tbody>
<tr>
<th class="w-33" scope="row">Name</th>
<td>{{ d.scale }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Ratio</th>
<td>{{ d.ratio }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Gauge</th>
<td>{{ d.gauge }}</td>
</tr>
<tr>
<th class="w-33" scope="row">Tracks</th>
<td>{{ d.tracks }}</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="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' d.pk %}">Edit</a>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block pagination %} {% block pagination %}
{% if data.has_other_pages %} {% if data.has_other_pages %}
<nav aria-label="Page navigation example"> <nav aria-label="Page navigation example">

View File

@@ -1,44 +1,11 @@
{% extends "cards.html" %} {% 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 class="table-group-divider">
<tr>
<th class="w-33" scope="row">Type</th>
<td>{{ d.type }}</td>
</tr>
<tr>
<th class="w-33" 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 %} {% block pagination %}
{% if data.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 data.has_previous %} {% if data.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a> <a class="page-link" href="{% url 'types_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
@@ -54,13 +21,13 @@
{% if i == data.paginator.ELLIPSIS %} {% if i == data.paginator.ELLIPSIS %}
<li class="page-item"><span class="page-link">{{ i }}</span></li> <li class="page-item"><span class="page-link">{{ i }}</span></li>
{% else %} {% else %}
<li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#main-content">{{ i }}</a></li> <li class="page-item"><a class="page-link" href="{% url 'types_pagination' page=i %}#main-content">{{ i }}</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if data.has_next %} {% if data.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a> <a class="page-link" href="{% url 'types_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
</li> </li>
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">

View File

@@ -3,7 +3,7 @@ from django.urls import path
from portal.views import ( from portal.views import (
GetData, GetData,
GetRoster, GetRoster,
GetRosterFiltered, GetObjectsFiltered,
GetFlatpage, GetFlatpage,
GetRollingStock, GetRollingStock,
GetConsist, GetConsist,
@@ -14,11 +14,11 @@ from portal.views import (
Types, Types,
Books, Books,
GetBook, GetBook,
SearchRoster, SearchObjects,
) )
urlpatterns = [ urlpatterns = [
path("", GetData.as_view(), name="index"), path("", GetData.as_view(template="home.html"), name="index"),
path("roster", GetRoster.as_view(), name="roster"), path("roster", GetRoster.as_view(), name="roster"),
path("roster/<int:page>", GetRoster.as_view(), name="roster_pagination"), path("roster/<int:page>", GetRoster.as_view(), name="roster_pagination"),
path( path(
@@ -26,9 +26,15 @@ urlpatterns = [
GetFlatpage.as_view(), GetFlatpage.as_view(),
name="flatpage", name="flatpage",
), ),
path("consists", Consists.as_view(), name="consists"),
path( path(
"consists/<int:page>", Consists.as_view(), name="consists_pagination" "consists",
Consists.as_view(template="consists.html"),
name="consists"
),
path(
"consists/<int:page>",
Consists.as_view(template="consists.html"),
name="consists_pagination"
), ),
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"), path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
path( path(
@@ -36,47 +42,75 @@ urlpatterns = [
GetConsist.as_view(), GetConsist.as_view(),
name="consist_pagination", name="consist_pagination",
), ),
path("companies", Companies.as_view(), name="companies"), path(
"companies",
Companies.as_view(template="companies.html"),
name="companies"
),
path( path(
"companies/<int:page>", "companies/<int:page>",
Companies.as_view(), Companies.as_view(template="companies.html"),
name="companies_pagination", name="companies_pagination",
), ),
path( path(
"manufacturers/<str:category>", "manufacturers/<str:category>",
Manufacturers.as_view(), Manufacturers.as_view(template="manufacturers.html"),
name="manufacturers" name="manufacturers"
), ),
path( path(
"manufacturers/<str:category>/<int:page>", "manufacturers/<str:category>/<int:page>",
Manufacturers.as_view(), Manufacturers.as_view(template="manufacturers.html"),
name="manufacturers_pagination", name="manufacturers_pagination",
), ),
path("scales", Scales.as_view(), name="scales"), path(
path("scales/<int:page>", Types.as_view(), name="scales_pagination"), "scales",
path("types", Types.as_view(), name="types"), Scales.as_view(template="scales.html"),
path("types/<int:page>", Types.as_view(), name="types_pagination"), name="scales"
path("bookshelf/books", Books.as_view(), name="books"), ),
path("bookshelf/books/<int:page>", Books.as_view(), name="books_pagination"), path(
"scales/<int:page>",
Scales.as_view(template="scales.html"),
name="scales_pagination"
),
path(
"types",
Types.as_view(template="types.html"),
name="types"
),
path(
"types/<int:page>",
Types.as_view(template="types.html"),
name="types_pagination"
),
path(
"bookshelf/books",
Books.as_view(template="bookshelf/books.html"),
name="books"
),
path(
"bookshelf/books/<int:page>",
Books.as_view(template="bookshelf/books.html"),
name="books_pagination"
),
path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"), path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"),
path( path(
"search", "search",
SearchRoster.as_view(http_method_names=["post"]), SearchObjects.as_view(http_method_names=["post"]),
name="search", name="search",
), ),
path( path(
"search/<str:search>/<int:page>", "search/<str:search>/<int:page>",
SearchRoster.as_view(), SearchObjects.as_view(),
name="search_pagination", name="search_pagination",
), ),
path( path(
"<str:_filter>/<str:search>", "<str:_filter>/<str:search>",
GetRosterFiltered.as_view(), GetObjectsFiltered.as_view(),
name="filtered", name="filtered",
), ),
path( path(
"<str:_filter>/<str:search>/<int:page>", "<str:_filter>/<str:search>/<int:page>",
GetRosterFiltered.as_view(), GetObjectsFiltered.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

@@ -5,6 +5,7 @@ from urllib.parse import unquote
from django.views import View from django.views import View
from django.http import Http404, HttpResponseBadRequest from django.http import Http404, HttpResponseBadRequest
from django.db.utils import OperationalError, ProgrammingError
from django.db.models import Q from django.db.models import Q
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@@ -15,11 +16,17 @@ from portal.models import Flatpage
from roster.models import RollingStock from roster.models import RollingStock
from consist.models import Consist from consist.models import Consist
from bookshelf.models import Book from bookshelf.models import Book
from metadata.models import Company, Manufacturer, Scale, RollingStockType, Tag from metadata.models import (
Company, Manufacturer, Scale, DecoderDocument, RollingStockType, Tag
)
def order_by_fields(): def order_by_fields():
order_by = get_site_conf().items_ordering try:
order_by = get_site_conf().items_ordering
except (OperationalError, ProgrammingError):
order_by = "type"
fields = [ fields = [
"rolling_class__type", "rolling_class__type",
"rolling_class__company", "rolling_class__company",
@@ -36,15 +43,22 @@ def order_by_fields():
class GetData(View): class GetData(View):
def __init__(self): title = "Home"
self.title = "Home" template = "roster.html"
self.template = "home.html" item_type = "rolling_stock"
self.data = RollingStock.objects.order_by(*order_by_fields()) queryset = 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()
paginator = Paginator(self.data, site_conf.items_per_page) data = []
for item in self.queryset:
data.append({
"type": self.item_type,
"item": item
})
paginator = Paginator(data, site_conf.items_per_page)
data = paginator.get_page(page) data = paginator.get_page(page)
page_range = paginator.get_elided_page_range( page_range = paginator.get_elided_page_range(
data.number, on_each_side=2, on_ends=1 data.number, on_each_side=2, on_ends=1
@@ -55,6 +69,7 @@ class GetData(View):
self.template, self.template,
{ {
"title": self.title, "title": self.title,
"type": self.item_type,
"data": data, "data": data,
"matches": paginator.count, "matches": paginator.count,
"page_range": page_range, "page_range": page_range,
@@ -63,13 +78,12 @@ class GetData(View):
class GetRoster(GetData): class GetRoster(GetData):
def __init__(self): title = "Roster"
self.title = "Rolling stock" item_type = "rolling_stock"
self.template = "roster.html" queryset = RollingStock.objects.order_by(*order_by_fields())
self.data = RollingStock.objects.order_by(*order_by_fields())
class SearchRoster(View): class SearchObjects(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 is None: if _filter is None:
@@ -111,20 +125,50 @@ class SearchRoster(View):
else: else:
raise Http404 raise Http404
# FIXME duplicated code!
data = []
rolling_stock = ( rolling_stock = (
RollingStock.objects.filter(query) RollingStock.objects.filter(query)
.distinct() .distinct()
.order_by(*order_by_fields()) .order_by(*order_by_fields())
) )
matches = rolling_stock.count() for item in rolling_stock:
data.append({
"type": "rolling_stock",
"item": item
})
if _filter is None:
consists = (
Consist.objects.filter(
Q(
Q(identifier__icontains=search)
| Q(company__name__icontains=search)
)
)
.distinct()
)
for item in consists:
data.append({
"type": "consist",
"item": item
})
books = (
Book.objects.filter(title__icontains=search)
.distinct()
)
for item in books:
data.append({
"type": "book",
"item": item
})
paginator = Paginator(rolling_stock, site_conf.items_per_page) paginator = Paginator(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 rolling_stock, matches, page_range return data, paginator.count, page_range
def split_search(self, search): def split_search(self, search):
search = search.strip().split(":") search = search.strip().split(":")
@@ -149,7 +193,7 @@ class SearchRoster(View):
encoded_search = base64.b64encode( encoded_search = base64.b64encode(
search.encode()).decode() search.encode()).decode()
_filter, keyword = self.split_search(search) _filter, keyword = self.split_search(search)
rolling_stock, matches, page_range = self.run_search( data, matches, page_range = self.run_search(
request, keyword, _filter, page request, keyword, _filter, page
) )
@@ -160,8 +204,8 @@ class SearchRoster(View):
"title": "Search: \"{}\"".format(search), "title": "Search: \"{}\"".format(search),
"search": search, "search": search,
"encoded_search": encoded_search, "encoded_search": encoded_search,
"data": data,
"matches": matches, "matches": matches,
"data": rolling_stock,
"page_range": page_range, "page_range": page_range,
}, },
) )
@@ -171,15 +215,17 @@ class SearchRoster(View):
return self.get(request, search, page) return self.get(request, search, page)
class GetRosterFiltered(View): class GetObjectsFiltered(View):
def run_filter(self, request, search, _filter, page=1): def run_filter(self, request, search, _filter, page=1):
site_conf = get_site_conf() site_conf = get_site_conf()
if _filter == "type": if _filter == "type":
title = RollingStockType.objects.get(slug__iexact=search) title = get_object_or_404(RollingStockType, slug__iexact=search)
query = Q(rolling_class__type__slug__iexact=search) query = Q(rolling_class__type__slug__iexact=search)
elif _filter == "company": elif _filter == "company":
title = get_object_or_404(Company, slug__iexact=search) title = get_object_or_404(Company, slug__iexact=search)
query = Q(rolling_class__company__slug__iexact=search) query = Q(rolling_class__company__slug__iexact=search)
query_2nd = Q(company__slug__iexact=search)
elif _filter == "manufacturer": elif _filter == "manufacturer":
title = get_object_or_404(Manufacturer, slug__iexact=search) title = get_object_or_404(Manufacturer, slug__iexact=search)
query = Q( query = Q(
@@ -189,9 +235,13 @@ class GetRosterFiltered(View):
elif _filter == "scale": elif _filter == "scale":
title = get_object_or_404(Scale, slug__iexact=search) title = get_object_or_404(Scale, slug__iexact=search)
query = Q(scale__slug__iexact=search) query = Q(scale__slug__iexact=search)
query_2nd = Q(
consist_item__rolling_stock__scale__slug__iexact=search
)
elif _filter == "tag": elif _filter == "tag":
title = get_object_or_404(Tag, slug__iexact=search) title = get_object_or_404(Tag, slug__iexact=search)
query = Q(tags__slug__iexact=search) query = Q(tags__slug__iexact=search)
query_2nd = query # For tags the 2nd level query doesn't change
else: else:
raise Http404 raise Http404
@@ -200,15 +250,44 @@ class GetRosterFiltered(View):
.distinct() .distinct()
.order_by(*order_by_fields()) .order_by(*order_by_fields())
) )
matches = rolling_stock.count()
paginator = Paginator(rolling_stock, site_conf.items_per_page) data = []
rolling_stock = paginator.get_page(page) for item in rolling_stock:
data.append({
"type": "rolling_stock",
"item": item
})
try: # Execute only if query_2nd is defined
consists = (
Consist.objects.filter(query_2nd)
.distinct()
)
for item in consists:
data.append({
"type": "consist",
"item": item
})
if _filter == "tag": # Books can be filtered only by tag
books = (
Book.objects.filter(query_2nd)
.distinct()
)
for item in books:
data.append({
"type": "book",
"item": item
})
except NameError:
pass
paginator = Paginator(data, site_conf.items_per_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 rolling_stock, title, matches, page_range return data, title, paginator.count, page_range
def get(self, request, search, _filter, page=1): def get(self, request, search, _filter, page=1):
data, title, matches, page_range = self.run_filter( data, title, matches, page_range = self.run_filter(
@@ -223,8 +302,8 @@ class GetRosterFiltered(View):
_filter.capitalize(), title), _filter.capitalize(), title),
"search": search, "search": search,
"filter": _filter, "filter": _filter,
"matches": matches,
"data": data, "data": data,
"matches": matches,
"page_range": page_range, "page_range": page_range,
}, },
) )
@@ -237,24 +316,36 @@ class GetRollingStock(View):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404
class_properties = ( # FIXME there's likely a better and more efficient way of doing this
rolling_stock.rolling_class.property.all() # but keeping KISS for now
if request.user.is_authenticated decoder_documents = []
else rolling_stock.rolling_class.property.filter( if request.user.is_authenticated:
class_properties = rolling_stock.rolling_class.property.all()
properties = rolling_stock.property.all()
documents = rolling_stock.document.all()
journal = rolling_stock.journal.all()
if rolling_stock.decoder:
decoder_documents = rolling_stock.decoder.document.all()
else:
class_properties = rolling_stock.rolling_class.property.filter(
property__private=False property__private=False
) )
) properties = rolling_stock.property.filter(
rolling_stock_properties = ( property__private=False
rolling_stock.property.all() )
if request.user.is_authenticated documents = rolling_stock.document.filter(private=False)
else rolling_stock.property.filter(property__private=False) journal = rolling_stock.journal.filter(private=False)
) if rolling_stock.decoder:
decoder_documents = rolling_stock.decoder.document.filter(
private=False
)
rolling_stock_journal = ( consists = [{
rolling_stock.journal.all() "type": "consist",
if request.user.is_authenticated "item": c
else rolling_stock.journal.filter(private=False) } for c in Consist.objects.filter(
) consist_item__rolling_stock=rolling_stock
)] # A dict with "item" is required by the consists card
return render( return render(
request, request,
@@ -263,17 +354,19 @@ class GetRollingStock(View):
"title": rolling_stock, "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, "properties": properties,
"rolling_stock_journal": rolling_stock_journal, "decoder_documents": decoder_documents,
"documents": documents,
"journal": journal,
"consists": consists,
}, },
) )
class Consists(GetData): class Consists(GetData):
def __init__(self): title = "Consists"
self.title = "Consists" item_type = "consist"
self.template = "consists.html" queryset = Consist.objects.all()
self.data = Consist.objects.all()
class GetConsist(View): class GetConsist(View):
@@ -283,15 +376,15 @@ 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 = [ data = [{
RollingStock.objects.get(uuid=r.rolling_stock_id) for r in "type": "rolling_stock",
consist.consist_item.all() "item": 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(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(
@@ -300,52 +393,47 @@ class GetConsist(View):
{ {
"title": consist, "title": consist,
"consist": consist, "consist": consist,
"data": rolling_stock, "data": data,
"page_range": page_range, "page_range": page_range,
}, },
) )
class Manufacturers(GetData): class Manufacturers(GetData):
def __init__(self): title = "Manufacturers"
self.title = "Manufacturers" item_type = "manufacturer"
self.template = "manufacturers.html" queryset = None # Set via method get
self.data = None # Set via method get
# overload get method to filter by category # overload get method to filter by category
def get(self, request, category, page=1): def get(self, request, category, page=1):
if category not in ("real", "model"): if category not in ("real", "model"):
raise Http404 raise Http404
self.data = Manufacturer.objects.filter(category=category) self.queryset = Manufacturer.objects.filter(category=category)
return super().get(request, page) return super().get(request, page)
class Companies(GetData): class Companies(GetData):
def __init__(self): title = "Companies"
self.title = "Companies" item_type = "company"
self.template = "companies.html" queryset = Company.objects.all()
self.data = Company.objects.all()
class Scales(GetData): class Scales(GetData):
def __init__(self): title = "Scales"
self.title = "Scales" item_type = "scale"
self.template = "scales.html" queryset = Scale.objects.all()
self.data = Scale.objects.all()
class Types(GetData): class Types(GetData):
def __init__(self): title = "Types"
self.title = "Types" item_type = "rolling_stock_type"
self.template = "types.html" queryset = RollingStockType.objects.all()
self.data = RollingStockType.objects.all()
class Books(GetData): class Books(GetData):
def __init__(self): title = "Books"
self.title = "Books" item_type = "book"
self.template = "bookshelf/books.html" queryset = Book.objects.all()
self.data = Book.objects.all()
class GetBook(View): class GetBook(View):
@@ -373,10 +461,12 @@ class GetBook(View):
class GetFlatpage(View): class GetFlatpage(View):
def get(self, request, flatpage): def get(self, request, flatpage):
_filter = Q(published=True) # Show only published pages
if request.user.is_authenticated:
_filter = Q() # Reset the filter if user is authenticated
try: try:
flatpage = Flatpage.objects.get( flatpage = Flatpage.objects.filter(_filter).get(path=flatpage)
Q(Q(path=flatpage) & Q(published=True))
)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404 raise Http404

View File

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

View File

@@ -1,10 +1,10 @@
from django.contrib import admin from django.contrib import admin
from django.db.utils import OperationalError from django.db.utils import OperationalError, ProgrammingError
from portal.utils import get_site_conf from portal.utils import get_site_conf
try: try:
site_name = get_site_conf().site_name site_name = get_site_conf().site_name
except OperationalError: except (OperationalError, ProgrammingError):
site_name = "Train Assets Manager" site_name = "Train Assets Manager"
admin.site.site_header = site_name admin.site.site_header = site_name

14
ram/ram/apps.py Normal file
View File

@@ -0,0 +1,14 @@
from django.conf import settings
from django.apps import AppConfig
class RamConfig(AppConfig):
name = "ram"
def ready(self):
cache_middleware = set([
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
])
if cache_middleware.issubset(settings.MIDDLEWARE):
from ram.signals import clear_cache # noqa: F401

View File

@@ -1,7 +1,8 @@
# vim: syntax=python # vim: syntax=python
from django.conf import settings
""" """
Django local_settings for ram project. Example of changes suitable for production
""" """
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
@@ -12,9 +13,23 @@ SECRET_KEY = (
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False
# SECURITY WARNING: cache middlewares must be loaded before cookies one
MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.cache.FetchFromCacheMiddleware",
] + settings.MIDDLEWARE
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
CACHE_MIDDLEWARE_SECONDS = 300
STORAGE_DIR = BASE_DIR / "storage" STORAGE_DIR = BASE_DIR / "storage"
ALLOWED_HOSTS = ["127.0.0.1"] ALLOWED_HOSTS = ["127.0.0.1", "myhost"]
CSRF_TRUSTED_ORIGINS = ["https://myhost"] CSRF_TRUSTED_ORIGINS = ["https://myhost"]
ROOT_URLCONF = "ram.urls"
STATIC_URL = "static/" STATIC_URL = "static/"
MEDIA_URL = "media/" MEDIA_URL = "media/"

View File

@@ -11,9 +11,8 @@ class Document(models.Model):
file = models.FileField( file = models.FileField(
upload_to="files/", upload_to="files/",
storage=DeduplicatedStorage(), storage=DeduplicatedStorage(),
null=True,
blank=True,
) )
private = models.BooleanField(default=False)
class Meta: class Meta:
abstract = True abstract = True
@@ -33,7 +32,8 @@ class Document(models.Model):
class Image(models.Model): class Image(models.Model):
order = models.PositiveIntegerField(default=0, blank=False, null=False) order = models.PositiveIntegerField(default=0, blank=False, null=False)
image = models.ImageField( image = models.ImageField(
upload_to="images/", storage=DeduplicatedStorage, null=True, blank=True upload_to="images/",
storage=DeduplicatedStorage,
) )
def image_thumbnail(self): def image_thumbnail(self):

View File

@@ -41,8 +41,6 @@ INSTALLED_APPS = [
"django.contrib.sessions", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"health_check",
"health_check.db",
"adminsortable2", "adminsortable2",
"django_countries", "django_countries",
"solo", "solo",
@@ -51,7 +49,7 @@ INSTALLED_APPS = [
"rest_framework", "rest_framework",
"ram", "ram",
"portal", "portal",
"driver", # "driver", # uncomment this to enable the "driver" API
"metadata", "metadata",
"roster", "roster",
"consist", "consist",
@@ -62,7 +60,7 @@ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
# 'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
@@ -147,7 +145,8 @@ MEDIA_ROOT = STORAGE_DIR / "media"
CKEDITOR_UPLOAD_PATH = "uploads/" CKEDITOR_UPLOAD_PATH = "uploads/"
COUNTRIES_OVERRIDE = { COUNTRIES_OVERRIDE = {
"ZZ": "Freelance", "EU": "Europe",
"XX": "None",
} }
# Image used on cards without a custom image uploaded. # Image used on cards without a custom image uploaded.

8
ram/ram/signals.py Normal file
View File

@@ -0,0 +1,8 @@
from django.core.cache import cache
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save)
def clear_cache(sender, **kwargs):
cache.clear()

View File

@@ -13,6 +13,7 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.apps import apps
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf.urls.static import static from django.conf.urls.static import static
@@ -23,14 +24,18 @@ urlpatterns = [
path("", lambda r: redirect("portal/")), path("", lambda r: redirect("portal/")),
path("ckeditor/", include("ckeditor_uploader.urls")), path("ckeditor/", include("ckeditor_uploader.urls")),
path("portal/", include("portal.urls")), path("portal/", include("portal.urls")),
path("ht/", include("health_check.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("api/v1/consist/", include("consist.urls")), path("api/v1/consist/", include("consist.urls")),
path("api/v1/roster/", include("roster.urls")), path("api/v1/roster/", include("roster.urls")),
path("api/v1/dcc/", include("driver.urls")),
path("api/v1/bookshelf/", include("bookshelf.urls")), path("api/v1/bookshelf/", include("bookshelf.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Enable the "/dcc" routing only if the "driver" app is active
if apps.is_installed("driver"):
urlpatterns += [
path("api/v1/dcc/", include("driver.urls")),
]
if settings.DEBUG: if settings.DEBUG:
from django.views.generic import TemplateView from django.views.generic import TemplateView
from rest_framework.schemas import get_schema_view from rest_framework.schemas import get_schema_view
@@ -54,3 +59,7 @@ if settings.DEBUG:
name="openapi-schema", name="openapi-schema",
), ),
] ]
if apps.is_installed("debug_toolbar"):
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls")),
]

View File

@@ -28,6 +28,7 @@ class RollingClass(admin.ModelAdmin):
"company__name", "company__name",
"type__type", "type__type",
) )
save_as = True
class RollingStockDocInline(admin.TabularInline): class RollingStockDocInline(admin.TabularInline):
@@ -64,6 +65,7 @@ class RollingStockDocumentAdmin(admin.ModelAdmin):
"__str__", "__str__",
"rolling_stock", "rolling_stock",
"description", "description",
"private",
"download", "download",
) )
search_fields = ( search_fields = (

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.5 on 2023-10-06 19:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("roster", "0018_rename_sku_rollingstock_item_number"),
]
operations = [
migrations.AddField(
model_name="rollingstockdocument",
name="private",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,49 @@
# Generated by Django 4.2.6 on 2023-10-30 13:16
import os
import sys
import shutil
import ram.utils
import roster.models
from django.db import migrations, models
from django.conf import settings
def move_images(apps, schema_editor):
sys.stdout.write("\n Processing files. Please await...")
for r in roster.models.RollingStockImage.objects.all():
fname = os.path.basename(r.image.path)
new_image = roster.models.rolling_stock_image_upload(r, fname)
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
os.makedirs(os.path.dirname(new_path), exist_ok=True)
try:
shutil.move(r.image.path, new_path)
except FileNotFoundError:
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
pass
r.image.name = new_image
r.save()
class Migration(migrations.Migration):
dependencies = [
("roster", "0019_rollingstockdocument_private"),
]
operations = [
migrations.AlterField(
model_name="rollingstockimage",
name="image",
field=models.ImageField(
blank=True,
null=True,
storage=ram.utils.DeduplicatedStorage,
upload_to=roster.models.rolling_stock_image_upload,
),
),
migrations.RunPython(
move_images,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 4.2.6 on 2023-11-04 22:53
from django.db import migrations, models
import ram.utils
import roster.models
class Migration(migrations.Migration):
dependencies = [
("roster", "0020_alter_rollingstockimage_image"),
]
operations = [
migrations.AlterField(
model_name="rollingstockdocument",
name="file",
field=models.FileField(
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
),
),
migrations.AlterField(
model_name="rollingstockimage",
name="image",
field=models.ImageField(
storage=ram.utils.DeduplicatedStorage,
upload_to=roster.models.rolling_stock_image_upload,
),
),
]

View File

@@ -1,4 +1,6 @@
import os
import re import re
import shutil
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
@@ -8,7 +10,7 @@ from django.dispatch import receiver
from ckeditor_uploader.fields import RichTextUploadingField from ckeditor_uploader.fields import RichTextUploadingField
from ram.models import Document, Image, PropertyInstance from ram.models import Document, Image, PropertyInstance
from ram.utils import get_image_preview from ram.utils import DeduplicatedStorage
from metadata.models import ( from metadata.models import (
Scale, Scale,
Manufacturer, Manufacturer,
@@ -106,6 +108,15 @@ class RollingStock(models.Model):
def company(self): def company(self):
return str(self.rolling_class.company) return str(self.rolling_class.company)
def delete(self, *args, **kwargs):
shutil.rmtree(
os.path.join(
settings.MEDIA_ROOT, "images", "rollingstock", str(self.uuid)
),
ignore_errors=True
)
super(RollingStock, self).delete(*args, **kwargs)
@receiver(models.signals.pre_save, sender=RollingStock) @receiver(models.signals.pre_save, sender=RollingStock)
def pre_save_running_number(sender, instance, *args, **kwargs): def pre_save_running_number(sender, instance, *args, **kwargs):
@@ -126,10 +137,23 @@ class RollingStockDocument(Document):
unique_together = ("rolling_stock", "file") unique_together = ("rolling_stock", "file")
def rolling_stock_image_upload(instance, filename):
return os.path.join(
"images",
"rollingstock",
str(instance.rolling_stock.uuid),
filename
)
class RollingStockImage(Image): class RollingStockImage(Image):
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=rolling_stock_image_upload,
storage=DeduplicatedStorage,
)
class RollingStockProperty(PropertyInstance): class RollingStockProperty(PropertyInstance):

View File

@@ -1 +1,2 @@
gunicorn gunicorn
# Optional: # pylibmc

View File

@@ -8,5 +8,8 @@ django-countries
django-health-check django-health-check
django-admin-sortable2 django-admin-sortable2
django-ckeditor django-ckeditor
# psycopg2-binary # Optional: # psycopg2-binary
pySerial # Optional: # pySerial
# Required by django-countries and not always installed
# by default on modern venvs (like Python 3.12 on Fedora 39)
setuptools