mirror of
https://github.com/daniviga/django-ram.git
synced 2025-08-09 07:37:50 +02:00
Compare commits
2 Commits
master
...
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:
|
strategy:
|
||||||
max-parallel: 2
|
max-parallel: 2
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ['3.12', '3.13']
|
python-version: ['3.9', '3.10', '3.11', '3.12']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,6 +10,7 @@ __pycache__/
|
|||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
@@ -131,4 +132,3 @@ dmypy.json
|
|||||||
ram/storage/
|
ram/storage/
|
||||||
!ram/storage/.gitignore
|
!ram/storage/.gitignore
|
||||||
arduino/CommandStation-EX/build/
|
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.
|
This project probably doesn't match your needs nor expectations. Be aware.
|
||||||
|
|
||||||
> [!CAUTION]
|
Your model train may also catch fire while using this software.
|
||||||
> Your model train may catch fire while using this software.
|
|
||||||
|
|
||||||
Check out [my own instance](https://daniele.mynarrowgauge.org).
|
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:
|
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?
|
- [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
|
- [vim-arduino](https://github.com/stevearc/vim-arduino): another IDE? No thanks
|
||||||
- [podman](https://podman.io/): because containers are fancy
|
- [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
|
## Requirements
|
||||||
|
|
||||||
- Python 3.11+
|
- Python 3.9+
|
||||||
- A USB port when running Arduino hardware (and adaptors if you have a Mac)
|
- A USB port when running Arduino hardware (and adaptors if you have a Mac)
|
||||||
|
|
||||||
## Web portal installation
|
## Web portal installation
|
||||||
|
|
||||||
### Using containers
|
### Using containers
|
||||||
|
|
||||||
Do it yourself, otherwise, raise a request :)
|
coming soon
|
||||||
|
|
||||||
### Manual installation
|
### Manual installation
|
||||||
|
|
||||||
@@ -110,8 +83,6 @@ $ python manage.py migrate
|
|||||||
$ python manage.py createsuperuser
|
$ python manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|
||||||
To load some sample metadata, see the [sample_data folder instructions](./sample_data/README.md).
|
|
||||||
|
|
||||||
Run Django
|
Run Django
|
||||||
|
|
||||||
```bash
|
```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,
|
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).
|
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
|
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
|
using an ESP8266 module or a [Mega+WiFi board](https://dcc-ex.com/advanced-setup/supported-microcontrollers/wifi-mega.html)).
|
||||||
[ESP32](https://dcc-ex.com/reference/hardware/microcontrollers/esp32.html) (recommended).
|
|
||||||
|
|
||||||
### Manual setup
|
### Customize the settings
|
||||||
|
|
||||||
You'll need [namp-ncat](https://nmap.org/ncat/) , and `stty` to setup the serial port.
|
The daemon comes with default settings in `config.ini`.
|
||||||
|
Settings may need to be customized based on your setup.
|
||||||
> [!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.
|
|
||||||
|
|
||||||
|
|
||||||
### Using containers
|
### Using containers
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cd connector
|
$ cd daemons
|
||||||
$ podman build -t dcc/connector .
|
$ podman build -t dcc/net-to-serial .
|
||||||
$ podman run -d --group-add keep-groups --device /dev/ttyACM0:/dev/arduino -p 2560:2560 dcc/connector
|
$ 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
|
### Test with a simulator
|
||||||
|
|
||||||
A [QEMU AVR based simulator](daemons/simulator/README.md) running DCC++ EX is bundled togheter with the connector
|
A [QEMU AVR based simulator](daemons/simulator/README.md) running DCC++ EX is bundled togheter with the `net-to-serial.py`
|
||||||
into a container. To run it:
|
daemon into a container. To run it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cd connector/simulator
|
$ cd daemons/simulator
|
||||||
$ podman build -t dcc/connector:sim .
|
$ podman build -t dcc/net-to-serial:sim .
|
||||||
$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim
|
$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!WARNING]
|
To be continued ...
|
||||||
> The simulator is intended for light development and testing purposes only and far from being a complete replacement for a real hardware.
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -184,12 +146,15 @@ $ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim
|
|||||||

|

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

|

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

|

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

|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Submodule arduino/CommandStation-EX updated: 3b15491608...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
|
PTY=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
sed -i "s/ttyACM0/pts\/${PTY}/" /opt/dcc/config.ini
|
||||||
|
|
||||||
qemu-system-avr -machine uno -bios /io/CommandStation-EX*.elf -serial pty -daemonize
|
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.contrib import admin
|
||||||
from django.utils.html import format_html, strip_tags
|
|
||||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||||
|
|
||||||
from ram.admin import publish, unpublish
|
from bookshelf.models import BookProperty, BookImage, Book, Author, Publisher
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
|
class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||||
model = BaseBookImage
|
model = BookImage
|
||||||
min_num = 0
|
min_num = 0
|
||||||
extra = 0
|
extra = 0
|
||||||
readonly_fields = ("image_thumbnail",)
|
readonly_fields = ("image_thumbnail",)
|
||||||
classes = ["collapse"]
|
classes = ["collapse"]
|
||||||
verbose_name = "Image"
|
|
||||||
|
|
||||||
|
|
||||||
class BookPropertyInline(admin.TabularInline):
|
class BookPropertyInline(admin.TabularInline):
|
||||||
model = BaseBookProperty
|
model = BookProperty
|
||||||
min_num = 0
|
min_num = 0
|
||||||
extra = 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)
|
@admin.register(Book)
|
||||||
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
inlines = (
|
inlines = (BookImageInline, BookPropertyInline,)
|
||||||
BookPropertyInline,
|
|
||||||
BookImageInline,
|
|
||||||
BookDocInline,
|
|
||||||
)
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"title",
|
"title",
|
||||||
"get_authors",
|
"get_authors",
|
||||||
"get_publisher",
|
"get_publisher",
|
||||||
"publication_year",
|
"publication_year",
|
||||||
"number_of_pages",
|
"number_of_pages"
|
||||||
"published",
|
|
||||||
)
|
)
|
||||||
autocomplete_fields = ("authors", "publisher", "shop")
|
|
||||||
readonly_fields = ("invoices", "creation_time", "updated_time")
|
|
||||||
search_fields = ("title", "publisher__name", "authors__last_name")
|
search_fields = ("title", "publisher__name", "authors__last_name")
|
||||||
list_filter = ("publisher__name", "authors")
|
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")
|
@admin.display(description="Publisher")
|
||||||
def get_publisher(self, obj):
|
def get_publisher(self, obj):
|
||||||
return obj.publisher.name
|
return obj.publisher.name
|
||||||
|
|
||||||
@admin.display(description="Authors")
|
@admin.display(description="Authors")
|
||||||
def get_authors(self, obj):
|
def get_authors(self, obj):
|
||||||
return obj.authors_list
|
return ", ".join(a.short_name() for a in obj.authors.all())
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Author)
|
@admin.register(Author)
|
||||||
class AuthorAdmin(admin.ModelAdmin):
|
class AuthorAdmin(admin.ModelAdmin):
|
||||||
search_fields = (
|
search_fields = ("first_name", "last_name",)
|
||||||
"first_name",
|
|
||||||
"last_name",
|
|
||||||
)
|
|
||||||
list_filter = ("last_name",)
|
list_filter = ("last_name",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Publisher)
|
@admin.register(Publisher)
|
||||||
class PublisherAdmin(admin.ModelAdmin):
|
class PublisherAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "country_flag")
|
list_display = ("name", "country")
|
||||||
search_fields = ("name",)
|
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
|
# 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
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import uuid
|
import uuid
|
||||||
@@ -48,8 +47,7 @@ class Migration(migrations.Migration):
|
|||||||
("ISBN", models.CharField(max_length=13, unique=True)),
|
("ISBN", models.CharField(max_length=13, unique=True)),
|
||||||
("publication_year", models.SmallIntegerField(blank=True, null=True)),
|
("publication_year", models.SmallIntegerField(blank=True, null=True)),
|
||||||
("purchase_date", models.DateField(blank=True, null=True)),
|
("purchase_date", models.DateField(blank=True, null=True)),
|
||||||
# ("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)),
|
("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)),
|
||||||
("notes", models.TextField(blank=True)),
|
|
||||||
("creation_time", models.DateTimeField(auto_now_add=True)),
|
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||||
("updated_time", models.DateTimeField(auto_now=True)),
|
("updated_time", models.DateTimeField(auto_now=True)),
|
||||||
("authors", models.ManyToManyField(to="bookshelf.author")),
|
("authors", models.ManyToManyField(to="bookshelf.author")),
|
||||||
|
@@ -12,7 +12,7 @@ from django.conf import settings
|
|||||||
|
|
||||||
def move_images(apps, schema_editor):
|
def move_images(apps, schema_editor):
|
||||||
sys.stdout.write("\n Processing files. Please await...")
|
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)
|
fname = os.path.basename(r.image.path)
|
||||||
new_image = bookshelf.models.book_image_upload(r, fname)
|
new_image = bookshelf.models.book_image_upload(r, fname)
|
||||||
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
|
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"),
|
("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 = [
|
operations = [
|
||||||
# migrations.AlterField(
|
migrations.AlterField(
|
||||||
# model_name="bookimage",
|
model_name="bookimage",
|
||||||
# name="image",
|
name="image",
|
||||||
# field=models.ImageField(
|
field=models.ImageField(
|
||||||
# blank=True,
|
blank=True,
|
||||||
# null=True,
|
null=True,
|
||||||
# storage=ram.utils.DeduplicatedStorage,
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
# upload_to=bookshelf.models.book_image_upload,
|
upload_to=bookshelf.models.book_image_upload,
|
||||||
# ),
|
),
|
||||||
# ),
|
),
|
||||||
# migrations.RunPython(
|
migrations.RunPython(
|
||||||
# move_images,
|
move_images,
|
||||||
# reverse_code=migrations.RunPython.noop
|
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,50 +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.RemoveConstraint(
|
|
||||||
model_name="basebookdocument",
|
|
||||||
name="unique_book_file",
|
|
||||||
),
|
|
||||||
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 os
|
||||||
import shutil
|
import shutil
|
||||||
|
from uuid import uuid4
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
|
||||||
|
from tinymce import models as tinymce
|
||||||
|
|
||||||
|
from metadata.models import Tag
|
||||||
from ram.utils import DeduplicatedStorage
|
from ram.utils import DeduplicatedStorage
|
||||||
from ram.models import BaseModel, Image, PropertyInstance
|
from ram.models import Image, PropertyInstance
|
||||||
from metadata.models import Scale, Manufacturer, Shop, Tag
|
|
||||||
|
|
||||||
|
|
||||||
class Publisher(models.Model):
|
class Publisher(models.Model):
|
||||||
@@ -32,12 +35,15 @@ class Author(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.last_name}, {self.first_name}"
|
return f"{self.last_name}, {self.first_name}"
|
||||||
|
|
||||||
@property
|
|
||||||
def short_name(self):
|
def short_name(self):
|
||||||
return f"{self.last_name} {self.first_name[0]}."
|
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
|
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
|
||||||
language = models.CharField(
|
language = models.CharField(
|
||||||
max_length=7,
|
max_length=7,
|
||||||
@@ -46,19 +52,25 @@ class BaseBook(BaseModel):
|
|||||||
)
|
)
|
||||||
number_of_pages = models.SmallIntegerField(null=True, blank=True)
|
number_of_pages = models.SmallIntegerField(null=True, blank=True)
|
||||||
publication_year = 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)
|
purchase_date = models.DateField(null=True, blank=True)
|
||||||
tags = models.ManyToManyField(
|
tags = models.ManyToManyField(
|
||||||
Tag, related_name="bookshelf", blank=True
|
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):
|
def delete(self, *args, **kwargs):
|
||||||
shutil.rmtree(
|
shutil.rmtree(
|
||||||
@@ -67,7 +79,7 @@ class BaseBook(BaseModel):
|
|||||||
),
|
),
|
||||||
ignore_errors=True
|
ignore_errors=True
|
||||||
)
|
)
|
||||||
super(BaseBook, self).delete(*args, **kwargs)
|
super(Book, self).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def book_image_upload(instance, filename):
|
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(
|
book = models.ForeignKey(
|
||||||
BaseBook, on_delete=models.CASCADE, related_name="image"
|
Book, on_delete=models.CASCADE, related_name="image"
|
||||||
)
|
)
|
||||||
image = models.ImageField(
|
image = models.ImageField(
|
||||||
upload_to=book_image_upload,
|
upload_to=book_image_upload,
|
||||||
@@ -89,67 +101,11 @@ class BaseBookImage(Image):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseBookProperty(PropertyInstance):
|
class BookProperty(PropertyInstance):
|
||||||
book = models.ForeignKey(
|
book = models.ForeignKey(
|
||||||
BaseBook,
|
Book,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
related_name="property",
|
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):
|
|
||||||
# if the object is new, return an empty string to avoid
|
|
||||||
# calling self.scales.all() which would raise a infinite recursion
|
|
||||||
if self.pk is None:
|
|
||||||
return str() # empty string
|
|
||||||
scales = self.get_scales()
|
|
||||||
return "%s %s %s" % (self.manufacturer.name, self.years, scales)
|
|
||||||
|
|
||||||
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 rest_framework import serializers
|
||||||
from bookshelf.models import Book, Catalog, Author, Publisher
|
from bookshelf.models import Book, Author, Publisher
|
||||||
from metadata.serializers import (
|
from metadata.serializers import TagSerializer
|
||||||
ScaleSerializer,
|
|
||||||
ManufacturerSerializer,
|
|
||||||
TagSerializer
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorSerializer(serializers.ModelSerializer):
|
class AuthorSerializer(serializers.ModelSerializer):
|
||||||
@@ -26,26 +22,5 @@ class BookSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
exclude = (
|
fields = "__all__"
|
||||||
"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",
|
|
||||||
)
|
|
||||||
read_only_fields = ("creation_time", "updated_time")
|
read_only_fields = ("creation_time", "updated_time")
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from bookshelf.views import BookList, BookGet, CatalogList, CatalogGet
|
from bookshelf.views import BookList, BookGet
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("book/list", BookList.as_view()),
|
path("book/list", BookList.as_view()),
|
||||||
path("book/get/<uuid:uuid>", BookGet.as_view()),
|
path("book/get/<str:uuid>", BookGet.as_view()),
|
||||||
path("catalog/list", CatalogList.as_view()),
|
|
||||||
path("catalog/get/<uuid:uuid>", CatalogGet.as_view()),
|
|
||||||
]
|
]
|
||||||
|
@@ -1,40 +1,18 @@
|
|||||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||||
from rest_framework.schemas.openapi import AutoSchema
|
from rest_framework.schemas.openapi import AutoSchema
|
||||||
|
|
||||||
from ram.views import CustomLimitOffsetPagination
|
from bookshelf.models import Book
|
||||||
from bookshelf.models import Book, Catalog
|
from bookshelf.serializers import BookSerializer
|
||||||
from bookshelf.serializers import BookSerializer, CatalogSerializer
|
|
||||||
|
|
||||||
|
|
||||||
class BookList(ListAPIView):
|
class BookList(ListAPIView):
|
||||||
|
queryset = Book.objects.all()
|
||||||
serializer_class = BookSerializer
|
serializer_class = BookSerializer
|
||||||
pagination_class = CustomLimitOffsetPagination
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return Book.objects.get_published(self.request.user)
|
|
||||||
|
|
||||||
|
|
||||||
class BookGet(RetrieveAPIView):
|
class BookGet(RetrieveAPIView):
|
||||||
|
queryset = Book.objects.all()
|
||||||
serializer_class = BookSerializer
|
serializer_class = BookSerializer
|
||||||
lookup_field = "uuid"
|
lookup_field = "uuid"
|
||||||
|
|
||||||
schema = AutoSchema(operation_id_base="retrieveBookByUUID")
|
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,42 +1,14 @@
|
|||||||
import html
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
# from django.forms import BaseInlineFormSet # for future reference
|
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||||
from django.utils.html import format_html, strip_tags
|
|
||||||
from adminsortable2.admin import (
|
|
||||||
SortableAdminBase,
|
|
||||||
SortableInlineAdminMixin,
|
|
||||||
# CustomInlineFormSetMixin, # for future reference
|
|
||||||
)
|
|
||||||
|
|
||||||
from ram.admin import publish, unpublish
|
|
||||||
from ram.utils import generate_csv
|
|
||||||
from consist.models import Consist, ConsistItem
|
from consist.models import Consist, ConsistItem
|
||||||
|
|
||||||
|
|
||||||
# for future reference
|
|
||||||
# class ConsistItemInlineFormSet(CustomInlineFormSetMixin, BaseInlineFormSet):
|
|
||||||
# def clean(self):
|
|
||||||
# super().clean()
|
|
||||||
|
|
||||||
|
|
||||||
class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||||
model = ConsistItem
|
model = ConsistItem
|
||||||
min_num = 1
|
min_num = 1
|
||||||
extra = 0
|
extra = 0
|
||||||
autocomplete_fields = ("rolling_stock",)
|
readonly_fields = ("address", "type", "company", "era")
|
||||||
readonly_fields = (
|
|
||||||
"preview",
|
|
||||||
"published",
|
|
||||||
"scale",
|
|
||||||
"manufacturer",
|
|
||||||
"item_number",
|
|
||||||
"company",
|
|
||||||
"type",
|
|
||||||
"era",
|
|
||||||
"address",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Consist)
|
@admin.register(Consist)
|
||||||
@@ -46,38 +18,26 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"creation_time",
|
"creation_time",
|
||||||
"updated_time",
|
"updated_time",
|
||||||
)
|
)
|
||||||
list_filter = ("company__name", "era", "scale", "published")
|
list_display = ("identifier", "company", "era")
|
||||||
list_display = ("__str__",) + list_filter + ("country_flag",)
|
list_filter = list_display
|
||||||
search_fields = ("identifier",) + list_filter
|
search_fields = list_display
|
||||||
save_as = True
|
save_as = True
|
||||||
|
|
||||||
@admin.display(description="Country")
|
|
||||||
def country_flag(self, obj):
|
|
||||||
return format_html(
|
|
||||||
'<img src="{}" /> {}'.format(obj.country.flag, obj.country)
|
|
||||||
)
|
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
"published",
|
|
||||||
"identifier",
|
"identifier",
|
||||||
"company",
|
|
||||||
"scale",
|
|
||||||
"era",
|
|
||||||
"consist_address",
|
"consist_address",
|
||||||
"description",
|
"company",
|
||||||
|
"era",
|
||||||
"image",
|
"image",
|
||||||
|
"notes",
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"Notes",
|
|
||||||
{"classes": ("collapse",), "fields": ("notes",)},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"Audit",
|
"Audit",
|
||||||
{
|
{
|
||||||
@@ -89,55 +49,3 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def download_csv(modeladmin, request, queryset):
|
|
||||||
header = [
|
|
||||||
"ID",
|
|
||||||
"Name",
|
|
||||||
"Published",
|
|
||||||
"Company",
|
|
||||||
"Country",
|
|
||||||
"Address",
|
|
||||||
"Scale",
|
|
||||||
"Era",
|
|
||||||
"Description",
|
|
||||||
"Tags",
|
|
||||||
"Length",
|
|
||||||
"Composition",
|
|
||||||
"Item name",
|
|
||||||
"Item type",
|
|
||||||
"Item ID",
|
|
||||||
]
|
|
||||||
data = []
|
|
||||||
for obj in queryset:
|
|
||||||
for item in obj.consist_item.all():
|
|
||||||
types = " + ".join(
|
|
||||||
"{}x {}".format(t["count"], t["type"])
|
|
||||||
for t in obj.get_type_count()
|
|
||||||
)
|
|
||||||
data.append(
|
|
||||||
[
|
|
||||||
obj.uuid,
|
|
||||||
obj.__str__(),
|
|
||||||
"X" if obj.published else "",
|
|
||||||
obj.company.name,
|
|
||||||
obj.company.country,
|
|
||||||
obj.consist_address,
|
|
||||||
obj.scale.scale,
|
|
||||||
obj.era,
|
|
||||||
html.unescape(strip_tags(obj.description)),
|
|
||||||
settings.CSV_SEPARATOR_ALT.join(
|
|
||||||
t.name for t in obj.tags.all()
|
|
||||||
),
|
|
||||||
obj.length,
|
|
||||||
types,
|
|
||||||
item.rolling_stock.__str__(),
|
|
||||||
item.type,
|
|
||||||
item.rolling_stock.uuid,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return generate_csv(header, data, "consists.csv")
|
|
||||||
download_csv.short_description = "Download selected items as CSV"
|
|
||||||
|
|
||||||
actions = [publish, unpublish, download_csv]
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
# Generated by Django 4.1 on 2022-08-23 15:54
|
# 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
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
@@ -12,9 +11,9 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
# migrations.AlterField(
|
migrations.AlterField(
|
||||||
# model_name="consist",
|
model_name="consist",
|
||||||
# name="notes",
|
name="notes",
|
||||||
# field=ckeditor_uploader.fields.RichTextUploadingField(blank=True),
|
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,18 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-04-27 19:53
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("consist", "0015_consist_description"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="consistitem",
|
|
||||||
name="order",
|
|
||||||
field=models.PositiveIntegerField(),
|
|
||||||
),
|
|
||||||
]
|
|
@@ -1,42 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-05-01 09:51
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def set_scale(apps, schema_editor):
|
|
||||||
Consist = apps.get_model("consist", "Consist")
|
|
||||||
|
|
||||||
for consist in Consist.objects.all():
|
|
||||||
try:
|
|
||||||
consist.scale = consist.consist_item.first().rolling_stock.scale
|
|
||||||
consist.save()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("consist", "0016_alter_consistitem_order"),
|
|
||||||
(
|
|
||||||
"metadata",
|
|
||||||
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="consist",
|
|
||||||
name="scale",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="metadata.scale",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
set_scale,
|
|
||||||
reverse_code=migrations.RunPython.noop
|
|
||||||
),
|
|
||||||
]
|
|
@@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-05-02 11:33
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("consist", "0017_consist_scale"),
|
|
||||||
(
|
|
||||||
"metadata",
|
|
||||||
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="consist",
|
|
||||||
name="scale",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="metadata.scale"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@@ -1,39 +1,34 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.text import Truncator
|
|
||||||
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 ram.utils import DeduplicatedStorage
|
||||||
from metadata.models import Company, Scale, Tag
|
from metadata.models import Company, Tag
|
||||||
from roster.models import RollingStock
|
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)
|
identifier = models.CharField(max_length=128, unique=False)
|
||||||
tags = models.ManyToManyField(Tag, related_name="consist", blank=True)
|
tags = models.ManyToManyField(Tag, related_name="consist", blank=True)
|
||||||
consist_address = models.SmallIntegerField(
|
consist_address = models.SmallIntegerField(
|
||||||
default=None,
|
default=None, null=True, blank=True
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
help_text="DCC consist address if enabled",
|
|
||||||
)
|
)
|
||||||
company = models.ForeignKey(Company, on_delete=models.CASCADE)
|
company = models.ForeignKey(Company, on_delete=models.CASCADE)
|
||||||
era = models.CharField(
|
era = models.CharField(max_length=32, blank=True)
|
||||||
max_length=32,
|
|
||||||
blank=True,
|
|
||||||
help_text="Era or epoch of the consist",
|
|
||||||
)
|
|
||||||
scale = models.ForeignKey(Scale, on_delete=models.CASCADE)
|
|
||||||
image = models.ImageField(
|
image = models.ImageField(
|
||||||
upload_to=os.path.join("images", "consists"),
|
upload_to=os.path.join("images", "consists"),
|
||||||
storage=DeduplicatedStorage,
|
storage=DeduplicatedStorage,
|
||||||
null=True,
|
null=True,
|
||||||
blank=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):
|
def __str__(self):
|
||||||
return "{0} {1}".format(self.company, self.identifier)
|
return "{0} {1}".format(self.company, self.identifier)
|
||||||
@@ -41,25 +36,6 @@ class Consist(BaseModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("consist", kwargs={"uuid": self.uuid})
|
return reverse("consist", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
@property
|
|
||||||
def length(self):
|
|
||||||
return self.consist_item.count()
|
|
||||||
|
|
||||||
def get_type_count(self):
|
|
||||||
return self.consist_item.annotate(
|
|
||||||
type=models.F("rolling_stock__rolling_class__type__type")
|
|
||||||
).values(
|
|
||||||
"type"
|
|
||||||
).annotate(
|
|
||||||
count=models.Count("rolling_stock"),
|
|
||||||
category=models.F("rolling_stock__rolling_class__type__category"),
|
|
||||||
order=models.Max("order"),
|
|
||||||
).order_by("order")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def country(self):
|
|
||||||
return self.company.country
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["company", "-creation_time"]
|
ordering = ["company", "-creation_time"]
|
||||||
|
|
||||||
@@ -69,81 +45,22 @@ class ConsistItem(models.Model):
|
|||||||
Consist, on_delete=models.CASCADE, related_name="consist_item"
|
Consist, on_delete=models.CASCADE, related_name="consist_item"
|
||||||
)
|
)
|
||||||
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE)
|
rolling_stock = models.ForeignKey(RollingStock, on_delete=models.CASCADE)
|
||||||
order = models.PositiveIntegerField(blank=False, null=False)
|
order = models.PositiveIntegerField(default=0, blank=False, null=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta(object):
|
||||||
ordering = ["order"]
|
ordering = ["order"]
|
||||||
constraints = [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["consist", "rolling_stock"],
|
|
||||||
name="one_stock_per_consist"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0}".format(self.rolling_stock)
|
return "{0}".format(self.rolling_stock)
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
rolling_stock = getattr(self, "rolling_stock", False)
|
|
||||||
if not rolling_stock:
|
|
||||||
return # exit if no inline are present
|
|
||||||
|
|
||||||
# FIXME this does not work when creating a new consist,
|
|
||||||
# because the consist is not saved yet and it must be moved
|
|
||||||
# to the admin form validation via InlineFormSet.clean()
|
|
||||||
consist = self.consist
|
|
||||||
if rolling_stock.scale != consist.scale:
|
|
||||||
raise ValidationError(
|
|
||||||
"The rolling stock and consist must be of the same scale."
|
|
||||||
)
|
|
||||||
if self.consist.published and not rolling_stock.published:
|
|
||||||
raise ValidationError(
|
|
||||||
"You must unpublish the the consist before using this item."
|
|
||||||
)
|
|
||||||
|
|
||||||
def published(self):
|
|
||||||
return self.rolling_stock.published
|
|
||||||
published.boolean = True
|
|
||||||
|
|
||||||
def preview(self):
|
|
||||||
return self.rolling_stock.image.first().image_thumbnail(100)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def manufacturer(self):
|
|
||||||
return Truncator(self.rolling_stock.manufacturer).chars(10)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def item_number(self):
|
|
||||||
return self.rolling_stock.item_number
|
|
||||||
|
|
||||||
@property
|
|
||||||
def scale(self):
|
|
||||||
return self.rolling_stock.scale
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self):
|
def type(self):
|
||||||
return self.rolling_stock.rolling_class.type.type
|
return self.rolling_stock.rolling_class.type
|
||||||
|
|
||||||
@property
|
|
||||||
def address(self):
|
def address(self):
|
||||||
return self.rolling_stock.address
|
return self.rolling_stock.address
|
||||||
|
|
||||||
@property
|
|
||||||
def company(self):
|
def company(self):
|
||||||
return self.rolling_stock.company
|
return self.rolling_stock.company()
|
||||||
|
|
||||||
@property
|
|
||||||
def era(self):
|
def era(self):
|
||||||
return self.rolling_stock.era
|
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:
|
class Meta:
|
||||||
model = Consist
|
model = Consist
|
||||||
exclude = ("notes",)
|
fields = "__all__"
|
||||||
read_only_fields = ("creation_time", "updated_time")
|
|
||||||
|
@@ -1,21 +1,15 @@
|
|||||||
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||||
|
|
||||||
from ram.views import CustomLimitOffsetPagination
|
|
||||||
from consist.models import Consist
|
from consist.models import Consist
|
||||||
from consist.serializers import ConsistSerializer
|
from consist.serializers import ConsistSerializer
|
||||||
|
|
||||||
|
|
||||||
class ConsistList(ListAPIView):
|
class ConsistList(ListAPIView):
|
||||||
|
queryset = Consist.objects.all()
|
||||||
serializer_class = ConsistSerializer
|
serializer_class = ConsistSerializer
|
||||||
pagination_class = CustomLimitOffsetPagination
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return Consist.objects.get_published(self.request.user)
|
|
||||||
|
|
||||||
|
|
||||||
class ConsistGet(RetrieveAPIView):
|
class ConsistGet(RetrieveAPIView):
|
||||||
|
queryset = Consist.objects.all()
|
||||||
serializer_class = ConsistSerializer
|
serializer_class = ConsistSerializer
|
||||||
lookup_field = "uuid"
|
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.contrib import admin
|
||||||
from django.utils.html import format_html
|
|
||||||
from adminsortable2.admin import SortableAdminMixin
|
from adminsortable2.admin import SortableAdminMixin
|
||||||
|
|
||||||
from repository.models import DecoderDocument
|
|
||||||
from metadata.models import (
|
from metadata.models import (
|
||||||
Property,
|
Property,
|
||||||
Decoder,
|
Decoder,
|
||||||
|
DecoderDocument,
|
||||||
Scale,
|
Scale,
|
||||||
Shop,
|
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
Company,
|
Company,
|
||||||
Tag,
|
Tag,
|
||||||
@@ -47,30 +45,18 @@ class ScaleAdmin(admin.ModelAdmin):
|
|||||||
@admin.register(Company)
|
@admin.register(Company)
|
||||||
class CompanyAdmin(admin.ModelAdmin):
|
class CompanyAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ("logo_thumbnail",)
|
readonly_fields = ("logo_thumbnail",)
|
||||||
list_display = ("name", "country_flag")
|
list_display = ("name", "country")
|
||||||
list_filter = ("name", "country")
|
list_filter = list_display
|
||||||
search_fields = ("name",)
|
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)
|
@admin.register(Manufacturer)
|
||||||
class ManufacturerAdmin(admin.ModelAdmin):
|
class ManufacturerAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ("logo_thumbnail",)
|
readonly_fields = ("logo_thumbnail",)
|
||||||
list_display = ("name", "category", "country_flag")
|
list_display = ("name", "category")
|
||||||
list_filter = ("category",)
|
list_filter = ("category",)
|
||||||
search_fields = ("name",)
|
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)
|
@admin.register(Tag)
|
||||||
class TagAdmin(admin.ModelAdmin):
|
class TagAdmin(admin.ModelAdmin):
|
||||||
@@ -84,10 +70,3 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
|
|||||||
list_display = ("__str__",)
|
list_display = ("__str__",)
|
||||||
list_filter = ("type", "category")
|
list_filter = ("type", "category")
|
||||||
search_fields = ("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",
|
|
||||||
),
|
|
||||||
]
|
|
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-05-04 20:45
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
(
|
|
||||||
"metadata",
|
|
||||||
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="company",
|
|
||||||
options={"ordering": ["slug"], "verbose_name_plural": "Companies"},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="manufacturer",
|
|
||||||
options={"ordering": ["category", "slug"]},
|
|
||||||
),
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="tag",
|
|
||||||
options={"ordering": ["slug"]},
|
|
||||||
),
|
|
||||||
]
|
|
@@ -3,19 +3,15 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.dispatch.dispatcher import receiver
|
from django.dispatch.dispatcher import receiver
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
|
||||||
|
from ram.models import Document
|
||||||
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
|
from ram.utils import DeduplicatedStorage, get_image_preview, slugify
|
||||||
from ram.managers import PublicManager
|
|
||||||
|
|
||||||
|
|
||||||
class Property(models.Model):
|
class Property(models.Model):
|
||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(max_length=128, unique=True)
|
||||||
private = models.BooleanField(
|
private = models.BooleanField(default=False)
|
||||||
default=False,
|
|
||||||
help_text="Property will be only visible to logged users",
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "Properties"
|
verbose_name_plural = "Properties"
|
||||||
@@ -24,8 +20,6 @@ class Property(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
objects = PublicManager()
|
|
||||||
|
|
||||||
|
|
||||||
class Manufacturer(models.Model):
|
class Manufacturer(models.Model):
|
||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(max_length=128, unique=True)
|
||||||
@@ -33,7 +27,6 @@ class Manufacturer(models.Model):
|
|||||||
category = models.CharField(
|
category = models.CharField(
|
||||||
max_length=64, choices=settings.MANUFACTURER_TYPES
|
max_length=64, choices=settings.MANUFACTURER_TYPES
|
||||||
)
|
)
|
||||||
country = CountryField(blank=True)
|
|
||||||
website = models.URLField(blank=True)
|
website = models.URLField(blank=True)
|
||||||
logo = models.ImageField(
|
logo = models.ImageField(
|
||||||
upload_to=os.path.join("images", "manufacturers"),
|
upload_to=os.path.join("images", "manufacturers"),
|
||||||
@@ -43,18 +36,17 @@ class Manufacturer(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["category", "slug"]
|
ordering = ["category", "name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"filtered",
|
"filtered", kwargs={
|
||||||
kwargs={
|
|
||||||
"_filter": "manufacturer",
|
"_filter": "manufacturer",
|
||||||
"search": self.slug,
|
"search": self.slug,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def logo_thumbnail(self):
|
def logo_thumbnail(self):
|
||||||
@@ -78,18 +70,17 @@ class Company(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "Companies"
|
verbose_name_plural = "Companies"
|
||||||
ordering = ["slug"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"filtered",
|
"filtered", kwargs={
|
||||||
kwargs={
|
|
||||||
"_filter": "company",
|
"_filter": "company",
|
||||||
"search": self.slug,
|
"search": self.slug,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def extended_name_pp(self):
|
def extended_name_pp(self):
|
||||||
@@ -117,8 +108,8 @@ class Decoder(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta(object):
|
||||||
ordering = ["manufacturer__name", "name"]
|
ordering = ["manufacturer", "name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0} - {1}".format(self.manufacturer, self.name)
|
return "{0} - {1}".format(self.manufacturer, self.name)
|
||||||
@@ -129,49 +120,37 @@ class Decoder(models.Model):
|
|||||||
image_thumbnail.short_description = "Preview"
|
image_thumbnail.short_description = "Preview"
|
||||||
|
|
||||||
|
|
||||||
def calculate_ratio(ratio):
|
class DecoderDocument(Document):
|
||||||
try:
|
decoder = models.ForeignKey(
|
||||||
num, den = ratio.split(":")
|
Decoder, on_delete=models.CASCADE, related_name="document"
|
||||||
return int(num) / float(den) * 10000
|
)
|
||||||
except (ValueError, ZeroDivisionError):
|
|
||||||
raise ValidationError("Invalid ratio format")
|
class Meta:
|
||||||
|
unique_together = ("decoder", "file")
|
||||||
|
|
||||||
|
|
||||||
class Scale(models.Model):
|
class Scale(models.Model):
|
||||||
scale = models.CharField(max_length=32, unique=True)
|
scale = models.CharField(max_length=32, unique=True)
|
||||||
slug = models.CharField(max_length=32, unique=True, editable=False)
|
slug = models.CharField(max_length=32, unique=True, editable=False)
|
||||||
ratio = models.CharField(max_length=16, validators=[calculate_ratio])
|
ratio = models.CharField(max_length=16, blank=True)
|
||||||
ratio_int = models.SmallIntegerField(editable=False, default=0)
|
gauge = models.CharField(max_length=16, blank=True)
|
||||||
tracks = models.FloatField(
|
tracks = models.CharField(max_length=16, blank=True)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-ratio_int", "-tracks", "scale"]
|
ordering = ["scale"]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"filtered",
|
"filtered", kwargs={
|
||||||
kwargs={
|
|
||||||
"_filter": "scale",
|
"_filter": "scale",
|
||||||
"search": self.slug,
|
"search": self.slug,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.scale)
|
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):
|
class RollingStockType(models.Model):
|
||||||
type = models.CharField(max_length=64)
|
type = models.CharField(max_length=64)
|
||||||
order = models.PositiveSmallIntegerField()
|
order = models.PositiveSmallIntegerField()
|
||||||
@@ -180,22 +159,16 @@ class RollingStockType(models.Model):
|
|||||||
)
|
)
|
||||||
slug = models.CharField(max_length=128, unique=True, editable=False)
|
slug = models.CharField(max_length=128, unique=True, editable=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta(object):
|
||||||
constraints = [
|
unique_together = ("category", "type")
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["category", "type"],
|
|
||||||
name="unique_category_type"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
ordering = ["order"]
|
ordering = ["order"]
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"filtered",
|
"filtered", kwargs={
|
||||||
kwargs={
|
|
||||||
"_filter": "type",
|
"_filter": "type",
|
||||||
"search": self.slug,
|
"search": self.slug,
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -206,36 +179,21 @@ class Tag(models.Model):
|
|||||||
name = models.CharField(max_length=128, unique=True)
|
name = models.CharField(max_length=128, unique=True)
|
||||||
slug = models.CharField(max_length=128, unique=True)
|
slug = models.CharField(max_length=128, unique=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta(object):
|
||||||
ordering = ["slug"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"filtered",
|
"filtered", kwargs={
|
||||||
kwargs={
|
|
||||||
"_filter": "tag",
|
"_filter": "tag",
|
||||||
"search": self.slug,
|
"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=Manufacturer)
|
||||||
@receiver(models.signals.pre_save, sender=Company)
|
@receiver(models.signals.pre_save, sender=Company)
|
||||||
@receiver(models.signals.pre_save, sender=Scale)
|
@receiver(models.signals.pre_save, sender=Scale)
|
||||||
|
@@ -1,15 +1,12 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from solo.admin import SingletonModelAdmin
|
from solo.admin import SingletonModelAdmin
|
||||||
from tinymce.widgets import TinyMCE
|
|
||||||
|
|
||||||
from ram.admin import publish, unpublish
|
|
||||||
from portal.models import SiteConfiguration, Flatpage
|
from portal.models import SiteConfiguration, Flatpage
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SiteConfiguration)
|
@admin.register(SiteConfiguration)
|
||||||
class SiteConfigurationAdmin(SingletonModelAdmin):
|
class SiteConfigurationAdmin(SingletonModelAdmin):
|
||||||
readonly_fields = ("site_name", "rest_api", "version")
|
readonly_fields = ("site_name",)
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
@@ -20,10 +17,8 @@ class SiteConfigurationAdmin(SingletonModelAdmin):
|
|||||||
"about",
|
"about",
|
||||||
"items_per_page",
|
"items_per_page",
|
||||||
"items_ordering",
|
"items_ordering",
|
||||||
"currency",
|
|
||||||
"footer",
|
"footer",
|
||||||
"footer_extended",
|
"footer_extended",
|
||||||
"disclaimer",
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -35,30 +30,11 @@ class SiteConfigurationAdmin(SingletonModelAdmin):
|
|||||||
"show_version",
|
"show_version",
|
||||||
"use_cdn",
|
"use_cdn",
|
||||||
"extra_head",
|
"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)
|
@admin.register(Flatpage)
|
||||||
class FlatpageAdmin(admin.ModelAdmin):
|
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
|
# Generated by Django 4.1 on 2022-08-23 15:54
|
||||||
|
|
||||||
# ckeditor dependency removal
|
import ckeditor.fields
|
||||||
# import ckeditor.fields
|
import ckeditor_uploader.fields
|
||||||
# import ckeditor_uploader.fields
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
@@ -13,24 +12,24 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
# migrations.AlterField(
|
migrations.AlterField(
|
||||||
# model_name="flatpage",
|
model_name="flatpage",
|
||||||
# name="content",
|
name="content",
|
||||||
# field=ckeditor_uploader.fields.RichTextUploadingField(),
|
field=ckeditor_uploader.fields.RichTextUploadingField(),
|
||||||
# ),
|
),
|
||||||
# migrations.AlterField(
|
migrations.AlterField(
|
||||||
# model_name="siteconfiguration",
|
model_name="siteconfiguration",
|
||||||
# name="about",
|
name="about",
|
||||||
# field=ckeditor.fields.RichTextField(blank=True),
|
field=ckeditor.fields.RichTextField(blank=True),
|
||||||
# ),
|
),
|
||||||
# migrations.AlterField(
|
migrations.AlterField(
|
||||||
# model_name="siteconfiguration",
|
model_name="siteconfiguration",
|
||||||
# name="footer",
|
name="footer",
|
||||||
# field=ckeditor.fields.RichTextField(blank=True),
|
field=ckeditor.fields.RichTextField(blank=True),
|
||||||
# ),
|
),
|
||||||
# migrations.AlterField(
|
migrations.AlterField(
|
||||||
# model_name="siteconfiguration",
|
model_name="siteconfiguration",
|
||||||
# name="footer_extended",
|
name="footer_extended",
|
||||||
# field=ckeditor.fields.RichTextField(blank=True),
|
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 tinymce import models as tinymce
|
||||||
|
|
||||||
from ram import __version__ as app_version
|
from ram import __version__ as app_version
|
||||||
from ram.managers import PublicManager
|
|
||||||
from ram.utils import slugify
|
from ram.utils import slugify
|
||||||
|
|
||||||
|
|
||||||
@@ -30,10 +29,8 @@ class SiteConfiguration(SingletonModel):
|
|||||||
],
|
],
|
||||||
default="type",
|
default="type",
|
||||||
)
|
)
|
||||||
currency = models.CharField(max_length=3, default="EUR")
|
|
||||||
footer = tinymce.HTMLField(blank=True)
|
footer = tinymce.HTMLField(blank=True)
|
||||||
footer_extended = tinymce.HTMLField(blank=True)
|
footer_extended = tinymce.HTMLField(blank=True)
|
||||||
disclaimer = tinymce.HTMLField(blank=True)
|
|
||||||
show_version = models.BooleanField(default=True)
|
show_version = models.BooleanField(default=True)
|
||||||
use_cdn = models.BooleanField(default=True)
|
use_cdn = models.BooleanField(default=True)
|
||||||
extra_head = models.TextField(blank=True)
|
extra_head = models.TextField(blank=True)
|
||||||
@@ -47,11 +44,9 @@ class SiteConfiguration(SingletonModel):
|
|||||||
def site_name(self):
|
def site_name(self):
|
||||||
return settings.SITE_NAME
|
return settings.SITE_NAME
|
||||||
|
|
||||||
@property
|
|
||||||
def version(self):
|
def version(self):
|
||||||
return app_version
|
return app_version
|
||||||
|
|
||||||
@property
|
|
||||||
def django_version(self):
|
def django_version(self):
|
||||||
return django.get_version()
|
return django.get_version()
|
||||||
|
|
||||||
@@ -64,10 +59,6 @@ class Flatpage(models.Model):
|
|||||||
creation_time = models.DateTimeField(auto_now_add=True)
|
creation_time = models.DateTimeField(auto_now_add=True)
|
||||||
updated_time = models.DateTimeField(auto_now=True)
|
updated_time = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "page"
|
|
||||||
verbose_name_plural = "pages"
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@@ -81,8 +72,6 @@ class Flatpage(models.Model):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = PublicManager()
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(models.signals.pre_save, sender=Flatpage)
|
@receiver(models.signals.pre_save, sender=Flatpage)
|
||||||
def tag_pre_save(sender, instance, **kwargs):
|
def tag_pre_save(sender, instance, **kwargs):
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
/*!
|
/*!
|
||||||
* Bootstrap Icons v1.13.1 (https://icons.getbootstrap.com/)
|
* Bootstrap Icons v1.11.1 (https://icons.getbootstrap.com/)
|
||||||
* Copyright 2019-2024 The Bootstrap Authors
|
* Copyright 2019-2023 The Bootstrap Authors
|
||||||
* Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-display: block;
|
font-display: block;
|
||||||
font-family: "bootstrap-icons";
|
font-family: "bootstrap-icons";
|
||||||
src: url("./fonts/bootstrap-icons.woff2?e34853135f9e39acf64315236852cd5a") format("woff2"),
|
src: url("./fonts/bootstrap-icons.woff2?2820a3852bdb9a5832199cc61cec4e65") format("woff2"),
|
||||||
url("./fonts/bootstrap-icons.woff?e34853135f9e39acf64315236852cd5a") format("woff");
|
url("./fonts/bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
.bi::before,
|
.bi::before,
|
||||||
@@ -2076,31 +2076,3 @@ url("./fonts/bootstrap-icons.woff?e34853135f9e39acf64315236852cd5a") format("wof
|
|||||||
.bi-suitcase2-fill::before { content: "\f901"; }
|
.bi-suitcase2-fill::before { content: "\f901"; }
|
||||||
.bi-suitcase2::before { content: "\f902"; }
|
.bi-suitcase2::before { content: "\f902"; }
|
||||||
.bi-vignette::before { content: "\f903"; }
|
.bi-vignette::before { content: "\f903"; }
|
||||||
.bi-bluesky::before { content: "\f7f9"; }
|
|
||||||
.bi-tux::before { content: "\f904"; }
|
|
||||||
.bi-beaker-fill::before { content: "\f905"; }
|
|
||||||
.bi-beaker::before { content: "\f906"; }
|
|
||||||
.bi-flask-fill::before { content: "\f907"; }
|
|
||||||
.bi-flask-florence-fill::before { content: "\f908"; }
|
|
||||||
.bi-flask-florence::before { content: "\f909"; }
|
|
||||||
.bi-flask::before { content: "\f90a"; }
|
|
||||||
.bi-leaf-fill::before { content: "\f90b"; }
|
|
||||||
.bi-leaf::before { content: "\f90c"; }
|
|
||||||
.bi-measuring-cup-fill::before { content: "\f90d"; }
|
|
||||||
.bi-measuring-cup::before { content: "\f90e"; }
|
|
||||||
.bi-unlock2-fill::before { content: "\f90f"; }
|
|
||||||
.bi-unlock2::before { content: "\f910"; }
|
|
||||||
.bi-battery-low::before { content: "\f911"; }
|
|
||||||
.bi-anthropic::before { content: "\f912"; }
|
|
||||||
.bi-apple-music::before { content: "\f913"; }
|
|
||||||
.bi-claude::before { content: "\f914"; }
|
|
||||||
.bi-openai::before { content: "\f915"; }
|
|
||||||
.bi-perplexity::before { content: "\f916"; }
|
|
||||||
.bi-css::before { content: "\f917"; }
|
|
||||||
.bi-javascript::before { content: "\f918"; }
|
|
||||||
.bi-typescript::before { content: "\f919"; }
|
|
||||||
.bi-fork-knife::before { content: "\f91a"; }
|
|
||||||
.bi-globe-americas-fill::before { content: "\f91b"; }
|
|
||||||
.bi-globe-asia-australia-fill::before { content: "\f91c"; }
|
|
||||||
.bi-globe-central-south-asia-fill::before { content: "\f91d"; }
|
|
||||||
.bi-globe-europe-africa-fill::before { content: "\f91e"; }
|
|
Binary file not shown.
Binary file not shown.
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;
|
max-height: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disable margin on last <p> in a <td> */
|
|
||||||
td > p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn > span {
|
.btn > span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -43,15 +38,17 @@ a.badge, a.badge:hover {
|
|||||||
border-top: calc(var(--bs-border-width) * 3) solid var(--bs-border-color);
|
border-top: calc(var(--bs-border-width) * 3) solid var(--bs-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav-journal ul,
|
#nav-notes > p {
|
||||||
#nav-journal ol {
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nav-journal ul, #nav-journal ol {
|
||||||
|
margin: 0;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav-journal p:last-child,
|
#nav-journal p {
|
||||||
#nav-journal ul:last-child,
|
margin: 0;
|
||||||
#nav-journal ol:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#footer > p {
|
#footer > p {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,7 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<svg width="26" height="16" enable-background="new 0 0 26 26" version="1" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
<svg width="32" height="16" preserveAspectRatio="xMidYMid" version="1.0" viewBox="0 0 24 12" xmlns="http://www.w3.org/2000/svg">
|
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" enable-background="accumulate" overflow="visible" stroke-width="2" />
|
||||||
<metadata>Created by potrace 1.15, written by Peter Selinger 2001-2017</metadata>
|
<style>
|
||||||
<g transform="matrix(.0039261 0 0 -.0039261 -1.4249 18.53)">
|
path {
|
||||||
<path d="m813 4723-103-4v-309h-355l14-330h369l6-42c39-273 39-1414 0-1659l-7-39h-368l-14-330h355v-318l41-7c23-4 126-7 229-7s206 3 229 7l41 7v318h670v-318l41-7c23-4 126-7 229-7s206 3 229 7l41 7v318h670v-318l37-7c48-9 432-9 472 0l31 7v318h680v-318l31-7c39-9 423-9 469 0l35 6v314l338 3 337 2v-318l38-7c48-9 416-9 465 0l37 7v318h335v2400h-335v307l-135 6c-74 3-196 3-270 0l-135-6v-307l-337 2-338 3v302l-132 6c-73 3-194 3-268 0l-135-6v-307h-680v307l-135 6c-74 3-196 3-270 0l-135-6v-307h-670v307l-135 6c-74 3-196 3-270 0l-135-6v-307h-670v310h-63c-35 0-111 2-168 4s-150 1-206-1zm1141-666c3-12 11-97 18-187 24-309 11-1402-18-1507l-6-23h-725l-7 32c-39 197-39 1454 0 1676l6 32h726zm1218-42c20-182 30-569 25-940-6-371-21-707-33-727-3-4-169-8-368-8h-363l-7 48c-38 277-38 1365 1 1647l6 45 366-2 366-3zm1203 53c39-103 45-1264 9-1660l-7-68h-735l-6 68c-35 381-35 1263 0 1610l6 62h364c283 0 366-3 369-12zm1219-42c37-316 37-1287 0-1628l-7-58h-734l-6 73c-37 424-31 1544 8 1655 3 9 86 12 368 12h364zm841-1686c-336 0-363 1-370 18-3 9-13 152-22 317-21 431-7 1292 23 1388 5 16 31 17 369 17z"/>
|
text-indent:0;
|
||||||
</g>
|
text-transform:none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -16,11 +16,11 @@
|
|||||||
<link rel="icon" href="{% static "favicon.png" %}" sizes="any">
|
<link rel="icon" href="{% static "favicon.png" %}" sizes="any">
|
||||||
<link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml">
|
<link rel="icon" href="{% static "favicon.svg" %}" type="image/svg+xml">
|
||||||
{% if site_conf.use_cdn %}
|
{% if site_conf.use_cdn %}
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
{% else %}
|
{% else %}
|
||||||
<link href="{% static "bootstrap@5.3.6/dist/css/bootstrap.min.css" %}" rel="stylesheet">
|
<link href="{% static "bootstrap@5.3.2/dist/css/bootstrap.min.css" %}" rel="stylesheet">
|
||||||
<link href="{% static "bootstrap-icons@1.13.1/font/bootstrap-icons.css" %}" rel="stylesheet">
|
<link href="{% static "bootstrap-icons@1.11.1/font/bootstrap-icons.css" %}" rel="stylesheet">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
|
<link href="{% static "css/main.css" %}?v={{ site_conf.version }}" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@@ -118,7 +118,6 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
var selectElement = document.getElementById('tabSelector');
|
var selectElement = document.getElementById('tabSelector');
|
||||||
try {
|
|
||||||
selectElement.addEventListener('change', function () {
|
selectElement.addEventListener('change', function () {
|
||||||
var selectedTabId = this.value;
|
var selectedTabId = this.value;
|
||||||
var tabs = document.querySelectorAll('.tab-pane');
|
var tabs = document.querySelectorAll('.tab-pane');
|
||||||
@@ -127,7 +126,6 @@
|
|||||||
});
|
});
|
||||||
document.getElementById(selectedTabId).classList.add('show', 'active');
|
document.getElementById(selectedTabId).classList.add('show', 'active');
|
||||||
});
|
});
|
||||||
} catch (TypeError) { /* pass */ }
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
@@ -140,10 +138,14 @@
|
|||||||
<div class="container d-flex">
|
<div class="container d-flex">
|
||||||
<div class="me-auto">
|
<div class="me-auto">
|
||||||
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
|
<a href="{% url 'index' %}" class="navbar-brand d-flex align-items-center">
|
||||||
<svg class="me-2" width="32" height="16" version="1.0" viewBox="0 0 24 12" xmlns="http://www.w3.org/2000/svg">
|
<svg class="me-2" width="26" height="16" viewBox="0 0 26 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g transform="matrix(.0039261 0 0 -.0039261 -1.4249 18.53)">
|
<path d="m2.8125 0.0010991a1.0001 1.0001 0 0 0-0.8125 1c0 0.55455-0.44545 1-1 1a1.0001 1.0001 0 0 0-1 1v10a1.0001 1.0001 0 0 0 1 1c0.55455 0 1 0.44546 1 1a1.0001 1.0001 0 0 0 1 1h20a1.0001 1.0001 0 0 0 1-1c0-0.55454 0.44546-1 1-1a1.0001 1.0001 0 0 0 1-1v-10a1.0001 1.0001 0 0 0-1-1c-0.55454 0-1-0.44545-1-1a1.0001 1.0001 0 0 0-1-1h-20a1.0001 1.0001 0 0 0-0.09375 0 1.0001 1.0001 0 0 0-0.09375 0zm0.78125 2h14.406v1h2v-1h2.4062c0.30628 0.76906 0.82469 1.2875 1.5938 1.5938v8.8125c-0.76906 0.30628-1.2875 0.82469-1.5938 1.5938h-2.4062v-1h-2v1h-14.406c-0.30628-0.76906-0.82469-1.2875-1.5938-1.5938v-8.8125c0.76906-0.30628 1.2875-0.82469 1.5938-1.5938zm14.406 2v2h2v-2zm0 3v2h2v-2zm0 3v2h2v-2z" stroke-width="2" />
|
||||||
<path d="m813 4723-103-4v-309h-355l14-330h369l6-42c39-273 39-1414 0-1659l-7-39h-368l-14-330h355v-318l41-7c23-4 126-7 229-7s206 3 229 7l41 7v318h670v-318l41-7c23-4 126-7 229-7s206 3 229 7l41 7v318h670v-318l37-7c48-9 432-9 472 0l31 7v318h680v-318l31-7c39-9 423-9 469 0l35 6v314l338 3 337 2v-318l38-7c48-9 416-9 465 0l37 7v318h335v2400h-335v307l-135 6c-74 3-196 3-270 0l-135-6v-307l-337 2-338 3v302l-132 6c-73 3-194 3-268 0l-135-6v-307h-680v307l-135 6c-74 3-196 3-270 0l-135-6v-307h-670v307l-135 6c-74 3-196 3-270 0l-135-6v-307h-670v310h-63c-35 0-111 2-168 4s-150 1-206-1zm1141-666c3-12 11-97 18-187 24-309 11-1402-18-1507l-6-23h-725l-7 32c-39 197-39 1454 0 1676l6 32h726zm1218-42c20-182 30-569 25-940-6-371-21-707-33-727-3-4-169-8-368-8h-363l-7 48c-38 277-38 1365 1 1647l6 45 366-2 366-3zm1203 53c39-103 45-1264 9-1660l-7-68h-735l-6 68c-35 381-35 1263 0 1610l6 62h364c283 0 366-3 369-12zm1219-42c37-316 37-1287 0-1628l-7-58h-734l-6 73c-37 424-31 1544 8 1655 3 9 86 12 368 12h364zm841-1686c-336 0-363 1-370 18-3 9-13 152-22 317-21 431-7 1292 23 1388 5 16 31 17 369 17z"/>
|
<style>
|
||||||
</g>
|
path {
|
||||||
|
text-indent:0;
|
||||||
|
text-transform:none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</svg>
|
</svg>
|
||||||
<strong>{{ site_conf.site_name }}</strong>
|
<strong>{{ site_conf.site_name }}</strong>
|
||||||
</a>
|
</a>
|
||||||
@@ -178,13 +180,13 @@
|
|||||||
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Manufacturer</a></li>
|
<li><a class="dropdown-item" href="{% url 'manufacturers' category='model' %}">Manufacturer</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li class="ps-2 text-secondary">Prototype</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 'companies' %}">Company</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Manufacturer</a></li>
|
<li><a class="dropdown-item" href="{% url 'manufacturers' category='real' %}">Manufacturer</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% show_bookshelf_menu %}
|
{% show_bookshelf_menu %}
|
||||||
{% show_flatpages_menu user %}
|
{% show_flatpages_menu %}
|
||||||
</ul>
|
</ul>
|
||||||
{% include 'includes/search.html' %}
|
{% include 'includes/search.html' %}
|
||||||
</div>
|
</div>
|
||||||
@@ -211,13 +213,12 @@
|
|||||||
<div class="container">{% block pagination %}{% endblock %}</div>
|
<div class="container">{% block pagination %}{% endblock %}</div>
|
||||||
</div>
|
</div>
|
||||||
{% block extra_content %}{% endblock %}
|
{% block extra_content %}{% endblock %}
|
||||||
{% include 'includes/symbols.html' %}
|
|
||||||
</main>
|
</main>
|
||||||
{% include 'includes/footer.html' %}
|
{% include 'includes/footer.html' %}
|
||||||
{% if site_conf.use_cdn %}
|
{% if site_conf.use_cdn %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/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 %}
|
{% else %}
|
||||||
<script src="{% static "bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js" %}"></script>
|
<script src="{% static "bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" %}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load dynamic_url %}
|
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if book.tags.all %}
|
{% if book.tags.all %}
|
||||||
@@ -9,10 +8,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not book.published %}
|
<small class="text-muted">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
<span class="badge text-bg-warning">Unpublished</span> |
|
|
||||||
{% endif %}
|
|
||||||
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -31,11 +27,11 @@
|
|||||||
{% if book.image.count > 1 %}
|
{% if book.image.count > 1 %}
|
||||||
<button class="carousel-control-prev" type="button" data-bs-target="#carouselControls" data-bs-slide="prev">
|
<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="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>
|
||||||
<button class="carousel-control-next" type="button" data-bs-target="#carouselControls" data-bs-slide="next">
|
<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="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>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -49,49 +45,35 @@
|
|||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
|
<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>
|
<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>
|
</nav>
|
||||||
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
||||||
<option value="nav-summary" selected>Summary</option>
|
<option value="nav-summary" selected>Summary</option>
|
||||||
{% if documents %}<option value="nav-documents">Documents</option>{% endif %}
|
{% if book.notes %}<option value="nav-notes">Notes</option>{% endif %}
|
||||||
</select>
|
</select>
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">Book</th>
|
||||||
{% if type == "catalog" %}Catalog
|
|
||||||
{% elif type == "book" %}Book{% endif %}
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<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>
|
<tr>
|
||||||
<th class="w-33" scope="row">Title</th>
|
<th class="w-33" scope="row">Title</th>
|
||||||
<td>{{ book.title }}</td>
|
<td>{{ book.title }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Authors</th>
|
<th scope="row">Authors</th>
|
||||||
<td>
|
<td>
|
||||||
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
|
<ul class="mb-0 list-unstyled">{% for a in book.authors.all %}<li>{{ a }}</li>{% endfor %}</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Publisher</th>
|
<th scope="row">Publisher</th>
|
||||||
<td>{{ book.publisher }}</td>
|
<td>{{ book.publisher }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">ISBN</th>
|
<th scope="row">ISBN</th>
|
||||||
<td>{{ book.ISBN|default:"-" }}</td>
|
<td>{{ book.ISBN|default:"-" }}</td>
|
||||||
@@ -108,41 +90,13 @@
|
|||||||
<th scope="row">Publication year</th>
|
<th scope="row">Publication year</th>
|
||||||
<td>{{ book.publication_year|default:"-" }}</td>
|
<td>{{ book.publication_year|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if book.description %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Description</th>
|
<th scope="row">Purchase date</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>
|
|
||||||
<td>{{ book.purchase_date|default:"-" }}</td>
|
<td>{{ book.purchase_date|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th scope="row">Price ({{ site_conf.currency }})</th>
|
|
||||||
<td>{{ book.price|default:"-" }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% if book_properties %}
|
||||||
{% if properties %}
|
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -150,7 +104,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
{% for p in properties %}
|
{% for p in book_properties %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">{{ p.property }}</th>
|
<th class="w-33" scope="row">{{ p.property }}</th>
|
||||||
<td>{{ p.value }}</td>
|
<td>{{ p.value }}</td>
|
||||||
@@ -160,27 +114,12 @@
|
|||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="nav-documents" role="tabpanel" aria-labelledby="nav-documents-tab">
|
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
|
||||||
<table class="table table-striped">
|
{{ book.notes | safe }}
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
{% extends "cards.html" %}
|
{% extends "cards.html" %}
|
||||||
{% block pagination %}
|
{% block pagination %}
|
||||||
{% if data.has_other_pages %}
|
{% if data.has_other_pages %}
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation example">
|
||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url '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>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
<span class="page-link">Previous</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for i in page_range %}
|
{% for i in page_range %}
|
||||||
@@ -21,17 +21,17 @@
|
|||||||
{% if i == data.paginator.ELLIPSIS %}
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item"><a class="page-link" href="{% url '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 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url '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>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
<span class="page-link">Next</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
@@ -4,12 +4,7 @@
|
|||||||
Bookshelf
|
Bookshelf
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink">
|
<ul class="dropdown-menu" aria-labelledby="bookshelfDropdownMenuLink">
|
||||||
{% if books_menu %}
|
|
||||||
<li><a class="dropdown-item" href="{% url 'books' %}">Books</a></li>
|
<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>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<p class="lead text-body-secondary">Results found: {{ matches }}</p>
|
<p class="lead text-muted">Results found: {{ matches }}</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block cards_layout %}
|
{% block cards_layout %}
|
||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
|
||||||
{% block cards %}
|
{% block cards %}
|
||||||
{% for d in data %}
|
{% for d in data %}
|
||||||
{% if d.type == "roster" %}
|
{% if d.type == "rolling_stock" %}
|
||||||
{% include "cards/roster.html" %}
|
{% include "cards/roster.html" %}
|
||||||
{% elif d.type == "company" %}
|
{% elif d.type == "company" %}
|
||||||
{% include "cards/company.html" %}
|
{% include "cards/company.html" %}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
{% include "cards/consist.html" %}
|
{% include "cards/consist.html" %}
|
||||||
{% elif d.type == "manufacturer" %}
|
{% elif d.type == "manufacturer" %}
|
||||||
{% include "cards/manufacturer.html" %}
|
{% include "cards/manufacturer.html" %}
|
||||||
{% elif d.type == "book" or d.type == "catalog" %}
|
{% elif d.type == "book" %}
|
||||||
{% include "cards/book.html" %}
|
{% include "cards/book.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -1,12 +1,7 @@
|
|||||||
{% load static %}
|
|
||||||
{% load dynamic_url %}
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
{% if d.item.image.exists %}
|
{% 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>
|
<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 %}
|
{% endif %}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text" style="position: relative;">
|
<p class="card-text" style="position: relative;">
|
||||||
@@ -23,28 +18,10 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">Book</th>
|
||||||
{% 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">Unpublished</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<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>
|
<tr>
|
||||||
<th class="w-33" scope="row">Authors</th>
|
<th class="w-33" scope="row">Authors</th>
|
||||||
<td>
|
<td>
|
||||||
@@ -55,7 +32,6 @@
|
|||||||
<th class="w-33" scope="row">Publisher</th>
|
<th class="w-33" scope="row">Publisher</th>
|
||||||
<td>{{ d.item.publisher }}</td>
|
<td>{{ d.item.publisher }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Language</th>
|
<th scope="row">Language</th>
|
||||||
<td>{{ d.item.get_language_display }}</td>
|
<td>{{ d.item.get_language_display }}</td>
|
||||||
@@ -72,7 +48,7 @@
|
|||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
<a class="btn btn-sm btn-outline-primary" href="{{ d.item.get_absolute_url }}">Show all data</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -7,14 +7,7 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">Company</th>
|
||||||
Company
|
|
||||||
<div class="float-end">
|
|
||||||
{% if d.item.freelance %}
|
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -34,15 +27,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Country</th>
|
<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>
|
</tr>
|
||||||
|
{% if d.item.freelance %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Notes</th>
|
||||||
|
<td>A <em>freelance</em> company</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
{% with items=d.item.num_items %}
|
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="company" search=d.item.slug %}">Show all rolling stock</a>
|
||||||
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="company" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_company_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
{% endwith %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -24,17 +24,7 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">Consist</th>
|
||||||
Consist
|
|
||||||
<div class="float-end">
|
|
||||||
{% if not d.item.published %}
|
|
||||||
<span class="badge text-bg-warning">Unpublished</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if d.item.company.freelance %}
|
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -46,10 +36,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Company</th>
|
<th class="w-33" scope="row">Company</th>
|
||||||
<td>
|
<td><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></td>
|
||||||
<img src="{{ d.item.company.country.flag }}" alt="{{ d.item.company.country }}">
|
|
||||||
<abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Era</th>
|
<th scope="row">Era</th>
|
||||||
@@ -57,7 +44,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Length</th>
|
<th scope="row">Length</th>
|
||||||
<td>{{ d.item.length }}</td>
|
<td>{{ d.item.consist_item.count }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -30,10 +30,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
{% with items=d.item.num_items %}
|
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="manufacturer" search=d.item.slug %}">Show all rolling stock</a>
|
||||||
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="manufacturer" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_manufacturer_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
{% endwith %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -20,10 +20,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
{% with items=d.item.num_items %}
|
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="type" search=d.item.slug %}">Show all rolling stock</a>
|
||||||
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="type" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_rollingstocktype_change' d.item.pk %}">Edit</a>{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load dcc %}
|
|
||||||
|
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
{% if d.item.image.exists %}
|
{% if d.item.image.exists %}
|
||||||
@@ -24,17 +22,7 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">Rolling stock</th>
|
||||||
Rolling stock
|
|
||||||
<div class="float-end">
|
|
||||||
{% if not d.item.published %}
|
|
||||||
<span class="badge text-bg-warning">Unpublished</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if d.item.company.freelance %}
|
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -45,8 +33,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Company</th>
|
<th scope="row">Company</th>
|
||||||
<td>
|
<td>
|
||||||
<img src="{{ d.item.company.country.flag }}" alt="{{ d.item.company.country }}">
|
<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>
|
||||||
<a href="{% url 'filtered' _filter="company" search=d.item.company.slug %}"><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></a>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -69,18 +56,33 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Scale</th>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Item number</th>
|
<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>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">DCC</th>
|
|
||||||
<td><a class="text-reset text-decoration-none" title="Symbols" href="" data-bs-toggle="modal" data-bs-target="#symbolsModal">{% dcc d.item %}</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{% if d.item.decoder %}
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2" scope="row">DCC data</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Decoder</th>
|
||||||
|
<td>{{ d.item.decoder }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Address</th>
|
||||||
|
<td>{{ d.item.address }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<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>
|
<a class="btn btn-sm btn-outline-primary" href="{{d.item.get_absolute_url}}">Show all data</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:roster_rollingstock_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
|
@@ -17,21 +17,19 @@
|
|||||||
<th class="w-33" scope="row">Ratio</th>
|
<th class="w-33" scope="row">Ratio</th>
|
||||||
<td>{{ d.item.ratio }}</td>
|
<td>{{ d.item.ratio }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">Tracks</th>
|
|
||||||
<td>{{ d.item.tracks }} mm</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Gauge</th>
|
<th class="w-33" scope="row">Gauge</th>
|
||||||
<td>{{ d.item.gauge }}</td>
|
<td>{{ d.item.gauge }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Tracks</th>
|
||||||
|
<td>{{ d.item.tracks }}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="d-grid gap-2 mb-1 d-md-block">
|
<div class="d-grid gap-2 mb-1 d-md-block">
|
||||||
{% with items=d.item.num_items %}
|
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show all rolling stock</a>
|
||||||
<a class="btn btn-sm btn-outline-primary{% if items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show {{ items }} item{{ items | pluralize}}</a>
|
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
{% endwith %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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,10 +7,7 @@
|
|||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% if not consist.published %}
|
<small class="text-muted">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
<span class="badge text-bg-warning">Unpublished</span> |
|
|
||||||
{% endif %}
|
|
||||||
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
@@ -28,15 +25,15 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block pagination %}
|
{% block pagination %}
|
||||||
{% if data.has_other_pages %}
|
{% if data.has_other_pages %}
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation example">
|
||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url '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>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
<span class="page-link">Previous</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for i in page_range %}
|
{% for i in page_range %}
|
||||||
@@ -54,11 +51,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'consist_pagination' uuid=consist.uuid page=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>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
<span class="page-link">Next</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -71,23 +68,18 @@
|
|||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
<nav class="nav nav-tabs d-none d-lg-flex flex-row mb-2" id="nav-tab" role="tablist">
|
<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>
|
<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>
|
</nav>
|
||||||
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
||||||
<option value="nav-summary" selected>Summary</option>
|
<option value="nav-summary" selected>Summary</option>
|
||||||
|
{% if consist.notes %}<option value="nav-notes">Notes</option>{% endif %}
|
||||||
</select>
|
</select>
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
<div class="tab-pane show active" id="nav-summary" role="tabpanel" aria-labelledby="nav-summary-tab">
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2" scope="row">
|
<th colspan="2" scope="row">Data</th>
|
||||||
Consist
|
|
||||||
<div class="float-end">
|
|
||||||
{% if consist.company.freelance %}
|
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -101,19 +93,23 @@
|
|||||||
<th scope="row">Era</th>
|
<th scope="row">Era</th>
|
||||||
<td>{{ consist.era }}</td>
|
<td>{{ consist.era }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if consist.description %}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Description</th>
|
|
||||||
<td>{{ consist.description | safe }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Length</th>
|
<th scope="row">Length</th>
|
||||||
<td>{{ consist.length }}</td>
|
<td>{{ data | length }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Composition</th>
|
<th scope="row">Notes</th>
|
||||||
<td>{% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} » {% endif %}{% endfor %}</td>
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
<tr>
|
||||||
|
<td>{{ consist.notes | safe }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
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" %}
|
{% extends "cards.html" %}
|
||||||
{% block pagination %}
|
{% block pagination %}
|
||||||
{% if data.has_other_pages %}
|
{% if data.has_other_pages %}
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Page navigation example">
|
||||||
<ul class="pagination flex-wrap justify-content-center mt-4 mb-0">
|
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||||
{% if data.has_previous %}
|
{% if data.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url '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>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
<span class="page-link">Previous</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for i in page_range %}
|
{% for i in page_range %}
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if data.has_next %}
|
{% if data.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link" href="{% url 'filtered_pagination' _filter=filter search=search page=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>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
<span class="page-link">Next</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -1,12 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if not flatpage.published %}
|
<small class="text-muted">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
<span class="badge text-bg-warning">Unpublished</span> |
|
|
||||||
{% endif %}
|
|
||||||
<small class="text-body-secondary">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
|
|
||||||
{% endblock %}
|
|
||||||
{% block carousel %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extra_content %}
|
{% block extra_content %}
|
||||||
<section class="py-4 text-start container">
|
<section class="py-4 text-start container">
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{% if flatpages_menu %}
|
{% if flatpages_menu %}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="flatpageDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<a class="nav-link dropdown-toggle" href="#" id="flatpageDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
Pages
|
Articles
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu" aria-labelledby="flatpageDropdownMenuLink">
|
<ul class="dropdown-menu" aria-labelledby="flatpageDropdownMenuLink">
|
||||||
{% for m in flatpages_menu %}
|
{% for m in flatpages_menu %}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{% extends "pagination.html" %}
|
{% extends "roster.html" %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<div class="text-body-secondary">{{ site_conf.about | safe }}</div>
|
<div class="text-muted">{{ site_conf.about | safe }}</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user