mirror of
https://github.com/daniviga/django-ram.git
synced 2025-08-08 07:07:49 +02:00
Compare commits
2 Commits
v0.16.9
...
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: 13488e1e93...2db2b0ecc6
Submodule arduino/WebThrottle-EX updated: eb43d7906f...c67e4080d0
Submodule arduino/arduino-cli updated: fa6eafcbbe...048415c5e6
@@ -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,317 +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 bookshelf.models import (
|
|
||||||
BaseBookProperty,
|
|
||||||
BaseBookImage,
|
|
||||||
BaseBookDocument,
|
|
||||||
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 BookDocInline(admin.TabularInline):
|
|
||||||
model = BaseBookDocument
|
|
||||||
min_num = 0
|
|
||||||
extra = 0
|
|
||||||
classes = ["collapse"]
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
@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 = ("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",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"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="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,
|
|
||||||
BookDocInline,
|
|
||||||
)
|
|
||||||
list_display = (
|
|
||||||
"__str__",
|
|
||||||
"manufacturer",
|
|
||||||
"years",
|
|
||||||
"get_scales",
|
|
||||||
"published",
|
|
||||||
)
|
|
||||||
autocomplete_fields = ("manufacturer",)
|
|
||||||
readonly_fields = ("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": (
|
|
||||||
"purchase_date",
|
|
||||||
"price",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"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
|
|
||||||
|
|
||||||
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,46 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-01-26 14:32
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def shop_from_property(apps, schema_editor):
|
|
||||||
basebook = apps.get_model("bookshelf", "BaseBook")
|
|
||||||
shop_model = apps.get_model("metadata", "Shop")
|
|
||||||
for row in basebook.objects.all():
|
|
||||||
property = row.property.filter(
|
|
||||||
property__name__icontains="shop"
|
|
||||||
).first()
|
|
||||||
if property:
|
|
||||||
shop, created = shop_model.objects.get_or_create(
|
|
||||||
name=property.value,
|
|
||||||
defaults={"on_line": False}
|
|
||||||
)
|
|
||||||
|
|
||||||
row.shop = shop
|
|
||||||
row.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bookshelf", "0021_basebookdocument_creation_time_and_more"),
|
|
||||||
("metadata", "0023_shop"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="basebook",
|
|
||||||
name="shop",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="metadata.shop",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.RunPython(
|
|
||||||
shop_from_property,
|
|
||||||
reverse_code=migrations.RunPython.noop
|
|
||||||
),
|
|
||||||
]
|
|
@@ -1,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, Document, 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,78 +101,11 @@ class BaseBookImage(Image):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseBookDocument(Document):
|
class BookProperty(PropertyInstance):
|
||||||
book = models.ForeignKey(
|
book = models.ForeignKey(
|
||||||
BaseBook, on_delete=models.CASCADE, related_name="document"
|
Book,
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "Documents"
|
|
||||||
constraints = [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["book", "file"],
|
|
||||||
name="unique_book_file"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class BaseBookProperty(PropertyInstance):
|
|
||||||
book = models.ForeignKey(
|
|
||||||
BaseBook,
|
|
||||||
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):
|
|
||||||
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,8 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
|
||||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||||
|
|
||||||
from ram.admin import publish, unpublish
|
|
||||||
from consist.models import Consist, ConsistItem
|
from consist.models import Consist, ConsistItem
|
||||||
|
|
||||||
|
|
||||||
@@ -10,15 +8,7 @@ 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",
|
|
||||||
"address",
|
|
||||||
"type",
|
|
||||||
"company",
|
|
||||||
"era",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Consist)
|
@admin.register(Consist)
|
||||||
@@ -28,37 +18,26 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"creation_time",
|
"creation_time",
|
||||||
"updated_time",
|
"updated_time",
|
||||||
)
|
)
|
||||||
list_filter = ("company", "era", "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",
|
||||||
"consist_address",
|
"consist_address",
|
||||||
"company",
|
"company",
|
||||||
"era",
|
"era",
|
||||||
"description",
|
|
||||||
"image",
|
"image",
|
||||||
|
"notes",
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"Notes",
|
|
||||||
{"classes": ("collapse",), "fields": ("notes",)},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"Audit",
|
"Audit",
|
||||||
{
|
{
|
||||||
@@ -70,4 +49,3 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
actions = [publish, unpublish]
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
# Generated by Django 4.1 on 2022-08-23 15:54
|
# 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,37 +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.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, 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",
|
|
||||||
)
|
|
||||||
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)
|
||||||
@@ -39,16 +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 country(self):
|
|
||||||
return self.company.country
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self.consist_item.filter(rolling_stock__published=False).exists():
|
|
||||||
raise ValidationError(
|
|
||||||
"You must publish all items in the consist before publishing the consist." # noqa: E501
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["company", "-creation_time"]
|
ordering = ["company", "-creation_time"]
|
||||||
|
|
||||||
@@ -58,55 +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(
|
order = models.PositiveIntegerField(default=0, blank=False, null=False)
|
||||||
default=1000, # make sure it is always added at the end
|
|
||||||
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 published(self):
|
|
||||||
return self.rolling_stock.published
|
|
||||||
published.boolean = True
|
|
||||||
|
|
||||||
def preview(self):
|
|
||||||
return self.rolling_stock.image.first().image_thumbnail(100)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def type(self):
|
def type(self):
|
||||||
return self.rolling_stock.rolling_class.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,19 +1,15 @@
|
|||||||
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 ram.admin import publish, unpublish
|
|
||||||
from metadata.models import (
|
from metadata.models import (
|
||||||
Property,
|
Property,
|
||||||
Decoder,
|
Decoder,
|
||||||
DecoderDocument,
|
DecoderDocument,
|
||||||
Scale,
|
Scale,
|
||||||
Shop,
|
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
Company,
|
Company,
|
||||||
Tag,
|
Tag,
|
||||||
RollingStockType,
|
RollingStockType,
|
||||||
GenericDocument,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -49,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):
|
||||||
@@ -86,55 +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(GenericDocument)
|
|
||||||
class GenericDocumentAdmin(admin.ModelAdmin):
|
|
||||||
readonly_fields = ("size", "creation_time", "updated_time")
|
|
||||||
list_display = (
|
|
||||||
"__str__",
|
|
||||||
"description",
|
|
||||||
"private",
|
|
||||||
"size",
|
|
||||||
"download",
|
|
||||||
)
|
|
||||||
search_fields = (
|
|
||||||
"description",
|
|
||||||
"file",
|
|
||||||
)
|
|
||||||
fieldsets = (
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"fields": (
|
|
||||||
"private",
|
|
||||||
"description",
|
|
||||||
"file",
|
|
||||||
"size",
|
|
||||||
"tags",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Notes",
|
|
||||||
{"classes": ("collapse",), "fields": ("notes",)},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Audit",
|
|
||||||
{
|
|
||||||
"classes": ("collapse",),
|
|
||||||
"fields": (
|
|
||||||
"creation_time",
|
|
||||||
"updated_time",
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
actions = [publish, unpublish]
|
|
||||||
|
|
||||||
|
|
||||||
@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")],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@@ -3,22 +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 tinymce import models as tinymce
|
|
||||||
|
|
||||||
from ram.models import Document
|
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"
|
||||||
@@ -27,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)
|
||||||
@@ -36,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"),
|
||||||
@@ -53,11 +43,10 @@ class Manufacturer(models.Model):
|
|||||||
|
|
||||||
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):
|
||||||
@@ -88,11 +77,10 @@ class Company(models.Model):
|
|||||||
|
|
||||||
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):
|
||||||
@@ -120,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)
|
||||||
@@ -138,57 +126,31 @@ class DecoderDocument(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
unique_together = ("decoder", "file")
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=["decoder", "file"],
|
|
||||||
name="unique_decoder_file"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_ratio(ratio):
|
|
||||||
try:
|
|
||||||
num, den = ratio.split(":")
|
|
||||||
return int(num) / float(den) * 10000
|
|
||||||
except (ValueError, ZeroDivisionError):
|
|
||||||
raise ValidationError("Invalid ratio format")
|
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -197,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):
|
||||||
@@ -223,7 +179,7 @@ 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 = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -231,36 +187,13 @@ class Tag(models.Model):
|
|||||||
|
|
||||||
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 GenericDocument(Document):
|
|
||||||
notes = tinymce.HTMLField(blank=True)
|
|
||||||
tags = models.ManyToManyField(Tag, blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "Generic Documents"
|
|
||||||
|
|
||||||
|
|
||||||
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.11.3 (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?dd67030699838ea613ee6dbda90effa6") format("woff2"),
|
src: url("./fonts/bootstrap-icons.woff2?2820a3852bdb9a5832199cc61cec4e65") format("woff2"),
|
||||||
url("./fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("woff");
|
url("./fonts/bootstrap-icons.woff?2820a3852bdb9a5832199cc61cec4e65") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
.bi::before,
|
.bi::before,
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -17,11 +17,6 @@ td > img.logo-xl {
|
|||||||
max-height: 96px;
|
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,6 +38,10 @@ 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-notes > p {
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
#nav-journal ul, #nav-journal ol {
|
#nav-journal ul, #nav-journal ol {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
|
@@ -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.3/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.11.3/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.3/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.11.3/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,16 +118,14 @@
|
|||||||
<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');
|
tabs.forEach(function (tab) {
|
||||||
tabs.forEach(function (tab) {
|
tab.classList.remove('show', 'active');
|
||||||
tab.classList.remove('show', 'active');
|
});
|
||||||
});
|
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 %}
|
||||||
@@ -182,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>
|
||||||
@@ -218,9 +216,9 @@
|
|||||||
</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.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<script src="{% static "bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" %}"></script>
|
<script src="{% static "bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" %}"></script>
|
||||||
{% endif %}
|
{% 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,7 +8,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<small class="text-body-secondary">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-muted">Updated {{ book.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -28,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>
|
||||||
@@ -46,54 +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 %}
|
|
||||||
<div class="float-end">
|
|
||||||
{% if not book.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
@@ -110,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>
|
||||||
@@ -152,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>
|
||||||
@@ -162,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">Draft</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,8 +27,14 @@
|
|||||||
</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">
|
||||||
|
@@ -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 d.item.company.freelance %}
|
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if not d.item.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
|
@@ -22,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 d.item.company.freelance %}
|
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if not d.item.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -43,7 +33,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Company</th>
|
<th scope="row">Company</th>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'filtered' _filter="company" search=d.item.company.slug %}"><abbr title="{{ d.item.company.extended_name }}">{{ d.item.company }}</abbr></a>
|
<a href="{% url 'filtered' _filter="company" search=d.item.rolling_class.company.slug %}"><abbr title="{{ d.item.rolling_class.company.extended_name }}">{{ d.item.rolling_class.company }}</abbr></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -66,15 +56,15 @@
|
|||||||
</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>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if d.item.decoder or d.item.decoder_interface %}
|
{% if d.item.decoder %}
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -83,19 +73,13 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Interface</th>
|
<th class="w-33" scope="row">Decoder</th>
|
||||||
<td>{{ d.item.get_decoder_interface }}</td>
|
|
||||||
</tr>
|
|
||||||
{% if d.item.decoder %}
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Decoder</th>
|
|
||||||
<td>{{ d.item.decoder }}</td>
|
<td>{{ d.item.decoder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Address</th>
|
<th scope="row">Address</th>
|
||||||
<td>{{ d.item.address }}</td>
|
<td>{{ d.item.address }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -17,18 +17,18 @@
|
|||||||
<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">
|
||||||
<a class="btn btn-sm btn-outline-primary{% if d.item.num_items == 0 %} disabled{% endif %}" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show all rolling stock</a>
|
<a class="btn btn-sm btn-outline-primary" href="{% url 'filtered' _filter="scale" search=d.item.slug %}">Show all rolling stock</a>
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:metadata_scale_change' d.item.pk %}">Edit</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
40
ram/portal/templates/companies.html
Normal file
40
ram/portal/templates/companies.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends "cards.html" %}
|
||||||
|
{% block pagination %}
|
||||||
|
{% if data.has_other_pages %}
|
||||||
|
<nav aria-label="Page navigation example">
|
||||||
|
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||||
|
{% if data.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'companies_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">Previous</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in page_range %}
|
||||||
|
{% if data.number == i %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ i }}</span>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item"><a class="page-link" href="{% url 'companies_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if data.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'companies_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">Next</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@@ -7,7 +7,7 @@
|
|||||||
{{ t.name }}</a>{# new line is required #}
|
{{ t.name }}</a>{# new line is required #}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
<small class="text-body-secondary">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-muted">Updated {{ consist.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
@@ -25,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 %}
|
||||||
@@ -51,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>
|
||||||
@@ -68,26 +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 %}
|
|
||||||
{% if not consist.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -101,12 +93,6 @@
|
|||||||
<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>{{ data | length }}</td>
|
<td>{{ data | length }}</td>
|
||||||
@@ -114,6 +100,20 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-group-divider">
|
||||||
|
<tr>
|
||||||
|
<td>{{ consist.notes | safe }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' consist.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:consist_consist_change' consist.pk %}">Edit</a>{% endif %}
|
||||||
|
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,19 +1,12 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<small class="text-body-secondary">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-muted">Updated {{ flatpage.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endblock %}
|
|
||||||
{% block carousel %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extra_content %}
|
{% block extra_content %}
|
||||||
<section class="py-4 text-start container">
|
<section class="py-4 text-start container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="mx-auto">
|
<div class="mx-auto">
|
||||||
{% if not flatpage.published %}
|
|
||||||
<div class="alert alert-warning" role="alert">
|
|
||||||
⚠️ This page is a <strong>draft</strong> and is not published.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div>{{ flatpage.content | safe }} </div>
|
<div>{{ flatpage.content | safe }} </div>
|
||||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||||
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:portal_flatpage_change' flatpage.pk %}">Edit</a>{% endif %}
|
{% if request.user.is_staff %}<a class="btn btn-sm btn-outline-danger" href="{% url 'admin:portal_flatpage_change' flatpage.pk %}">Edit</a>{% endif %}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{% if flatpages_menu %}
|
{% 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 %}
|
||||||
|
@@ -1,34 +1,18 @@
|
|||||||
<footer class="text-body-secondary py-4">
|
<footer class="text-muted py-4">
|
||||||
<div class="container d-lg-flex justify-content-between">
|
|
||||||
<div id="footer" class="mb-1">
|
|
||||||
<p>© {% now "Y" %}</p> {{ site_conf.footer | safe }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="footer_extended">
|
<p class="float-end mb-1">
|
||||||
|
<a href="#">Back to top</a>
|
||||||
|
</p>
|
||||||
|
<div id="footer" class="mb-1">
|
||||||
|
<p>© {% now "Y" %}</p> {{ site_conf.footer | safe }}
|
||||||
|
</div>
|
||||||
|
<div id="footer_extended" class="mb-0">
|
||||||
{{ site_conf.footer_extended | safe }}
|
{{ site_conf.footer_extended | safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container d-flex text-body-secondary">
|
|
||||||
<p class="flex-fill small">Made with ❤️ for 🚂 and <i class="bi bi-github"></i> <a href="https://github.com/daniviga/django-ram">django-ram</a>
|
<div class="container">
|
||||||
{% if site_conf.show_version %}<br>Version {{ site_conf.version }}{% endif %}</p>
|
<p class="small text-muted">Made with ❤️ for 🚂 and <a href="https://github.com/daniviga/django-ram">django-ram</a>
|
||||||
<p class="text-end fs-5">
|
{% if site_conf.show_version %}<br>Version {{ site_conf.version }}{% endif %}
|
||||||
{% if site_conf.disclaimer %}<a class="text-reset" title="Disclaimer" href="" data-bs-toggle="modal" data-bs-target="#disclaimerModal"><i class="bi bi-info-square-fill"></i></a> {% endif %}
|
|
||||||
<a class="text-reset" title="Back to top" href="#"><i class="bi bi-arrow-up-left-square-fill"></i></a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- Modal -->
|
|
||||||
<div class="modal fade" id="disclaimerModal" tabindex="-1" aria-labelledby="disclaimerLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h1 class="modal-title fs-5" id="disclaimerLabel">Disclaimer</h1>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
{{ site_conf.disclaimer | safe }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@@ -18,12 +18,7 @@
|
|||||||
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
|
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a></li>
|
<li><a class="dropdown-item" href="{% url 'admin:portal_siteconfiguration_changelist' %}">Site configuration</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li>
|
<li><a class="dropdown-item text-danger" href="{% url 'admin:logout' %}?next={{ request.path }}">Logout</a></li>
|
||||||
<form id="logout-form" method="post" action="{% url 'admin:logout' %}?next={{ request.path }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button class="btn btn-link dropdown-item text-danger" type="submit">Log out</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="nav-link" href="{% url 'admin:login' %}?next={{ request.path }}">Log in</a>
|
<a class="nav-link" href="{% url 'admin:login' %}?next={{ request.path }}">Log in</a>
|
||||||
|
@@ -2,15 +2,15 @@
|
|||||||
{% block pagination %}
|
{% block pagination %}
|
||||||
{% if data.has_other_pages %}
|
{% if data.has_other_pages %}
|
||||||
{% with data.0.item.category as c %}
|
{% with data.0.item.category as c %}
|
||||||
<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 'manufacturers_pagination' category=c page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||||
</li>
|
</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 %}
|
||||||
@@ -28,11 +28,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 'manufacturers_pagination' category=c page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url 'manufacturers_pagination' category=c page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||||
</li>
|
</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>
|
@@ -8,7 +8,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<small class="text-body-secondary">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
|
<small class="text-muted">Updated {{ rolling_stock.updated_time | date:"M d, Y H:i" }}</small>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block carousel %}
|
{% block carousel %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
{% if rolling_stock.image.count > 1 %}
|
{% if rolling_stock.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>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
{% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %}
|
{% if rolling_stock.decoder %}<button class="nav-link" id="nav-dcc-tab" data-bs-toggle="tab" data-bs-target="#nav-dcc" type="button" role="tab" aria-controls="nav-dcc" aria-selected="false">DCC</button>{% endif %}
|
||||||
{% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
|
{% if documents or decoder_documents %}<button class="nav-link" id="nav-documents-tab" data-bs-toggle="tab" data-bs-target="#nav-documents" type="button" role="tab" aria-controls="nav-documents" aria-selected="false">Documents</button>{% endif %}
|
||||||
{% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
|
{% if journal %}<button class="nav-link" id="nav-journal-tab" data-bs-toggle="tab" data-bs-target="#nav-journal" type="button" role="tab" aria-controls="nav-journal" aria-selected="false">Journal</button>{% endif %}
|
||||||
{% if set %}<button class="nav-link" id="nav-set-tab" data-bs-toggle="tab" data-bs-target="#nav-set" type="button" role="tab" aria-controls="nav-set" aria-selected="false">Set</button>{% endif %}
|
{% if rolling_stock.notes %}<button class="nav-link" id="nav-notes-tab" data-bs-toggle="tab" data-bs-target="#nav-notes" type="button" role="tab" aria-controls="nav-notes" aria-selected="false">Notes</button>{% endif %}
|
||||||
{% if consists %}<button class="nav-link" id="nav-consists-tab" data-bs-toggle="tab" data-bs-target="#nav-consists" type="button" role="tab" aria-controls="nav-consists" aria-selected="false">Consists</button>{% endif %}
|
{% if consists %}<button class="nav-link" id="nav-consists-tab" data-bs-toggle="tab" data-bs-target="#nav-consists" type="button" role="tab" aria-controls="nav-consists" aria-selected="false">Consists</button>{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
<select class="form-select d-lg-none mb-2" id="tabSelector" aria-label="Tab selector">
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
{% if rolling_stock.decoder %}<option value="nav-dcc">DCC</option>{% endif %}
|
{% if rolling_stock.decoder %}<option value="nav-dcc">DCC</option>{% endif %}
|
||||||
{% if documents or decoder_documents %}<option value="nav-documents">Documents</option>{% endif %}
|
{% if documents or decoder_documents %}<option value="nav-documents">Documents</option>{% endif %}
|
||||||
{% if journal %}<option value="nav-journal">Journal</option>{% endif %}
|
{% if journal %}<option value="nav-journal">Journal</option>{% endif %}
|
||||||
{% if set %}<option value="nav-set">Set</option>{% endif %}
|
{% if rolling_stock.notes %}<option value="nav-notes">Notes</option>{% endif %}
|
||||||
{% if consists %}<option value="nav-consists">Consists</option>{% endif %}
|
{% if consists %}<option value="nav-consists">Consists</option>{% endif %}
|
||||||
</select>
|
</select>
|
||||||
{% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %}
|
{% with class=rolling_stock.rolling_class company=rolling_stock.rolling_class.company %}
|
||||||
@@ -71,17 +71,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">Data</th>
|
||||||
Rolling stock
|
|
||||||
<div class="mt-1 float-end">
|
|
||||||
{% if company.freelance %}
|
|
||||||
<span class="badge text-bg-secondary">Freelance</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if not rolling_stock.published %}
|
|
||||||
<span class="badge text-bg-warning">Draft</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -92,13 +82,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Company</th>
|
<th scope="row">Company</th>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> {{ company.extended_name_pp }}
|
<a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company }}</a> {{ company.extended_name_pp }}
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Country</th>
|
|
||||||
<td>
|
|
||||||
<img src="{{ company.country.flag }}" alt="{{ company.country }}"> {{ company.country.name }}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -130,11 +114,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Scale</th>
|
<th scope="row">Scale</th>
|
||||||
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }} mm">{{ rolling_stock.scale }}</abbr></a></td>
|
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Item number</th>
|
<th scope="row">Item number</th>
|
||||||
<td>{{ rolling_stock.item_number }}{%if rolling_stock.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=rolling_stock.manufacturer.slug search=rolling_stock.item_number_slug %}">SET</a>{% endif %}</td>
|
<td>{{ rolling_stock.item_number }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -148,7 +132,7 @@
|
|||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Interface</th>
|
<th class="w-33" scope="row">Interface</th>
|
||||||
<td>{{ rolling_stock.get_decoder_interface }}</td>
|
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if rolling_stock.decoder %}
|
{% if rolling_stock.decoder %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -182,11 +166,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Scale</th>
|
<th scope="row">Scale</th>
|
||||||
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }} mm">{{ rolling_stock.scale }}</abbr></a></td>
|
<td><a href="{% url 'filtered' _filter="scale" search=rolling_stock.scale %}"><abbr title="{{ rolling_stock.scale.ratio }} - {{ rolling_stock.scale.tracks }}">{{ rolling_stock.scale }}</abbr></a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Item number</th>
|
<th scope="row">Item number</th>
|
||||||
<td>{{ rolling_stock.item_number }}{%if rolling_stock.set %} | <a class="badge text-bg-primary" href="{% url 'manufacturer' manufacturer=rolling_stock.manufacturer.slug search=rolling_stock.item_number_slug %}">SET</a>{% endif %}</td>
|
<td>{{ rolling_stock.item_number }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Era</th>
|
<th scope="row">Era</th>
|
||||||
@@ -194,42 +178,14 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Production year</th>
|
<th scope="row">Production year</th>
|
||||||
<td>{{ rolling_stock.production_year | default:"-" }}</td>
|
<td>{{ rolling_stock.production_year|default:"-" }}</td>
|
||||||
</tr>
|
|
||||||
{% if rolling_stock.description %}
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">Description</th>
|
|
||||||
<td>{{ rolling_stock.description | safe }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% if request.user.is_staff %}
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="2" scope="row">Purchase</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="table-group-divider">
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">Shop</th>
|
|
||||||
<td>
|
|
||||||
{{ rolling_stock.shop | default:"-" }}
|
|
||||||
{% if rolling_stock.shop.website %} <a href="{{ rolling_stock.shop.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Purchase date</th>
|
<th scope="row">Purchase date</th>
|
||||||
<td>{{ rolling_stock.purchase_date | default:"-" }}</td>
|
<td>{{ rolling_stock.purchase_date|default:"-" }}</td>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Price ({{ site_conf.currency }})</th>
|
|
||||||
<td>{{ rolling_stock.price | default:"-" }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
|
||||||
{% if properties %}
|
{% if properties %}
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -267,20 +223,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Manufacturer</th>
|
<th scope="row">Manufacturer</th>
|
||||||
<td>
|
<td>
|
||||||
{% for m in class.manufacturer.all %}
|
{%if class.manufacturer %}
|
||||||
{% if not forloop.first %} / {% endif %}
|
<a href="{% url 'filtered' _filter="manufacturer" search=class.manufacturer.slug %}">{{ class.manufacturer }}</a>{% if class.manufacturer.website %} <a href="{{ class.manufacturer.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
||||||
<a href="{% url 'filtered' _filter="manufacturer" search=m.slug %}">{{ m }}</a>{% if m.website %} <a href="{{ m.website }}" target="_blank"><i class="bi bi-box-arrow-up-right"></i></a>{% endif %}
|
{% else %}-{% endif %}
|
||||||
{% empty %}
|
|
||||||
-
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if class.description %}
|
|
||||||
<tr>
|
|
||||||
<th class="w-33" scope="row">Description</th>
|
|
||||||
<td>{{ class.description | safe }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if class_properties %}
|
{% if class_properties %}
|
||||||
@@ -305,12 +252,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 data</th>
|
||||||
Company data
|
|
||||||
{% if company.freelance %}
|
|
||||||
<span class="mt-1 float-end badge text-bg-secondary">Freelance</span>
|
|
||||||
{% endif %}
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
@@ -322,16 +264,18 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Name</th>
|
<th class="w-33" scope="row">Name</th>
|
||||||
<td>
|
<td><a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> {{ company.extended_name_pp }}</td>
|
||||||
<a href="{% url 'filtered' _filter="company" search=company.slug %}">{{ company.name }}</a> {{ company.extended_name_pp }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Country</th>
|
<th class="w-33" scope="row">Country</th>
|
||||||
<td>
|
<td>{{ company.country.name }} <img src="{{ company.country.flag }}" alt="{{ company.country }}">
|
||||||
<img src="{{ company.country.flag }}" alt="{{ company.country }}"> {{ company.country.name }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if company.freelance %}
|
||||||
|
<tr>
|
||||||
|
<th class="w-33" scope="row">Notes</th>
|
||||||
|
<td>A <em>freelance</em> company</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,7 +289,7 @@
|
|||||||
<tbody class="table-group-divider">
|
<tbody class="table-group-divider">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Interface</th>
|
<th scope="row">Interface</th>
|
||||||
<td>{{ rolling_stock.get_decoder_interface }}</td>
|
<td>{{ rolling_stock.get_decoder_interface_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-33" scope="row">Address</th>
|
<th class="w-33" scope="row">Address</th>
|
||||||
@@ -357,11 +301,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Manufacturer</th>
|
<th scope="row">Manufacturer</th>
|
||||||
<td>{{ rolling_stock.decoder.manufacturer | default:"-" }}</td>
|
<td>{{ rolling_stock.decoder.manufacturer|default:"-" }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Version</th>
|
<th scope="row">Version</th>
|
||||||
<td>{{ rolling_stock.decoder.version | default:"-"}}</td>
|
<td>{{ rolling_stock.decoder.version }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Sound</th>
|
<th scope="row">Sound</th>
|
||||||
@@ -425,12 +369,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="nav-set" role="tabpanel" aria-labelledby="nav-set-tab">
|
<div class="tab-pane" id="nav-notes" role="tabpanel" aria-labelledby="nav-notes-tab">
|
||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3">
|
{{ rolling_stock.notes | safe }}
|
||||||
{% for d in set %}
|
|
||||||
{% include "cards/roster.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="nav-consists" role="tabpanel" aria-labelledby="nav-cosists-tab">
|
<div class="tab-pane" id="nav-consists" role="tabpanel" aria-labelledby="nav-cosists-tab">
|
||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mb-3">
|
||||||
|
@@ -1,17 +1,16 @@
|
|||||||
{% extends "cards.html" %}
|
{% extends "cards.html" %}
|
||||||
{% load dynamic_url %}
|
|
||||||
|
|
||||||
{% 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="{% dynamic_pagination type page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url 'roster_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||||
</li>
|
</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 %}
|
||||||
@@ -23,17 +22,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="{% dynamic_pagination type page=i %}#main-content">{{ i }}</a></li>
|
<li class="page-item"><a class="page-link" href="{% url 'roster_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% 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="{% dynamic_pagination type page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url 'roster_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||||
</li>
|
</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>
|
40
ram/portal/templates/scales.html
Normal file
40
ram/portal/templates/scales.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends "cards.html" %}
|
||||||
|
{% block pagination %}
|
||||||
|
{% if data.has_other_pages %}
|
||||||
|
<nav aria-label="Page navigation example">
|
||||||
|
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||||
|
{% if data.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'scales_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">Previous</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in page_range %}
|
||||||
|
{% if data.number == i %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ i }}</span>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item"><a class="page-link" href="{% url 'scales_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if data.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'scales_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">Next</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
{% block pagination %}
|
{% 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 'search_pagination' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-left"></i></a>
|
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||||
</li>
|
</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 %}
|
||||||
@@ -28,11 +28,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 'search_pagination' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1"><i class="bi bi-chevron-right"></i></a>
|
<a class="page-link" href="{% url 'search_pagination' search=encoded_search page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||||
</li>
|
</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>
|
||||||
|
40
ram/portal/templates/types.html
Normal file
40
ram/portal/templates/types.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{% extends "cards.html" %}
|
||||||
|
{% block pagination %}
|
||||||
|
{% if data.has_other_pages %}
|
||||||
|
<nav aria-label="Page navigation example">
|
||||||
|
<ul class="pagination justify-content-center mt-4 mb-0">
|
||||||
|
{% if data.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'types_pagination' page=data.previous_page_number %}#main-content" tabindex="-1">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">Previous</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in page_range %}
|
||||||
|
{% if data.number == i %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ i }}</span>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
{% if i == data.paginator.ELLIPSIS %}
|
||||||
|
<li class="page-item"><span class="page-link">{{ i }}</span></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item"><a class="page-link" href="{% url 'types_pagination' page=i %}#main-content">{{ i }}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if data.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="{% url 'types_pagination' page=data.next_page_number %}#main-content" tabindex="-1">Next</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<span class="page-link">Next</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@@ -1,21 +0,0 @@
|
|||||||
from django import template
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
|
||||||
def dynamic_admin_url(app_name, model_name, object_id=None):
|
|
||||||
if object_id:
|
|
||||||
return reverse(
|
|
||||||
f'admin:{app_name}_{model_name}_change',
|
|
||||||
args=[object_id]
|
|
||||||
)
|
|
||||||
return reverse(f'admin:{app_name}_{model_name}_changelist')
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
|
||||||
def dynamic_pagination(reverse_name, page):
|
|
||||||
if reverse_name.endswith('y'):
|
|
||||||
return reverse(f'{reverse_name[:-1]}ies_pagination', args=[page])
|
|
||||||
return reverse(f'{reverse_name}s_pagination', args=[page])
|
|
@@ -1,21 +1,16 @@
|
|||||||
from django import template
|
from django import template
|
||||||
from portal.models import Flatpage
|
from portal.models import Flatpage
|
||||||
from bookshelf.models import Book, Catalog
|
from bookshelf.models import Book
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('bookshelf/bookshelf_menu.html')
|
@register.inclusion_tag('bookshelf/bookshelf_menu.html')
|
||||||
def show_bookshelf_menu():
|
def show_bookshelf_menu():
|
||||||
# FIXME: Filter out unpublished books and catalogs?
|
return {"bookshelf_menu": Book.objects.exists()}
|
||||||
return {
|
|
||||||
"bookshelf_menu": (Book.objects.exists() or Catalog.objects.exists()),
|
|
||||||
"books_menu": Book.objects.exists(),
|
|
||||||
"catalogs_menu": Catalog.objects.exists(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('flatpages/flatpages_menu.html')
|
@register.inclusion_tag('flatpages/flatpages_menu.html')
|
||||||
def show_flatpages_menu(user):
|
def show_flatpages_menu():
|
||||||
menu = Flatpage.objects.get_published(user).order_by("name")
|
menu = Flatpage.objects.filter(published=True).order_by("name")
|
||||||
return {"flatpages_menu": menu}
|
return {"flatpages_menu": menu}
|
||||||
|
@@ -4,7 +4,6 @@ from portal.views import (
|
|||||||
GetData,
|
GetData,
|
||||||
GetRoster,
|
GetRoster,
|
||||||
GetObjectsFiltered,
|
GetObjectsFiltered,
|
||||||
GetManufacturerItem,
|
|
||||||
GetFlatpage,
|
GetFlatpage,
|
||||||
GetRollingStock,
|
GetRollingStock,
|
||||||
GetConsist,
|
GetConsist,
|
||||||
@@ -14,19 +13,14 @@ from portal.views import (
|
|||||||
Scales,
|
Scales,
|
||||||
Types,
|
Types,
|
||||||
Books,
|
Books,
|
||||||
Catalogs,
|
GetBook,
|
||||||
GetBookCatalog,
|
|
||||||
SearchObjects,
|
SearchObjects,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", GetData.as_view(template="home.html"), name="index"),
|
path("", GetData.as_view(template="home.html"), name="index"),
|
||||||
path("roster", GetRoster.as_view(), name="roster"),
|
path("roster", GetRoster.as_view(), name="roster"),
|
||||||
path(
|
path("roster/<int:page>", GetRoster.as_view(), name="roster_pagination"),
|
||||||
"roster/page/<int:page>",
|
|
||||||
GetRoster.as_view(),
|
|
||||||
name="rosters_pagination"
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"page/<str:flatpage>",
|
"page/<str:flatpage>",
|
||||||
GetFlatpage.as_view(),
|
GetFlatpage.as_view(),
|
||||||
@@ -34,122 +28,88 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"consists",
|
"consists",
|
||||||
Consists.as_view(),
|
Consists.as_view(template="consists.html"),
|
||||||
name="consists"
|
name="consists"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"consists/page/<int:page>",
|
"consists/<int:page>",
|
||||||
Consists.as_view(),
|
Consists.as_view(template="consists.html"),
|
||||||
name="consists_pagination"
|
name="consists_pagination"
|
||||||
),
|
),
|
||||||
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
|
path("consist/<uuid:uuid>", GetConsist.as_view(), name="consist"),
|
||||||
path(
|
path(
|
||||||
"consist/<uuid:uuid>/page/<int:page>",
|
"consist/<uuid:uuid>/<int:page>",
|
||||||
GetConsist.as_view(),
|
GetConsist.as_view(),
|
||||||
name="consist_pagination",
|
name="consist_pagination",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"companies",
|
"companies",
|
||||||
Companies.as_view(),
|
Companies.as_view(template="companies.html"),
|
||||||
name="companies"
|
name="companies"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"companies/page/<int:page>",
|
"companies/<int:page>",
|
||||||
Companies.as_view(),
|
Companies.as_view(template="companies.html"),
|
||||||
name="companies_pagination",
|
name="companies_pagination",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"manufacturers/<str:category>",
|
"manufacturers/<str:category>",
|
||||||
Manufacturers.as_view(template="pagination_manufacturers.html"),
|
Manufacturers.as_view(template="manufacturers.html"),
|
||||||
name="manufacturers"
|
name="manufacturers"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"manufacturers/<str:category>/page/<int:page>",
|
"manufacturers/<str:category>/<int:page>",
|
||||||
Manufacturers.as_view(template="pagination_manufacturers.html"),
|
Manufacturers.as_view(template="manufacturers.html"),
|
||||||
name="manufacturers_pagination",
|
name="manufacturers_pagination",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"scales",
|
"scales",
|
||||||
Scales.as_view(),
|
Scales.as_view(template="scales.html"),
|
||||||
name="scales"
|
name="scales"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"scales/page/<int:page>",
|
"scales/<int:page>",
|
||||||
Scales.as_view(),
|
Scales.as_view(template="scales.html"),
|
||||||
name="scales_pagination"
|
name="scales_pagination"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"types",
|
"types",
|
||||||
Types.as_view(),
|
Types.as_view(template="types.html"),
|
||||||
name="rolling_stock_types"
|
name="types"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"types/page/<int:page>",
|
"types/<int:page>",
|
||||||
Types.as_view(),
|
Types.as_view(template="types.html"),
|
||||||
name="rolling_stock_types_pagination"
|
name="types_pagination"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"bookshelf/books",
|
"bookshelf/books",
|
||||||
Books.as_view(),
|
Books.as_view(template="bookshelf/books.html"),
|
||||||
name="books"
|
name="books"
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"bookshelf/books/page/<int:page>",
|
"bookshelf/books/<int:page>",
|
||||||
Books.as_view(),
|
Books.as_view(template="bookshelf/books.html"),
|
||||||
name="books_pagination"
|
name="books_pagination"
|
||||||
),
|
),
|
||||||
path(
|
path("bookshelf/book/<uuid:uuid>", GetBook.as_view(), name="book"),
|
||||||
"bookshelf/<str:selector>/<uuid:uuid>",
|
|
||||||
GetBookCatalog.as_view(),
|
|
||||||
name="bookshelf_item"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"bookshelf/catalogs",
|
|
||||||
Catalogs.as_view(),
|
|
||||||
name="catalogs"
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"bookshelf/catalogs/page/<int:page>",
|
|
||||||
Catalogs.as_view(),
|
|
||||||
name="catalogs_pagination"
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"search",
|
"search",
|
||||||
SearchObjects.as_view(http_method_names=["post"]),
|
SearchObjects.as_view(http_method_names=["post"]),
|
||||||
name="search",
|
name="search",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"search/<str:search>/page/<int:page>",
|
"search/<str:search>/<int:page>",
|
||||||
SearchObjects.as_view(),
|
SearchObjects.as_view(),
|
||||||
name="search_pagination",
|
name="search_pagination",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"manufacturer/<str:manufacturer>",
|
|
||||||
GetManufacturerItem.as_view(),
|
|
||||||
name="manufacturer",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"manufacturer/<str:manufacturer>/page/<int:page>",
|
|
||||||
GetManufacturerItem.as_view(),
|
|
||||||
name="manufacturer_pagination",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"manufacturer/<str:manufacturer>/<str:search>",
|
|
||||||
GetManufacturerItem.as_view(),
|
|
||||||
name="manufacturer",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"manufacturer/<str:manufacturer>/<str:search>/page/<int:page>",
|
|
||||||
GetManufacturerItem.as_view(),
|
|
||||||
name="manufacturer_pagination",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"<str:_filter>/<str:search>",
|
"<str:_filter>/<str:search>",
|
||||||
GetObjectsFiltered.as_view(),
|
GetObjectsFiltered.as_view(),
|
||||||
name="filtered",
|
name="filtered",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<str:_filter>/<str:search>/page/<int:page>",
|
"<str:_filter>/<str:search>/<int:page>",
|
||||||
GetObjectsFiltered.as_view(),
|
GetObjectsFiltered.as_view(),
|
||||||
name="filtered_pagination",
|
name="filtered_pagination",
|
||||||
),
|
),
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
import base64
|
import base64
|
||||||
import operator
|
import operator
|
||||||
from itertools import chain
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.http import Http404, HttpResponseBadRequest
|
from django.http import Http404, HttpResponseBadRequest
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
from django.db.models import Q, Count
|
from django.db.models import Q
|
||||||
from django.shortcuts import render, get_object_or_404, get_list_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
|
||||||
@@ -16,25 +15,13 @@ from portal.utils import get_site_conf
|
|||||||
from portal.models import Flatpage
|
from portal.models import Flatpage
|
||||||
from roster.models import RollingStock
|
from roster.models import RollingStock
|
||||||
from consist.models import Consist
|
from consist.models import Consist
|
||||||
from bookshelf.models import Book, Catalog
|
from bookshelf.models import Book
|
||||||
from metadata.models import (
|
from metadata.models import (
|
||||||
Company,
|
Company, Manufacturer, Scale, DecoderDocument, RollingStockType, Tag
|
||||||
Manufacturer,
|
|
||||||
Scale,
|
|
||||||
RollingStockType,
|
|
||||||
Tag,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_items_per_page():
|
def order_by_fields():
|
||||||
try:
|
|
||||||
items_per_page = get_site_conf().items_per_page
|
|
||||||
except (OperationalError, ProgrammingError):
|
|
||||||
items_per_page = 6
|
|
||||||
return items_per_page
|
|
||||||
|
|
||||||
|
|
||||||
def get_order_by_field():
|
|
||||||
try:
|
try:
|
||||||
order_by = get_site_conf().items_ordering
|
order_by = get_site_conf().items_ordering
|
||||||
except (OperationalError, ProgrammingError):
|
except (OperationalError, ProgrammingError):
|
||||||
@@ -55,33 +42,26 @@ def get_order_by_field():
|
|||||||
return (fields[2], fields[0], fields[1], fields[3])
|
return (fields[2], fields[0], fields[1], fields[3])
|
||||||
|
|
||||||
|
|
||||||
class Render404(View):
|
|
||||||
def get(self, request, exception):
|
|
||||||
return render(request, "base.html", {"title": "404 page not found"})
|
|
||||||
|
|
||||||
|
|
||||||
class GetData(View):
|
class GetData(View):
|
||||||
title = "Home"
|
title = "Home"
|
||||||
template = "pagination.html"
|
template = "roster.html"
|
||||||
item_type = "roster"
|
item_type = "rolling_stock"
|
||||||
filter = Q() # empty filter by default
|
queryset = RollingStock.objects.order_by(*order_by_fields())
|
||||||
|
|
||||||
def get_data(self, request):
|
|
||||||
return (
|
|
||||||
RollingStock.objects.get_published(request.user)
|
|
||||||
.order_by(*get_order_by_field())
|
|
||||||
.filter(self.filter)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get(self, request, page=1):
|
def get(self, request, page=1):
|
||||||
data = []
|
site_conf = get_site_conf()
|
||||||
for item in self.get_data(request):
|
|
||||||
data.append({"type": self.item_type, "item": item})
|
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
data = []
|
||||||
|
for item in self.queryset:
|
||||||
|
data.append({
|
||||||
|
"type": self.item_type,
|
||||||
|
"item": item
|
||||||
|
})
|
||||||
|
|
||||||
|
paginator = Paginator(data, site_conf.items_per_page)
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
page_range = paginator.get_elided_page_range(
|
page_range = paginator.get_elided_page_range(
|
||||||
data.number, on_each_side=1, on_ends=1
|
data.number, on_each_side=2, on_ends=1
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
@@ -98,17 +78,14 @@ class GetData(View):
|
|||||||
|
|
||||||
|
|
||||||
class GetRoster(GetData):
|
class GetRoster(GetData):
|
||||||
title = "The Roster"
|
title = "Roster"
|
||||||
item_type = "roster"
|
item_type = "rolling_stock"
|
||||||
|
queryset = RollingStock.objects.order_by(*order_by_fields())
|
||||||
def get_data(self, request):
|
|
||||||
return RollingStock.objects.get_published(request.user).order_by(
|
|
||||||
*get_order_by_field()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SearchObjects(View):
|
class SearchObjects(View):
|
||||||
def run_search(self, request, search, _filter, page=1):
|
def run_search(self, request, search, _filter, page=1):
|
||||||
|
site_conf = get_site_conf()
|
||||||
if _filter is None:
|
if _filter is None:
|
||||||
query = reduce(
|
query = reduce(
|
||||||
operator.or_,
|
operator.or_,
|
||||||
@@ -149,22 +126,20 @@ class SearchObjects(View):
|
|||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
# FIXME duplicated code!
|
# FIXME duplicated code!
|
||||||
# FIXME see if it makes sense to filter calatogs and books by scale
|
|
||||||
# and manufacturer as well
|
|
||||||
data = []
|
data = []
|
||||||
roster = (
|
rolling_stock = (
|
||||||
RollingStock.objects.get_published(request.user)
|
RollingStock.objects.filter(query)
|
||||||
.filter(query)
|
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by(*get_order_by_field())
|
.order_by(*order_by_fields())
|
||||||
)
|
)
|
||||||
for item in roster:
|
for item in rolling_stock:
|
||||||
data.append({"type": "roster", "item": item})
|
data.append({
|
||||||
|
"type": "rolling_stock",
|
||||||
|
"item": item
|
||||||
|
})
|
||||||
if _filter is None:
|
if _filter is None:
|
||||||
consists = (
|
consists = (
|
||||||
Consist.objects.get_published(request.user)
|
Consist.objects.filter(
|
||||||
.filter(
|
|
||||||
Q(
|
Q(
|
||||||
Q(identifier__icontains=search)
|
Q(identifier__icontains=search)
|
||||||
| Q(company__name__icontains=search)
|
| Q(company__name__icontains=search)
|
||||||
@@ -173,24 +148,24 @@ class SearchObjects(View):
|
|||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
for item in consists:
|
for item in consists:
|
||||||
data.append({"type": "consist", "item": item})
|
data.append({
|
||||||
|
"type": "consist",
|
||||||
|
"item": item
|
||||||
|
})
|
||||||
books = (
|
books = (
|
||||||
Book.objects.get_published(request.user)
|
Book.objects.filter(title__icontains=search)
|
||||||
.filter(title__icontains=search)
|
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
catalogs = (
|
for item in books:
|
||||||
Catalog.objects.get_published(request.user)
|
data.append({
|
||||||
.filter(manufacturer__name__icontains=search)
|
"type": "book",
|
||||||
.distinct()
|
"item": item
|
||||||
)
|
})
|
||||||
for item in list(chain(books, catalogs)):
|
|
||||||
data.append({"type": "book", "item": item})
|
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, site_conf.items_per_page)
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
page_range = paginator.get_elided_page_range(
|
page_range = paginator.get_elided_page_range(
|
||||||
data.number, on_each_side=1, on_ends=1
|
data.number, on_each_side=2, on_ends=1
|
||||||
)
|
)
|
||||||
|
|
||||||
return data, paginator.count, page_range
|
return data, paginator.count, page_range
|
||||||
@@ -215,7 +190,8 @@ class SearchObjects(View):
|
|||||||
encoded_search = search
|
encoded_search = search
|
||||||
search = base64.b64decode(search.encode()).decode()
|
search = base64.b64decode(search.encode()).decode()
|
||||||
except Exception:
|
except Exception:
|
||||||
encoded_search = base64.b64encode(search.encode()).decode()
|
encoded_search = base64.b64encode(
|
||||||
|
search.encode()).decode()
|
||||||
_filter, keyword = self.split_search(search)
|
_filter, keyword = self.split_search(search)
|
||||||
data, matches, page_range = self.run_search(
|
data, matches, page_range = self.run_search(
|
||||||
request, keyword, _filter, page
|
request, keyword, _filter, page
|
||||||
@@ -225,7 +201,7 @@ class SearchObjects(View):
|
|||||||
request,
|
request,
|
||||||
"search.html",
|
"search.html",
|
||||||
{
|
{
|
||||||
"title": 'Search: "{}"'.format(search),
|
"title": "Search: \"{}\"".format(search),
|
||||||
"search": search,
|
"search": search,
|
||||||
"encoded_search": encoded_search,
|
"encoded_search": encoded_search,
|
||||||
"data": data,
|
"data": data,
|
||||||
@@ -239,65 +215,10 @@ class SearchObjects(View):
|
|||||||
return self.get(request, search, page)
|
return self.get(request, search, page)
|
||||||
|
|
||||||
|
|
||||||
class GetManufacturerItem(View):
|
|
||||||
def get(self, request, manufacturer, search="all", page=1):
|
|
||||||
manufacturer = get_object_or_404(
|
|
||||||
Manufacturer, slug__iexact=manufacturer
|
|
||||||
)
|
|
||||||
|
|
||||||
if search != "all":
|
|
||||||
roster = get_list_or_404(
|
|
||||||
RollingStock.objects.get_published(request.user).order_by(
|
|
||||||
*get_order_by_field()
|
|
||||||
),
|
|
||||||
Q(
|
|
||||||
Q(manufacturer=manufacturer)
|
|
||||||
& Q(item_number_slug__exact=search)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
title = "{0}: {1}".format(
|
|
||||||
manufacturer,
|
|
||||||
# all returned records must have the same `item_number``;
|
|
||||||
# just pick it up the first result, otherwise `search`
|
|
||||||
roster[0].item_number if roster else search,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
roster = (
|
|
||||||
RollingStock.objects.get_published(request.user)
|
|
||||||
.filter(
|
|
||||||
Q(manufacturer=manufacturer)
|
|
||||||
| Q(rolling_class__manufacturer=manufacturer)
|
|
||||||
)
|
|
||||||
.distinct()
|
|
||||||
.order_by(*get_order_by_field())
|
|
||||||
)
|
|
||||||
title = "Manufacturer: {0}".format(manufacturer)
|
|
||||||
|
|
||||||
data = []
|
|
||||||
for item in roster:
|
|
||||||
data.append({"type": "roster", "item": item})
|
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
|
||||||
data = paginator.get_page(page)
|
|
||||||
page_range = paginator.get_elided_page_range(
|
|
||||||
data.number, on_each_side=1, on_ends=1
|
|
||||||
)
|
|
||||||
return render(
|
|
||||||
request,
|
|
||||||
"manufacturer.html",
|
|
||||||
{
|
|
||||||
"title": title,
|
|
||||||
"manufacturer": manufacturer,
|
|
||||||
"search": search,
|
|
||||||
"data": data,
|
|
||||||
"matches": paginator.count,
|
|
||||||
"page_range": page_range,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GetObjectsFiltered(View):
|
class GetObjectsFiltered(View):
|
||||||
def run_filter(self, request, search, _filter, page=1):
|
def run_filter(self, request, search, _filter, page=1):
|
||||||
|
site_conf = get_site_conf()
|
||||||
|
|
||||||
if _filter == "type":
|
if _filter == "type":
|
||||||
title = get_object_or_404(RollingStockType, slug__iexact=search)
|
title = get_object_or_404(RollingStockType, slug__iexact=search)
|
||||||
query = Q(rolling_class__type__slug__iexact=search)
|
query = Q(rolling_class__type__slug__iexact=search)
|
||||||
@@ -305,6 +226,12 @@ class GetObjectsFiltered(View):
|
|||||||
title = get_object_or_404(Company, slug__iexact=search)
|
title = get_object_or_404(Company, slug__iexact=search)
|
||||||
query = Q(rolling_class__company__slug__iexact=search)
|
query = Q(rolling_class__company__slug__iexact=search)
|
||||||
query_2nd = Q(company__slug__iexact=search)
|
query_2nd = Q(company__slug__iexact=search)
|
||||||
|
elif _filter == "manufacturer":
|
||||||
|
title = get_object_or_404(Manufacturer, slug__iexact=search)
|
||||||
|
query = Q(
|
||||||
|
Q(rolling_class__manufacturer__slug__iexact=search)
|
||||||
|
| Q(manufacturer__slug__iexact=search)
|
||||||
|
)
|
||||||
elif _filter == "scale":
|
elif _filter == "scale":
|
||||||
title = get_object_or_404(Scale, slug__iexact=search)
|
title = get_object_or_404(Scale, slug__iexact=search)
|
||||||
query = Q(scale__slug__iexact=search)
|
query = Q(scale__slug__iexact=search)
|
||||||
@@ -318,47 +245,46 @@ class GetObjectsFiltered(View):
|
|||||||
else:
|
else:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
roster = (
|
rolling_stock = (
|
||||||
RollingStock.objects.get_published(request.user)
|
RollingStock.objects.filter(query)
|
||||||
.filter(query)
|
|
||||||
.distinct()
|
.distinct()
|
||||||
.order_by(*get_order_by_field())
|
.order_by(*order_by_fields())
|
||||||
)
|
)
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for item in roster:
|
for item in rolling_stock:
|
||||||
data.append({"type": "roster", "item": item})
|
data.append({
|
||||||
|
"type": "rolling_stock",
|
||||||
|
"item": item
|
||||||
|
})
|
||||||
|
|
||||||
try: # Execute only if query_2nd is defined
|
try: # Execute only if query_2nd is defined
|
||||||
consists = (
|
consists = (
|
||||||
Consist.objects.get_published(request.user)
|
Consist.objects.filter(query_2nd)
|
||||||
.filter(query_2nd)
|
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
for item in consists:
|
for item in consists:
|
||||||
data.append({"type": "consist", "item": item})
|
data.append({
|
||||||
|
"type": "consist",
|
||||||
|
"item": item
|
||||||
|
})
|
||||||
if _filter == "tag": # Books can be filtered only by tag
|
if _filter == "tag": # Books can be filtered only by tag
|
||||||
books = (
|
books = (
|
||||||
Book.objects.get_published(request.user)
|
Book.objects.filter(query_2nd)
|
||||||
.filter(query_2nd)
|
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
for item in books:
|
for item in books:
|
||||||
data.append({"type": "book", "item": item})
|
data.append({
|
||||||
catalogs = (
|
"type": "book",
|
||||||
Catalog.objects.get_published(request.user)
|
"item": item
|
||||||
.filter(query_2nd)
|
})
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
for item in catalogs:
|
|
||||||
data.append({"type": "catalog", "item": item})
|
|
||||||
except NameError:
|
except NameError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, site_conf.items_per_page)
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
page_range = paginator.get_elided_page_range(
|
page_range = paginator.get_elided_page_range(
|
||||||
data.number, on_each_side=1, on_ends=1
|
data.number, on_each_side=2, on_ends=1
|
||||||
)
|
)
|
||||||
|
|
||||||
return data, title, paginator.count, page_range
|
return data, title, paginator.count, page_range
|
||||||
@@ -372,7 +298,8 @@ class GetObjectsFiltered(View):
|
|||||||
request,
|
request,
|
||||||
"filter.html",
|
"filter.html",
|
||||||
{
|
{
|
||||||
"title": "{0}: {1}".format(_filter.capitalize(), title),
|
"title": "{0}: {1}".format(
|
||||||
|
_filter.capitalize(), title),
|
||||||
"search": search,
|
"search": search,
|
||||||
"filter": _filter,
|
"filter": _filter,
|
||||||
"data": data,
|
"data": data,
|
||||||
@@ -385,44 +312,40 @@ class GetObjectsFiltered(View):
|
|||||||
class GetRollingStock(View):
|
class GetRollingStock(View):
|
||||||
def get(self, request, uuid):
|
def get(self, request, uuid):
|
||||||
try:
|
try:
|
||||||
rolling_stock = RollingStock.objects.get_published(
|
rolling_stock = RollingStock.objects.get(uuid=uuid)
|
||||||
request.user
|
|
||||||
).get(uuid=uuid)
|
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
# FIXME there's likely a better and more efficient way of doing this
|
# FIXME there's likely a better and more efficient way of doing this
|
||||||
# but keeping KISS for now
|
# but keeping KISS for now
|
||||||
decoder_documents = []
|
decoder_documents = []
|
||||||
class_properties = rolling_stock.rolling_class.property.get_public(
|
if request.user.is_authenticated:
|
||||||
request.user
|
class_properties = rolling_stock.rolling_class.property.all()
|
||||||
)
|
properties = rolling_stock.property.all()
|
||||||
properties = rolling_stock.property.get_public(request.user)
|
documents = rolling_stock.document.all()
|
||||||
documents = rolling_stock.document.get_public(request.user)
|
journal = rolling_stock.journal.all()
|
||||||
journal = rolling_stock.journal.get_public(request.user)
|
if rolling_stock.decoder:
|
||||||
if rolling_stock.decoder:
|
decoder_documents = rolling_stock.decoder.document.all()
|
||||||
decoder_documents = rolling_stock.decoder.document.get_public(
|
else:
|
||||||
request.user
|
class_properties = rolling_stock.rolling_class.property.filter(
|
||||||
|
property__private=False
|
||||||
)
|
)
|
||||||
|
properties = rolling_stock.property.filter(
|
||||||
consists = [
|
property__private=False
|
||||||
{"type": "consist", "item": c}
|
|
||||||
for c in Consist.objects.get_published(request.user).filter(
|
|
||||||
consist_item__rolling_stock=rolling_stock
|
|
||||||
)
|
)
|
||||||
] # A dict with "item" is required by the consists card
|
documents = rolling_stock.document.filter(private=False)
|
||||||
|
journal = rolling_stock.journal.filter(private=False)
|
||||||
set = [
|
if rolling_stock.decoder:
|
||||||
{"type": "set", "item": s}
|
decoder_documents = rolling_stock.decoder.document.filter(
|
||||||
for s in RollingStock.objects.get_published(request.user)
|
private=False
|
||||||
.filter(
|
|
||||||
Q(
|
|
||||||
Q(item_number__exact=rolling_stock.item_number)
|
|
||||||
& Q(set=True)
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
.order_by(*get_order_by_field())
|
consists = [{
|
||||||
]
|
"type": "consist",
|
||||||
|
"item": c
|
||||||
|
} for c in Consist.objects.filter(
|
||||||
|
consist_item__rolling_stock=rolling_stock
|
||||||
|
)] # A dict with "item" is required by the consists card
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -435,7 +358,6 @@ class GetRollingStock(View):
|
|||||||
"decoder_documents": decoder_documents,
|
"decoder_documents": decoder_documents,
|
||||||
"documents": documents,
|
"documents": documents,
|
||||||
"journal": journal,
|
"journal": journal,
|
||||||
"set": set,
|
|
||||||
"consists": consists,
|
"consists": consists,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -444,33 +366,25 @@ class GetRollingStock(View):
|
|||||||
class Consists(GetData):
|
class Consists(GetData):
|
||||||
title = "Consists"
|
title = "Consists"
|
||||||
item_type = "consist"
|
item_type = "consist"
|
||||||
|
queryset = Consist.objects.all()
|
||||||
def get_data(self, request):
|
|
||||||
return Consist.objects.get_published(request.user).all()
|
|
||||||
|
|
||||||
|
|
||||||
class GetConsist(View):
|
class GetConsist(View):
|
||||||
def get(self, request, uuid, page=1):
|
def get(self, request, uuid, page=1):
|
||||||
|
site_conf = get_site_conf()
|
||||||
try:
|
try:
|
||||||
consist = Consist.objects.get_published(request.user).get(
|
consist = Consist.objects.get(uuid=uuid)
|
||||||
uuid=uuid
|
|
||||||
)
|
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
data = [
|
data = [{
|
||||||
{
|
"type": "rolling_stock",
|
||||||
"type": "roster",
|
"item": RollingStock.objects.get(uuid=r.rolling_stock_id)
|
||||||
"item": RollingStock.objects.get_published(request.user).get(
|
} for r in consist.consist_item.all()]
|
||||||
uuid=r.rolling_stock_id
|
|
||||||
),
|
|
||||||
}
|
|
||||||
for r in consist.consist_item.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, site_conf.items_per_page)
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
page_range = paginator.get_elided_page_range(
|
page_range = paginator.get_elided_page_range(
|
||||||
data.number, on_each_side=1, on_ends=1
|
data.number, on_each_side=2, on_ends=1
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
@@ -488,25 +402,20 @@ class GetConsist(View):
|
|||||||
class Manufacturers(GetData):
|
class Manufacturers(GetData):
|
||||||
title = "Manufacturers"
|
title = "Manufacturers"
|
||||||
item_type = "manufacturer"
|
item_type = "manufacturer"
|
||||||
|
queryset = None # Set via method get
|
||||||
def get_data(self, request):
|
|
||||||
return Manufacturer.objects.filter(self.filter)
|
|
||||||
|
|
||||||
# overload get method to filter by category
|
# overload get method to filter by category
|
||||||
def get(self, request, category, page=1):
|
def get(self, request, category, page=1):
|
||||||
if category not in ("real", "model"):
|
if category not in ("real", "model"):
|
||||||
raise Http404
|
raise Http404
|
||||||
self.filter = Q(category=category)
|
self.queryset = Manufacturer.objects.filter(category=category)
|
||||||
|
|
||||||
return super().get(request, page)
|
return super().get(request, page)
|
||||||
|
|
||||||
|
|
||||||
class Companies(GetData):
|
class Companies(GetData):
|
||||||
title = "Companies"
|
title = "Companies"
|
||||||
item_type = "company"
|
item_type = "company"
|
||||||
|
queryset = Company.objects.all()
|
||||||
def get_data(self, request):
|
|
||||||
return Company.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class Scales(GetData):
|
class Scales(GetData):
|
||||||
@@ -514,72 +423,50 @@ class Scales(GetData):
|
|||||||
item_type = "scale"
|
item_type = "scale"
|
||||||
queryset = Scale.objects.all()
|
queryset = Scale.objects.all()
|
||||||
|
|
||||||
def get_data(self, request):
|
|
||||||
return Scale.objects.annotate(
|
|
||||||
num_items=Count("rollingstock")
|
|
||||||
) # .filter(num_items__gt=0) to filter data with no items
|
|
||||||
|
|
||||||
|
|
||||||
class Types(GetData):
|
class Types(GetData):
|
||||||
title = "Types"
|
title = "Types"
|
||||||
item_type = "rolling_stock_type"
|
item_type = "rolling_stock_type"
|
||||||
|
queryset = RollingStockType.objects.all()
|
||||||
def get_data(self, request):
|
|
||||||
return RollingStockType.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class Books(GetData):
|
class Books(GetData):
|
||||||
title = "Books"
|
title = "Books"
|
||||||
item_type = "book"
|
item_type = "book"
|
||||||
|
queryset = Book.objects.all()
|
||||||
def get_data(self, request):
|
|
||||||
return Book.objects.get_published(request.user).all()
|
|
||||||
|
|
||||||
|
|
||||||
class Catalogs(GetData):
|
class GetBook(View):
|
||||||
title = "Catalogs"
|
def get(self, request, uuid):
|
||||||
item_type = "catalog"
|
|
||||||
|
|
||||||
def get_data(self, request):
|
|
||||||
return Catalog.objects.get_published(request.user).all()
|
|
||||||
|
|
||||||
|
|
||||||
class GetBookCatalog(View):
|
|
||||||
def get_object(self, request, uuid, selector):
|
|
||||||
if selector == "book":
|
|
||||||
return Book.objects.get_published(request.user).get(uuid=uuid)
|
|
||||||
elif selector == "catalog":
|
|
||||||
return Catalog.objects.get_published(request.user).get(uuid=uuid)
|
|
||||||
else:
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
def get(self, request, uuid, selector):
|
|
||||||
try:
|
try:
|
||||||
book = self.get_object(request, uuid, selector)
|
book = Book.objects.get(uuid=uuid)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
properties = book.property.get_public(request.user)
|
book_properties = (
|
||||||
documents = book.document.get_public(request.user)
|
book.property.all()
|
||||||
|
if request.user.is_authenticated
|
||||||
|
else book.property.filter(property__private=False)
|
||||||
|
)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"bookshelf/book.html",
|
"bookshelf/book.html",
|
||||||
{
|
{
|
||||||
"title": book,
|
"title": book,
|
||||||
|
"book_properties": book_properties,
|
||||||
"book": book,
|
"book": book,
|
||||||
"documents": documents,
|
|
||||||
"properties": properties,
|
|
||||||
"type": selector
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GetFlatpage(View):
|
class GetFlatpage(View):
|
||||||
def get(self, request, flatpage):
|
def get(self, request, flatpage):
|
||||||
|
_filter = Q(published=True) # Show only published pages
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
_filter = Q() # Reset the filter if user is authenticated
|
||||||
|
|
||||||
try:
|
try:
|
||||||
flatpage = Flatpage.objects.get_published(request.user).get(
|
flatpage = Flatpage.objects.filter(_filter).get(path=flatpage)
|
||||||
path=flatpage
|
|
||||||
)
|
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
from ram.utils import git_suffix
|
from ram.utils import git_suffix
|
||||||
|
|
||||||
__version__ = "0.16.9"
|
__version__ = "0.10.0"
|
||||||
__version__ += git_suffix(__file__)
|
__version__ += git_suffix(__file__)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user