mirror of
https://github.com/daniviga/django-ram.git
synced 2025-08-07 14:47:49 +02:00
Compare commits
2 Commits
better-doc
...
fix-warnin
Author | SHA1 | Date | |
---|---|---|---|
9cb3fb1d8a
|
|||
ed8ffb5ece
|
2
.github/workflows/django.yml
vendored
2
.github/workflows/django.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
python-version: ['3.12', '3.13']
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,6 +10,7 @@ __pycache__/
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
@@ -131,4 +132,3 @@ dmypy.json
|
||||
ram/storage/
|
||||
!ram/storage/.gitignore
|
||||
arduino/CommandStation-EX/build/
|
||||
utils
|
||||
|
100
README.md
100
README.md
@@ -23,8 +23,7 @@ security assesment, pentest, ISO certification, etc.
|
||||
|
||||
This project probably doesn't match your needs nor expectations. Be aware.
|
||||
|
||||
> [!CAUTION]
|
||||
> Your model train may catch fire while using this software.
|
||||
Your model train may also catch fire while using this software.
|
||||
|
||||
Check out [my own instance](https://daniele.mynarrowgauge.org).
|
||||
|
||||
@@ -41,49 +40,23 @@ Project is based on the following technologies and components:
|
||||
|
||||
It has been developed with:
|
||||
|
||||
- [neovim](https://neovim.io/): because `vim` rocks, `neovim` rocks more
|
||||
- [vim](https://www.vim.org/): because it rocks
|
||||
- [arduino-cli](https://github.com/arduino/arduino-cli/): a mouse? What the heck?
|
||||
- [vim-arduino](https://github.com/stevearc/vim-arduino): another IDE? No thanks
|
||||
- [podman](https://podman.io/): because containers are fancy
|
||||
- [QEMU (avr)](https://qemu-project.gitlab.io/qemu/system/target-avr.html): QEMU can even make toasts!
|
||||
- [QEMU (avr)](https://qemu-project.gitlab.io/qemu/system/target-avr.html): QEMU can even make toast!
|
||||
|
||||
## Future developments
|
||||
|
||||
A bunch of random, probably useless, ideas:
|
||||
|
||||
### A bookshelf
|
||||
|
||||
✅DONE
|
||||
|
||||
Because books matter more than model trains themselves.
|
||||
|
||||
### Live assets KPI collection
|
||||
|
||||
Realtime data usage is collected via a daemon connected over TCP to the EX-CommandStation and recorded for every asset with a DCC address.
|
||||
|
||||
### Asset lifecycle
|
||||
|
||||
Data is collected to compute the asset usage and then the wear level of its components (eg. the engine).
|
||||
|
||||
### Required mainentance forecast
|
||||
|
||||
Eventually data is used to "forecast" any required maintenance, like for example the replacement of carbon brushes, gear and motor oiling.
|
||||
|
||||
### Asset export to JMRI
|
||||
|
||||
Export assets (locomotives) into the JMRI format to be loaded in the JMRI
|
||||
roster.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- Python 3.9+
|
||||
- A USB port when running Arduino hardware (and adaptors if you have a Mac)
|
||||
|
||||
## Web portal installation
|
||||
|
||||
### Using containers
|
||||
|
||||
Do it yourself, otherwise, raise a request :)
|
||||
coming soon
|
||||
|
||||
### Manual installation
|
||||
|
||||
@@ -110,8 +83,6 @@ $ python manage.py migrate
|
||||
$ python manage.py createsuperuser
|
||||
```
|
||||
|
||||
To load some sample metadata, see the [sample_data folder instructions](./sample_data/README.md).
|
||||
|
||||
Run Django
|
||||
|
||||
```bash
|
||||
@@ -128,52 +99,43 @@ connected via serial port, to the network, allowing commands to be sent via a
|
||||
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, a [Mega+WiFi board](https://dcc-ex.com/reference/hardware/microcontrollers/wifi-mega.html), or an
|
||||
[ESP32](https://dcc-ex.com/reference/hardware/microcontrollers/esp32.html) (recommended).
|
||||
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)).
|
||||
|
||||
### Manual setup
|
||||
### Customize the settings
|
||||
|
||||
You'll need [namp-ncat](https://nmap.org/ncat/) , and `stty` to setup the serial port.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Other variants of `nc` or `ncat` may not work as expected.
|
||||
|
||||
Then you can run the following commands:
|
||||
|
||||
```bash
|
||||
$ stty -F /dev/ttyACM0 -echo 115200
|
||||
$ ncat -n -k -l 2560 </dev/ttyACM0 >/dev/ttyACM0
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You'll might need to change the serial port (`/dev/ttyACM0`) to match your board.
|
||||
|
||||
> [!NOTE]
|
||||
> Your user will also need access to the device file, so you might need to add it to the `dialout` group.
|
||||
The daemon comes with default settings in `config.ini`.
|
||||
Settings may need to be customized based on your setup.
|
||||
|
||||
|
||||
### Using containers
|
||||
|
||||
```bash
|
||||
$ cd connector
|
||||
$ podman build -t dcc/connector .
|
||||
$ podman run -d --group-add keep-groups --device /dev/ttyACM0:/dev/arduino -p 2560:2560 dcc/connector
|
||||
$ cd daemons
|
||||
$ podman build -t dcc/net-to-serial .
|
||||
$ podman run --group-add keep-groups --device /dev/ttyACM0 -p 2560:2560 dcc/net-to-serial
|
||||
```
|
||||
|
||||
### Manual setup
|
||||
|
||||
```bash
|
||||
$ cd daemons
|
||||
$ pip install -r requirements.txt
|
||||
$ python ./net-to-serial.py
|
||||
```
|
||||
|
||||
### Test with a simulator
|
||||
|
||||
A [QEMU AVR based simulator](daemons/simulator/README.md) running DCC++ EX is bundled togheter with the connector
|
||||
into a container. To run it:
|
||||
A [QEMU AVR based simulator](daemons/simulator/README.md) running DCC++ EX is bundled togheter with the `net-to-serial.py`
|
||||
daemon into a container. To run it:
|
||||
|
||||
```bash
|
||||
$ cd connector/simulator
|
||||
$ podman build -t dcc/connector:sim .
|
||||
$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim
|
||||
$ cd daemons/simulator
|
||||
$ podman build -t dcc/net-to-serial:sim .
|
||||
$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> The simulator is intended for light development and testing purposes only and far from being a complete replacement for a real hardware.
|
||||
To be continued ...
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -184,12 +146,15 @@ $ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim
|
||||

|
||||
|
||||
|
||||
|
||||
#### Dark mode
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
### Backoffice
|
||||
|
||||

|
||||
@@ -201,3 +166,8 @@ $ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim
|
||||
### Rest API
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Submodule arduino/CommandStation-EX updated: 13488e1e93...2db2b0ecc6
Submodule arduino/WebThrottle-EX updated: eb43d7906f...c67e4080d0
Submodule arduino/arduino-cli updated: fa6eafcbbe...048415c5e6
Submodule arduino/dcc-ex.github.io updated: a0f886b69f...9acc446358
Submodule arduino/vim-arduino updated: 2ded67cdf0...111db616db
@@ -1,9 +0,0 @@
|
||||
FROM alpine:edge
|
||||
|
||||
RUN apk add --no-cache coreutils nmap-ncat
|
||||
|
||||
EXPOSE 2560/tcp
|
||||
|
||||
SHELL ["/bin/ash", "-c"]
|
||||
CMD stty -F /dev/arduino -echo 115200 && \
|
||||
ncat -n -k -l 2560 </dev/arduino >/dev/arduino
|
@@ -1,19 +0,0 @@
|
||||
# Use a container to implement a serial to net bridge
|
||||
|
||||
This uses `ncat` from [nmap](https://nmap.org/ncat/) to bridge a serial port to a network port. The serial port is passed to the Podman command (eg. `/dev/ttyACM0`) and the network port is `2560`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Other variants of `nc` or `ncat` may not work as expected.
|
||||
|
||||
## Build and run the container
|
||||
|
||||
```bash
|
||||
$ podman buil -t dcc/bridge .
|
||||
$ podman run -d --group-add keep-groups --device=/dev/ttyACM0:/dev/arduino -p 2560:2560 --name dcc-bridge dcc/bridge
|
||||
```
|
||||
|
||||
It can be tested with `telnet`:
|
||||
|
||||
```bash
|
||||
$ telnet localhost 2560
|
||||
```
|
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
FROM dcc/bridge
|
||||
|
||||
RUN apk update && apk add --no-cache qemu-system-avr \
|
||||
&& mkdir /io
|
||||
ADD start.sh /usr/local/bin
|
||||
ADD CommandStation-EX*.elf /io
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/start.sh"]
|
@@ -1,13 +0,0 @@
|
||||
# Connector and AVR simulator
|
||||
|
||||
> [!WARNING]
|
||||
> The simulator is intended for light development and testing purposes only and far from being a complete replacement for a real hardware.
|
||||
|
||||
`qemu-system-avr` tries to use all the CPU cycles (leaving a CPU core stuck at 100%; limit CPU core usage to 10% via `--cpus 0.1`. It can be adjusted on slower machines.
|
||||
|
||||
```bash
|
||||
$ podman build -t dcc/connector:sim .
|
||||
$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim
|
||||
```
|
||||
|
||||
All traffic will be collected on the container's `stderr` for debugging purposes.
|
9
daemons/Dockerfile
Normal file
9
daemons/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM python:3.11-alpine
|
||||
|
||||
RUN mkdir /opt/dcc && pip -q install pyserial
|
||||
ADD net-to-serial.py config.ini /opt/dcc
|
||||
RUN python3 -q -m compileall /opt/dcc/net-to-serial.py
|
||||
|
||||
EXPOSE 2560/tcp
|
||||
|
||||
CMD ["python3", "/opt/dcc/net-to-serial.py"]
|
3
daemons/README.md
Normal file
3
daemons/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## DCC++ EX connector
|
||||
|
||||
See [README.md](../README.md)
|
14
daemons/config.ini
Normal file
14
daemons/config.ini
Normal file
@@ -0,0 +1,14 @@
|
||||
[Daemon]
|
||||
LogLevel = debug
|
||||
ListeningIP = 0.0.0.0
|
||||
ListeningPort = 2560
|
||||
MaxClients = 10
|
||||
|
||||
[Serial]
|
||||
# UNO
|
||||
Port = /dev/ttyACM0
|
||||
# Mega WiFi
|
||||
# Port = /dev/ttyUSB0
|
||||
Baudrate = 115200
|
||||
# Timeout in milliseconds
|
||||
Timeout = 50
|
120
daemons/net-to-serial.py
Executable file
120
daemons/net-to-serial.py
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import logging
|
||||
import serial
|
||||
import asyncio
|
||||
import configparser
|
||||
|
||||
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,
|
||||
)
|
||||
self.ser.baudrate = config["Serial"]["Baudrate"]
|
||||
self.max_clients = int(config["Daemon"]["MaxClients"])
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self.ser.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def __read_serial(self):
|
||||
"""Serial reader wrapper"""
|
||||
response = b""
|
||||
while True:
|
||||
line = self.ser.read_until()
|
||||
if not line.strip(): # empty line
|
||||
break
|
||||
if line.decode().startswith("<*"):
|
||||
logging.debug("Serial debug: {}".format(line))
|
||||
else:
|
||||
response += line
|
||||
logging.debug("Serial read: {}".format(response))
|
||||
|
||||
return response
|
||||
|
||||
def __write_serial(self, data):
|
||||
"""Serial writer wrapper"""
|
||||
self.ser.write(data)
|
||||
|
||||
async def handle_echo(self, reader, writer):
|
||||
"""Process a request from socket and return the response"""
|
||||
logging.info(
|
||||
"Clients already connected: {} (max: {})".format(
|
||||
len(self.connected_clients),
|
||||
self.max_clients,
|
||||
)
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
async def return_board(self):
|
||||
"""Return the board signature"""
|
||||
line = ""
|
||||
# drain the serial until we are ready to go
|
||||
self.__write_serial(b"<s>")
|
||||
while "DCC-EX" not in line:
|
||||
line = self.__read_serial().decode()
|
||||
board = re.findall(r"<iDCC-EX.*>", line)[0]
|
||||
return board
|
||||
|
||||
|
||||
async def main():
|
||||
config = configparser.ConfigParser()
|
||||
config.read(
|
||||
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"],
|
||||
)
|
||||
addr = server.sockets[0].getsockname()
|
||||
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.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())
|
1
daemons/requirements.txt
Normal file
1
daemons/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
PySerial
|
BIN
daemons/simulator/CommandStation-EX-uno-7311f2c.elf
Executable file
BIN
daemons/simulator/CommandStation-EX-uno-7311f2c.elf
Executable file
Binary file not shown.
7
daemons/simulator/Dockerfile
Normal file
7
daemons/simulator/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM dcc/net-to-serial
|
||||
|
||||
RUN apk update && apk add qemu-system-avr && mkdir /io
|
||||
ADD start.sh /opt/dcc
|
||||
ADD CommandStation-EX*.elf /io
|
||||
|
||||
ENTRYPOINT ["/opt/dcc/start.sh"]
|
8
daemons/simulator/README.md
Normal file
8
daemons/simulator/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# AVR Simulator
|
||||
|
||||
`qemu-system-avr` tries to use all the CPU cicles (leaving a CPU core stuck at 100%; limit CPU core usage to 10% via `--cpus 0.1`. It can be adjusted on slower machines.
|
||||
|
||||
```bash
|
||||
$ podman build -t dcc/net-to-serial:sim .
|
||||
$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim
|
||||
```
|
@@ -7,5 +7,7 @@ if [ -c /dev/pts/0 ]; then
|
||||
PTY=1
|
||||
fi
|
||||
|
||||
sed -i "s/ttyACM0/pts\/${PTY}/" /opt/dcc/config.ini
|
||||
|
||||
qemu-system-avr -machine uno -bios /io/CommandStation-EX*.elf -serial pty -daemonize
|
||||
ncat -n -k -l 2560 -o /dev/stderr </dev/pts/${PTY} >/dev/pts/${PTY}
|
||||
/opt/dcc/net-to-serial.py
|
@@ -1,346 +1,52 @@
|
||||
import html
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html, strip_tags
|
||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||
|
||||
from ram.admin import publish, unpublish
|
||||
from ram.utils import generate_csv
|
||||
from portal.utils import get_site_conf
|
||||
from repository.models import BookDocument, CatalogDocument
|
||||
from bookshelf.models import (
|
||||
BaseBookProperty,
|
||||
BaseBookImage,
|
||||
Book,
|
||||
Author,
|
||||
Publisher,
|
||||
Catalog,
|
||||
)
|
||||
from bookshelf.models import BookProperty, BookImage, Book, Author, Publisher
|
||||
|
||||
|
||||
class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||
model = BaseBookImage
|
||||
model = BookImage
|
||||
min_num = 0
|
||||
extra = 0
|
||||
readonly_fields = ("image_thumbnail",)
|
||||
classes = ["collapse"]
|
||||
verbose_name = "Image"
|
||||
|
||||
|
||||
class BookPropertyInline(admin.TabularInline):
|
||||
model = BaseBookProperty
|
||||
model = BookProperty
|
||||
min_num = 0
|
||||
extra = 0
|
||||
autocomplete_fields = ("property",)
|
||||
verbose_name = "Property"
|
||||
verbose_name_plural = "Properties"
|
||||
|
||||
|
||||
class BookDocInline(admin.TabularInline):
|
||||
model = BookDocument
|
||||
min_num = 0
|
||||
extra = 0
|
||||
classes = ["collapse"]
|
||||
|
||||
|
||||
class CatalogDocInline(BookDocInline):
|
||||
model = CatalogDocument
|
||||
|
||||
|
||||
@admin.register(Book)
|
||||
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||
inlines = (
|
||||
BookPropertyInline,
|
||||
BookImageInline,
|
||||
BookDocInline,
|
||||
)
|
||||
inlines = (BookImageInline, BookPropertyInline,)
|
||||
list_display = (
|
||||
"title",
|
||||
"get_authors",
|
||||
"get_publisher",
|
||||
"publication_year",
|
||||
"number_of_pages",
|
||||
"published",
|
||||
"number_of_pages"
|
||||
)
|
||||
autocomplete_fields = ("authors", "publisher", "shop")
|
||||
readonly_fields = ("invoices", "creation_time", "updated_time")
|
||||
search_fields = ("title", "publisher__name", "authors__last_name")
|
||||
list_filter = ("publisher__name", "authors")
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"published",
|
||||
"title",
|
||||
"authors",
|
||||
"publisher",
|
||||
"ISBN",
|
||||
"language",
|
||||
"number_of_pages",
|
||||
"publication_year",
|
||||
"description",
|
||||
"tags",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Purchase data",
|
||||
{
|
||||
"fields": (
|
||||
"shop",
|
||||
"purchase_date",
|
||||
"price",
|
||||
"invoices",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Notes",
|
||||
{"classes": ("collapse",), "fields": ("notes",)},
|
||||
),
|
||||
(
|
||||
"Audit",
|
||||
{
|
||||
"classes": ("collapse",),
|
||||
"fields": (
|
||||
"creation_time",
|
||||
"updated_time",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
form.base_fields["price"].label = "Price ({})".format(
|
||||
get_site_conf().currency
|
||||
)
|
||||
return form
|
||||
|
||||
@admin.display(description="Invoices")
|
||||
def invoices(self, obj):
|
||||
if obj.invoice.exists():
|
||||
html = "<br>".join(
|
||||
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
|
||||
i.file.url, i
|
||||
) for i in obj.invoice.all())
|
||||
else:
|
||||
html = "-"
|
||||
return format_html(html)
|
||||
|
||||
@admin.display(description="Publisher")
|
||||
def get_publisher(self, obj):
|
||||
return obj.publisher.name
|
||||
|
||||
@admin.display(description="Authors")
|
||||
def get_authors(self, obj):
|
||||
return obj.authors_list
|
||||
|
||||
def download_csv(modeladmin, request, queryset):
|
||||
header = [
|
||||
"Title",
|
||||
"Authors",
|
||||
"Publisher",
|
||||
"ISBN",
|
||||
"Language",
|
||||
"Number of Pages",
|
||||
"Publication Year",
|
||||
"Description",
|
||||
"Tags",
|
||||
"Shop",
|
||||
"Purchase Date",
|
||||
"Price ({})".format(get_site_conf().currency),
|
||||
"Notes",
|
||||
"Properties",
|
||||
]
|
||||
|
||||
data = []
|
||||
for obj in queryset:
|
||||
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||
"{}:{}".format(property.property.name, property.value)
|
||||
for property in obj.property.all()
|
||||
)
|
||||
data.append(
|
||||
[
|
||||
obj.title,
|
||||
obj.authors_list.replace(",", settings.CSV_SEPARATOR_ALT),
|
||||
obj.publisher.name,
|
||||
obj.ISBN,
|
||||
dict(settings.LANGUAGES)[obj.language],
|
||||
obj.number_of_pages,
|
||||
obj.publication_year,
|
||||
html.unescape(strip_tags(obj.description)),
|
||||
settings.CSV_SEPARATOR_ALT.join(
|
||||
t.name for t in obj.tags.all()
|
||||
),
|
||||
obj.shop,
|
||||
obj.purchase_date,
|
||||
obj.price,
|
||||
html.unescape(strip_tags(obj.notes)),
|
||||
properties,
|
||||
]
|
||||
)
|
||||
|
||||
return generate_csv(header, data, "bookshelf_books.csv")
|
||||
|
||||
download_csv.short_description = "Download selected items as CSV"
|
||||
actions = [publish, unpublish, download_csv]
|
||||
return ", ".join(a.short_name() for a in obj.authors.all())
|
||||
|
||||
|
||||
@admin.register(Author)
|
||||
class AuthorAdmin(admin.ModelAdmin):
|
||||
search_fields = (
|
||||
"first_name",
|
||||
"last_name",
|
||||
)
|
||||
search_fields = ("first_name", "last_name",)
|
||||
list_filter = ("last_name",)
|
||||
|
||||
|
||||
@admin.register(Publisher)
|
||||
class PublisherAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "country_flag")
|
||||
list_display = ("name", "country")
|
||||
search_fields = ("name",)
|
||||
|
||||
@admin.display(description="Country")
|
||||
def country_flag(self, obj):
|
||||
return format_html(
|
||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Catalog)
|
||||
class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||
inlines = (
|
||||
BookPropertyInline,
|
||||
BookImageInline,
|
||||
CatalogDocInline,
|
||||
)
|
||||
list_display = (
|
||||
"__str__",
|
||||
"manufacturer",
|
||||
"years",
|
||||
"get_scales",
|
||||
"published",
|
||||
)
|
||||
autocomplete_fields = ("manufacturer",)
|
||||
readonly_fields = ("invoices", "creation_time", "updated_time")
|
||||
search_fields = ("manufacturer__name", "years", "scales__scale")
|
||||
list_filter = ("manufacturer__name", "publication_year", "scales__scale")
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"published",
|
||||
"manufacturer",
|
||||
"years",
|
||||
"scales",
|
||||
"ISBN",
|
||||
"language",
|
||||
"number_of_pages",
|
||||
"publication_year",
|
||||
"description",
|
||||
"tags",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Purchase data",
|
||||
{
|
||||
"fields": (
|
||||
"shop",
|
||||
"purchase_date",
|
||||
"price",
|
||||
"invoices",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Notes",
|
||||
{"classes": ("collapse",), "fields": ("notes",)},
|
||||
),
|
||||
(
|
||||
"Audit",
|
||||
{
|
||||
"classes": ("collapse",),
|
||||
"fields": (
|
||||
"creation_time",
|
||||
"updated_time",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
form.base_fields["price"].label = "Price ({})".format(
|
||||
get_site_conf().currency
|
||||
)
|
||||
return form
|
||||
|
||||
@admin.display(description="Invoices")
|
||||
def invoices(self, obj):
|
||||
if obj.invoice.exists():
|
||||
html = "<br>".join(
|
||||
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
|
||||
i.file.url, i
|
||||
) for i in obj.invoice.all())
|
||||
else:
|
||||
html = "-"
|
||||
return format_html(html)
|
||||
|
||||
def download_csv(modeladmin, request, queryset):
|
||||
header = [
|
||||
"Catalog",
|
||||
"Manufacturer",
|
||||
"Years",
|
||||
"Scales",
|
||||
"ISBN",
|
||||
"Language",
|
||||
"Number of Pages",
|
||||
"Publication Year",
|
||||
"Description",
|
||||
"Tags",
|
||||
"Shop",
|
||||
"Purchase Date",
|
||||
"Price ({})".format(get_site_conf().currency),
|
||||
"Notes",
|
||||
"Properties",
|
||||
]
|
||||
|
||||
data = []
|
||||
for obj in queryset:
|
||||
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||
"{}:{}".format(property.property.name, property.value)
|
||||
for property in obj.property.all()
|
||||
)
|
||||
data.append(
|
||||
[
|
||||
obj.__str__(),
|
||||
obj.manufacturer.name,
|
||||
obj.years,
|
||||
obj.get_scales(),
|
||||
obj.ISBN,
|
||||
dict(settings.LANGUAGES)[obj.language],
|
||||
obj.number_of_pages,
|
||||
obj.publication_year,
|
||||
html.unescape(strip_tags(obj.description)),
|
||||
settings.CSV_SEPARATOR_ALT.join(
|
||||
t.name for t in obj.tags.all()
|
||||
),
|
||||
obj.shop,
|
||||
obj.purchase_date,
|
||||
obj.price,
|
||||
html.unescape(strip_tags(obj.notes)),
|
||||
properties,
|
||||
]
|
||||
)
|
||||
|
||||
return generate_csv(header, data, "bookshelf_catalogs.csv")
|
||||
|
||||
download_csv.short_description = "Download selected items as CSV"
|
||||
actions = [publish, unpublish, download_csv]
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-01 20:16
|
||||
|
||||
# ckeditor removal
|
||||
# import ckeditor_uploader.fields
|
||||
import ckeditor_uploader.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
@@ -48,8 +47,7 @@ class Migration(migrations.Migration):
|
||||
("ISBN", models.CharField(max_length=13, unique=True)),
|
||||
("publication_year", models.SmallIntegerField(blank=True, null=True)),
|
||||
("purchase_date", models.DateField(blank=True, null=True)),
|
||||
# ("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)),
|
||||
("notes", models.TextField(blank=True)),
|
||||
("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)),
|
||||
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_time", models.DateTimeField(auto_now=True)),
|
||||
("authors", models.ManyToManyField(to="bookshelf.author")),
|
||||
|
@@ -12,7 +12,7 @@ from django.conf import settings
|
||||
|
||||
def move_images(apps, schema_editor):
|
||||
sys.stdout.write("\n Processing files. Please await...")
|
||||
for r in bookshelf.models.BaseBookImage.objects.all():
|
||||
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)
|
||||
@@ -31,21 +31,19 @@ class Migration(migrations.Migration):
|
||||
("bookshelf", "0008_alter_author_options_alter_publisher_options"),
|
||||
]
|
||||
|
||||
# Migration is stale and shouldn't be used since model hes been heavily
|
||||
# modified since then. Leaving it here for reference.
|
||||
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
|
||||
# ),
|
||||
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
|
||||
),
|
||||
]
|
||||
|
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-02 14:31
|
||||
|
||||
import tinymce.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0012_alter_book_notes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="book",
|
||||
name="description",
|
||||
field=tinymce.models.HTMLField(blank=True),
|
||||
),
|
||||
]
|
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 13:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0013_book_description"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="book",
|
||||
name="published",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-26 22:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0014_book_published"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="book",
|
||||
name="authors",
|
||||
field=models.ManyToManyField(blank=True, to="bookshelf.author"),
|
||||
),
|
||||
]
|
@@ -1,141 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-27 16:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def basebook_to_book(apps, schema_editor):
|
||||
basebook = apps.get_model("bookshelf", "BaseBook")
|
||||
book = apps.get_model("bookshelf", "Book")
|
||||
for row in basebook.objects.all():
|
||||
b = book.objects.create(
|
||||
basebook_ptr=row,
|
||||
title=row.old_title,
|
||||
publisher=row.old_publisher,
|
||||
)
|
||||
b.authors.set(row.old_authors.all())
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0015_alter_book_authors"),
|
||||
("metadata", "0019_alter_scale_gauge"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="Book",
|
||||
options={"ordering": ["creation_time"]},
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name="BookImage",
|
||||
new_name="BaseBookImage",
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name="BookProperty",
|
||||
new_name="BaseBookProperty",
|
||||
),
|
||||
migrations.RenameModel(
|
||||
old_name="Book",
|
||||
new_name="BaseBook",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="basebook",
|
||||
old_name="title",
|
||||
new_name="old_title",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="basebook",
|
||||
old_name="authors",
|
||||
new_name="old_authors",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="basebook",
|
||||
old_name="publisher",
|
||||
new_name="old_publisher",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="basebookimage",
|
||||
options={"ordering": ["order"], "verbose_name_plural": "Images"},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Book",
|
||||
fields=[
|
||||
(
|
||||
"basebook_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="bookshelf.basebook",
|
||||
),
|
||||
),
|
||||
("title", models.CharField(max_length=200)),
|
||||
(
|
||||
"authors",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
to="bookshelf.author"
|
||||
),
|
||||
),
|
||||
(
|
||||
"publisher",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="bookshelf.publisher"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["title"],
|
||||
},
|
||||
),
|
||||
migrations.RunPython(
|
||||
basebook_to_book,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="basebook",
|
||||
name="old_title",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="basebook",
|
||||
name="old_authors",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="basebook",
|
||||
name="old_publisher",
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Catalog",
|
||||
fields=[
|
||||
(
|
||||
"basebook_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="bookshelf.basebook",
|
||||
),
|
||||
),
|
||||
("years", models.CharField(max_length=12)),
|
||||
(
|
||||
"manufacturer",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="metadata.manufacturer",
|
||||
),
|
||||
),
|
||||
("scales", models.ManyToManyField(to="metadata.scale")),
|
||||
],
|
||||
options={
|
||||
"ordering": ["manufacturer", "publication_year"],
|
||||
},
|
||||
bases=("bookshelf.basebook",),
|
||||
),
|
||||
]
|
@@ -1,52 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-12-22 20:38
|
||||
|
||||
import django.db.models.deletion
|
||||
import ram.utils
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0016_basebook_book_catalogue"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="basebook",
|
||||
options={},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="BaseBookDocument",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("description", models.CharField(blank=True, max_length=128)),
|
||||
(
|
||||
"file",
|
||||
models.FileField(
|
||||
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
|
||||
),
|
||||
),
|
||||
("private", models.BooleanField(default=False)),
|
||||
(
|
||||
"book",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="document",
|
||||
to="bookshelf.basebook",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("book", "file")},
|
||||
},
|
||||
),
|
||||
]
|
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-22 20:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0017_alter_basebook_options_basebookdocument"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="basebookdocument",
|
||||
options={"verbose_name_plural": "Documents"},
|
||||
),
|
||||
]
|
@@ -1,36 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-29 17:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def price_to_property(apps, schema_editor):
|
||||
basebook = apps.get_model("bookshelf", "BaseBook")
|
||||
for row in basebook.objects.all():
|
||||
prop = row.property.filter(property__name__icontains="price")
|
||||
for p in prop:
|
||||
try:
|
||||
row.price = float(p.value)
|
||||
except ValueError:
|
||||
pass
|
||||
row.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0018_alter_basebookdocument_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="basebook",
|
||||
name="price",
|
||||
field=models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=10, null=True
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
price_to_property,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-08 22:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0019_basebook_price"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="basebookdocument",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="basebookdocument",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("book", "file"), name="unique_book_file"
|
||||
),
|
||||
),
|
||||
]
|
@@ -1,34 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-18 11:20
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0020_alter_basebookdocument_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="basebookdocument",
|
||||
name="creation_time",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="basebookdocument",
|
||||
name="updated_time",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="basebookdocument",
|
||||
name="private",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Document will be visible only to logged users"
|
||||
),
|
||||
),
|
||||
]
|
@@ -1,46 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-26 14:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def shop_from_property(apps, schema_editor):
|
||||
basebook = apps.get_model("bookshelf", "BaseBook")
|
||||
shop_model = apps.get_model("metadata", "Shop")
|
||||
for row in basebook.objects.all():
|
||||
property = row.property.filter(
|
||||
property__name__icontains="shop"
|
||||
).first()
|
||||
if property:
|
||||
shop, created = shop_model.objects.get_or_create(
|
||||
name=property.value,
|
||||
defaults={"on_line": False}
|
||||
)
|
||||
|
||||
row.shop = shop
|
||||
row.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0021_basebookdocument_creation_time_and_more"),
|
||||
("metadata", "0023_shop"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="basebook",
|
||||
name="shop",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="metadata.shop",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
shop_from_property,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-02-09 13:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("bookshelf", "0022_basebook_shop"),
|
||||
("repository", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="BaseBookDocument",
|
||||
),
|
||||
]
|
@@ -1,13 +1,16 @@
|
||||
import os
|
||||
import shutil
|
||||
from uuid import uuid4
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
from tinymce import models as tinymce
|
||||
|
||||
from metadata.models import Tag
|
||||
from ram.utils import DeduplicatedStorage
|
||||
from ram.models import BaseModel, Image, PropertyInstance
|
||||
from metadata.models import Scale, Manufacturer, Shop, Tag
|
||||
from ram.models import Image, PropertyInstance
|
||||
|
||||
|
||||
class Publisher(models.Model):
|
||||
@@ -32,12 +35,15 @@ class Author(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.last_name}, {self.first_name}"
|
||||
|
||||
@property
|
||||
def short_name(self):
|
||||
return f"{self.last_name} {self.first_name[0]}."
|
||||
|
||||
|
||||
class BaseBook(BaseModel):
|
||||
class Book(models.Model):
|
||||
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
title = models.CharField(max_length=200)
|
||||
authors = models.ManyToManyField(Author)
|
||||
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
|
||||
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
|
||||
language = models.CharField(
|
||||
max_length=7,
|
||||
@@ -46,19 +52,25 @@ class BaseBook(BaseModel):
|
||||
)
|
||||
number_of_pages = models.SmallIntegerField(null=True, blank=True)
|
||||
publication_year = models.SmallIntegerField(null=True, blank=True)
|
||||
shop = models.ForeignKey(
|
||||
Shop, on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
price = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
purchase_date = models.DateField(null=True, blank=True)
|
||||
tags = models.ManyToManyField(
|
||||
Tag, related_name="bookshelf", blank=True
|
||||
)
|
||||
notes = tinymce.HTMLField(blank=True)
|
||||
creation_time = models.DateTimeField(auto_now_add=True)
|
||||
updated_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def publisher_name(self):
|
||||
return self.publisher.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("book", kwargs={"uuid": self.uuid})
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
shutil.rmtree(
|
||||
@@ -67,7 +79,7 @@ class BaseBook(BaseModel):
|
||||
),
|
||||
ignore_errors=True
|
||||
)
|
||||
super(BaseBook, self).delete(*args, **kwargs)
|
||||
super(Book, self).delete(*args, **kwargs)
|
||||
|
||||
|
||||
def book_image_upload(instance, filename):
|
||||
@@ -79,9 +91,9 @@ def book_image_upload(instance, filename):
|
||||
)
|
||||
|
||||
|
||||
class BaseBookImage(Image):
|
||||
class BookImage(Image):
|
||||
book = models.ForeignKey(
|
||||
BaseBook, on_delete=models.CASCADE, related_name="image"
|
||||
Book, on_delete=models.CASCADE, related_name="image"
|
||||
)
|
||||
image = models.ImageField(
|
||||
upload_to=book_image_upload,
|
||||
@@ -89,63 +101,11 @@ class BaseBookImage(Image):
|
||||
)
|
||||
|
||||
|
||||
class BaseBookProperty(PropertyInstance):
|
||||
class BookProperty(PropertyInstance):
|
||||
book = models.ForeignKey(
|
||||
BaseBook,
|
||||
Book,
|
||||
on_delete=models.CASCADE,
|
||||
null=False,
|
||||
blank=False,
|
||||
related_name="property",
|
||||
)
|
||||
|
||||
|
||||
class Book(BaseBook):
|
||||
title = models.CharField(max_length=200)
|
||||
authors = models.ManyToManyField(Author, blank=True)
|
||||
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
ordering = ["title"]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def publisher_name(self):
|
||||
return self.publisher.name
|
||||
|
||||
@property
|
||||
def authors_list(self):
|
||||
return ", ".join(a.short_name for a in self.authors.all())
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"bookshelf_item",
|
||||
kwargs={"selector": "book", "uuid": self.uuid}
|
||||
)
|
||||
|
||||
|
||||
class Catalog(BaseBook):
|
||||
manufacturer = models.ForeignKey(
|
||||
Manufacturer,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
years = models.CharField(max_length=12)
|
||||
scales = models.ManyToManyField(Scale)
|
||||
|
||||
class Meta:
|
||||
ordering = ["manufacturer", "publication_year"]
|
||||
|
||||
def __str__(self):
|
||||
scales = self.get_scales()
|
||||
return "%s %s %s" % (self.manufacturer.name, self.years, scales)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"bookshelf_item",
|
||||
kwargs={"selector": "catalog", "uuid": self.uuid}
|
||||
)
|
||||
|
||||
def get_scales(self):
|
||||
return "/".join([s.scale for s in self.scales.all()])
|
||||
get_scales.short_description = "Scales"
|
||||
|
@@ -1,10 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
from bookshelf.models import Book, Catalog, Author, Publisher
|
||||
from metadata.serializers import (
|
||||
ScaleSerializer,
|
||||
ManufacturerSerializer,
|
||||
TagSerializer
|
||||
)
|
||||
from bookshelf.models import Book, Author, Publisher
|
||||
from metadata.serializers import TagSerializer
|
||||
|
||||
|
||||
class AuthorSerializer(serializers.ModelSerializer):
|
||||
@@ -26,26 +22,5 @@ class BookSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
exclude = (
|
||||
"notes",
|
||||
"shop",
|
||||
"purchase_date",
|
||||
"price",
|
||||
)
|
||||
read_only_fields = ("creation_time", "updated_time")
|
||||
|
||||
|
||||
class CatalogSerializer(serializers.ModelSerializer):
|
||||
scales = ScaleSerializer(many=True)
|
||||
manufacturer = ManufacturerSerializer()
|
||||
tags = TagSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Catalog
|
||||
exclude = (
|
||||
"notes",
|
||||
"shop",
|
||||
"purchase_date",
|
||||
"price",
|
||||
)
|
||||
fields = "__all__"
|
||||
read_only_fields = ("creation_time", "updated_time")
|
||||
|
@@ -1,9 +1,7 @@
|
||||
from django.urls import path
|
||||
from bookshelf.views import BookList, BookGet, CatalogList, CatalogGet
|
||||
from bookshelf.views import BookList, BookGet
|
||||
|
||||
urlpatterns = [
|
||||
path("book/list", BookList.as_view()),
|
||||
path("book/get/<uuid:uuid>", BookGet.as_view()),
|
||||
path("catalog/list", CatalogList.as_view()),
|
||||
path("catalog/get/<uuid:uuid>", CatalogGet.as_view()),
|
||||
path("book/get/<str:uuid>", BookGet.as_view()),
|
||||
]
|
||||
|
@@ -1,40 +1,18 @@
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||
from rest_framework.schemas.openapi import AutoSchema
|
||||
|
||||
from ram.views import CustomLimitOffsetPagination
|
||||
from bookshelf.models import Book, Catalog
|
||||
from bookshelf.serializers import BookSerializer, CatalogSerializer
|
||||
from bookshelf.models import Book
|
||||
from bookshelf.serializers import BookSerializer
|
||||
|
||||
|
||||
class BookList(ListAPIView):
|
||||
queryset = Book.objects.all()
|
||||
serializer_class = BookSerializer
|
||||
pagination_class = CustomLimitOffsetPagination
|
||||
|
||||
def get_queryset(self):
|
||||
return Book.objects.get_published(self.request.user)
|
||||
|
||||
|
||||
class BookGet(RetrieveAPIView):
|
||||
queryset = Book.objects.all()
|
||||
serializer_class = BookSerializer
|
||||
lookup_field = "uuid"
|
||||
|
||||
schema = AutoSchema(operation_id_base="retrieveBookByUUID")
|
||||
|
||||
def get_queryset(self):
|
||||
return Book.objects.get_published(self.request.user)
|
||||
|
||||
|
||||
class CatalogList(ListAPIView):
|
||||
serializer_class = CatalogSerializer
|
||||
pagination_class = CustomLimitOffsetPagination
|
||||
|
||||
def get_queryset(self):
|
||||
return Catalog.objects.get_published(self.request.user)
|
||||
|
||||
|
||||
class CatalogGet(RetrieveAPIView):
|
||||
serializer_class = CatalogSerializer
|
||||
lookup_field = "uuid"
|
||||
schema = AutoSchema(operation_id_base="retrieveCatalogByUUID")
|
||||
|
||||
def get_queryset(self):
|
||||
return Book.objects.get_published(self.request.user)
|
||||
|
@@ -1,8 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||
|
||||
from ram.admin import publish, unpublish
|
||||
from consist.models import Consist, ConsistItem
|
||||
|
||||
|
||||
@@ -10,15 +8,7 @@ class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||
model = ConsistItem
|
||||
min_num = 1
|
||||
extra = 0
|
||||
autocomplete_fields = ("rolling_stock",)
|
||||
readonly_fields = (
|
||||
"preview",
|
||||
"published",
|
||||
"address",
|
||||
"type",
|
||||
"company",
|
||||
"era",
|
||||
)
|
||||
readonly_fields = ("address", "type", "company", "era")
|
||||
|
||||
|
||||
@admin.register(Consist)
|
||||
@@ -28,37 +18,26 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||
"creation_time",
|
||||
"updated_time",
|
||||
)
|
||||
list_filter = ("company", "era", "published")
|
||||
list_display = ("__str__",) + list_filter + ("country_flag",)
|
||||
search_fields = ("identifier",) + list_filter
|
||||
list_display = ("identifier", "company", "era")
|
||||
list_filter = list_display
|
||||
search_fields = list_display
|
||||
save_as = True
|
||||
|
||||
@admin.display(description="Country")
|
||||
def country_flag(self, obj):
|
||||
return format_html(
|
||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country)
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": (
|
||||
"published",
|
||||
"identifier",
|
||||
"consist_address",
|
||||
"company",
|
||||
"era",
|
||||
"description",
|
||||
"image",
|
||||
"notes",
|
||||
"tags",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Notes",
|
||||
{"classes": ("collapse",), "fields": ("notes",)},
|
||||
),
|
||||
(
|
||||
"Audit",
|
||||
{
|
||||
@@ -70,4 +49,3 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||
},
|
||||
),
|
||||
)
|
||||
actions = [publish, unpublish]
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# Generated by Django 4.1 on 2022-08-23 15:54
|
||||
|
||||
# ckeditor removal
|
||||
# import ckeditor_uploader.fields
|
||||
import ckeditor_uploader.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -12,9 +11,9 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
# migrations.AlterField(
|
||||
# model_name="consist",
|
||||
# name="notes",
|
||||
# field=ckeditor_uploader.fields.RichTextUploadingField(blank=True),
|
||||
# ),
|
||||
migrations.AlterField(
|
||||
model_name="consist",
|
||||
name="notes",
|
||||
field=ckeditor_uploader.fields.RichTextUploadingField(blank=True),
|
||||
),
|
||||
]
|
||||
|
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-20 12:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("consist", "0010_alter_consist_notes"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="consist",
|
||||
name="consist_address",
|
||||
field=models.SmallIntegerField(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="DCC consist address if enabled",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="consist",
|
||||
name="era",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Era or epoch of the consist", max_length=32
|
||||
),
|
||||
),
|
||||
]
|
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 12:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("consist", "0011_alter_consist_consist_address_alter_consist_era"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="consist",
|
||||
name="published",
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-08 21:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("consist", "0012_consist_published"),
|
||||
("roster", "0030_rollingstock_price"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddConstraint(
|
||||
model_name="consistitem",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("consist", "rolling_stock"), name="one_stock_per_consist"
|
||||
),
|
||||
),
|
||||
]
|
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-08 22:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("consist", "0013_consistitem_one_stock_per_consist"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="consistitem",
|
||||
name="order",
|
||||
field=models.PositiveIntegerField(default=1000),
|
||||
),
|
||||
]
|
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-27 21:15
|
||||
|
||||
import tinymce.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("consist", "0014_alter_consistitem_order"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="consist",
|
||||
name="description",
|
||||
field=tinymce.models.HTMLField(blank=True),
|
||||
),
|
||||
]
|
@@ -1,37 +1,34 @@
|
||||
import os
|
||||
|
||||
from uuid import uuid4
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.dispatch import receiver
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from ram.models import BaseModel
|
||||
from tinymce import models as tinymce
|
||||
|
||||
from ram.utils import DeduplicatedStorage
|
||||
from metadata.models import Company, Tag
|
||||
from roster.models import RollingStock
|
||||
|
||||
|
||||
class Consist(BaseModel):
|
||||
class Consist(models.Model):
|
||||
uuid = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
identifier = models.CharField(max_length=128, unique=False)
|
||||
tags = models.ManyToManyField(Tag, related_name="consist", blank=True)
|
||||
consist_address = models.SmallIntegerField(
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="DCC consist address if enabled",
|
||||
default=None, null=True, blank=True
|
||||
)
|
||||
company = models.ForeignKey(Company, on_delete=models.CASCADE)
|
||||
era = models.CharField(
|
||||
max_length=32,
|
||||
blank=True,
|
||||
help_text="Era or epoch of the consist",
|
||||
)
|
||||
era = models.CharField(max_length=32, blank=True)
|
||||
image = models.ImageField(
|
||||
upload_to=os.path.join("images", "consists"),
|
||||
storage=DeduplicatedStorage,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
notes = tinymce.HTMLField(blank=True)
|
||||
creation_time = models.DateTimeField(auto_now_add=True)
|
||||
updated_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{0} {1}".format(self.company, self.identifier)
|
||||
@@ -39,16 +36,6 @@ class Consist(BaseModel):
|
||||
def get_absolute_url(self):
|
||||
return reverse("consist", kwargs={"uuid": self.uuid})
|
||||
|
||||
@property
|
||||
def country(self):
|
||||
return self.company.country
|
||||
|
||||
def clean(self):
|
||||
if self.consist_item.filter(rolling_stock__published=False).exists():
|
||||
raise ValidationError(
|
||||
"You must publish all items in the consist before publishing the consist." # noqa: E501
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ["company", "-creation_time"]
|
||||
|
||||
@@ -58,55 +45,22 @@ class ConsistItem(models.Model):
|
||||
Consist, on_delete=models.CASCADE, related_name="consist_item"
|
||||
)
|
||||
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE)
|
||||
order = models.PositiveIntegerField(
|
||||
default=1000, # make sure it is always added at the end
|
||||
blank=False,
|
||||
null=False
|
||||
)
|
||||
order = models.PositiveIntegerField(default=0, blank=False, null=False)
|
||||
|
||||
class Meta:
|
||||
class Meta(object):
|
||||
ordering = ["order"]
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["consist", "rolling_stock"],
|
||||
name="one_stock_per_consist"
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "{0}".format(self.rolling_stock)
|
||||
|
||||
def published(self):
|
||||
return self.rolling_stock.published
|
||||
published.boolean = True
|
||||
|
||||
def preview(self):
|
||||
return self.rolling_stock.image.first().image_thumbnail(100)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self.rolling_stock.rolling_class.type
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return self.rolling_stock.address
|
||||
|
||||
@property
|
||||
def company(self):
|
||||
return self.rolling_stock.company
|
||||
return self.rolling_stock.company()
|
||||
|
||||
@property
|
||||
def era(self):
|
||||
return self.rolling_stock.era
|
||||
|
||||
|
||||
# Unpublish any consist that contains an unpublished rolling stock
|
||||
# this signal is called after a rolling stock is saved
|
||||
# it is hosted here to avoid circular imports
|
||||
@receiver(models.signals.post_save, sender=RollingStock)
|
||||
def post_save_unpublish_consist(sender, instance, *args, **kwargs):
|
||||
if not instance.published:
|
||||
consists = Consist.objects.filter(consist_item__rolling_stock=instance)
|
||||
for consist in consists:
|
||||
consist.published = False
|
||||
consist.save()
|
||||
|
@@ -21,5 +21,4 @@ class ConsistSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Consist
|
||||
exclude = ("notes",)
|
||||
read_only_fields = ("creation_time", "updated_time")
|
||||
fields = "__all__"
|
||||
|
@@ -1,21 +1,15 @@
|
||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||
|
||||
from ram.views import CustomLimitOffsetPagination
|
||||
from consist.models import Consist
|
||||
from consist.serializers import ConsistSerializer
|
||||
|
||||
|
||||
class ConsistList(ListAPIView):
|
||||
queryset = Consist.objects.all()
|
||||
serializer_class = ConsistSerializer
|
||||
pagination_class = CustomLimitOffsetPagination
|
||||
|
||||
def get_queryset(self):
|
||||
return Consist.objects.get_published(self.request.user)
|
||||
|
||||
|
||||
class ConsistGet(RetrieveAPIView):
|
||||
queryset = Consist.objects.all()
|
||||
serializer_class = ConsistSerializer
|
||||
lookup_field = "uuid"
|
||||
|
||||
def get_queryset(self):
|
||||
return Consist.objects.get_published(self.request.user)
|
||||
|
@@ -1,13 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from adminsortable2.admin import SortableAdminMixin
|
||||
|
||||
from repository.models import DecoderDocument
|
||||
from metadata.models import (
|
||||
Property,
|
||||
Decoder,
|
||||
DecoderDocument,
|
||||
Scale,
|
||||
Shop,
|
||||
Manufacturer,
|
||||
Company,
|
||||
Tag,
|
||||
@@ -47,30 +45,18 @@ class ScaleAdmin(admin.ModelAdmin):
|
||||
@admin.register(Company)
|
||||
class CompanyAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("logo_thumbnail",)
|
||||
list_display = ("name", "country_flag")
|
||||
list_filter = ("name", "country")
|
||||
list_display = ("name", "country")
|
||||
list_filter = list_display
|
||||
search_fields = ("name",)
|
||||
|
||||
@admin.display(description="Country")
|
||||
def country_flag(self, obj):
|
||||
return format_html(
|
||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Manufacturer)
|
||||
class ManufacturerAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("logo_thumbnail",)
|
||||
list_display = ("name", "category", "country_flag")
|
||||
list_display = ("name", "category")
|
||||
list_filter = ("category",)
|
||||
search_fields = ("name",)
|
||||
|
||||
@admin.display(description="Country")
|
||||
def country_flag(self, obj):
|
||||
return format_html(
|
||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Tag)
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
@@ -84,10 +70,3 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
|
||||
list_display = ("__str__",)
|
||||
list_filter = ("type", "category")
|
||||
search_fields = ("type", "category")
|
||||
|
||||
|
||||
@admin.register(Shop)
|
||||
class ShopAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "on_line", "active")
|
||||
list_filter = ("on_line", "active")
|
||||
search_fields = ("name",)
|
||||
|
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-04-20 12:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("metadata", "0016_alter_decoderdocument_file"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="property",
|
||||
name="private",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Property will be only visible to logged users"
|
||||
),
|
||||
),
|
||||
]
|
@@ -1,69 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 21:17
|
||||
|
||||
import django.db.migrations.operations.special
|
||||
import metadata.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def gen_ratio(apps, schema_editor):
|
||||
Scale = apps.get_model('metadata', 'Scale')
|
||||
for row in Scale.objects.all():
|
||||
row.ratio_int = metadata.models.calculate_ratio(row.ratio)
|
||||
row.save(update_fields=['ratio_int'])
|
||||
|
||||
|
||||
def convert_tarcks(apps, schema_editor):
|
||||
Scale = apps.get_model("metadata", "Scale")
|
||||
for row in Scale.objects.all():
|
||||
row.tracks = "".join(
|
||||
filter(
|
||||
lambda x: str.isdigit(x) or x == "." or x == ",",
|
||||
row.tracks
|
||||
)
|
||||
)
|
||||
row.save(update_fields=["tracks"])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('metadata', '0017_alter_property_private'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='decoder',
|
||||
options={'ordering': ['manufacturer__name', 'name']},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='scale',
|
||||
options={'ordering': ['ratio_int', 'scale']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='scale',
|
||||
name='ratio_int',
|
||||
field=models.SmallIntegerField(default=0, editable=False),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=gen_ratio,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='scale',
|
||||
name='ratio',
|
||||
field=models.CharField(max_length=16, validators=[metadata.models.calculate_ratio]),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='scale',
|
||||
options={'ordering': ['-ratio_int', '-tracks', 'scale']},
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=convert_tarcks,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='scale',
|
||||
name='tracks',
|
||||
field=models.FloatField(help_text='Distance between model tracks in mm'),
|
||||
),
|
||||
]
|
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 21:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("metadata", "0018_alter_decoder_options_alter_scale_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="scale",
|
||||
name="gauge",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Distance between real tracks. Please specify the unit (mm, in, ...)",
|
||||
max_length=16,
|
||||
),
|
||||
),
|
||||
]
|
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-08 22:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("metadata", "0019_alter_scale_gauge"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name="decoderdocument",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="rollingstocktype",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="decoderdocument",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("decoder", "file"), name="unique_decoder_file"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="rollingstocktype",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("category", "type"), name="unique_category_type"
|
||||
),
|
||||
),
|
||||
]
|
@@ -1,40 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-17 09:31
|
||||
|
||||
import ram.utils
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("metadata", "0020_alter_decoderdocument_unique_together_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GenericDocument",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("description", models.CharField(blank=True, max_length=128)),
|
||||
(
|
||||
"file",
|
||||
models.FileField(
|
||||
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
|
||||
),
|
||||
),
|
||||
("private", models.BooleanField(default=False)),
|
||||
("tags", models.ManyToManyField(blank=True, to="metadata.tag")),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "Generic Documents",
|
||||
},
|
||||
),
|
||||
]
|
@@ -1,66 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-18 11:20
|
||||
|
||||
import django.utils.timezone
|
||||
import django_countries.fields
|
||||
import tinymce.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("metadata", "0021_genericdocument"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="decoderdocument",
|
||||
name="creation_time",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="decoderdocument",
|
||||
name="updated_time",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="genericdocument",
|
||||
name="creation_time",
|
||||
field=models.DateTimeField(
|
||||
auto_now_add=True, default=django.utils.timezone.now
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="genericdocument",
|
||||
name="notes",
|
||||
field=tinymce.models.HTMLField(blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="genericdocument",
|
||||
name="updated_time",
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="manufacturer",
|
||||
name="country",
|
||||
field=django_countries.fields.CountryField(blank=True, max_length=2),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="decoderdocument",
|
||||
name="private",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Document will be visible only to logged users"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="genericdocument",
|
||||
name="private",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Document will be visible only to logged users"
|
||||
),
|
||||
),
|
||||
]
|
@@ -1,40 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-26 14:27
|
||||
|
||||
import django_countries.fields
|
||||
import django.db.models.functions.text
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("metadata", "0022_decoderdocument_creation_time_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Shop",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=128, unique=True)),
|
||||
(
|
||||
"country",
|
||||
django_countries.fields.CountryField(blank=True, max_length=2),
|
||||
),
|
||||
("website", models.URLField(blank=True)),
|
||||
("on_line", models.BooleanField(default=True)),
|
||||
("active", models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
"ordering": [django.db.models.functions.text.Lower("name")],
|
||||
},
|
||||
),
|
||||
]
|
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-02-09 13:47
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("metadata", "0023_shop"),
|
||||
("repository", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="genericdocument",
|
||||
name="tags",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="DecoderDocument",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="GenericDocument",
|
||||
),
|
||||
]
|
@@ -3,19 +3,15 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.dispatch.dispatcher import receiver
|
||||
from django.core.exceptions import ValidationError
|
||||
from django_countries.fields import CountryField
|
||||
|
||||
from ram.models import Document
|
||||
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
|
||||
from ram.managers import PublicManager
|
||||
|
||||
|
||||
class Property(models.Model):
|
||||
name = models.CharField(max_length=128, unique=True)
|
||||
private = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Property will be only visible to logged users",
|
||||
)
|
||||
private = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Properties"
|
||||
@@ -24,8 +20,6 @@ class Property(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
objects = PublicManager()
|
||||
|
||||
|
||||
class Manufacturer(models.Model):
|
||||
name = models.CharField(max_length=128, unique=True)
|
||||
@@ -33,7 +27,6 @@ class Manufacturer(models.Model):
|
||||
category = models.CharField(
|
||||
max_length=64, choices=settings.MANUFACTURER_TYPES
|
||||
)
|
||||
country = CountryField(blank=True)
|
||||
website = models.URLField(blank=True)
|
||||
logo = models.ImageField(
|
||||
upload_to=os.path.join("images", "manufacturers"),
|
||||
@@ -50,11 +43,10 @@ class Manufacturer(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"filtered",
|
||||
kwargs={
|
||||
"filtered", kwargs={
|
||||
"_filter": "manufacturer",
|
||||
"search": self.slug,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def logo_thumbnail(self):
|
||||
@@ -85,11 +77,10 @@ class Company(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"filtered",
|
||||
kwargs={
|
||||
"filtered", kwargs={
|
||||
"_filter": "company",
|
||||
"search": self.slug,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def extended_name_pp(self):
|
||||
@@ -117,8 +108,8 @@ class Decoder(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ["manufacturer__name", "name"]
|
||||
class Meta(object):
|
||||
ordering = ["manufacturer", "name"]
|
||||
|
||||
def __str__(self):
|
||||
return "{0} - {1}".format(self.manufacturer, self.name)
|
||||
@@ -129,49 +120,37 @@ class Decoder(models.Model):
|
||||
image_thumbnail.short_description = "Preview"
|
||||
|
||||
|
||||
def calculate_ratio(ratio):
|
||||
try:
|
||||
num, den = ratio.split(":")
|
||||
return int(num) / float(den) * 10000
|
||||
except (ValueError, ZeroDivisionError):
|
||||
raise ValidationError("Invalid ratio format")
|
||||
class DecoderDocument(Document):
|
||||
decoder = models.ForeignKey(
|
||||
Decoder, on_delete=models.CASCADE, related_name="document"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ("decoder", "file")
|
||||
|
||||
|
||||
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, validators=[calculate_ratio])
|
||||
ratio_int = models.SmallIntegerField(editable=False, default=0)
|
||||
tracks = models.FloatField(
|
||||
help_text="Distance between model tracks in mm",
|
||||
)
|
||||
gauge = models.CharField(
|
||||
max_length=16,
|
||||
blank=True,
|
||||
help_text="Distance between real tracks. Please specify the unit (mm, in, ...)", # noqa: E501
|
||||
)
|
||||
ratio = models.CharField(max_length=16, blank=True)
|
||||
gauge = models.CharField(max_length=16, blank=True)
|
||||
tracks = models.CharField(max_length=16, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-ratio_int", "-tracks", "scale"]
|
||||
ordering = ["scale"]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"filtered",
|
||||
kwargs={
|
||||
"filtered", kwargs={
|
||||
"_filter": "scale",
|
||||
"search": self.slug,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.scale)
|
||||
|
||||
|
||||
@receiver(models.signals.pre_save, sender=Scale)
|
||||
def scale_save(sender, instance, **kwargs):
|
||||
instance.ratio_int = calculate_ratio(instance.ratio)
|
||||
|
||||
|
||||
class RollingStockType(models.Model):
|
||||
type = models.CharField(max_length=64)
|
||||
order = models.PositiveSmallIntegerField()
|
||||
@@ -180,22 +159,16 @@ class RollingStockType(models.Model):
|
||||
)
|
||||
slug = models.CharField(max_length=128, unique=True, editable=False)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["category", "type"],
|
||||
name="unique_category_type"
|
||||
)
|
||||
]
|
||||
class Meta(object):
|
||||
unique_together = ("category", "type")
|
||||
ordering = ["order"]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"filtered",
|
||||
kwargs={
|
||||
"filtered", kwargs={
|
||||
"_filter": "type",
|
||||
"search": self.slug,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
@@ -206,7 +179,7 @@ class Tag(models.Model):
|
||||
name = models.CharField(max_length=128, unique=True)
|
||||
slug = models.CharField(max_length=128, unique=True)
|
||||
|
||||
class Meta:
|
||||
class Meta(object):
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
@@ -214,28 +187,13 @@ class Tag(models.Model):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"filtered",
|
||||
kwargs={
|
||||
"filtered", kwargs={
|
||||
"_filter": "tag",
|
||||
"search": self.slug,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Shop(models.Model):
|
||||
name = models.CharField(max_length=128, unique=True)
|
||||
country = CountryField(blank=True)
|
||||
website = models.URLField(blank=True)
|
||||
on_line = models.BooleanField(default=True)
|
||||
active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = [models.functions.Lower("name"),]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
@receiver(models.signals.pre_save, sender=Manufacturer)
|
||||
@receiver(models.signals.pre_save, sender=Company)
|
||||
@receiver(models.signals.pre_save, sender=Scale)
|
||||
|
@@ -1,15 +1,12 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from solo.admin import SingletonModelAdmin
|
||||
from tinymce.widgets import TinyMCE
|
||||
|
||||
from ram.admin import publish, unpublish
|
||||
from portal.models import SiteConfiguration, Flatpage
|
||||
|
||||
|
||||
@admin.register(SiteConfiguration)
|
||||
class SiteConfigurationAdmin(SingletonModelAdmin):
|
||||
readonly_fields = ("site_name", "rest_api", "version")
|
||||
readonly_fields = ("site_name",)
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
@@ -20,10 +17,8 @@ class SiteConfigurationAdmin(SingletonModelAdmin):
|
||||
"about",
|
||||
"items_per_page",
|
||||
"items_ordering",
|
||||
"currency",
|
||||
"footer",
|
||||
"footer_extended",
|
||||
"disclaimer",
|
||||
)
|
||||
},
|
||||
),
|
||||
@@ -35,30 +30,11 @@ class SiteConfigurationAdmin(SingletonModelAdmin):
|
||||
"show_version",
|
||||
"use_cdn",
|
||||
"extra_head",
|
||||
"rest_api",
|
||||
"version",
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@admin.display(description="REST API enabled", boolean=True)
|
||||
def rest_api(self, obj):
|
||||
return settings.REST_ENABLED
|
||||
|
||||
@admin.display()
|
||||
def version(self, obj):
|
||||
return "{} (Django {})".format(obj.version, obj.django_version)
|
||||
|
||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||
if db_field.name in ("footer", "footer_extended", "disclaimer"):
|
||||
return db_field.formfield(
|
||||
widget=TinyMCE(
|
||||
mce_attrs={"height": "200"},
|
||||
)
|
||||
)
|
||||
return super().formfield_for_dbfield(db_field, **kwargs)
|
||||
|
||||
|
||||
@admin.register(Flatpage)
|
||||
class FlatpageAdmin(admin.ModelAdmin):
|
||||
@@ -90,4 +66,3 @@ class FlatpageAdmin(admin.ModelAdmin):
|
||||
},
|
||||
),
|
||||
)
|
||||
actions = [publish, unpublish]
|
||||
|
@@ -1,8 +1,7 @@
|
||||
# Generated by Django 4.1 on 2022-08-23 15:54
|
||||
|
||||
# ckeditor dependency removal
|
||||
# import ckeditor.fields
|
||||
# import ckeditor_uploader.fields
|
||||
import ckeditor.fields
|
||||
import ckeditor_uploader.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -13,24 +12,24 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
# migrations.AlterField(
|
||||
# model_name="flatpage",
|
||||
# name="content",
|
||||
# field=ckeditor_uploader.fields.RichTextUploadingField(),
|
||||
# ),
|
||||
# migrations.AlterField(
|
||||
# model_name="siteconfiguration",
|
||||
# name="about",
|
||||
# field=ckeditor.fields.RichTextField(blank=True),
|
||||
# ),
|
||||
# migrations.AlterField(
|
||||
# model_name="siteconfiguration",
|
||||
# name="footer",
|
||||
# field=ckeditor.fields.RichTextField(blank=True),
|
||||
# ),
|
||||
# migrations.AlterField(
|
||||
# model_name="siteconfiguration",
|
||||
# name="footer_extended",
|
||||
# field=ckeditor.fields.RichTextField(blank=True),
|
||||
# ),
|
||||
migrations.AlterField(
|
||||
model_name="flatpage",
|
||||
name="content",
|
||||
field=ckeditor_uploader.fields.RichTextUploadingField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="siteconfiguration",
|
||||
name="about",
|
||||
field=ckeditor.fields.RichTextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="siteconfiguration",
|
||||
name="footer",
|
||||
field=ckeditor.fields.RichTextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="siteconfiguration",
|
||||
name="footer_extended",
|
||||
field=ckeditor.fields.RichTextField(blank=True),
|
||||
),
|
||||
]
|
||||
|
@@ -1,21 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-29 15:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"portal",
|
||||
"0017_alter_flatpage_content_alter_siteconfiguration_about_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="siteconfiguration",
|
||||
name="currency",
|
||||
field=models.CharField(default="EUR", max_length=3),
|
||||
),
|
||||
]
|
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-30 16:39
|
||||
|
||||
import tinymce.models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("portal", "0018_siteconfiguration_currency"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="siteconfiguration",
|
||||
name="disclaimer",
|
||||
field=tinymce.models.HTMLField(blank=True),
|
||||
),
|
||||
]
|
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2025-02-01 23:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("portal", "0019_siteconfiguration_disclaimer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="flatpage",
|
||||
options={"verbose_name": "page", "verbose_name_plural": "pages"},
|
||||
),
|
||||
]
|
@@ -9,7 +9,6 @@ from solo.models import SingletonModel
|
||||
from tinymce import models as tinymce
|
||||
|
||||
from ram import __version__ as app_version
|
||||
from ram.managers import PublicManager
|
||||
from ram.utils import slugify
|
||||
|
||||
|
||||
@@ -30,10 +29,8 @@ class SiteConfiguration(SingletonModel):
|
||||
],
|
||||
default="type",
|
||||
)
|
||||
currency = models.CharField(max_length=3, default="EUR")
|
||||
footer = tinymce.HTMLField(blank=True)
|
||||
footer_extended = tinymce.HTMLField(blank=True)
|
||||
disclaimer = tinymce.HTMLField(blank=True)
|
||||
show_version = models.BooleanField(default=True)
|
||||
use_cdn = models.BooleanField(default=True)
|
||||
extra_head = models.TextField(blank=True)
|
||||
@@ -47,11 +44,9 @@ class SiteConfiguration(SingletonModel):
|
||||
def site_name(self):
|
||||
return settings.SITE_NAME
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return app_version
|
||||
|
||||
@property
|
||||
def django_version(self):
|
||||
return django.get_version()
|
||||
|
||||
@@ -64,10 +59,6 @@ class Flatpage(models.Model):
|
||||
creation_time = models.DateTimeField(auto_now_add=True)
|
||||
updated_time = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "page"
|
||||
verbose_name_plural = "pages"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -81,8 +72,6 @@ class Flatpage(models.Model):
|
||||
)
|
||||
)
|
||||
|
||||
objects = PublicManager()
|
||||
|
||||
|
||||
@receiver(models.signals.pre_save, sender=Flatpage)
|
||||
def tag_pre_save(sender, instance, **kwargs):
|
||||
|
@@ -1,14 +1,14 @@
|
||||
/*!
|
||||
* Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/)
|
||||
* Copyright 2019-2024 The Bootstrap Authors
|
||||
* 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?dd67030699838ea613ee6dbda90effa6") format("woff2"),
|
||||
url("./fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("woff");
|
||||
src: url("./fonts/bootstrap-icons.woff2?2820a3852bdb9a5832199cc61cec4e65") format("woff2"),
|
||||
url("./fonts/bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff");
|
||||
}
|
||||
|
||||
.bi::before,
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -17,11 +17,6 @@ td > img.logo-xl {
|
||||
max-height: 96px;
|
||||
}
|
||||
|
||||
/* Disable margin on last <p> in a <td> */
|
||||
td > p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.btn > span {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -43,6 +38,10 @@ a.badge, a.badge:hover {
|
||||
border-top: calc(var(--bs-border-width) * 3) solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
#nav-notes > p {
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
#nav-journal ul, #nav-journal ol {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
|
@@ -16,11 +16,11 @@
|
||||
<link rel="icon" href="{% static "favicon.png" %}" sizes="any">
|
||||
<link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml">
|
||||
{% if site_conf.use_cdn %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.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@5.3.3/dist/css/bootstrap.min.css" %}" rel="stylesheet">
|
||||
<link href="{% static "bootstrap-icons@1.11.3/font/bootstrap-icons.css" %}" rel="stylesheet">
|
||||
<link href="{% static "bootstrap@5.3.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>
|
||||
@@ -118,16 +118,14 @@
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var selectElement = document.getElementById('tabSelector');
|
||||
try {
|
||||
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');
|
||||
});
|
||||
} catch (TypeError) { /* pass */ }
|
||||
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 %}
|
||||
@@ -182,13 +180,13 @@
|
||||
<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 'rolling_stock_types' %}">Type</a></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>
|
||||
</li>
|
||||
{% show_bookshelf_menu %}
|
||||
{% show_flatpages_menu user %}
|
||||
{% show_flatpages_menu %}
|
||||
</ul>
|
||||
{% include 'includes/search.html' %}
|
||||
</div>
|
||||
@@ -218,9 +216,9 @@
|
||||
</main>
|
||||
{% include 'includes/footer.html' %}
|
||||
{% if site_conf.use_cdn %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% else %}
|
||||
<script src="{% static "bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" %}"></script>
|
||||
<script src="{% static "bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" %}"></script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,5 +1,4 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load dynamic_url %}
|
||||
|
||||
{% block header %}
|
||||
{% if book.tags.all %}
|
||||
@@ -9,7 +8,7 @@
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
||||
<small class="text-muted">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
||||
{% endblock %}
|
||||
{% block carousel %}
|
||||
<div class="row">
|
||||
@@ -28,11 +27,11 @@
|
||||
{% if book.image.count > 1 %}
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#carouselControls" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -46,54 +45,35 @@
|
||||
<div class="mx-auto">
|
||||
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
|
||||
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
|
||||
{% if 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 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 %}
|
||||
</nav>
|
||||
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
||||
<option value="nav-summary" selected>Summary</option>
|
||||
{% if documents %}<option value="nav-documents">Documents</option>{% endif %}
|
||||
{% if book.notes %}<option value="nav-notes">Notes</option>{% endif %}
|
||||
</select>
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">
|
||||
{% if type == "catalog" %}Catalog
|
||||
{% elif type == "book" %}Book{% endif %}
|
||||
<div class="float-end">
|
||||
{% if not book.published %}
|
||||
<span class="badge text-bg-warning">Draft</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<th colspan="2" scope="row">Book</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
{% if type == "catalog" %}
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Manufacturer</th>
|
||||
<td>{{ book.manufacturer }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Scales</th>
|
||||
<td>{{ book.get_scales }}</td>
|
||||
</tr>
|
||||
{% elif type == "book" %}
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Title</th>
|
||||
<td>{{ book.title }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Authors</th>
|
||||
<td>
|
||||
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
|
||||
</td>
|
||||
<th scope="row">Authors</th>
|
||||
<td>
|
||||
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Publisher</th>
|
||||
<th scope="row">Publisher</th>
|
||||
<td>{{ book.publisher }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">ISBN</th>
|
||||
<td>{{ book.ISBN|default:"-" }}</td>
|
||||
@@ -110,41 +90,13 @@
|
||||
<th scope="row">Publication year</th>
|
||||
<td>{{ book.publication_year|default:"-" }}</td>
|
||||
</tr>
|
||||
{% if book.description %}
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Description</th>
|
||||
<td>{{ book.description | safe }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if request.user.is_staff %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">Purchase</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Shop</th>
|
||||
<td>
|
||||
{{ book.shop|default:"-" }}
|
||||
{% if book.shop.website %} <a href="{{ book.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Purchase date</th>
|
||||
<th scope="row">Purchase date</th>
|
||||
<td>{{ book.purchase_date|default:"-" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Price ({{ site_conf.currency }})</th>
|
||||
<td>{{ book.price|default:"-" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if properties %}
|
||||
{% if book_properties %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -152,7 +104,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
{% for p in properties %}
|
||||
{% for p in book_properties %}
|
||||
<tr>
|
||||
<th class="w-33" scope="row">{{ p.property }}</th>
|
||||
<td>{{ p.value }}</td>
|
||||
@@ -162,27 +114,12 @@
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3" scope="row">Documents</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
{% for d in documents.all %}
|
||||
<tr>
|
||||
<td class="w-33">{{ d.description }}</td>
|
||||
<td><a href="{{ d.file.url }}" target="_blank">{{ d.filename }}</a></td>
|
||||
<td class="text-end">{{ d.file.size | filesizeformat }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
|
||||
{{ book.notes | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% dynamic_admin_url 'bookshelf' type book.pk %}">Edit</a>{% endif %}
|
||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:bookshelf_book_change' book.pk %}">Edit</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,15 +1,15 @@
|
||||
{% extends "cards.html" %}
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||
<a class="page-link" href="{% url 'books_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
@@ -21,17 +21,17 @@
|
||||
{% if i == data.paginator.ELLIPSIS %}
|
||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=i %}#main-content">{{ i }}</a></li>
|
||||
<li class="page-item"><a class="page-link" href="{% url 'books_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'manufacturer_pagination' manufacturer=manufacturer.slug search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||
<a class="page-link" href="{% url 'books_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
@@ -4,12 +4,7 @@
|
||||
Bookshelf
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink">
|
||||
{% if books_menu %}
|
||||
<li><a class="dropdown-item" href="{% url 'books' %}">Books</a></li>
|
||||
{% endif %}
|
||||
{% if catalogs_menu %}
|
||||
<li><a class="dropdown-item" href="{% url 'catalogs' %}">Catalogs</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% block header %}
|
||||
<p class="lead text-body-secondary">Results found: {{ matches }}</p>
|
||||
<p class="lead text-muted">Results found: {{ matches }}</p>
|
||||
{% endblock %}
|
||||
{% block cards_layout %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
||||
{% block cards %}
|
||||
{% for d in data %}
|
||||
{% if d.type == "roster" %}
|
||||
{% if d.type == "rolling_stock" %}
|
||||
{% include "cards/roster.html" %}
|
||||
{% elif d.type == "company" %}
|
||||
{% include "cards/company.html" %}
|
||||
@@ -18,7 +18,7 @@
|
||||
{% include "cards/consist.html" %}
|
||||
{% elif d.type == "manufacturer" %}
|
||||
{% include "cards/manufacturer.html" %}
|
||||
{% elif d.type == "book" or d.type == "catalog" %}
|
||||
{% elif d.type == "book" %}
|
||||
{% include "cards/book.html" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
@@ -1,12 +1,7 @@
|
||||
{% load static %}
|
||||
{% load dynamic_url %}
|
||||
<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;">
|
||||
@@ -23,28 +18,10 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">
|
||||
{% if d.type == "catalog" %}Catalog
|
||||
{% elif d.type == "book" %}Book{% endif %}
|
||||
<div class="float-end">
|
||||
{% if not d.item.published %}
|
||||
<span class="badge text-bg-warning">Draft</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<th colspan="2" scope="row">Book</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
{% if d.type == "catalog" %}
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Manufacturer</th>
|
||||
<td>{{ d.item.manufacturer }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Scales</th>
|
||||
<td>{{ d.item.get_scales }}</td>
|
||||
</tr>
|
||||
{% elif d.type == "book" %}
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Authors</th>
|
||||
<td>
|
||||
@@ -55,7 +32,6 @@
|
||||
<th class="w-33" scope="row">Publisher</th>
|
||||
<td>{{ d.item.publisher }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Language</th>
|
||||
<td>{{ d.item.get_language_display }}</td>
|
||||
@@ -72,7 +48,7 @@
|
||||
</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="{% dynamic_admin_url 'bookshelf' d.type d.item.pk %}">Edit</a>{% endif %}
|
||||
{% 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>
|
||||
|
@@ -7,14 +7,7 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">
|
||||
Company
|
||||
<div class="float-end">
|
||||
{% if d.item.freelance %}
|
||||
<span class="badge text-bg-secondary">Freelance</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<th colspan="2" scope="row">Company</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
@@ -34,8 +27,14 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Country</th>
|
||||
<td><img src="{{ d.item.country.flag }}" alt="{{ d.item.country }}"> {{ d.item.country.name }}</td>
|
||||
<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">
|
||||
|
@@ -24,17 +24,7 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">
|
||||
Consist
|
||||
<div class="float-end">
|
||||
{% if d.item.company.freelance %}
|
||||
<span class="badge text-bg-secondary">Freelance</span>
|
||||
{% endif %}
|
||||
{% if not d.item.published %}
|
||||
<span class="badge text-bg-warning">Draft</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<th colspan="2" scope="row">Consist</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
|
@@ -22,17 +22,7 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">
|
||||
Rolling stock
|
||||
<div class="float-end">
|
||||
{% if d.item.company.freelance %}
|
||||
<span class="badge text-bg-secondary">Freelance</span>
|
||||
{% endif %}
|
||||
{% if not d.item.published %}
|
||||
<span class="badge text-bg-warning">Draft</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<th colspan="2" scope="row">Rolling stock</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
@@ -43,7 +33,7 @@
|
||||
<tr>
|
||||
<th scope="row">Company</th>
|
||||
<td>
|
||||
<a href="{% url 'filtered' _filter="company" search=d.item.company.slug %}"><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></a>
|
||||
<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>
|
||||
@@ -66,15 +56,15 @@
|
||||
</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 }} mm">{{ d.item.scale }}</abbr></a></td>
|
||||
<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 }}{%if d.item.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=d.item.manufacturer.slug search=d.item.item_number_slug %}">SET</a>{% endif %}</td>
|
||||
<td>{{ d.item.item_number }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% if d.item.decoder or d.item.decoder_interface %}
|
||||
{% if d.item.decoder %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -83,19 +73,13 @@
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Interface</th>
|
||||
<td>{{ d.item.get_decoder_interface }}</td>
|
||||
</tr>
|
||||
{% if d.item.decoder %}
|
||||
<tr>
|
||||
<th scope="row">Decoder</th>
|
||||
<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>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
@@ -17,18 +17,18 @@
|
||||
<th class="w-33" scope="row">Ratio</th>
|
||||
<td>{{ d.item.ratio }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Tracks</th>
|
||||
<td>{{ d.item.tracks }} mm</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{% if d.item.num_items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show all rolling stock</a>
|
||||
<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>
|
||||
|
40
ram/portal/templates/companies.html
Normal file
40
ram/portal/templates/companies.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "cards.html" %}
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'companies_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
{% if data.number == i %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ i }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if i == data.paginator.ELLIPSIS %}
|
||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{% url 'companies_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'companies_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -7,7 +7,7 @@
|
||||
{{ t.name }}</a>{# new line is required #}
|
||||
{% endfor %}
|
||||
</p>
|
||||
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
||||
<small class="text-muted">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block carousel %}
|
||||
@@ -25,15 +25,15 @@
|
||||
{% endblock %}
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
@@ -51,11 +51,11 @@
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
@@ -68,26 +68,18 @@
|
||||
<div class="mx-auto">
|
||||
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
|
||||
<button class="nav-link active" id="nav-summary-tab" data-bs-toggle="tab" data-bs-target="#nav-summary" type="button" role="tab" aria-controls="nav-summary" aria-selected="true">Summary</button>
|
||||
{% if consist.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
|
||||
</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-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">
|
||||
Consist
|
||||
<div class="float-end">
|
||||
{% if consist.company.freelance %}
|
||||
<span class="badge text-bg-secondary">Freelance</span>
|
||||
{% endif %}
|
||||
{% if not consist.published %}
|
||||
<span class="badge text-bg-warning">Draft</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<th colspan="2" scope="row">Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
@@ -101,12 +93,6 @@
|
||||
<th scope="row">Era</th>
|
||||
<td>{{ consist.era }}</td>
|
||||
</tr>
|
||||
{% if consist.description %}
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{{ consist.description | safe }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Length</th>
|
||||
<td>{{ data | length }}</td>
|
||||
@@ -114,6 +100,20 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="row">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
<tr>
|
||||
<td>{{ consist.notes | safe }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' consist.pk %}">Edit</a>{% endif %}
|
||||
|
40
ram/portal/templates/consists.html
Normal file
40
ram/portal/templates/consists.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "cards.html" %}
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'consists_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
{% if data.number == i %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ i }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if i == data.paginator.ELLIPSIS %}
|
||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{% url 'consists_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'consists_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -1,15 +1,15 @@
|
||||
{% extends "cards.html" %}
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
@@ -27,11 +27,11 @@
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
@@ -1,19 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<small class="text-body-secondary">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
|
||||
{% endblock %}
|
||||
{% block carousel %}
|
||||
<small class="text-muted">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
|
||||
{% endblock %}
|
||||
{% block extra_content %}
|
||||
<section class="py-4 text-start container">
|
||||
<div class="row">
|
||||
<div class="mx-auto">
|
||||
{% if not flatpage.published %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
⚠️ This page is a <strong>draft</strong> and is not published.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>{{ flatpage.content | safe }} </div>
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:portal_flatpage_change' flatpage.pk %}">Edit</a>{% endif %}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{% if flatpages_menu %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="flatpageDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Pages
|
||||
Articles
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="flatpageDropdownMenuLink">
|
||||
{% for m in flatpages_menu %}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{% extends "pagination.html" %}
|
||||
{% extends "roster.html" %}
|
||||
|
||||
{% block header %}
|
||||
<div class="text-body-secondary">{{ site_conf.about | safe }}</div>
|
||||
<div class="text-muted">{{ site_conf.about | safe }}</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,34 +1,18 @@
|
||||
<footer class="text-body-secondary py-4">
|
||||
<div class="container d-lg-flex justify-content-between">
|
||||
<div id="footer" class="mb-1">
|
||||
<p>© {% now "Y" %}</p> {{ site_conf.footer | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="text-muted py-4">
|
||||
<div class="container">
|
||||
<div id="footer_extended">
|
||||
<p class="float-end mb-1">
|
||||
<a href="#">Back to top</a>
|
||||
</p>
|
||||
<div id="footer" class="mb-1">
|
||||
<p>© {% now "Y" %}</p> {{ site_conf.footer | safe }}
|
||||
</div>
|
||||
<div id="footer_extended" class="mb-0">
|
||||
{{ site_conf.footer_extended | safe }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container d-flex text-body-secondary">
|
||||
<p class="flex-fill small">Made with ❤️ for 🚂 and <i class="bi bi-github"></i> <a href="https://github.com/daniviga/django-ram">django-ram</a>
|
||||
{% if site_conf.show_version %}<br>Version {{ site_conf.version }}{% endif %}</p>
|
||||
<p class="text-end fs-5">
|
||||
{% if site_conf.disclaimer %}<a class="text-reset" title="Disclaimer" href="" data-bs-toggle="modal" data-bs-target="#disclaimerModal"><i class="bi bi-info-square-fill"></i></a> {% endif %}
|
||||
<a class="text-reset" title="Back to top" href="#"><i class="bi bi-arrow-up-left-square-fill"></i></a>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="disclaimerModal" tabindex="-1" aria-labelledby="disclaimerLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="disclaimerLabel">Disclaimer</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ site_conf.disclaimer | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<p class="small text-muted">Made with ❤️ for 🚂 and <a href="https://github.com/daniviga/django-ram">django-ram</a>
|
||||
{% if site_conf.show_version %}<br>Version {{ site_conf.version }}{% endif %}
|
||||
</div>
|
||||
</footer>
|
||||
|
@@ -18,12 +18,7 @@
|
||||
<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><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<form id="logout-form" method="post" action="{% url 'admin:logout' %}?next={{ request.path }}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-link dropdown-item text-danger" type="submit">Log out</button>
|
||||
</form>
|
||||
</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>
|
||||
|
@@ -2,15 +2,15 @@
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
{% with data.0.item.category as c %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
@@ -28,11 +28,11 @@
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
@@ -8,7 +8,7 @@
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<small class="text-body-secondary">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
|
||||
<small class="text-muted">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
|
||||
{% endblock %}
|
||||
{% block carousel %}
|
||||
<div class="row">
|
||||
@@ -27,11 +27,11 @@
|
||||
{% if rolling_stock.image.count > 1 %}
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#carouselControls" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
{% 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 documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
|
||||
{% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
|
||||
{% if set %}<button class="nav-link" id="nav-set-tab" data-bs-toggle="tab" data-bs-target="#nav-set" type="button" role="tab" aria-controls="nav-set" aria-selected="false">Set</button>{% endif %}
|
||||
{% 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 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>
|
||||
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
||||
@@ -62,7 +62,7 @@
|
||||
{% 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 set %}<option value="nav-set">Set</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 %}
|
||||
@@ -71,17 +71,7 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">
|
||||
Rolling stock
|
||||
<div class="mt-1 float-end">
|
||||
{% if company.freelance %}
|
||||
<span class="badge text-bg-secondary">Freelance</span>
|
||||
{% endif %}
|
||||
{% if not rolling_stock.published %}
|
||||
<span class="badge text-bg-warning">Draft</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
<th colspan="2" scope="row">Data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
@@ -92,13 +82,7 @@
|
||||
<tr>
|
||||
<th scope="row">Company</th>
|
||||
<td>
|
||||
<a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> {{ company.extended_name_pp }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Country</th>
|
||||
<td>
|
||||
<img src="{{ company.country.flag }}" alt="{{ company.country }}"> {{ company.country.name }}
|
||||
<a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> {{ company.extended_name_pp }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -130,11 +114,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Scale</th>
|
||||
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }} mm">{{ rolling_stock.scale }}</abbr></a></td>
|
||||
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Item number</th>
|
||||
<td>{{ rolling_stock.item_number }}{%if rolling_stock.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=rolling_stock.manufacturer.slug search=rolling_stock.item_number_slug %}">SET</a>{% endif %}</td>
|
||||
<td>{{ rolling_stock.item_number }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -148,7 +132,7 @@
|
||||
<tbody class="table-group-divider">
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Interface</th>
|
||||
<td>{{ rolling_stock.get_decoder_interface }}</td>
|
||||
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
|
||||
</tr>
|
||||
{% if rolling_stock.decoder %}
|
||||
<tr>
|
||||
@@ -182,11 +166,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Scale</th>
|
||||
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }} mm">{{ rolling_stock.scale }}</abbr></a></td>
|
||||
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Item number</th>
|
||||
<td>{{ rolling_stock.item_number }}{%if rolling_stock.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=rolling_stock.manufacturer.slug search=rolling_stock.item_number_slug %}">SET</a>{% endif %}</td>
|
||||
<td>{{ rolling_stock.item_number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Era</th>
|
||||
@@ -194,42 +178,14 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Production year</th>
|
||||
<td>{{ rolling_stock.production_year | default:"-" }}</td>
|
||||
</tr>
|
||||
{% if rolling_stock.description %}
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Description</th>
|
||||
<td>{{ rolling_stock.description | safe }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if request.user.is_staff %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">Purchase</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Shop</th>
|
||||
<td>
|
||||
{{ rolling_stock.shop | default:"-" }}
|
||||
{% if rolling_stock.shop.website %} <a href="{{ rolling_stock.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||
</td>
|
||||
<td>{{ rolling_stock.production_year|default:"-" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Purchase date</th>
|
||||
<td>{{ rolling_stock.purchase_date | default:"-" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Price ({{ site_conf.currency }})</th>
|
||||
<td>{{ rolling_stock.price | default:"-" }}</td>
|
||||
<th scope="row">Purchase date</th>
|
||||
<td>{{ rolling_stock.purchase_date|default:"-" }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if properties %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
@@ -267,20 +223,11 @@
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>
|
||||
{% for m in class.manufacturer.all %}
|
||||
{% if not forloop.first %} / {% endif %}
|
||||
<a href="{% url 'filtered' _filter="manufacturer" search=m.slug %}">{{ m }}</a>{% if m.website %} <a href="{{ m.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||
{% empty %}
|
||||
-
|
||||
{% endfor %}
|
||||
{%if class.manufacturer %}
|
||||
<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>
|
||||
{% if class.description %}
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Description</th>
|
||||
<td>{{ class.description | safe }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if class_properties %}
|
||||
@@ -305,12 +252,7 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" scope="row">
|
||||
Company data
|
||||
{% if company.freelance %}
|
||||
<span class="mt-1 float-end badge text-bg-secondary">Freelance</span>
|
||||
{% endif %}
|
||||
</th>
|
||||
<th colspan="2" scope="row">Company data</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-group-divider">
|
||||
@@ -322,16 +264,18 @@
|
||||
{% 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>
|
||||
<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>
|
||||
<img src="{{ company.country.flag }}" alt="{{ company.country }}"> {{ company.country.name }}
|
||||
</td>
|
||||
<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>
|
||||
@@ -345,7 +289,7 @@
|
||||
<tbody class="table-group-divider">
|
||||
<tr>
|
||||
<th scope="row">Interface</th>
|
||||
<td>{{ rolling_stock.get_decoder_interface }}</td>
|
||||
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="w-33" scope="row">Address</th>
|
||||
@@ -357,11 +301,11 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Manufacturer</th>
|
||||
<td>{{ rolling_stock.decoder.manufacturer | default:"-" }}</td>
|
||||
<td>{{ rolling_stock.decoder.manufacturer|default:"-" }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Version</th>
|
||||
<td>{{ rolling_stock.decoder.version | default:"-"}}</td>
|
||||
<td>{{ rolling_stock.decoder.version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Sound</th>
|
||||
@@ -425,12 +369,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane" id="nav-set" role="tabpanel" aria-labelledby="nav-set-tab">
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3">
|
||||
{% for d in set %}
|
||||
{% include "cards/roster.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
|
||||
{{ rolling_stock.notes | safe }}
|
||||
</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">
|
||||
|
@@ -1,17 +1,16 @@
|
||||
{% extends "cards.html" %}
|
||||
{% load dynamic_url %}
|
||||
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% dynamic_pagination type page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||
<a class="page-link" href="{% url 'roster_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
@@ -23,17 +22,17 @@
|
||||
{% if i == data.paginator.ELLIPSIS %}
|
||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{% dynamic_pagination type page=i %}#main-content">{{ i }}</a></li>
|
||||
<li class="page-item"><a class="page-link" href="{% url 'roster_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% dynamic_pagination type page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||
<a class="page-link" href="{% url 'roster_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
40
ram/portal/templates/scales.html
Normal file
40
ram/portal/templates/scales.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "cards.html" %}
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
{% if data.number == i %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ i }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if i == data.paginator.ELLIPSIS %}
|
||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -2,15 +2,15 @@
|
||||
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
||||
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
@@ -28,11 +28,11 @@
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
||||
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
40
ram/portal/templates/types.html
Normal file
40
ram/portal/templates/types.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "cards.html" %}
|
||||
{% block pagination %}
|
||||
{% if data.has_other_pages %}
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||
{% if data.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'types_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Previous</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for i in page_range %}
|
||||
{% if data.number == i %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ i }}</span>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if i == data.paginator.ELLIPSIS %}
|
||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||
{% else %}
|
||||
<li class="page-item"><a class="page-link" href="{% url 'types_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if data.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{% url 'types_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link">Next</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -1,21 +0,0 @@
|
||||
from django import template
|
||||
from django.urls import reverse
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def dynamic_admin_url(app_name, model_name, object_id=None):
|
||||
if object_id:
|
||||
return reverse(
|
||||
f'admin:{app_name}_{model_name}_change',
|
||||
args=[object_id]
|
||||
)
|
||||
return reverse(f'admin:{app_name}_{model_name}_changelist')
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def dynamic_pagination(reverse_name, page):
|
||||
if reverse_name.endswith('y'):
|
||||
return reverse(f'{reverse_name[:-1]}ies_pagination', args=[page])
|
||||
return reverse(f'{reverse_name}s_pagination', args=[page])
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user