7 Commits

Author SHA1 Message Date
8e0a18d707 Update theme icon based on the selected one 2023-09-18 21:39:15 +02:00
46a2aa7011 Bump version 2023-09-18 20:23:03 +02:00
a176682615 Improve header html code 2023-09-18 20:21:41 +02:00
ad4591da04 Bootstrap 5.3 (#21)
* Migarte to Bootstrap 5.3
* Cleanup and version bump
2023-09-18 14:24:27 +02:00
2f2b96b2bb Add requirements-prod.txt 2023-05-07 14:52:49 +02:00
6cf3ad03cc Net-to-serial broadcast messages to all clients and other cleanups (#20)
* Cleanup and maintenance

* Net-to-serial broadcast messages to all clients

This will make all clients to stay in sync with any operation
occurring, like when having multiple JMRI instances

* Update README and python version in containers
2023-03-06 18:25:34 +01:00
2c5f0dcd6f Use slugs to filter 2023-01-09 22:54:15 +01:00
28 changed files with 493 additions and 198 deletions

View File

@@ -96,7 +96,8 @@ Browse to `http://localhost:8000`
The DCC++ EX connector exposes an Arduino board running DCC++ EX Command Station,
connected via serial port, to the network, allowing commands to be sent via a
TCP socket.
TCP socket. A response generated by the DCC++ EX board is sent to all connected clients,
providing synchronization between multiple clients (eg. multiple JMRI instances).
Its use is not needed when running DCC++ EX from a [WiFi](https://dcc-ex.com/get-started/wifi-setup.html) capable board (like when
using an ESP8266 module or a [Mega+WiFi board](https://dcc-ex.com/advanced-setup/supported-microcontrollers/wifi-mega.html)).
@@ -112,7 +113,7 @@ Settings may need to be customized based on your setup.
```bash
$ cd daemons
$ podman build -t dcc/net-to-serial .
$ podman run -d -p 2560:2560 dcc/net-to-serial
$ podman run --group-add keep-groups --device /dev/ttyACM0 -p 2560:2560 dcc/net-to-serial
```
### Manual setup

View File

@@ -1,4 +1,4 @@
FROM python:3.10-alpine
FROM python:3.11-alpine
RUN mkdir /opt/dcc && pip -q install pyserial
ADD net-to-serial.py config.ini /opt/dcc

3
daemons/README.md Normal file
View File

@@ -0,0 +1,3 @@
## DCC++ EX connector
See [README.md](../README.md)

View File

@@ -2,6 +2,7 @@
LogLevel = debug
ListeningIP = 0.0.0.0
ListeningPort = 2560
MaxClients = 10
[Serial]
# UNO

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import re
import time
import logging
import serial
import asyncio
@@ -10,11 +9,15 @@ from pathlib import Path
class SerialDaemon:
connected_clients = set()
def __init__(self, config):
self.ser = serial.Serial(
config["Serial"]["Port"],
timeout=int(config["Serial"]["Timeout"])/1000)
timeout=int(config["Serial"]["Timeout"]) / 1000,
)
self.ser.baudrate = config["Serial"]["Baudrate"]
self.max_clients = int(config["Daemon"]["MaxClients"])
def __del__(self):
try:
@@ -43,19 +46,32 @@ class SerialDaemon:
async def handle_echo(self, reader, writer):
"""Process a request from socket and return the response"""
while 1: # keep connection to client open
data = await reader.read(100)
if not data: # client has disconnected
break
logging.info(
"Clients already connected: {} (max: {})".format(
len(self.connected_clients),
self.max_clients,
)
)
addr = writer.get_extra_info('peername')
logging.info("Received {} from {}".format(data, addr[0]))
self.__write_serial(data)
response = self.__read_serial()
writer.write(response)
await writer.drain()
logging.info("Sent: {}".format(response))
addr = writer.get_extra_info("peername")[0]
if len(self.connected_clients) < self.max_clients:
self.connected_clients.add(writer)
while True: # keep connection to client open
data = await reader.read(100)
if not data: # client has disconnected
break
logging.info("Received {} from {}".format(data, addr))
self.__write_serial(data)
response = self.__read_serial()
for client in self.connected_clients:
client.write(response)
await client.drain()
logging.info("Sent: {}".format(response))
self.connected_clients.remove(writer)
else:
logging.warning(
"TooManyClients: client {} disconnected".format(addr)
)
writer.close()
await writer.wait_closed()
@@ -68,33 +84,37 @@ class SerialDaemon:
while "DCC-EX" not in line:
line = self.__read_serial().decode()
board = re.findall(r"<iDCC-EX.*>", line)[0]
return(board)
return board
async def main():
config = configparser.ConfigParser()
config.read(
Path(__file__).resolve().parent / "config.ini") # mimick os.path.join
Path(__file__).resolve().parent / "config.ini"
) # mimick os.path.join
logging.basicConfig(level=config["Daemon"]["LogLevel"].upper())
sd = SerialDaemon(config)
server = await asyncio.start_server(
sd.handle_echo,
config["Daemon"]["ListeningIP"],
config["Daemon"]["ListeningPort"])
config["Daemon"]["ListeningPort"],
)
addr = server.sockets[0].getsockname()
logging.warning("Serving on {} port {}".format(addr[0], addr[1]))
logging.warning(
logging.info("Serving on {} port {}".format(addr[0], addr[1]))
logging.info(
"Proxying to {} (Baudrate: {}, Timeout: {})".format(
config["Serial"]["Port"],
config["Serial"]["Baudrate"],
config["Serial"]["Timeout"]))
logging.warning("Initializing board")
logging.warning("Board {} ready".format(
await sd.return_board()))
config["Serial"]["Timeout"],
)
)
logging.info("Initializing board")
logging.info("Board {} ready".format(await sd.return_board()))
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())

Binary file not shown.

View File

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

View File

@@ -1,6 +1,7 @@
from urllib.parse import quote
from django.db import models
from django.urls import reverse
from django.conf import settings
from django.dispatch.dispatcher import receiver
from django_countries.fields import CountryField
@@ -22,6 +23,7 @@ class Property(models.Model):
class Manufacturer(models.Model):
name = models.CharField(max_length=128, unique=True)
slug = models.CharField(max_length=128, unique=True, editable=False)
category = models.CharField(
max_length=64, choices=settings.MANUFACTURER_TYPES
)
@@ -36,8 +38,13 @@ class Manufacturer(models.Model):
def __str__(self):
return self.name
def safe_name(self):
return quote(self.__str__().lower(), safe="& ")
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "manufacturer",
"search": self.slug,
}
)
def logo_thumbnail(self):
return get_image_preview(self.logo.url)
@@ -47,6 +54,7 @@ class Manufacturer(models.Model):
class Company(models.Model):
name = models.CharField(max_length=64, unique=True)
slug = models.CharField(max_length=64, unique=True, editable=False)
extended_name = models.CharField(max_length=128, blank=True)
country = CountryField()
freelance = models.BooleanField(default=False)
@@ -61,8 +69,13 @@ class Company(models.Model):
def __str__(self):
return self.name
def safe_name(self):
return quote(self.__str__().lower(), safe="& ")
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "company",
"search": self.slug,
}
)
def logo_thumbnail(self):
return get_image_preview(self.logo.url)
@@ -94,6 +107,7 @@ class Decoder(models.Model):
class Scale(models.Model):
scale = models.CharField(max_length=32, unique=True)
slug = models.CharField(max_length=32, unique=True, editable=False)
ratio = models.CharField(max_length=16, blank=True)
gauge = models.CharField(max_length=16, blank=True)
tracks = models.CharField(max_length=16, blank=True)
@@ -101,11 +115,40 @@ class Scale(models.Model):
class Meta:
ordering = ["scale"]
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "scale",
"search": self.slug,
}
)
def __str__(self):
return str(self.scale)
def safe_name(self):
return quote(self.__str__(), safe="& ")
class RollingStockType(models.Model):
type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField()
category = models.CharField(
max_length=64, choices=settings.ROLLING_STOCK_TYPES
)
slug = models.CharField(max_length=128, unique=True, editable=False)
class Meta(object):
unique_together = ("category", "type")
ordering = ["order"]
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "type",
"search": self.slug,
}
)
def __str__(self):
return "{0} {1}".format(self.type, self.category)
class Tag(models.Model):
@@ -115,28 +158,20 @@ class Tag(models.Model):
def __str__(self):
return self.name
def safe_name(self):
return self.slug
def get_absolute_url(self):
return reverse(
"filtered", kwargs={
"_filter": "tag",
"search": self.slug,
}
)
@receiver(models.signals.pre_save, sender=Manufacturer)
@receiver(models.signals.pre_save, sender=Company)
@receiver(models.signals.pre_save, sender=Scale)
@receiver(models.signals.pre_save, sender=RollingStockType)
@receiver(models.signals.pre_save, sender=Tag)
def slug_pre_save(sender, instance, **kwargs):
instance.slug = slugify(instance.name)
class RollingStockType(models.Model):
type = models.CharField(max_length=64)
order = models.PositiveSmallIntegerField()
category = models.CharField(
max_length=64, choices=settings.ROLLING_STOCK_TYPES
)
class Meta(object):
unique_together = ("category", "type")
ordering = ["order"]
def __str__(self):
return "{0} {1}".format(self.type, self.category)
def safe_name(self):
return quote(self.__str__().lower(), safe="& ")
instance.slug = slugify(instance.__str__())

View File

@@ -1,8 +1,14 @@
/*!
* Bootstrap Icons v1.11.1 (https://icons.getbootstrap.com/)
* Copyright 2019-2023 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
*/
@font-face {
font-display: block;
font-family: "bootstrap-icons";
src: url("./fonts/bootstrap-icons.woff2?24e3eb84d0bcaf83d77f904c78ac1f47") format("woff2"),
url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("woff");
src: url("./fonts/bootstrap-icons.woff2?2820a3852bdb9a5832199cc61cec4e65") format("woff2"),
url("./fonts/bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff");
}
.bi::before,
@@ -441,7 +447,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-cloud-fog2::before { content: "\f2a2"; }
.bi-cloud-hail-fill::before { content: "\f2a3"; }
.bi-cloud-hail::before { content: "\f2a4"; }
.bi-cloud-haze-1::before { content: "\f2a5"; }
.bi-cloud-haze-fill::before { content: "\f2a6"; }
.bi-cloud-haze::before { content: "\f2a7"; }
.bi-cloud-haze2-fill::before { content: "\f2a8"; }
@@ -1437,21 +1442,16 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-dpad::before { content: "\f687"; }
.bi-ear-fill::before { content: "\f688"; }
.bi-ear::before { content: "\f689"; }
.bi-envelope-check-1::before { content: "\f68a"; }
.bi-envelope-check-fill::before { content: "\f68b"; }
.bi-envelope-check::before { content: "\f68c"; }
.bi-envelope-dash-1::before { content: "\f68d"; }
.bi-envelope-dash-fill::before { content: "\f68e"; }
.bi-envelope-dash::before { content: "\f68f"; }
.bi-envelope-exclamation-1::before { content: "\f690"; }
.bi-envelope-exclamation-fill::before { content: "\f691"; }
.bi-envelope-exclamation::before { content: "\f692"; }
.bi-envelope-plus-fill::before { content: "\f693"; }
.bi-envelope-plus::before { content: "\f694"; }
.bi-envelope-slash-1::before { content: "\f695"; }
.bi-envelope-slash-fill::before { content: "\f696"; }
.bi-envelope-slash::before { content: "\f697"; }
.bi-envelope-x-1::before { content: "\f698"; }
.bi-envelope-x-fill::before { content: "\f699"; }
.bi-envelope-x::before { content: "\f69a"; }
.bi-explicit-fill::before { content: "\f69b"; }
@@ -1461,8 +1461,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-list-columns-reverse::before { content: "\f69f"; }
.bi-list-columns::before { content: "\f6a0"; }
.bi-meta::before { content: "\f6a1"; }
.bi-mortorboard-fill::before { content: "\f6a2"; }
.bi-mortorboard::before { content: "\f6a3"; }
.bi-nintendo-switch::before { content: "\f6a4"; }
.bi-pc-display-horizontal::before { content: "\f6a5"; }
.bi-pc-display::before { content: "\f6a6"; }
@@ -1481,7 +1479,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-send-check::before { content: "\f6b3"; }
.bi-send-dash-fill::before { content: "\f6b4"; }
.bi-send-dash::before { content: "\f6b5"; }
.bi-send-exclamation-1::before { content: "\f6b6"; }
.bi-send-exclamation-fill::before { content: "\f6b7"; }
.bi-send-exclamation::before { content: "\f6b8"; }
.bi-send-fill::before { content: "\f6b9"; }
@@ -1493,7 +1490,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-send-x::before { content: "\f6bf"; }
.bi-send::before { content: "\f6c0"; }
.bi-steam::before { content: "\f6c1"; }
.bi-terminal-dash-1::before { content: "\f6c2"; }
.bi-terminal-dash::before { content: "\f6c3"; }
.bi-terminal-plus::before { content: "\f6c4"; }
.bi-terminal-split::before { content: "\f6c5"; }
@@ -1523,7 +1519,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-usb-symbol::before { content: "\f6dd"; }
.bi-usb::before { content: "\f6de"; }
.bi-boombox-fill::before { content: "\f6df"; }
.bi-displayport-1::before { content: "\f6e0"; }
.bi-displayport::before { content: "\f6e1"; }
.bi-gpu-card::before { content: "\f6e2"; }
.bi-memory::before { content: "\f6e3"; }
@@ -1536,8 +1531,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-pci-card::before { content: "\f6ea"; }
.bi-router-fill::before { content: "\f6eb"; }
.bi-router::before { content: "\f6ec"; }
.bi-ssd-fill::before { content: "\f6ed"; }
.bi-ssd::before { content: "\f6ee"; }
.bi-thunderbolt-fill::before { content: "\f6ef"; }
.bi-thunderbolt::before { content: "\f6f0"; }
.bi-usb-drive-fill::before { content: "\f6f1"; }
@@ -1644,7 +1637,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-filetype-pdf::before { content: "\f756"; }
.bi-filetype-php::before { content: "\f757"; }
.bi-filetype-png::before { content: "\f758"; }
.bi-filetype-ppt-1::before { content: "\f759"; }
.bi-filetype-ppt::before { content: "\f75a"; }
.bi-filetype-psd::before { content: "\f75b"; }
.bi-filetype-py::before { content: "\f75c"; }
@@ -1660,7 +1652,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-filetype-txt::before { content: "\f766"; }
.bi-filetype-wav::before { content: "\f767"; }
.bi-filetype-woff::before { content: "\f768"; }
.bi-filetype-xls-1::before { content: "\f769"; }
.bi-filetype-xls::before { content: "\f76a"; }
.bi-filetype-xml::before { content: "\f76b"; }
.bi-filetype-yml::before { content: "\f76c"; }
@@ -1703,56 +1694,38 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-filetype-json::before { content: "\f791"; }
.bi-filetype-pptx::before { content: "\f792"; }
.bi-filetype-xlsx::before { content: "\f793"; }
.bi-1-circle-1::before { content: "\f794"; }
.bi-1-circle-fill-1::before { content: "\f795"; }
.bi-1-circle-fill::before { content: "\f796"; }
.bi-1-circle::before { content: "\f797"; }
.bi-1-square-fill::before { content: "\f798"; }
.bi-1-square::before { content: "\f799"; }
.bi-2-circle-1::before { content: "\f79a"; }
.bi-2-circle-fill-1::before { content: "\f79b"; }
.bi-2-circle-fill::before { content: "\f79c"; }
.bi-2-circle::before { content: "\f79d"; }
.bi-2-square-fill::before { content: "\f79e"; }
.bi-2-square::before { content: "\f79f"; }
.bi-3-circle-1::before { content: "\f7a0"; }
.bi-3-circle-fill-1::before { content: "\f7a1"; }
.bi-3-circle-fill::before { content: "\f7a2"; }
.bi-3-circle::before { content: "\f7a3"; }
.bi-3-square-fill::before { content: "\f7a4"; }
.bi-3-square::before { content: "\f7a5"; }
.bi-4-circle-1::before { content: "\f7a6"; }
.bi-4-circle-fill-1::before { content: "\f7a7"; }
.bi-4-circle-fill::before { content: "\f7a8"; }
.bi-4-circle::before { content: "\f7a9"; }
.bi-4-square-fill::before { content: "\f7aa"; }
.bi-4-square::before { content: "\f7ab"; }
.bi-5-circle-1::before { content: "\f7ac"; }
.bi-5-circle-fill-1::before { content: "\f7ad"; }
.bi-5-circle-fill::before { content: "\f7ae"; }
.bi-5-circle::before { content: "\f7af"; }
.bi-5-square-fill::before { content: "\f7b0"; }
.bi-5-square::before { content: "\f7b1"; }
.bi-6-circle-1::before { content: "\f7b2"; }
.bi-6-circle-fill-1::before { content: "\f7b3"; }
.bi-6-circle-fill::before { content: "\f7b4"; }
.bi-6-circle::before { content: "\f7b5"; }
.bi-6-square-fill::before { content: "\f7b6"; }
.bi-6-square::before { content: "\f7b7"; }
.bi-7-circle-1::before { content: "\f7b8"; }
.bi-7-circle-fill-1::before { content: "\f7b9"; }
.bi-7-circle-fill::before { content: "\f7ba"; }
.bi-7-circle::before { content: "\f7bb"; }
.bi-7-square-fill::before { content: "\f7bc"; }
.bi-7-square::before { content: "\f7bd"; }
.bi-8-circle-1::before { content: "\f7be"; }
.bi-8-circle-fill-1::before { content: "\f7bf"; }
.bi-8-circle-fill::before { content: "\f7c0"; }
.bi-8-circle::before { content: "\f7c1"; }
.bi-8-square-fill::before { content: "\f7c2"; }
.bi-8-square::before { content: "\f7c3"; }
.bi-9-circle-1::before { content: "\f7c4"; }
.bi-9-circle-fill-1::before { content: "\f7c5"; }
.bi-9-circle-fill::before { content: "\f7c6"; }
.bi-9-circle::before { content: "\f7c7"; }
.bi-9-square-fill::before { content: "\f7c8"; }
@@ -1771,8 +1744,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-browser-edge::before { content: "\f7d5"; }
.bi-browser-firefox::before { content: "\f7d6"; }
.bi-browser-safari::before { content: "\f7d7"; }
.bi-c-circle-1::before { content: "\f7d8"; }
.bi-c-circle-fill-1::before { content: "\f7d9"; }
.bi-c-circle-fill::before { content: "\f7da"; }
.bi-c-circle::before { content: "\f7db"; }
.bi-c-square-fill::before { content: "\f7dc"; }
@@ -1783,8 +1754,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-car-front::before { content: "\f7e1"; }
.bi-cassette-fill::before { content: "\f7e2"; }
.bi-cassette::before { content: "\f7e3"; }
.bi-cc-circle-1::before { content: "\f7e4"; }
.bi-cc-circle-fill-1::before { content: "\f7e5"; }
.bi-cc-circle-fill::before { content: "\f7e6"; }
.bi-cc-circle::before { content: "\f7e7"; }
.bi-cc-square-fill::before { content: "\f7e8"; }
@@ -1803,8 +1772,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-filetype-sql::before { content: "\f7f5"; }
.bi-fire::before { content: "\f7f6"; }
.bi-google-play::before { content: "\f7f7"; }
.bi-h-circle-1::before { content: "\f7f8"; }
.bi-h-circle-fill-1::before { content: "\f7f9"; }
.bi-h-circle-fill::before { content: "\f7fa"; }
.bi-h-circle::before { content: "\f7fb"; }
.bi-h-square-fill::before { content: "\f7fc"; }
@@ -1813,8 +1780,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-lungs-fill::before { content: "\f7ff"; }
.bi-lungs::before { content: "\f800"; }
.bi-microsoft-teams::before { content: "\f801"; }
.bi-p-circle-1::before { content: "\f802"; }
.bi-p-circle-fill-1::before { content: "\f803"; }
.bi-p-circle-fill::before { content: "\f804"; }
.bi-p-circle::before { content: "\f805"; }
.bi-p-square-fill::before { content: "\f806"; }
@@ -1823,8 +1788,6 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-pass::before { content: "\f809"; }
.bi-prescription::before { content: "\f80a"; }
.bi-prescription2::before { content: "\f80b"; }
.bi-r-circle-1::before { content: "\f80c"; }
.bi-r-circle-fill-1::before { content: "\f80d"; }
.bi-r-circle-fill::before { content: "\f80e"; }
.bi-r-circle::before { content: "\f80f"; }
.bi-r-square-fill::before { content: "\f810"; }
@@ -2016,3 +1979,100 @@ url("./fonts/bootstrap-icons.woff?24e3eb84d0bcaf83d77f904c78ac1f47") format("wof
.bi-sina-weibo::before { content: "\f8ca"; }
.bi-tencent-qq::before { content: "\f8cb"; }
.bi-wikipedia::before { content: "\f8cc"; }
.bi-alphabet-uppercase::before { content: "\f2a5"; }
.bi-alphabet::before { content: "\f68a"; }
.bi-amazon::before { content: "\f68d"; }
.bi-arrows-collapse-vertical::before { content: "\f690"; }
.bi-arrows-expand-vertical::before { content: "\f695"; }
.bi-arrows-vertical::before { content: "\f698"; }
.bi-arrows::before { content: "\f6a2"; }
.bi-ban-fill::before { content: "\f6a3"; }
.bi-ban::before { content: "\f6b6"; }
.bi-bing::before { content: "\f6c2"; }
.bi-cake::before { content: "\f6e0"; }
.bi-cake2::before { content: "\f6ed"; }
.bi-cookie::before { content: "\f6ee"; }
.bi-copy::before { content: "\f759"; }
.bi-crosshair::before { content: "\f769"; }
.bi-crosshair2::before { content: "\f794"; }
.bi-emoji-astonished-fill::before { content: "\f795"; }
.bi-emoji-astonished::before { content: "\f79a"; }
.bi-emoji-grimace-fill::before { content: "\f79b"; }
.bi-emoji-grimace::before { content: "\f7a0"; }
.bi-emoji-grin-fill::before { content: "\f7a1"; }
.bi-emoji-grin::before { content: "\f7a6"; }
.bi-emoji-surprise-fill::before { content: "\f7a7"; }
.bi-emoji-surprise::before { content: "\f7ac"; }
.bi-emoji-tear-fill::before { content: "\f7ad"; }
.bi-emoji-tear::before { content: "\f7b2"; }
.bi-envelope-arrow-down-fill::before { content: "\f7b3"; }
.bi-envelope-arrow-down::before { content: "\f7b8"; }
.bi-envelope-arrow-up-fill::before { content: "\f7b9"; }
.bi-envelope-arrow-up::before { content: "\f7be"; }
.bi-feather::before { content: "\f7bf"; }
.bi-feather2::before { content: "\f7c4"; }
.bi-floppy-fill::before { content: "\f7c5"; }
.bi-floppy::before { content: "\f7d8"; }
.bi-floppy2-fill::before { content: "\f7d9"; }
.bi-floppy2::before { content: "\f7e4"; }
.bi-gitlab::before { content: "\f7e5"; }
.bi-highlighter::before { content: "\f7f8"; }
.bi-marker-tip::before { content: "\f802"; }
.bi-nvme-fill::before { content: "\f803"; }
.bi-nvme::before { content: "\f80c"; }
.bi-opencollective::before { content: "\f80d"; }
.bi-pci-card-network::before { content: "\f8cd"; }
.bi-pci-card-sound::before { content: "\f8ce"; }
.bi-radar::before { content: "\f8cf"; }
.bi-send-arrow-down-fill::before { content: "\f8d0"; }
.bi-send-arrow-down::before { content: "\f8d1"; }
.bi-send-arrow-up-fill::before { content: "\f8d2"; }
.bi-send-arrow-up::before { content: "\f8d3"; }
.bi-sim-slash-fill::before { content: "\f8d4"; }
.bi-sim-slash::before { content: "\f8d5"; }
.bi-sourceforge::before { content: "\f8d6"; }
.bi-substack::before { content: "\f8d7"; }
.bi-threads-fill::before { content: "\f8d8"; }
.bi-threads::before { content: "\f8d9"; }
.bi-transparency::before { content: "\f8da"; }
.bi-twitter-x::before { content: "\f8db"; }
.bi-type-h4::before { content: "\f8dc"; }
.bi-type-h5::before { content: "\f8dd"; }
.bi-type-h6::before { content: "\f8de"; }
.bi-backpack-fill::before { content: "\f8df"; }
.bi-backpack::before { content: "\f8e0"; }
.bi-backpack2-fill::before { content: "\f8e1"; }
.bi-backpack2::before { content: "\f8e2"; }
.bi-backpack3-fill::before { content: "\f8e3"; }
.bi-backpack3::before { content: "\f8e4"; }
.bi-backpack4-fill::before { content: "\f8e5"; }
.bi-backpack4::before { content: "\f8e6"; }
.bi-brilliance::before { content: "\f8e7"; }
.bi-cake-fill::before { content: "\f8e8"; }
.bi-cake2-fill::before { content: "\f8e9"; }
.bi-duffle-fill::before { content: "\f8ea"; }
.bi-duffle::before { content: "\f8eb"; }
.bi-exposure::before { content: "\f8ec"; }
.bi-gender-neuter::before { content: "\f8ed"; }
.bi-highlights::before { content: "\f8ee"; }
.bi-luggage-fill::before { content: "\f8ef"; }
.bi-luggage::before { content: "\f8f0"; }
.bi-mailbox-flag::before { content: "\f8f1"; }
.bi-mailbox2-flag::before { content: "\f8f2"; }
.bi-noise-reduction::before { content: "\f8f3"; }
.bi-passport-fill::before { content: "\f8f4"; }
.bi-passport::before { content: "\f8f5"; }
.bi-person-arms-up::before { content: "\f8f6"; }
.bi-person-raised-hand::before { content: "\f8f7"; }
.bi-person-standing-dress::before { content: "\f8f8"; }
.bi-person-standing::before { content: "\f8f9"; }
.bi-person-walking::before { content: "\f8fa"; }
.bi-person-wheelchair::before { content: "\f8fb"; }
.bi-shadows::before { content: "\f8fc"; }
.bi-suitcase-fill::before { content: "\f8fd"; }
.bi-suitcase-lg-fill::before { content: "\f8fe"; }
.bi-suitcase-lg::before { content: "\f8ff"; }
.bi-suitcase::before { content: "\f900"; }
.bi-suitcase2-fill::before { content: "\f901"; }
.bi-suitcase2::before { content: "\f902"; }
.bi-vignette::before { content: "\f903"; }

View File

@@ -1,5 +1,5 @@
/* Switch SVG logo to white on dark mode */
html.dark .navbar-light svg {
html[data-bs-theme='dark'] .navbar svg {
fill: #fff;
}

View File

@@ -4,7 +4,7 @@
{% get_solo 'portal.SiteConfiguration' as site_conf %}
<!doctype html>
<html lang="en">
<html lang="en" data-bs-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -16,11 +16,11 @@
<link rel="icon" href="{% static "favicon.png" %}" sizes="any">
<link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml">
{% if site_conf.use_cdn %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
{% else %}
<link href="{% static "bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" %}" rel="stylesheet">
<link href="{% static "bootstrap-icons@1.10.3/font/bootstrap-icons.css" %}" rel="stylesheet">
<link href="{% static "bootstrap@5.3.2/dist/css/bootstrap.min.css" %}" rel="stylesheet">
<link href="{% static "bootstrap-icons@1.11.1/font/bootstrap-icons.css" %}" rel="stylesheet">
{% endif %}
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
<style>
@@ -36,44 +36,116 @@
font-size: 3.5rem;
}
}
.d-light-inline { display: inline !important; }
.d-dark-inline { display: none !important; }
html.dark .d-light-inline { display: none !important; }
html.dark .d-dark-inline { display: inline !important; }
</style>
<script>
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
'use strict'
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark')
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')
if (!themeSwitcher) {
return
}
const activeThemeIcon = document.querySelector('.theme-icon-active i')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const biOfActiveBtn = btnToActive.querySelector('.theme-icon i').getAttribute('class')
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
})
btnToActive.classList.add('active')
btnToActive.setAttribute('aria-pressed', 'true')
activeThemeIcon.setAttribute('class', biOfActiveBtn)
if (focus) {
themeSwitcher.focus()
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value')
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
})
})
})
})()
</script>
{% block extra_head %}
{{ site_conf.extra_head | safe }}
{% endblock %}
</head>
<body>
<header>
<div class="navbar navbar-light bg-light shadow-sm">
<div class="container">
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
<svg class="me-2" width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" enable-background="accumulate" overflow="visible" stroke-width="2" />
<style>
path {
text-indent:0;
text-transform:none;
}
</style>
</svg>
<strong>{{ site_conf.site_name }}</strong>
</a>
<div class="btn-group" role="group" aria-label="Login menu">
{% include 'includes/login.html' %}
<a id="darkmode-button" class="btn btn-sm btn-outline-dark"><i class="bi bi-moon d-none d-light-inline" title="Switch to dark mode"></i><i class="bi bi-sun d-none d-dark-inline" title="Switch to light mode"></i></a>
<nav class="navbar navbar-expand-lg bg-body-tertiary shadow-sm">
<div class="container d-flex">
<div class="me-auto">
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
<svg class="me-2" width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" enable-background="accumulate" overflow="visible" stroke-width="2" />
<style>
path {
text-indent:0;
text-transform:none;
}
</style>
</svg>
<strong>{{ site_conf.site_name }}</strong>
</a>
</div>
{% include 'includes/login.html' %}
</div>
</div>
</nav>
</header>
<main>
<div class="container py-2">
<nav class="navbar navbar-expand-lg navbar-light">
<nav class="navbar navbar-expand-lg">
<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">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item dropdown">
@@ -115,7 +187,7 @@
</div>
</div>
</section>
<div class="album py-4 bg-light">
<div class="album py-4 bg-body-tertiary">
<div class="container">
{% block carousel %}
{% endblock %}
@@ -129,17 +201,9 @@
</main>
{% include 'includes/footer.html' %}
{% if site_conf.use_cdn %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% else %}
<script src="{% static "bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" %}"></script>
<script src="{% static "bootstrap-dark-5@1.1.3/dist/js/darkmode.min.js" %}"></script>
<script src="{% static "bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" %}"></script>
{% endif %}
<!-- script src="https://cdn.jsdelivr.net/npm/masonry-layout@4.2.2/dist/masonry.pkgd.min.js" integrity="sha384-GNFwBvfVxBkLMJpYMOABq3c+d3KnQxudP/mGPkzpZSTYykLBNsZEnG2D9G/X/+7D" crossorigin="anonymous" async></script -->
<script>
document.querySelector("#darkmode-button").onclick = function(e){
darkmode.toggleDarkMode();
}
</script>
</body>
</html>

View File

@@ -10,7 +10,7 @@
<div class="col">
<div class="card shadow-sm">
{% for i in d.image.all %}
{% if forloop.first %}<a href="{{d.get_absolute_url}}"><img src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %}
{% if forloop.first %}<a href="{{d.get_absolute_url}}"><img class="card-img-top" src="{{ i.image.url }}" alt="Card image cap"></a>{% endif %}
{% endfor %}
<div class="card-body">
<p class="card-text" style="position: relative;">
@@ -38,7 +38,7 @@
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=d.rolling_class.company.safe_name %}"><abbr title="{{ d.rolling_class.company.extended_name }}">{{ d.rolling_class.company }}</abbr></a>
<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>
@@ -56,12 +56,12 @@
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{%if d.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=d.manufacturer.safe_name %}">{{ 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 %}
<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.safe_name %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }}">{{ d.scale }}</abbr></a></td>
<td><a href="{% url 'filtered' _filter="scale" search=d.scale.slug %}"><abbr title="{{ d.scale.ratio }} - {{ d.scale.tracks }}">{{ d.scale }}</abbr></a></td>
</tr>
<tr>
<th scope="row">SKU</th>

View File

@@ -42,7 +42,7 @@
</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.safe_name %}">Show all rolling stock</a>
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>

View File

@@ -1,19 +1,47 @@
{% if request.user.is_staff %}
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" id="dropdownMenu2" data-bs-toggle="dropdown" aria-expanded="false">
Welcome back, <strong>{{ request.user }}</strong>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li><a class="dropdown-item" href="{% url 'admin:roster_rollingstock_changelist' %}">Rolling stock</a></li>
<li><a class="dropdown-item" href="{% url 'admin:consist_consist_changelist' %}">Consists</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'metadata' %}">Metadata</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_flatpage_changelist' %}">Pages</a></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: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><a class="dropdown-item text-danger" href="{% url 'admin:logout' %}?next={{ request.path }}">Logout</a></li>
</ul>
{% else %}
<a class="btn btn-sm btn-outline-dark" href="{% url 'admin:login' %}?next={{ request.path }}">Log in</a>
{% endif %}
<div class="navbar-collapse justify-content-end" id="loginNavbar">
<ul class="navbar-nav">
<li class="nav-item dropdown">
{% if request.user.is_staff %}
<a class="nav-link dropdown-toggle" href="#" role="button" id="dropdownLogin" data-bs-toggle="dropdown" aria-expanded="false">
Welcome back, <strong>{{ request.user }}</strong>
</a>
<ul class="dropdown-menu" aria-labelledby="dropdownLogin">
<li><a class="dropdown-item" href="{% url 'admin:roster_rollingstock_changelist' %}">Rolling stock</a></li>
<li><a class="dropdown-item" href="{% url 'admin:consist_consist_changelist' %}">Consists</a></li>
<li><a class="dropdown-item" href="{% url 'admin:app_list' 'metadata' %}">Metadata</a></li>
<li><a class="dropdown-item" href="{% url 'admin:portal_flatpage_changelist' %}">Pages</a></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: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><a class="dropdown-item text-danger" href="{% url 'admin:logout' %}?next={{ request.path }}">Logout</a></li>
</ul>
{% else %}
<a class="nav-link" href="{% url 'admin:login' %}?next={{ request.path }}">Log in</a>
{% endif %}
</li>
<li class="nav-item dropdown">
<a class="theme-icon-active nav-link dropdown-toggle" href="#" type="button" id="bd-theme" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-circle-half"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme">
<li>
<button class="theme-icon dropdown-item" data-bs-theme-value="light" aria-pressed="false">
<span class="me-2"><i class="bi bi-sun-fill"></i></span> Light
</button>
</li>
<li>
<button class="theme-icon dropdown-item" data-bs-theme-value="dark" aria-pressed="false">
<span class="me-2"><i class="bi bi-moon-stars-fill"></i></span> Dark
</button>
</li>
<li>
<button class="theme-icon dropdown-item" data-bs-theme-value="auto" aria-pressed="false">
<span class="me-2"><i class="bi bi-circle-half"></i></span> Auto
</button>
</li>
</ul>
</li>
</ul>
</div>

View File

@@ -1,6 +1,6 @@
<form class="d-flex needs-validation" action="{% url 'search' %}" method="post" novalidate>
<div class="input-group has-validation">
<input class="form-control me-2" 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">
<option value="company: ">
<option value="manufacturer: ">

View File

@@ -34,7 +34,7 @@
</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.safe_name %}">Show all rolling stock</a>
<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>

View File

@@ -70,7 +70,7 @@
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.safe_name %}"><abbr title="{{ rolling_stock.rolling_class.company.extended_name }}">{{ rolling_stock.rolling_class.company }}</abbr></a>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.slug %}"><abbr title="{{ rolling_stock.rolling_class.company.extended_name }}">{{ rolling_stock.rolling_class.company }}</abbr></a>
</td>
</tr>
<tr>
@@ -97,7 +97,7 @@
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.safe_name %}">{{ 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 }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
<tr>
@@ -147,7 +147,7 @@
<tr>
<th width="35%" scope="row">Manufacturer</th>
<td>{%if rolling_stock.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.manufacturer.safe_name %}">{{ 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 }}{% if rolling_stock.manufacturer.website %}</a> <a href="{{ rolling_stock.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
<tr>
@@ -209,7 +209,7 @@
<tr>
<th scope="row">Company</th>
<td>
<a href="{% url 'filtered' _filter="company" search=rolling_stock.rolling_class.company.safe_name %}">{{ rolling_stock.rolling_class.company }}</a> ({{ rolling_stock.rolling_class.company.extended_name }})
<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>
@@ -219,7 +219,7 @@
<tr>
<th scope="row">Manufacturer</th>
<td>{%if rolling_stock.rolling_class.manufacturer %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.rolling_class.manufacturer.safe_name %}">{{ 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 %}
<a href="{% url 'filtered' _filter="manufacturer" search=rolling_stock.rolling_class.manufacturer.slug %}">{{ rolling_stock.rolling_class.manufacturer }}{% if rolling_stock.rolling_class.manufacturer.website %}</a> <a href="{{ rolling_stock.rolling_class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
{% endif %}</td>
</tr>
</tbody>

View File

@@ -32,7 +32,7 @@
</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.safe_name %}">Show all rolling stock</a>
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=d.slug %}">Show all rolling stock</a>
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.pk %}">Edit</a>{% endif %}
</div>
</div>

View File

@@ -24,7 +24,7 @@
</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.safe_name %}">Show all rolling stock</a>
<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>

View File

@@ -174,33 +174,22 @@ class GetRosterFiltered(View):
def run_filter(self, request, search, _filter, page=1):
site_conf = get_site_conf()
if _filter == "type":
type_ = " ".join(search.split()[:-1])
category = search.split()[-1]
try:
title = (
RollingStockType.objects.filter(type__iexact=type_)
.get(category__iexact=category)
)
except ObjectDoesNotExist:
raise Http404
query = Q(
Q(rolling_class__type__type__iexact=type_)
& Q(rolling_class__type__category__iexact=category)
)
title = RollingStockType.objects.get(slug__iexact=search)
query = Q(rolling_class__type__slug__iexact=search)
elif _filter == "company":
title = get_object_or_404(Company, name__iexact=search)
query = Q(rolling_class__company__name__iexact=search)
title = get_object_or_404(Company, slug__iexact=search)
query = Q(rolling_class__company__slug__iexact=search)
elif _filter == "manufacturer":
title = get_object_or_404(Manufacturer, name__iexact=search)
title = get_object_or_404(Manufacturer, slug__iexact=search)
query = Q(
Q(rolling_class__manufacturer__name__iexact=search)
| Q(manufacturer__name__iexact=search)
Q(rolling_class__manufacturer__slug__iexact=search)
| Q(manufacturer__slug__iexact=search)
)
elif _filter == "scale":
title = get_object_or_404(Scale, scale__iexact=search)
query = Q(scale__scale__iexact=search)
title = get_object_or_404(Scale, slug__iexact=search)
query = Q(scale__slug__iexact=search)
elif _filter == "tag":
title = get_object_or_404(Tag, slug=search)
title = get_object_or_404(Tag, slug__iexact=search)
query = Q(tags__slug__iexact=search)
else:
raise Http404

View File

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

1
requirements-prod.txt Normal file
View File

@@ -0,0 +1 @@
gunicorn