mirror of
https://github.com/daniviga/django-ram.git
synced 2025-08-07 06:37:50 +02:00
Compare commits
236 Commits
Author | SHA1 | Date | |
---|---|---|---|
955397acd5
|
|||
672cadd7e1
|
|||
464fe57536
|
|||
bd16c7eee7
|
|||
cc2e374558
|
|||
1c25ac9b14
|
|||
de126a735d
|
|||
18b5ab8053
|
|||
3acc80e2ad
|
|||
552ba39970
|
|||
222e2075ec
|
|||
b5c57dcd94
|
|||
b81c63898f
|
|||
76b266b1f9
|
|||
86657a3b9f
|
|||
d0d25424fb
|
|||
292b95b8ed
|
|||
dea7a594bc
|
|||
60195bc99f
|
|||
7673f0514a
|
|||
40f42a9ee9
|
|||
2e06e94fde
|
|||
ece8d1ad94
|
|||
e9ec126ada
|
|||
1222116874
|
|||
85741f090c
|
|||
88d718fa94
|
|||
a2c857a3cd
|
|||
647894bca7
|
|||
c8cc8c5ed0
|
|||
e80dc604a7 | |||
5088f19b33
|
|||
50bfc44978
|
|||
453729b05c
|
|||
5d89cb96d2 | |||
04757d868a | |||
b897141212 | |||
3df8b461a0
|
|||
284632892d
|
|||
bb58dcf6fa
|
|||
c971ff9601
|
|||
b10e1f3952 | |||
d16e00d66b | |||
1a8f2aace8 | |||
0413c1c5ab | |||
f914c79786
|
|||
456f1b7294
|
|||
f19a0995b0
|
|||
3dd134f132
|
|||
ddcf06994d | |||
c467fb24ca | |||
db79a02c85 | |||
d237129c99 | |||
af54acae86 | |||
90211562f9 | |||
1e7f72e9ec
|
|||
26be22c0bd
|
|||
f286ec9780 | |||
ead9fe649b | |||
206b9aea57 | |||
8557e2b778 | |||
6457486445 | |||
ee5b5f0b3a | |||
159bc66b59 | |||
0ea9978ffb | |||
026ab06354 | |||
7eddd1b52b | |||
11515d79ef | |||
f2b817103f | |||
2d00436a87 | |||
6ff5450124 | |||
f4af44c41c | |||
e3ae18a4bd | |||
2695358d9b | |||
3fbae0417e | |||
7a51ab9095 | |||
dad40b3ee7 | |||
d55bce6e78 | |||
cbf6c942b9 | |||
64f616d89f | |||
f8246c31d3 | |||
005ea11011 | |||
83444266cb | |||
1a3b30ace3 | |||
21c99f73c3 | |||
b5b88f7714 | |||
119d25ede6 | |||
41d9338459 | |||
32785f321a | |||
5b975355a1 | |||
7d8c539e47 | |||
9a832bca82 | |||
54254bda7d | |||
1c07c6a7a9 | |||
61b6d7a84e | |||
d0854a4cff | |||
456272b93a
|
|||
35905bafdf | |||
6a9f37ca05 | |||
54a68d9b1f | |||
aa02404dfe
|
|||
e4ad98fa38 | |||
b37f5420c5 | |||
4b74a69f3f | |||
e7d34ce8e0
|
|||
19eb70c492 | |||
4428b8c11d | |||
8400a5acd3
|
|||
7dadf23f5f
|
|||
4a12201d22
|
|||
830da80302 | |||
416ca5bbc6
|
|||
03fc82c38d
|
|||
ec8684dbc0
|
|||
7ec8baf733
|
|||
86589ad718
|
|||
98fed02a40
|
|||
9602f67e0e
|
|||
5bb6279095
|
|||
84cdee42a6
|
|||
168b424df7
|
|||
e1400fe720
|
|||
26dea2fb35
|
|||
ef767ec33d
|
|||
b23801dbf0
|
|||
c7fa54e90e
|
|||
9164ba494f
|
|||
97989c3384
|
|||
7865bf04f0
|
|||
e6f1480894
|
|||
8d8ede4c06
|
|||
87e1107156 | |||
448ecae070
|
|||
2b0fdc4487
|
|||
764240d67a
|
|||
424b17ae58
|
|||
c73efb01e4 | |||
a21baac10c
|
|||
4b0361acc1
|
|||
425eed3d83 | |||
2d48463474
|
|||
08226247c7
|
|||
4f52736d97
|
|||
bf8c2331c0
|
|||
1de4938ae7 | |||
817d53d39a
|
|||
12ac33f4a2
|
|||
cbd76e4f66
|
|||
bcfed3534c
|
|||
22bee7d95d
|
|||
98c696b2d9
|
|||
996ddd67ea
|
|||
3f905877e7
|
|||
968ebeb0b6
|
|||
b8572c1701
|
|||
124f3c2a8b
|
|||
f4023f105f | |||
a189646aa5
|
|||
7a103cca56
|
|||
2fe221d0f4
|
|||
6355460e01
|
|||
75074d5e90
|
|||
5d536ce568 | |||
9483648a1f
|
|||
8c15441fe5
|
|||
5ebce9480e
|
|||
64eefe43aa | |||
8e0a18d707
|
|||
46a2aa7011
|
|||
a176682615
|
|||
ad4591da04 | |||
2f2b96b2bb
|
|||
6cf3ad03cc | |||
2c5f0dcd6f
|
|||
3545824016 | |||
78f9faee5e
|
|||
6fbea294da
|
|||
35bdffdb3f
|
|||
dccf467d38 | |||
9dfa9172f4
|
|||
9279142a41
|
|||
aff1d20260
|
|||
9b8ec6ba6b
|
|||
c0b1b0b37b
|
|||
169763e237 | |||
bbe0758c6b
|
|||
c73305fd85
|
|||
4a3fbda3dc
|
|||
295965710f | |||
c152f43aa6
|
|||
8ed92dc5f0
|
|||
b70aa27a13
|
|||
3860ed70fd
|
|||
68a18fcf58 | |||
e45d11d4b1
|
|||
32b5522a1e | |||
89b666dab2 | |||
ffad964373
|
|||
538dc0bd80
|
|||
8bd2635c28
|
|||
feda1f6cb4
|
|||
2c851b2822
|
|||
e5ba2cfaec | |||
091f426242
|
|||
f603fd3e2d
|
|||
a3b2112e03
|
|||
055b0bab59
|
|||
3aea2ae340 | |||
242fe6814d
|
|||
90ffadb2ab
|
|||
21bf09687a
|
|||
c1a45ad4c9
|
|||
29180572c1
|
|||
d30d9fc9ed
|
|||
4ed95d0edf
|
|||
24bd2aa53c
|
|||
5ef51cb9b7
|
|||
65493ba068 | |||
ca459c467b
|
|||
575c938205
|
|||
7cc917d9f7
|
|||
0fe0644d1b
|
|||
f7987f06d5
|
|||
2af772a722
|
|||
f580bcffc5
|
|||
6accb66006
|
|||
602c8359e9
|
|||
46477c4576
|
|||
f56accb4ff
|
|||
5a7b7fd79e | |||
dcdad71b1b | |||
321ae1065e | |||
e8efa5d87a | |||
97254b302c
|
|||
b8aa34ce1d
|
|||
e023edbeeb
|
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.9', '3.10']
|
python-version: ['3.12', '3.13']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,7 +10,6 @@ __pycache__/
|
|||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
@@ -132,3 +131,4 @@ dmypy.json
|
|||||||
ram/storage/
|
ram/storage/
|
||||||
!ram/storage/.gitignore
|
!ram/storage/.gitignore
|
||||||
arduino/CommandStation-EX/build/
|
arduino/CommandStation-EX/build/
|
||||||
|
utils
|
||||||
|
124
README.md
124
README.md
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
[](https://github.com/daniviga/django-rma/actions/workflows/django.yml)
|
[](https://github.com/daniviga/django-rma/actions/workflows/django.yml)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
A `jff` (just for fun) project that aims to create a
|
A `jff` (just for fun) project that aims to create a
|
||||||
model railroad assets manager that allows to:
|
model railroad assets manager that allows to:
|
||||||
@@ -22,9 +21,12 @@ it has been developed with a commitment of few minutes a day;
|
|||||||
it lacks any kind of documentation, code review, architectural review,
|
it lacks any kind of documentation, code review, architectural review,
|
||||||
security assesment, pentest, ISO certification, etc.
|
security assesment, pentest, ISO certification, etc.
|
||||||
|
|
||||||
This project probably doesn't match you needs nor expectations. Be aware.
|
This project probably doesn't match your needs nor expectations. Be aware.
|
||||||
|
|
||||||
Your model train may also catch fire while using this software.
|
> [!CAUTION]
|
||||||
|
> Your model train may catch fire while using this software.
|
||||||
|
|
||||||
|
Check out [my own instance](https://daniele.mynarrowgauge.org).
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
@@ -39,23 +41,49 @@ Project is based on the following technologies and components:
|
|||||||
|
|
||||||
It has been developed with:
|
It has been developed with:
|
||||||
|
|
||||||
- [vim](https://www.vim.org/): because it rocks
|
- [neovim](https://neovim.io/): because `vim` rocks, `neovim` rocks more
|
||||||
- [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 toast!
|
- [QEMU (avr)](https://qemu-project.gitlab.io/qemu/system/target-avr.html): QEMU can even make toasts!
|
||||||
|
|
||||||
|
## 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.8+
|
- Python 3.11+
|
||||||
- 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
|
||||||
|
|
||||||
coming soon
|
Do it yourself, otherwise, raise a request :)
|
||||||
|
|
||||||
### Manual installation
|
### Manual installation
|
||||||
|
|
||||||
@@ -82,6 +110,8 @@ $ 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
|
||||||
@@ -95,59 +125,71 @@ Browse to `http://localhost:8000`
|
|||||||
|
|
||||||
The DCC++ EX connector exposes an Arduino board running DCC++ EX Command Station,
|
The DCC++ EX connector exposes an Arduino board running DCC++ EX Command Station,
|
||||||
connected via serial port, to the network, allowing commands to be sent via a
|
connected via serial port, to the network, allowing commands to be sent via a
|
||||||
TCP socket.
|
TCP socket. A response generated by the DCC++ EX board is sent to all connected clients,
|
||||||
|
providing synchronization between multiple clients (eg. multiple JMRI instances).
|
||||||
|
|
||||||
Its use is not needed when running DCC++ EX from a [WiFi](https://dcc-ex.com/get-started/wifi-setup.html) capable board (like when
|
Its use is not needed when running DCC++ EX from a [WiFi](https://dcc-ex.com/get-started/wifi-setup.html) capable board, like when
|
||||||
using an ESP8266 module or a [Mega+WiFi board](https://dcc-ex.com/advanced-setup/supported-microcontrollers/wifi-mega.html)).
|
using an ESP8266 module, a [Mega+WiFi board](https://dcc-ex.com/reference/hardware/microcontrollers/wifi-mega.html), or an
|
||||||
|
[ESP32](https://dcc-ex.com/reference/hardware/microcontrollers/esp32.html) (recommended).
|
||||||
|
|
||||||
### Customize the settings
|
### Manual setup
|
||||||
|
|
||||||
The daemon comes with default settings in `config.ini`.
|
You'll need [namp-ncat](https://nmap.org/ncat/) , and `stty` to setup the serial port.
|
||||||
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 daemons
|
$ cd connector
|
||||||
$ podman build -t dcc/net-to-serial .
|
$ podman build -t dcc/connector .
|
||||||
$ podman run -d -p 2560:2560 dcc/net-to-serial
|
$ podman run -d --group-add keep-groups --device /dev/ttyACM0:/dev/arduino -p 2560:2560 dcc/connector
|
||||||
```
|
|
||||||
|
|
||||||
### 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 `net-to-serial.py`
|
A [QEMU AVR based simulator](daemons/simulator/README.md) running DCC++ EX is bundled togheter with the connector
|
||||||
daemon into a container. To run it:
|
into a container. To run it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ cd daemons/simulator
|
$ cd connector/simulator
|
||||||
$ podman build -t dcc/net-to-serial:sim .
|
$ podman build -t dcc/connector:sim .
|
||||||
$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/net-to-serial:sim
|
$ podman run --init --cpus 0.1 -d -p 2560:2560 dcc/connector:sim
|
||||||
```
|
```
|
||||||
|
|
||||||
To be continued ...
|
> [!WARNING]
|
||||||
|
> The simulator is intended for light development and testing purposes only and far from being a complete replacement for a real hardware.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||

|

|
||||||
---
|
---
|
||||||

|

|
||||||
---
|
|
||||||

|
|
||||||
---
|
|
||||||

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

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

|

|
||||||
@@ -158,10 +200,4 @@ To be continued ...
|
|||||||
|
|
||||||
### Rest API
|
### Rest API
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Submodule arduino/CommandStation-EX updated: c47dd101a5...3b15491608
Submodule arduino/WebThrottle-EX updated: ca33f6a1e3...eb43d7906f
Submodule arduino/arduino-cli updated: c1b10f562f...fa6eafcbbe
Submodule arduino/dcc-ex.github.io updated: a4367c9df0...a0f886b69f
Submodule arduino/vim-arduino updated: bf371b9690...2ded67cdf0
9
connector/Dockerfile
Normal file
9
connector/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
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
|
19
connector/README.md
Normal file
19
connector/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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
|
||||||
|
```
|
BIN
connector/simulator/CommandStation-EX-uno-13488e1.elf
Executable file
BIN
connector/simulator/CommandStation-EX-uno-13488e1.elf
Executable file
Binary file not shown.
8
connector/simulator/Dockerfile
Normal file
8
connector/simulator/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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"]
|
13
connector/simulator/README.md
Normal file
13
connector/simulator/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 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.
|
@@ -7,7 +7,5 @@ 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
|
||||||
/opt/dcc/net-to-serial.py
|
ncat -n -k -l 2560 -o /dev/stderr </dev/pts/${PTY} >/dev/pts/${PTY}
|
@@ -1,9 +0,0 @@
|
|||||||
FROM python:3.10-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"]
|
|
@@ -1,13 +0,0 @@
|
|||||||
[Daemon]
|
|
||||||
LogLevel = debug
|
|
||||||
ListeningIP = 0.0.0.0
|
|
||||||
ListeningPort = 2560
|
|
||||||
|
|
||||||
[Serial]
|
|
||||||
# UNO
|
|
||||||
Port = /dev/ttyACM0
|
|
||||||
# Mega WiFi
|
|
||||||
# Port = /dev/ttyUSB0
|
|
||||||
Baudrate = 115200
|
|
||||||
# Timeout in milliseconds
|
|
||||||
Timeout = 50
|
|
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import logging
|
|
||||||
import serial
|
|
||||||
import asyncio
|
|
||||||
import configparser
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class SerialDaemon:
|
|
||||||
def __init__(self, config):
|
|
||||||
self.ser = serial.Serial(
|
|
||||||
config["Serial"]["Port"],
|
|
||||||
timeout=int(config["Serial"]["Timeout"])/1000)
|
|
||||||
self.ser.baudrate = config["Serial"]["Baudrate"]
|
|
||||||
|
|
||||||
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"""
|
|
||||||
while 1: # keep connection to client open
|
|
||||||
data = await reader.read(100)
|
|
||||||
if not data: # client has disconnected
|
|
||||||
break
|
|
||||||
|
|
||||||
addr = writer.get_extra_info('peername')
|
|
||||||
logging.info("Received {} from {}".format(data, addr[0]))
|
|
||||||
|
|
||||||
self.__write_serial(data)
|
|
||||||
response = self.__read_serial()
|
|
||||||
writer.write(response)
|
|
||||||
await writer.drain()
|
|
||||||
logging.info("Sent: {}".format(response))
|
|
||||||
|
|
||||||
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.warning("Serving on {} port {}".format(addr[0], addr[1]))
|
|
||||||
logging.warning(
|
|
||||||
"Proxying to {} (Baudrate: {}, Timeout: {})".format(
|
|
||||||
config["Serial"]["Port"],
|
|
||||||
config["Serial"]["Baudrate"],
|
|
||||||
config["Serial"]["Timeout"]))
|
|
||||||
logging.warning("Initializing board")
|
|
||||||
logging.warning("Board {} ready".format(
|
|
||||||
await sd.return_board()))
|
|
||||||
|
|
||||||
async with server:
|
|
||||||
await server.serve_forever()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
@@ -1 +0,0 @@
|
|||||||
PySerial
|
|
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
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"]
|
|
@@ -1,8 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
0
ram/bookshelf/__init__.py
Normal file
0
ram/bookshelf/__init__.py
Normal file
346
ram/bookshelf/admin.py
Normal file
346
ram/bookshelf/admin.py
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import html
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.html import format_html, strip_tags
|
||||||
|
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
||||||
|
|
||||||
|
from ram.admin import publish, unpublish
|
||||||
|
from ram.utils import generate_csv
|
||||||
|
from portal.utils import get_site_conf
|
||||||
|
from repository.models import BookDocument, CatalogDocument
|
||||||
|
from bookshelf.models import (
|
||||||
|
BaseBookProperty,
|
||||||
|
BaseBookImage,
|
||||||
|
Book,
|
||||||
|
Author,
|
||||||
|
Publisher,
|
||||||
|
Catalog,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BookImageInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||||
|
model = BaseBookImage
|
||||||
|
min_num = 0
|
||||||
|
extra = 0
|
||||||
|
readonly_fields = ("image_thumbnail",)
|
||||||
|
classes = ["collapse"]
|
||||||
|
verbose_name = "Image"
|
||||||
|
|
||||||
|
|
||||||
|
class BookPropertyInline(admin.TabularInline):
|
||||||
|
model = BaseBookProperty
|
||||||
|
min_num = 0
|
||||||
|
extra = 0
|
||||||
|
autocomplete_fields = ("property",)
|
||||||
|
verbose_name = "Property"
|
||||||
|
verbose_name_plural = "Properties"
|
||||||
|
|
||||||
|
|
||||||
|
class BookDocInline(admin.TabularInline):
|
||||||
|
model = BookDocument
|
||||||
|
min_num = 0
|
||||||
|
extra = 0
|
||||||
|
classes = ["collapse"]
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogDocInline(BookDocInline):
|
||||||
|
model = CatalogDocument
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Book)
|
||||||
|
class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
|
inlines = (
|
||||||
|
BookPropertyInline,
|
||||||
|
BookImageInline,
|
||||||
|
BookDocInline,
|
||||||
|
)
|
||||||
|
list_display = (
|
||||||
|
"title",
|
||||||
|
"get_authors",
|
||||||
|
"get_publisher",
|
||||||
|
"publication_year",
|
||||||
|
"number_of_pages",
|
||||||
|
"published",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("authors", "publisher", "shop")
|
||||||
|
readonly_fields = ("invoices", "creation_time", "updated_time")
|
||||||
|
search_fields = ("title", "publisher__name", "authors__last_name")
|
||||||
|
list_filter = ("publisher__name", "authors")
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"published",
|
||||||
|
"title",
|
||||||
|
"authors",
|
||||||
|
"publisher",
|
||||||
|
"ISBN",
|
||||||
|
"language",
|
||||||
|
"number_of_pages",
|
||||||
|
"publication_year",
|
||||||
|
"description",
|
||||||
|
"tags",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Purchase data",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"shop",
|
||||||
|
"purchase_date",
|
||||||
|
"price",
|
||||||
|
"invoices",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{"classes": ("collapse",), "fields": ("notes",)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Audit",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"creation_time",
|
||||||
|
"updated_time",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
form.base_fields["price"].label = "Price ({})".format(
|
||||||
|
get_site_conf().currency
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
|
||||||
|
@admin.display(description="Invoices")
|
||||||
|
def invoices(self, obj):
|
||||||
|
if obj.invoice.exists():
|
||||||
|
html = "<br>".join(
|
||||||
|
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
|
||||||
|
i.file.url, i
|
||||||
|
) for i in obj.invoice.all())
|
||||||
|
else:
|
||||||
|
html = "-"
|
||||||
|
return format_html(html)
|
||||||
|
|
||||||
|
@admin.display(description="Publisher")
|
||||||
|
def get_publisher(self, obj):
|
||||||
|
return obj.publisher.name
|
||||||
|
|
||||||
|
@admin.display(description="Authors")
|
||||||
|
def get_authors(self, obj):
|
||||||
|
return obj.authors_list
|
||||||
|
|
||||||
|
def download_csv(modeladmin, request, queryset):
|
||||||
|
header = [
|
||||||
|
"Title",
|
||||||
|
"Authors",
|
||||||
|
"Publisher",
|
||||||
|
"ISBN",
|
||||||
|
"Language",
|
||||||
|
"Number of Pages",
|
||||||
|
"Publication Year",
|
||||||
|
"Description",
|
||||||
|
"Tags",
|
||||||
|
"Shop",
|
||||||
|
"Purchase Date",
|
||||||
|
"Price ({})".format(get_site_conf().currency),
|
||||||
|
"Notes",
|
||||||
|
"Properties",
|
||||||
|
]
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for obj in queryset:
|
||||||
|
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
"{}:{}".format(property.property.name, property.value)
|
||||||
|
for property in obj.property.all()
|
||||||
|
)
|
||||||
|
data.append(
|
||||||
|
[
|
||||||
|
obj.title,
|
||||||
|
obj.authors_list.replace(",", settings.CSV_SEPARATOR_ALT),
|
||||||
|
obj.publisher.name,
|
||||||
|
obj.ISBN,
|
||||||
|
dict(settings.LANGUAGES)[obj.language],
|
||||||
|
obj.number_of_pages,
|
||||||
|
obj.publication_year,
|
||||||
|
html.unescape(strip_tags(obj.description)),
|
||||||
|
settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
t.name for t in obj.tags.all()
|
||||||
|
),
|
||||||
|
obj.shop,
|
||||||
|
obj.purchase_date,
|
||||||
|
obj.price,
|
||||||
|
html.unescape(strip_tags(obj.notes)),
|
||||||
|
properties,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return generate_csv(header, data, "bookshelf_books.csv")
|
||||||
|
|
||||||
|
download_csv.short_description = "Download selected items as CSV"
|
||||||
|
actions = [publish, unpublish, download_csv]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Author)
|
||||||
|
class AuthorAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = (
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
)
|
||||||
|
list_filter = ("last_name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Publisher)
|
||||||
|
class PublisherAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "country_flag")
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
@admin.display(description="Country")
|
||||||
|
def country_flag(self, obj):
|
||||||
|
return format_html(
|
||||||
|
'<img src="{}" /> {}'.format(obj.country.flag, obj.country.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Catalog)
|
||||||
|
class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
|
inlines = (
|
||||||
|
BookPropertyInline,
|
||||||
|
BookImageInline,
|
||||||
|
CatalogDocInline,
|
||||||
|
)
|
||||||
|
list_display = (
|
||||||
|
"__str__",
|
||||||
|
"manufacturer",
|
||||||
|
"years",
|
||||||
|
"get_scales",
|
||||||
|
"published",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("manufacturer",)
|
||||||
|
readonly_fields = ("invoices", "creation_time", "updated_time")
|
||||||
|
search_fields = ("manufacturer__name", "years", "scales__scale")
|
||||||
|
list_filter = ("manufacturer__name", "publication_year", "scales__scale")
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"published",
|
||||||
|
"manufacturer",
|
||||||
|
"years",
|
||||||
|
"scales",
|
||||||
|
"ISBN",
|
||||||
|
"language",
|
||||||
|
"number_of_pages",
|
||||||
|
"publication_year",
|
||||||
|
"description",
|
||||||
|
"tags",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Purchase data",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"shop",
|
||||||
|
"purchase_date",
|
||||||
|
"price",
|
||||||
|
"invoices",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{"classes": ("collapse",), "fields": ("notes",)},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Audit",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"creation_time",
|
||||||
|
"updated_time",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
form = super().get_form(request, obj, **kwargs)
|
||||||
|
form.base_fields["price"].label = "Price ({})".format(
|
||||||
|
get_site_conf().currency
|
||||||
|
)
|
||||||
|
return form
|
||||||
|
|
||||||
|
@admin.display(description="Invoices")
|
||||||
|
def invoices(self, obj):
|
||||||
|
if obj.invoice.exists():
|
||||||
|
html = "<br>".join(
|
||||||
|
"<a href=\"{}\" target=\"_blank\">{}</a>".format(
|
||||||
|
i.file.url, i
|
||||||
|
) for i in obj.invoice.all())
|
||||||
|
else:
|
||||||
|
html = "-"
|
||||||
|
return format_html(html)
|
||||||
|
|
||||||
|
def download_csv(modeladmin, request, queryset):
|
||||||
|
header = [
|
||||||
|
"Catalog",
|
||||||
|
"Manufacturer",
|
||||||
|
"Years",
|
||||||
|
"Scales",
|
||||||
|
"ISBN",
|
||||||
|
"Language",
|
||||||
|
"Number of Pages",
|
||||||
|
"Publication Year",
|
||||||
|
"Description",
|
||||||
|
"Tags",
|
||||||
|
"Shop",
|
||||||
|
"Purchase Date",
|
||||||
|
"Price ({})".format(get_site_conf().currency),
|
||||||
|
"Notes",
|
||||||
|
"Properties",
|
||||||
|
]
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for obj in queryset:
|
||||||
|
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
"{}:{}".format(property.property.name, property.value)
|
||||||
|
for property in obj.property.all()
|
||||||
|
)
|
||||||
|
data.append(
|
||||||
|
[
|
||||||
|
obj.__str__(),
|
||||||
|
obj.manufacturer.name,
|
||||||
|
obj.years,
|
||||||
|
obj.get_scales(),
|
||||||
|
obj.ISBN,
|
||||||
|
dict(settings.LANGUAGES)[obj.language],
|
||||||
|
obj.number_of_pages,
|
||||||
|
obj.publication_year,
|
||||||
|
html.unescape(strip_tags(obj.description)),
|
||||||
|
settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
t.name for t in obj.tags.all()
|
||||||
|
),
|
||||||
|
obj.shop,
|
||||||
|
obj.purchase_date,
|
||||||
|
obj.price,
|
||||||
|
html.unescape(strip_tags(obj.notes)),
|
||||||
|
properties,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return generate_csv(header, data, "bookshelf_catalogs.csv")
|
||||||
|
|
||||||
|
download_csv.short_description = "Download selected items as CSV"
|
||||||
|
actions = [publish, unpublish, download_csv]
|
6
ram/bookshelf/apps.py
Normal file
6
ram/bookshelf/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BookshelfConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "bookshelf"
|
121
ram/bookshelf/migrations/0001_initial.py
Normal file
121
ram/bookshelf/migrations/0001_initial.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-10-01 20:16
|
||||||
|
|
||||||
|
# ckeditor removal
|
||||||
|
# import ckeditor_uploader.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0012_alter_decoder_manufacturer_decoderdocument"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Author",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("first_name", models.CharField(max_length=100)),
|
||||||
|
("last_name", models.CharField(max_length=100)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Book",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("title", models.CharField(max_length=200)),
|
||||||
|
("ISBN", models.CharField(max_length=13, unique=True)),
|
||||||
|
("publication_year", models.SmallIntegerField(blank=True, null=True)),
|
||||||
|
("purchase_date", models.DateField(blank=True, null=True)),
|
||||||
|
# ("notes", ckeditor_uploader.fields.RichTextUploadingField(blank=True)),
|
||||||
|
("notes", models.TextField(blank=True)),
|
||||||
|
("creation_time", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_time", models.DateTimeField(auto_now=True)),
|
||||||
|
("authors", models.ManyToManyField(to="bookshelf.author")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Publisher",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.CharField(max_length=200)),
|
||||||
|
("website", models.URLField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BookProperty",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("value", models.CharField(max_length=256)),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="property",
|
||||||
|
to="bookshelf.book",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"property",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="metadata.property",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name_plural": "Properties",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="book",
|
||||||
|
name="publisher",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="bookshelf.publisher"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="book",
|
||||||
|
name="tags",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name="bookshelf", to="metadata.tag"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,142 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-10-01 21:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django_countries.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="book",
|
||||||
|
name="language",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("af", "Afrikaans"),
|
||||||
|
("ar", "Arabic"),
|
||||||
|
("ar-dz", "Algerian Arabic"),
|
||||||
|
("ast", "Asturian"),
|
||||||
|
("az", "Azerbaijani"),
|
||||||
|
("bg", "Bulgarian"),
|
||||||
|
("be", "Belarusian"),
|
||||||
|
("bn", "Bengali"),
|
||||||
|
("br", "Breton"),
|
||||||
|
("bs", "Bosnian"),
|
||||||
|
("ca", "Catalan"),
|
||||||
|
("ckb", "Central Kurdish (Sorani)"),
|
||||||
|
("cs", "Czech"),
|
||||||
|
("cy", "Welsh"),
|
||||||
|
("da", "Danish"),
|
||||||
|
("de", "German"),
|
||||||
|
("dsb", "Lower Sorbian"),
|
||||||
|
("el", "Greek"),
|
||||||
|
("en", "English"),
|
||||||
|
("en-au", "Australian English"),
|
||||||
|
("en-gb", "British English"),
|
||||||
|
("eo", "Esperanto"),
|
||||||
|
("es", "Spanish"),
|
||||||
|
("es-ar", "Argentinian Spanish"),
|
||||||
|
("es-co", "Colombian Spanish"),
|
||||||
|
("es-mx", "Mexican Spanish"),
|
||||||
|
("es-ni", "Nicaraguan Spanish"),
|
||||||
|
("es-ve", "Venezuelan Spanish"),
|
||||||
|
("et", "Estonian"),
|
||||||
|
("eu", "Basque"),
|
||||||
|
("fa", "Persian"),
|
||||||
|
("fi", "Finnish"),
|
||||||
|
("fr", "French"),
|
||||||
|
("fy", "Frisian"),
|
||||||
|
("ga", "Irish"),
|
||||||
|
("gd", "Scottish Gaelic"),
|
||||||
|
("gl", "Galician"),
|
||||||
|
("he", "Hebrew"),
|
||||||
|
("hi", "Hindi"),
|
||||||
|
("hr", "Croatian"),
|
||||||
|
("hsb", "Upper Sorbian"),
|
||||||
|
("hu", "Hungarian"),
|
||||||
|
("hy", "Armenian"),
|
||||||
|
("ia", "Interlingua"),
|
||||||
|
("id", "Indonesian"),
|
||||||
|
("ig", "Igbo"),
|
||||||
|
("io", "Ido"),
|
||||||
|
("is", "Icelandic"),
|
||||||
|
("it", "Italian"),
|
||||||
|
("ja", "Japanese"),
|
||||||
|
("ka", "Georgian"),
|
||||||
|
("kab", "Kabyle"),
|
||||||
|
("kk", "Kazakh"),
|
||||||
|
("km", "Khmer"),
|
||||||
|
("kn", "Kannada"),
|
||||||
|
("ko", "Korean"),
|
||||||
|
("ky", "Kyrgyz"),
|
||||||
|
("lb", "Luxembourgish"),
|
||||||
|
("lt", "Lithuanian"),
|
||||||
|
("lv", "Latvian"),
|
||||||
|
("mk", "Macedonian"),
|
||||||
|
("ml", "Malayalam"),
|
||||||
|
("mn", "Mongolian"),
|
||||||
|
("mr", "Marathi"),
|
||||||
|
("ms", "Malay"),
|
||||||
|
("my", "Burmese"),
|
||||||
|
("nb", "Norwegian Bokmål"),
|
||||||
|
("ne", "Nepali"),
|
||||||
|
("nl", "Dutch"),
|
||||||
|
("nn", "Norwegian Nynorsk"),
|
||||||
|
("os", "Ossetic"),
|
||||||
|
("pa", "Punjabi"),
|
||||||
|
("pl", "Polish"),
|
||||||
|
("pt", "Portuguese"),
|
||||||
|
("pt-br", "Brazilian Portuguese"),
|
||||||
|
("ro", "Romanian"),
|
||||||
|
("ru", "Russian"),
|
||||||
|
("sk", "Slovak"),
|
||||||
|
("sl", "Slovenian"),
|
||||||
|
("sq", "Albanian"),
|
||||||
|
("sr", "Serbian"),
|
||||||
|
("sr-latn", "Serbian Latin"),
|
||||||
|
("sv", "Swedish"),
|
||||||
|
("sw", "Swahili"),
|
||||||
|
("ta", "Tamil"),
|
||||||
|
("te", "Telugu"),
|
||||||
|
("tg", "Tajik"),
|
||||||
|
("th", "Thai"),
|
||||||
|
("tk", "Turkmen"),
|
||||||
|
("tr", "Turkish"),
|
||||||
|
("tt", "Tatar"),
|
||||||
|
("udm", "Udmurt"),
|
||||||
|
("uk", "Ukrainian"),
|
||||||
|
("ur", "Urdu"),
|
||||||
|
("uz", "Uzbek"),
|
||||||
|
("vi", "Vietnamese"),
|
||||||
|
("zh-hans", "Simplified Chinese"),
|
||||||
|
("zh-hant", "Traditional Chinese"),
|
||||||
|
],
|
||||||
|
default="en",
|
||||||
|
max_length=7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="book",
|
||||||
|
name="numbers_of_pages",
|
||||||
|
field=models.SmallIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="publisher",
|
||||||
|
name="country",
|
||||||
|
field=django_countries.fields.CountryField(blank=True, max_length=2),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="book",
|
||||||
|
name="ISBN",
|
||||||
|
field=models.CharField(blank=True, max_length=13),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="publisher",
|
||||||
|
name="website",
|
||||||
|
field=models.URLField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
51
ram/bookshelf/migrations/0003_bookimage.py
Normal file
51
ram/bookshelf/migrations/0003_bookimage.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-10-02 10:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ram.utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0002_book_language_book_numbers_of_pages_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="BookImage",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("order", models.PositiveIntegerField(default=0)),
|
||||||
|
(
|
||||||
|
"image",
|
||||||
|
models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/books/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="image",
|
||||||
|
to="bookshelf.book",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["order"],
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-10-02 20:38
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0003_bookimage"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="book",
|
||||||
|
old_name="numbers_of_pages",
|
||||||
|
new_name="number_of_pages",
|
||||||
|
),
|
||||||
|
]
|
17
ram/bookshelf/migrations/0005_alter_book_options.py
Normal file
17
ram/bookshelf/migrations/0005_alter_book_options.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-10-03 19:57
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0004_rename_numbers_of_pages_book_number_of_pages"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="book",
|
||||||
|
options={"ordering": ["authors__last_name", "title"]},
|
||||||
|
),
|
||||||
|
]
|
18
ram/bookshelf/migrations/0006_alter_book_isbn.py
Normal file
18
ram/bookshelf/migrations/0006_alter_book_isbn.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-10-03 20:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0005_alter_book_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="book",
|
||||||
|
name="ISBN",
|
||||||
|
field=models.CharField(blank=True, max_length=17),
|
||||||
|
),
|
||||||
|
]
|
17
ram/bookshelf/migrations/0007_alter_book_options.py
Normal file
17
ram/bookshelf/migrations/0007_alter_book_options.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-10-03 21:07
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0006_alter_book_isbn"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="book",
|
||||||
|
options={"ordering": ["title"]},
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-10-09 21:08
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0007_alter_book_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="author",
|
||||||
|
options={"ordering": ["last_name", "first_name"]},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="publisher",
|
||||||
|
options={"ordering": ["name"]},
|
||||||
|
),
|
||||||
|
]
|
51
ram/bookshelf/migrations/0009_alter_bookimage_image.py
Normal file
51
ram/bookshelf/migrations/0009_alter_bookimage_image.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-10-30 13:16
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import ram.utils
|
||||||
|
import bookshelf.models
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def move_images(apps, schema_editor):
|
||||||
|
sys.stdout.write("\n Processing files. Please await...")
|
||||||
|
for r in bookshelf.models.BaseBookImage.objects.all():
|
||||||
|
fname = os.path.basename(r.image.path)
|
||||||
|
new_image = bookshelf.models.book_image_upload(r, fname)
|
||||||
|
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
|
||||||
|
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
||||||
|
try:
|
||||||
|
shutil.move(r.image.path, new_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
|
||||||
|
pass
|
||||||
|
r.image.name = new_image
|
||||||
|
r.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0008_alter_author_options_alter_publisher_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Migration is stale and shouldn't be used since model hes been heavily
|
||||||
|
# modified since then. Leaving it here for reference.
|
||||||
|
operations = [
|
||||||
|
# migrations.AlterField(
|
||||||
|
# model_name="bookimage",
|
||||||
|
# name="image",
|
||||||
|
# field=models.ImageField(
|
||||||
|
# blank=True,
|
||||||
|
# null=True,
|
||||||
|
# storage=ram.utils.DeduplicatedStorage,
|
||||||
|
# upload_to=bookshelf.models.book_image_upload,
|
||||||
|
# ),
|
||||||
|
# ),
|
||||||
|
# migrations.RunPython(
|
||||||
|
# move_images,
|
||||||
|
# reverse_code=migrations.RunPython.noop
|
||||||
|
# ),
|
||||||
|
]
|
22
ram/bookshelf/migrations/0010_alter_bookimage_image.py
Normal file
22
ram/bookshelf/migrations/0010_alter_bookimage_image.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-04 22:53
|
||||||
|
|
||||||
|
import bookshelf.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
import ram.utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0009_alter_bookimage_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="bookimage",
|
||||||
|
name="image",
|
||||||
|
field=models.ImageField(
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to=bookshelf.models.book_image_upload,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
121
ram/bookshelf/migrations/0011_alter_book_language.py
Normal file
121
ram/bookshelf/migrations/0011_alter_book_language.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Generated by Django 5.0.1 on 2024-01-20 21:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0010_alter_bookimage_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="book",
|
||||||
|
name="language",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("af", "Afrikaans"),
|
||||||
|
("ar", "Arabic"),
|
||||||
|
("ar-dz", "Algerian Arabic"),
|
||||||
|
("ast", "Asturian"),
|
||||||
|
("az", "Azerbaijani"),
|
||||||
|
("bg", "Bulgarian"),
|
||||||
|
("be", "Belarusian"),
|
||||||
|
("bn", "Bengali"),
|
||||||
|
("br", "Breton"),
|
||||||
|
("bs", "Bosnian"),
|
||||||
|
("ca", "Catalan"),
|
||||||
|
("ckb", "Central Kurdish (Sorani)"),
|
||||||
|
("cs", "Czech"),
|
||||||
|
("cy", "Welsh"),
|
||||||
|
("da", "Danish"),
|
||||||
|
("de", "German"),
|
||||||
|
("dsb", "Lower Sorbian"),
|
||||||
|
("el", "Greek"),
|
||||||
|
("en", "English"),
|
||||||
|
("en-au", "Australian English"),
|
||||||
|
("en-gb", "British English"),
|
||||||
|
("eo", "Esperanto"),
|
||||||
|
("es", "Spanish"),
|
||||||
|
("es-ar", "Argentinian Spanish"),
|
||||||
|
("es-co", "Colombian Spanish"),
|
||||||
|
("es-mx", "Mexican Spanish"),
|
||||||
|
("es-ni", "Nicaraguan Spanish"),
|
||||||
|
("es-ve", "Venezuelan Spanish"),
|
||||||
|
("et", "Estonian"),
|
||||||
|
("eu", "Basque"),
|
||||||
|
("fa", "Persian"),
|
||||||
|
("fi", "Finnish"),
|
||||||
|
("fr", "French"),
|
||||||
|
("fy", "Frisian"),
|
||||||
|
("ga", "Irish"),
|
||||||
|
("gd", "Scottish Gaelic"),
|
||||||
|
("gl", "Galician"),
|
||||||
|
("he", "Hebrew"),
|
||||||
|
("hi", "Hindi"),
|
||||||
|
("hr", "Croatian"),
|
||||||
|
("hsb", "Upper Sorbian"),
|
||||||
|
("hu", "Hungarian"),
|
||||||
|
("hy", "Armenian"),
|
||||||
|
("ia", "Interlingua"),
|
||||||
|
("id", "Indonesian"),
|
||||||
|
("ig", "Igbo"),
|
||||||
|
("io", "Ido"),
|
||||||
|
("is", "Icelandic"),
|
||||||
|
("it", "Italian"),
|
||||||
|
("ja", "Japanese"),
|
||||||
|
("ka", "Georgian"),
|
||||||
|
("kab", "Kabyle"),
|
||||||
|
("kk", "Kazakh"),
|
||||||
|
("km", "Khmer"),
|
||||||
|
("kn", "Kannada"),
|
||||||
|
("ko", "Korean"),
|
||||||
|
("ky", "Kyrgyz"),
|
||||||
|
("lb", "Luxembourgish"),
|
||||||
|
("lt", "Lithuanian"),
|
||||||
|
("lv", "Latvian"),
|
||||||
|
("mk", "Macedonian"),
|
||||||
|
("ml", "Malayalam"),
|
||||||
|
("mn", "Mongolian"),
|
||||||
|
("mr", "Marathi"),
|
||||||
|
("ms", "Malay"),
|
||||||
|
("my", "Burmese"),
|
||||||
|
("nb", "Norwegian Bokmål"),
|
||||||
|
("ne", "Nepali"),
|
||||||
|
("nl", "Dutch"),
|
||||||
|
("nn", "Norwegian Nynorsk"),
|
||||||
|
("os", "Ossetic"),
|
||||||
|
("pa", "Punjabi"),
|
||||||
|
("pl", "Polish"),
|
||||||
|
("pt", "Portuguese"),
|
||||||
|
("pt-br", "Brazilian Portuguese"),
|
||||||
|
("ro", "Romanian"),
|
||||||
|
("ru", "Russian"),
|
||||||
|
("sk", "Slovak"),
|
||||||
|
("sl", "Slovenian"),
|
||||||
|
("sq", "Albanian"),
|
||||||
|
("sr", "Serbian"),
|
||||||
|
("sr-latn", "Serbian Latin"),
|
||||||
|
("sv", "Swedish"),
|
||||||
|
("sw", "Swahili"),
|
||||||
|
("ta", "Tamil"),
|
||||||
|
("te", "Telugu"),
|
||||||
|
("tg", "Tajik"),
|
||||||
|
("th", "Thai"),
|
||||||
|
("tk", "Turkmen"),
|
||||||
|
("tr", "Turkish"),
|
||||||
|
("tt", "Tatar"),
|
||||||
|
("udm", "Udmurt"),
|
||||||
|
("ug", "Uyghur"),
|
||||||
|
("uk", "Ukrainian"),
|
||||||
|
("ur", "Urdu"),
|
||||||
|
("uz", "Uzbek"),
|
||||||
|
("vi", "Vietnamese"),
|
||||||
|
("zh-hans", "Simplified Chinese"),
|
||||||
|
("zh-hant", "Traditional Chinese"),
|
||||||
|
],
|
||||||
|
default="en",
|
||||||
|
max_length=7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
19
ram/bookshelf/migrations/0012_alter_book_notes.py
Normal file
19
ram/bookshelf/migrations/0012_alter_book_notes.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-02-17 12:19
|
||||||
|
|
||||||
|
import tinymce.models
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0011_alter_book_language"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="book",
|
||||||
|
name="notes",
|
||||||
|
field=tinymce.models.HTMLField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
19
ram/bookshelf/migrations/0013_book_description.py
Normal file
19
ram/bookshelf/migrations/0013_book_description.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
18
ram/bookshelf/migrations/0014_book_published.py
Normal file
18
ram/bookshelf/migrations/0014_book_published.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
18
ram/bookshelf/migrations/0015_alter_book_authors.py
Normal file
18
ram/bookshelf/migrations/0015_alter_book_authors.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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"),
|
||||||
|
),
|
||||||
|
]
|
141
ram/bookshelf/migrations/0016_basebook_book_catalogue.py
Normal file
141
ram/bookshelf/migrations/0016_basebook_book_catalogue.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# 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",),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,52 @@
|
|||||||
|
# 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")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,17 @@
|
|||||||
|
# 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"},
|
||||||
|
),
|
||||||
|
]
|
36
ram/bookshelf/migrations/0019_basebook_price.py
Normal file
36
ram/bookshelf/migrations/0019_basebook_price.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 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
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,23 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,34 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
50
ram/bookshelf/migrations/0022_basebook_shop.py
Normal file
50
ram/bookshelf/migrations/0022_basebook_shop.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-01-26 14:32
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def shop_from_property(apps, schema_editor):
|
||||||
|
basebook = apps.get_model("bookshelf", "BaseBook")
|
||||||
|
shop_model = apps.get_model("metadata", "Shop")
|
||||||
|
for row in basebook.objects.all():
|
||||||
|
property = row.property.filter(
|
||||||
|
property__name__icontains="shop"
|
||||||
|
).first()
|
||||||
|
if property:
|
||||||
|
shop, created = shop_model.objects.get_or_create(
|
||||||
|
name=property.value,
|
||||||
|
defaults={"on_line": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
row.shop = shop
|
||||||
|
row.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0021_basebookdocument_creation_time_and_more"),
|
||||||
|
("metadata", "0023_shop"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name="basebookdocument",
|
||||||
|
name="unique_book_file",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="basebook",
|
||||||
|
name="shop",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="metadata.shop",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
shop_from_property,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
17
ram/bookshelf/migrations/0023_delete_basebookdocument.py
Normal file
17
ram/bookshelf/migrations/0023_delete_basebookdocument.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-09 13:47
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("bookshelf", "0022_basebook_shop"),
|
||||||
|
("repository", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="BaseBookDocument",
|
||||||
|
),
|
||||||
|
]
|
0
ram/bookshelf/migrations/__init__.py
Normal file
0
ram/bookshelf/migrations/__init__.py
Normal file
155
ram/bookshelf/models.py
Normal file
155
ram/bookshelf/models.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
|
from django_countries.fields import CountryField
|
||||||
|
|
||||||
|
from ram.utils import DeduplicatedStorage
|
||||||
|
from ram.models import BaseModel, Image, PropertyInstance
|
||||||
|
from metadata.models import Scale, Manufacturer, Shop, Tag
|
||||||
|
|
||||||
|
|
||||||
|
class Publisher(models.Model):
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
country = CountryField(blank=True)
|
||||||
|
website = models.URLField(blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Author(models.Model):
|
||||||
|
first_name = models.CharField(max_length=100)
|
||||||
|
last_name = models.CharField(max_length=100)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["last_name", "first_name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.last_name}, {self.first_name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_name(self):
|
||||||
|
return f"{self.last_name} {self.first_name[0]}."
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBook(BaseModel):
|
||||||
|
ISBN = models.CharField(max_length=17, blank=True) # 13 + dashes
|
||||||
|
language = models.CharField(
|
||||||
|
max_length=7,
|
||||||
|
choices=settings.LANGUAGES,
|
||||||
|
default='en'
|
||||||
|
)
|
||||||
|
number_of_pages = models.SmallIntegerField(null=True, blank=True)
|
||||||
|
publication_year = models.SmallIntegerField(null=True, blank=True)
|
||||||
|
shop = models.ForeignKey(
|
||||||
|
Shop, on_delete=models.CASCADE, null=True, blank=True
|
||||||
|
)
|
||||||
|
price = models.DecimalField(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
purchase_date = models.DateField(null=True, blank=True)
|
||||||
|
tags = models.ManyToManyField(
|
||||||
|
Tag, related_name="bookshelf", blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
shutil.rmtree(
|
||||||
|
os.path.join(
|
||||||
|
settings.MEDIA_ROOT, "images", "books", str(self.uuid)
|
||||||
|
),
|
||||||
|
ignore_errors=True
|
||||||
|
)
|
||||||
|
super(BaseBook, self).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def book_image_upload(instance, filename):
|
||||||
|
return os.path.join(
|
||||||
|
"images",
|
||||||
|
"books",
|
||||||
|
str(instance.book.uuid),
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBookImage(Image):
|
||||||
|
book = models.ForeignKey(
|
||||||
|
BaseBook, on_delete=models.CASCADE, related_name="image"
|
||||||
|
)
|
||||||
|
image = models.ImageField(
|
||||||
|
upload_to=book_image_upload,
|
||||||
|
storage=DeduplicatedStorage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBookProperty(PropertyInstance):
|
||||||
|
book = models.ForeignKey(
|
||||||
|
BaseBook,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
related_name="property",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Book(BaseBook):
|
||||||
|
title = models.CharField(max_length=200)
|
||||||
|
authors = models.ManyToManyField(Author, blank=True)
|
||||||
|
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["title"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
@property
|
||||||
|
def publisher_name(self):
|
||||||
|
return self.publisher.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def authors_list(self):
|
||||||
|
return ", ".join(a.short_name for a in self.authors.all())
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"bookshelf_item",
|
||||||
|
kwargs={"selector": "book", "uuid": self.uuid}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Catalog(BaseBook):
|
||||||
|
manufacturer = models.ForeignKey(
|
||||||
|
Manufacturer,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
years = models.CharField(max_length=12)
|
||||||
|
scales = models.ManyToManyField(Scale)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["manufacturer", "publication_year"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# if the object is new, return an empty string to avoid
|
||||||
|
# calling self.scales.all() which would raise a infinite recursion
|
||||||
|
if self.pk is None:
|
||||||
|
return str() # empty string
|
||||||
|
scales = self.get_scales()
|
||||||
|
return "%s %s %s" % (self.manufacturer.name, self.years, scales)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"bookshelf_item",
|
||||||
|
kwargs={"selector": "catalog", "uuid": self.uuid}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_scales(self):
|
||||||
|
return "/".join([s.scale for s in self.scales.all()])
|
||||||
|
get_scales.short_description = "Scales"
|
51
ram/bookshelf/serializers.py
Normal file
51
ram/bookshelf/serializers.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from bookshelf.models import Book, Catalog, Author, Publisher
|
||||||
|
from metadata.serializers import (
|
||||||
|
ScaleSerializer,
|
||||||
|
ManufacturerSerializer,
|
||||||
|
TagSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Author
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class PublisherSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Publisher
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class BookSerializer(serializers.ModelSerializer):
|
||||||
|
authors = AuthorSerializer(many=True)
|
||||||
|
publisher = PublisherSerializer()
|
||||||
|
tags = TagSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Book
|
||||||
|
exclude = (
|
||||||
|
"notes",
|
||||||
|
"shop",
|
||||||
|
"purchase_date",
|
||||||
|
"price",
|
||||||
|
)
|
||||||
|
read_only_fields = ("creation_time", "updated_time")
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogSerializer(serializers.ModelSerializer):
|
||||||
|
scales = ScaleSerializer(many=True)
|
||||||
|
manufacturer = ManufacturerSerializer()
|
||||||
|
tags = TagSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Catalog
|
||||||
|
exclude = (
|
||||||
|
"notes",
|
||||||
|
"shop",
|
||||||
|
"purchase_date",
|
||||||
|
"price",
|
||||||
|
)
|
||||||
|
read_only_fields = ("creation_time", "updated_time")
|
3
ram/bookshelf/tests.py
Normal file
3
ram/bookshelf/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
9
ram/bookshelf/urls.py
Normal file
9
ram/bookshelf/urls.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from bookshelf.views import BookList, BookGet, CatalogList, CatalogGet
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("book/list", BookList.as_view()),
|
||||||
|
path("book/get/<uuid:uuid>", BookGet.as_view()),
|
||||||
|
path("catalog/list", CatalogList.as_view()),
|
||||||
|
path("catalog/get/<uuid:uuid>", CatalogGet.as_view()),
|
||||||
|
]
|
40
ram/bookshelf/views.py
Normal file
40
ram/bookshelf/views.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from rest_framework.generics import ListAPIView, RetrieveAPIView
|
||||||
|
from rest_framework.schemas.openapi import AutoSchema
|
||||||
|
|
||||||
|
from ram.views import CustomLimitOffsetPagination
|
||||||
|
from bookshelf.models import Book, Catalog
|
||||||
|
from bookshelf.serializers import BookSerializer, CatalogSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class BookList(ListAPIView):
|
||||||
|
serializer_class = BookSerializer
|
||||||
|
pagination_class = CustomLimitOffsetPagination
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Book.objects.get_published(self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class BookGet(RetrieveAPIView):
|
||||||
|
serializer_class = BookSerializer
|
||||||
|
lookup_field = "uuid"
|
||||||
|
schema = AutoSchema(operation_id_base="retrieveBookByUUID")
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Book.objects.get_published(self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogList(ListAPIView):
|
||||||
|
serializer_class = CatalogSerializer
|
||||||
|
pagination_class = CustomLimitOffsetPagination
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Catalog.objects.get_published(self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogGet(RetrieveAPIView):
|
||||||
|
serializer_class = CatalogSerializer
|
||||||
|
lookup_field = "uuid"
|
||||||
|
schema = AutoSchema(operation_id_base="retrieveCatalogByUUID")
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Book.objects.get_published(self.request.user)
|
@@ -1,14 +1,42 @@
|
|||||||
from django.contrib import admin
|
import html
|
||||||
from adminsortable2.admin import SortableAdminBase, SortableInlineAdminMixin
|
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import admin
|
||||||
|
# from django.forms import BaseInlineFormSet # for future reference
|
||||||
|
from django.utils.html import format_html, strip_tags
|
||||||
|
from adminsortable2.admin import (
|
||||||
|
SortableAdminBase,
|
||||||
|
SortableInlineAdminMixin,
|
||||||
|
# CustomInlineFormSetMixin, # for future reference
|
||||||
|
)
|
||||||
|
|
||||||
|
from ram.admin import publish, unpublish
|
||||||
|
from ram.utils import generate_csv
|
||||||
from consist.models import Consist, ConsistItem
|
from consist.models import Consist, ConsistItem
|
||||||
|
|
||||||
|
|
||||||
|
# for future reference
|
||||||
|
# class ConsistItemInlineFormSet(CustomInlineFormSetMixin, BaseInlineFormSet):
|
||||||
|
# def clean(self):
|
||||||
|
# super().clean()
|
||||||
|
|
||||||
|
|
||||||
class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
class ConsistItemInline(SortableInlineAdminMixin, admin.TabularInline):
|
||||||
model = ConsistItem
|
model = ConsistItem
|
||||||
min_num = 1
|
min_num = 1
|
||||||
extra = 0
|
extra = 0
|
||||||
readonly_fields = ("address", "type", "company", "era")
|
autocomplete_fields = ("rolling_stock",)
|
||||||
|
readonly_fields = (
|
||||||
|
"preview",
|
||||||
|
"published",
|
||||||
|
"scale",
|
||||||
|
"manufacturer",
|
||||||
|
"item_number",
|
||||||
|
"company",
|
||||||
|
"type",
|
||||||
|
"era",
|
||||||
|
"address",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Consist)
|
@admin.register(Consist)
|
||||||
@@ -18,25 +46,38 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"creation_time",
|
"creation_time",
|
||||||
"updated_time",
|
"updated_time",
|
||||||
)
|
)
|
||||||
list_display = ("identifier", "company", "era")
|
list_filter = ("company__name", "era", "scale", "published")
|
||||||
list_filter = list_display
|
list_display = ("__str__",) + list_filter + ("country_flag",)
|
||||||
search_fields = list_display
|
search_fields = ("identifier",) + list_filter
|
||||||
|
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",
|
|
||||||
"company",
|
"company",
|
||||||
|
"scale",
|
||||||
"era",
|
"era",
|
||||||
|
"consist_address",
|
||||||
|
"description",
|
||||||
"image",
|
"image",
|
||||||
"notes",
|
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Notes",
|
||||||
|
{"classes": ("collapse",), "fields": ("notes",)},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"Audit",
|
"Audit",
|
||||||
{
|
{
|
||||||
@@ -48,3 +89,55 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def download_csv(modeladmin, request, queryset):
|
||||||
|
header = [
|
||||||
|
"ID",
|
||||||
|
"Name",
|
||||||
|
"Published",
|
||||||
|
"Company",
|
||||||
|
"Country",
|
||||||
|
"Address",
|
||||||
|
"Scale",
|
||||||
|
"Era",
|
||||||
|
"Description",
|
||||||
|
"Tags",
|
||||||
|
"Length",
|
||||||
|
"Composition",
|
||||||
|
"Item name",
|
||||||
|
"Item type",
|
||||||
|
"Item ID",
|
||||||
|
]
|
||||||
|
data = []
|
||||||
|
for obj in queryset:
|
||||||
|
for item in obj.consist_item.all():
|
||||||
|
types = " + ".join(
|
||||||
|
"{}x {}".format(t["count"], t["type"])
|
||||||
|
for t in obj.get_type_count()
|
||||||
|
)
|
||||||
|
data.append(
|
||||||
|
[
|
||||||
|
obj.uuid,
|
||||||
|
obj.__str__(),
|
||||||
|
"X" if obj.published else "",
|
||||||
|
obj.company.name,
|
||||||
|
obj.company.country,
|
||||||
|
obj.consist_address,
|
||||||
|
obj.scale.scale,
|
||||||
|
obj.era,
|
||||||
|
html.unescape(strip_tags(obj.description)),
|
||||||
|
settings.CSV_SEPARATOR_ALT.join(
|
||||||
|
t.name for t in obj.tags.all()
|
||||||
|
),
|
||||||
|
obj.length,
|
||||||
|
types,
|
||||||
|
item.rolling_stock.__str__(),
|
||||||
|
item.type,
|
||||||
|
item.rolling_stock.uuid,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return generate_csv(header, data, "consists.csv")
|
||||||
|
download_csv.short_description = "Download selected items as CSV"
|
||||||
|
|
||||||
|
actions = [publish, unpublish, download_csv]
|
||||||
|
20
ram/consist/migrations/0005_alter_consist_notes.py
Normal file
20
ram/consist/migrations/0005_alter_consist_notes.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.1 on 2022-08-23 15:54
|
||||||
|
|
||||||
|
# ckeditor removal
|
||||||
|
# import ckeditor_uploader.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0004_alter_consist_company"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# migrations.AlterField(
|
||||||
|
# model_name="consist",
|
||||||
|
# name="notes",
|
||||||
|
# field=ckeditor_uploader.fields.RichTextUploadingField(blank=True),
|
||||||
|
# ),
|
||||||
|
]
|
34
ram/consist/migrations/0006_md_to_html.py
Normal file
34
ram/consist/migrations/0006_md_to_html.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 4.1 on 2022-08-24 15:30
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def md_to_html(apps, schema_editor):
|
||||||
|
fields = {
|
||||||
|
"Consist": ["notes"],
|
||||||
|
}
|
||||||
|
|
||||||
|
for m in fields.items():
|
||||||
|
model = apps.get_model("consist", m[0])
|
||||||
|
|
||||||
|
for row in model.objects.all():
|
||||||
|
for field in m[1]:
|
||||||
|
html = markdown.markdown(getattr(row, field))
|
||||||
|
row.__dict__[field] = html
|
||||||
|
|
||||||
|
row.save(update_fields=m[1])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0005_alter_consist_notes"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
md_to_html,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
24
ram/consist/migrations/0007_alter_consist_image.py
Normal file
24
ram/consist/migrations/0007_alter_consist_image.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 4.1.2 on 2022-11-27 00:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import ram.utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0006_md_to_html"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="consist",
|
||||||
|
name="image",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
17
ram/consist/migrations/0008_alter_consist_options.py
Normal file
17
ram/consist/migrations/0008_alter_consist_options.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 4.1.3 on 2023-01-02 15:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0007_alter_consist_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="consist",
|
||||||
|
options={"ordering": ["company", "-creation_time"]},
|
||||||
|
),
|
||||||
|
]
|
51
ram/consist/migrations/0009_alter_consist_image.py
Normal file
51
ram/consist/migrations/0009_alter_consist_image.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-10-31 09:41
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import ram.utils
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def move_images(apps, schema_editor):
|
||||||
|
sys.stdout.write("\n Processing files. Please await...")
|
||||||
|
model = apps.get_model("consist", "Consist")
|
||||||
|
for r in model.objects.all():
|
||||||
|
if not r.image: # exit the loop if there's no image
|
||||||
|
continue
|
||||||
|
fname = os.path.basename(r.image.path)
|
||||||
|
new_image = os.path.join("images", "consists", fname)
|
||||||
|
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
|
||||||
|
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
||||||
|
try:
|
||||||
|
shutil.move(r.image.path, new_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
sys.stderr.write(" !! FileNotFoundError: {}\n".format(new_image))
|
||||||
|
pass
|
||||||
|
r.image.name = new_image
|
||||||
|
r.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0008_alter_consist_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="consist",
|
||||||
|
name="image",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/consists",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
move_images,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
19
ram/consist/migrations/0010_alter_consist_notes.py
Normal file
19
ram/consist/migrations/0010_alter_consist_notes.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-02-17 12:19
|
||||||
|
|
||||||
|
import tinymce.models
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0009_alter_consist_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="consist",
|
||||||
|
name="notes",
|
||||||
|
field=tinymce.models.HTMLField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,30 @@
|
|||||||
|
# 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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
ram/consist/migrations/0012_consist_published.py
Normal file
18
ram/consist/migrations/0012_consist_published.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,20 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
ram/consist/migrations/0014_alter_consistitem_order.py
Normal file
18
ram/consist/migrations/0014_alter_consistitem_order.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
19
ram/consist/migrations/0015_consist_description.py
Normal file
19
ram/consist/migrations/0015_consist_description.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
18
ram/consist/migrations/0016_alter_consistitem_order.py
Normal file
18
ram/consist/migrations/0016_alter_consistitem_order.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-04-27 19:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0015_consist_description"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="consistitem",
|
||||||
|
name="order",
|
||||||
|
field=models.PositiveIntegerField(),
|
||||||
|
),
|
||||||
|
]
|
42
ram/consist/migrations/0017_consist_scale.py
Normal file
42
ram/consist/migrations/0017_consist_scale.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-05-01 09:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def set_scale(apps, schema_editor):
|
||||||
|
Consist = apps.get_model("consist", "Consist")
|
||||||
|
|
||||||
|
for consist in Consist.objects.all():
|
||||||
|
try:
|
||||||
|
consist.scale = consist.consist_item.first().rolling_stock.scale
|
||||||
|
consist.save()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0016_alter_consistitem_order"),
|
||||||
|
(
|
||||||
|
"metadata",
|
||||||
|
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="consist",
|
||||||
|
name="scale",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="metadata.scale",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
set_scale,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
25
ram/consist/migrations/0018_alter_consist_scale.py
Normal file
25
ram/consist/migrations/0018_alter_consist_scale.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-05-02 11:33
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("consist", "0017_consist_scale"),
|
||||||
|
(
|
||||||
|
"metadata",
|
||||||
|
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="consist",
|
||||||
|
name="scale",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="metadata.scale"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@@ -1,24 +1,39 @@
|
|||||||
from uuid import uuid4
|
import os
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.text import Truncator
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from metadata.models import Company, Tag
|
from ram.models import BaseModel
|
||||||
|
from ram.utils import DeduplicatedStorage
|
||||||
|
from metadata.models import Company, Scale, Tag
|
||||||
from roster.models import RollingStock
|
from roster.models import RollingStock
|
||||||
|
|
||||||
|
|
||||||
class Consist(models.Model):
|
class Consist(BaseModel):
|
||||||
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, null=True, blank=True
|
default=None,
|
||||||
|
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(max_length=32, blank=True)
|
era = models.CharField(
|
||||||
image = models.ImageField(upload_to="images/", null=True, blank=True)
|
max_length=32,
|
||||||
notes = models.TextField(blank=True)
|
blank=True,
|
||||||
creation_time = models.DateTimeField(auto_now_add=True)
|
help_text="Era or epoch of the consist",
|
||||||
updated_time = models.DateTimeField(auto_now=True)
|
)
|
||||||
|
scale = models.ForeignKey(Scale, on_delete=models.CASCADE)
|
||||||
|
image = models.ImageField(
|
||||||
|
upload_to=os.path.join("images", "consists"),
|
||||||
|
storage=DeduplicatedStorage,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0} {1}".format(self.company, self.identifier)
|
return "{0} {1}".format(self.company, self.identifier)
|
||||||
@@ -26,8 +41,27 @@ class Consist(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("consist", kwargs={"uuid": self.uuid})
|
return reverse("consist", kwargs={"uuid": self.uuid})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
return self.consist_item.count()
|
||||||
|
|
||||||
|
def get_type_count(self):
|
||||||
|
return self.consist_item.annotate(
|
||||||
|
type=models.F("rolling_stock__rolling_class__type__type")
|
||||||
|
).values(
|
||||||
|
"type"
|
||||||
|
).annotate(
|
||||||
|
count=models.Count("rolling_stock"),
|
||||||
|
category=models.F("rolling_stock__rolling_class__type__category"),
|
||||||
|
order=models.Max("order"),
|
||||||
|
).order_by("order")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def country(self):
|
||||||
|
return self.company.country
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["creation_time"]
|
ordering = ["company", "-creation_time"]
|
||||||
|
|
||||||
|
|
||||||
class ConsistItem(models.Model):
|
class ConsistItem(models.Model):
|
||||||
@@ -35,22 +69,81 @@ 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(default=0, blank=False, null=False)
|
order = models.PositiveIntegerField(blank=False, null=False)
|
||||||
|
|
||||||
class Meta(object):
|
class Meta:
|
||||||
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 type(self):
|
def clean(self):
|
||||||
return self.rolling_stock.rolling_class.type
|
rolling_stock = getattr(self, "rolling_stock", False)
|
||||||
|
if not rolling_stock:
|
||||||
|
return # exit if no inline are present
|
||||||
|
|
||||||
|
# FIXME this does not work when creating a new consist,
|
||||||
|
# because the consist is not saved yet and it must be moved
|
||||||
|
# to the admin form validation via InlineFormSet.clean()
|
||||||
|
consist = self.consist
|
||||||
|
if rolling_stock.scale != consist.scale:
|
||||||
|
raise ValidationError(
|
||||||
|
"The rolling stock and consist must be of the same scale."
|
||||||
|
)
|
||||||
|
if self.consist.published and not rolling_stock.published:
|
||||||
|
raise ValidationError(
|
||||||
|
"You must unpublish the the consist before using this item."
|
||||||
|
)
|
||||||
|
|
||||||
|
def published(self):
|
||||||
|
return self.rolling_stock.published
|
||||||
|
published.boolean = True
|
||||||
|
|
||||||
|
def preview(self):
|
||||||
|
return self.rolling_stock.image.first().image_thumbnail(100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manufacturer(self):
|
||||||
|
return Truncator(self.rolling_stock.manufacturer).chars(10)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def item_number(self):
|
||||||
|
return self.rolling_stock.item_number
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scale(self):
|
||||||
|
return self.rolling_stock.scale
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
return self.rolling_stock.rolling_class.type.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,4 +21,5 @@ class ConsistSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Consist
|
model = Consist
|
||||||
fields = "__all__"
|
exclude = ("notes",)
|
||||||
|
read_only_fields = ("creation_time", "updated_time")
|
||||||
|
@@ -1,15 +1,21 @@
|
|||||||
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,10 +1,13 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.utils.html import format_html
|
||||||
from adminsortable2.admin import SortableAdminMixin
|
from adminsortable2.admin import SortableAdminMixin
|
||||||
|
|
||||||
|
from repository.models import DecoderDocument
|
||||||
from metadata.models import (
|
from metadata.models import (
|
||||||
Property,
|
Property,
|
||||||
Decoder,
|
Decoder,
|
||||||
Scale,
|
Scale,
|
||||||
|
Shop,
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
Company,
|
Company,
|
||||||
Tag,
|
Tag,
|
||||||
@@ -14,14 +17,23 @@ from metadata.models import (
|
|||||||
|
|
||||||
@admin.register(Property)
|
@admin.register(Property)
|
||||||
class PropertyAdmin(admin.ModelAdmin):
|
class PropertyAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "private")
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
class DecoderDocInline(admin.TabularInline):
|
||||||
|
model = DecoderDocument
|
||||||
|
min_num = 0
|
||||||
|
extra = 0
|
||||||
|
classes = ["collapse"]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Decoder)
|
@admin.register(Decoder)
|
||||||
class DecoderAdmin(admin.ModelAdmin):
|
class DecoderAdmin(admin.ModelAdmin):
|
||||||
|
inlines = (DecoderDocInline,)
|
||||||
readonly_fields = ("image_thumbnail",)
|
readonly_fields = ("image_thumbnail",)
|
||||||
list_display = ("__str__", "interface")
|
list_display = ("__str__", "sound")
|
||||||
list_filter = ("manufacturer", "interface")
|
list_filter = ("manufacturer", "sound")
|
||||||
search_fields = ("name", "manufacturer__name")
|
search_fields = ("name", "manufacturer__name")
|
||||||
|
|
||||||
|
|
||||||
@@ -35,18 +47,30 @@ 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")
|
list_display = ("name", "country_flag")
|
||||||
list_filter = list_display
|
list_filter = ("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(Manufacturer)
|
@admin.register(Manufacturer)
|
||||||
class ManufacturerAdmin(admin.ModelAdmin):
|
class ManufacturerAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ("logo_thumbnail",)
|
readonly_fields = ("logo_thumbnail",)
|
||||||
list_display = ("name", "category")
|
list_display = ("name", "category", "country_flag")
|
||||||
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):
|
||||||
@@ -60,3 +84,10 @@ class RollingStockTypeAdmin(SortableAdminMixin, admin.ModelAdmin):
|
|||||||
list_display = ("__str__",)
|
list_display = ("__str__",)
|
||||||
list_filter = ("type", "category")
|
list_filter = ("type", "category")
|
||||||
search_fields = ("type", "category")
|
search_fields = ("type", "category")
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Shop)
|
||||||
|
class ShopAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "on_line", "active")
|
||||||
|
list_filter = ("on_line", "active")
|
||||||
|
search_fields = ("name",)
|
||||||
|
18
ram/metadata/migrations/0008_remove_decoder_interface.py
Normal file
18
ram/metadata/migrations/0008_remove_decoder_interface.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.2 on 2022-10-31 23:01
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0007_rename_track_scale_tracks"),
|
||||||
|
("roster", "0013_rollingstock_decoder_interface"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="decoder",
|
||||||
|
name="interface",
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 4.1.2 on 2022-11-27 00:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import ram.utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0008_remove_decoder_interface"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="company",
|
||||||
|
name="logo",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="decoder",
|
||||||
|
name="image",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="manufacturer",
|
||||||
|
name="logo",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
26
ram/metadata/migrations/0010_alter_manufacturer_category.py
Normal file
26
ram/metadata/migrations/0010_alter_manufacturer_category.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-06 00:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0009_alter_company_logo_alter_decoder_image_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="manufacturer",
|
||||||
|
name="category",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("model", "Model"),
|
||||||
|
("real", "Real"),
|
||||||
|
("accessory", "Accessory"),
|
||||||
|
("other", "Other"),
|
||||||
|
],
|
||||||
|
max_length=64,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
93
ram/metadata/migrations/0011_company_slug_and_more.py
Normal file
93
ram/metadata/migrations/0011_company_slug_and_more.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-01-09 11:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
from ram.utils import slugify
|
||||||
|
|
||||||
|
|
||||||
|
def create_slug(apps, schema_editor):
|
||||||
|
fields = ["Company", "Manufacturer", "RollingStockType", "Scale"]
|
||||||
|
|
||||||
|
for m in fields:
|
||||||
|
model = apps.get_model("metadata", m)
|
||||||
|
|
||||||
|
for row in model.objects.all():
|
||||||
|
if hasattr(row, "type"):
|
||||||
|
row.slug = slugify("{} {}".format(row.type, row.category))
|
||||||
|
elif hasattr(row, "scale"):
|
||||||
|
row.slug = slugify(row.scale)
|
||||||
|
else:
|
||||||
|
row.slug = slugify(row.name)
|
||||||
|
|
||||||
|
row.save(update_fields=["slug"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0010_alter_manufacturer_category"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="company",
|
||||||
|
name="slug",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False, max_length=64, blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="manufacturer",
|
||||||
|
name="slug",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False, max_length=128, blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="rollingstocktype",
|
||||||
|
name="slug",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False, max_length=128, blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="scale",
|
||||||
|
name="slug",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False, max_length=32, blank=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
create_slug,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="company",
|
||||||
|
name="slug",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False, max_length=64, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="manufacturer",
|
||||||
|
name="slug",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False, max_length=128, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rollingstocktype",
|
||||||
|
name="slug",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False, max_length=128, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="scale",
|
||||||
|
name="slug",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False, max_length=32, unique=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,59 @@
|
|||||||
|
# Generated by Django 4.2 on 2023-09-30 21:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ram.utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0011_company_slug_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="decoder",
|
||||||
|
name="manufacturer",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
limit_choices_to={"category": "accessory"},
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="metadata.manufacturer",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="DecoderDocument",
|
||||||
|
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(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage(),
|
||||||
|
upload_to="files/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"decoder",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="document",
|
||||||
|
to="metadata.decoder",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"unique_together": {("decoder", "file")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
18
ram/metadata/migrations/0013_decoderdocument_private.py
Normal file
18
ram/metadata/migrations/0013_decoderdocument_private.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.5 on 2023-10-06 19:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0012_alter_decoder_manufacturer_decoderdocument"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="decoderdocument",
|
||||||
|
name="private",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-10-10 12:44
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0013_decoderdocument_private"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="decoder",
|
||||||
|
options={"ordering": ["manufacturer", "name"]},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="tag",
|
||||||
|
options={"ordering": ["name"]},
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,80 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-10-30 13:16
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import ram.utils
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def move_images(apps, schema_editor):
|
||||||
|
fields = {
|
||||||
|
"Company": ["companies", "logo"],
|
||||||
|
"Decoder": ["decoders", "image"],
|
||||||
|
"Manufacturer": ["manufacturers", "logo"],
|
||||||
|
}
|
||||||
|
sys.stdout.write("\n Processing files. Please await...")
|
||||||
|
for m in fields.items():
|
||||||
|
model = apps.get_model("metadata", m[0])
|
||||||
|
for r in model.objects.all():
|
||||||
|
field = getattr(r, m[1][1])
|
||||||
|
if not field: # exit the loop if there's no image
|
||||||
|
continue
|
||||||
|
fname = os.path.basename(field.path)
|
||||||
|
new_image = os.path.join("images", m[1][0], fname)
|
||||||
|
new_path = os.path.join(settings.MEDIA_ROOT, new_image)
|
||||||
|
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
||||||
|
try:
|
||||||
|
shutil.move(field.path, new_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
sys.stderr.write(
|
||||||
|
" !! FileNotFoundError: {}\n".format(new_image)
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
field.name = new_image
|
||||||
|
r.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0014_alter_decoder_options_alter_tag_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="company",
|
||||||
|
name="logo",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/companies",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="decoder",
|
||||||
|
name="image",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/decoders",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="manufacturer",
|
||||||
|
name="logo",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
storage=ram.utils.DeduplicatedStorage,
|
||||||
|
upload_to="images/manufacturers",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
move_images,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
20
ram/metadata/migrations/0016_alter_decoderdocument_file.py
Normal file
20
ram/metadata/migrations/0016_alter_decoderdocument_file.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.2.6 on 2023-11-04 22:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import ram.utils
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0015_alter_company_logo_alter_decoder_image_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="decoderdocument",
|
||||||
|
name="file",
|
||||||
|
field=models.FileField(
|
||||||
|
storage=ram.utils.DeduplicatedStorage(), upload_to="files/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
20
ram/metadata/migrations/0017_alter_property_private.py
Normal file
20
ram/metadata/migrations/0017_alter_property_private.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,69 @@
|
|||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
]
|
22
ram/metadata/migrations/0019_alter_scale_gauge.py
Normal file
22
ram/metadata/migrations/0019_alter_scale_gauge.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,33 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
40
ram/metadata/migrations/0021_genericdocument.py
Normal file
40
ram/metadata/migrations/0021_genericdocument.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,66 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
40
ram/metadata/migrations/0023_shop.py
Normal file
40
ram/metadata/migrations/0023_shop.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 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")],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-02-09 13:47
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("metadata", "0023_shop"),
|
||||||
|
("repository", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="genericdocument",
|
||||||
|
name="tags",
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="DecoderDocument",
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name="GenericDocument",
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2025-05-04 20:45
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"metadata",
|
||||||
|
"0024_remove_genericdocument_tags_delete_decoderdocument_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="company",
|
||||||
|
options={"ordering": ["slug"], "verbose_name_plural": "Companies"},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="manufacturer",
|
||||||
|
options={"ordering": ["category", "slug"]},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="tag",
|
||||||
|
options={"ordering": ["slug"]},
|
||||||
|
),
|
||||||
|
]
|
@@ -1,14 +1,21 @@
|
|||||||
|
import os
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.dispatch.dispatcher import receiver
|
from django.dispatch.dispatcher import receiver
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django_countries.fields import CountryField
|
from django_countries.fields import CountryField
|
||||||
|
|
||||||
from ram.utils import 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(default=False)
|
private = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Property will be only visible to logged users",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "Properties"
|
verbose_name_plural = "Properties"
|
||||||
@@ -17,21 +24,39 @@ 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)
|
||||||
|
slug = models.CharField(max_length=128, unique=True, editable=False)
|
||||||
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(upload_to="images/", null=True, blank=True)
|
logo = models.ImageField(
|
||||||
|
upload_to=os.path.join("images", "manufacturers"),
|
||||||
|
storage=DeduplicatedStorage,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["category", "name"]
|
ordering = ["category", "slug"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"filtered",
|
||||||
|
kwargs={
|
||||||
|
"_filter": "manufacturer",
|
||||||
|
"search": self.slug,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def logo_thumbnail(self):
|
def logo_thumbnail(self):
|
||||||
return get_image_preview(self.logo.url)
|
return get_image_preview(self.logo.url)
|
||||||
|
|
||||||
@@ -40,18 +65,36 @@ class Manufacturer(models.Model):
|
|||||||
|
|
||||||
class Company(models.Model):
|
class Company(models.Model):
|
||||||
name = models.CharField(max_length=64, unique=True)
|
name = models.CharField(max_length=64, unique=True)
|
||||||
|
slug = models.CharField(max_length=64, unique=True, editable=False)
|
||||||
extended_name = models.CharField(max_length=128, blank=True)
|
extended_name = models.CharField(max_length=128, blank=True)
|
||||||
country = CountryField()
|
country = CountryField()
|
||||||
freelance = models.BooleanField(default=False)
|
freelance = models.BooleanField(default=False)
|
||||||
logo = models.ImageField(upload_to="images/", null=True, blank=True)
|
logo = models.ImageField(
|
||||||
|
upload_to=os.path.join("images", "companies"),
|
||||||
|
storage=DeduplicatedStorage,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "Companies"
|
verbose_name_plural = "Companies"
|
||||||
ordering = ["name"]
|
ordering = ["slug"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"filtered",
|
||||||
|
kwargs={
|
||||||
|
"_filter": "company",
|
||||||
|
"search": self.slug,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def extended_name_pp(self):
|
||||||
|
return "({})".format(self.extended_name) if self.extended_name else ""
|
||||||
|
|
||||||
def logo_thumbnail(self):
|
def logo_thumbnail(self):
|
||||||
return get_image_preview(self.logo.url)
|
return get_image_preview(self.logo.url)
|
||||||
|
|
||||||
@@ -63,14 +106,19 @@ class Decoder(models.Model):
|
|||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
Manufacturer,
|
Manufacturer,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
limit_choices_to={"category": "model"},
|
limit_choices_to={"category": "accessory"},
|
||||||
)
|
)
|
||||||
version = models.CharField(max_length=64, blank=True)
|
version = models.CharField(max_length=64, blank=True)
|
||||||
interface = models.PositiveSmallIntegerField(
|
|
||||||
choices=settings.DECODER_INTERFACES, null=True, blank=True
|
|
||||||
)
|
|
||||||
sound = models.BooleanField(default=False)
|
sound = models.BooleanField(default=False)
|
||||||
image = models.ImageField(upload_to="images/", null=True, blank=True)
|
image = models.ImageField(
|
||||||
|
upload_to=os.path.join("images", "decoders"),
|
||||||
|
storage=DeduplicatedStorage,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["manufacturer__name", "name"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0} - {1}".format(self.manufacturer, self.name)
|
return "{0} - {1}".format(self.manufacturer, self.name)
|
||||||
@@ -81,30 +129,47 @@ class Decoder(models.Model):
|
|||||||
image_thumbnail.short_description = "Preview"
|
image_thumbnail.short_description = "Preview"
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_ratio(ratio):
|
||||||
|
try:
|
||||||
|
num, den = ratio.split(":")
|
||||||
|
return int(num) / float(den) * 10000
|
||||||
|
except (ValueError, ZeroDivisionError):
|
||||||
|
raise ValidationError("Invalid ratio format")
|
||||||
|
|
||||||
|
|
||||||
class Scale(models.Model):
|
class Scale(models.Model):
|
||||||
scale = models.CharField(max_length=32, unique=True)
|
scale = models.CharField(max_length=32, unique=True)
|
||||||
ratio = models.CharField(max_length=16, blank=True)
|
slug = models.CharField(max_length=32, unique=True, editable=False)
|
||||||
gauge = models.CharField(max_length=16, blank=True)
|
ratio = models.CharField(max_length=16, validators=[calculate_ratio])
|
||||||
tracks = models.CharField(max_length=16, blank=True)
|
ratio_int = models.SmallIntegerField(editable=False, default=0)
|
||||||
|
tracks = models.FloatField(
|
||||||
|
help_text="Distance between model tracks in mm",
|
||||||
|
)
|
||||||
|
gauge = models.CharField(
|
||||||
|
max_length=16,
|
||||||
|
blank=True,
|
||||||
|
help_text="Distance between real tracks. Please specify the unit (mm, in, ...)", # noqa: E501
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["scale"]
|
ordering = ["-ratio_int", "-tracks", "scale"]
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"filtered",
|
||||||
|
kwargs={
|
||||||
|
"_filter": "scale",
|
||||||
|
"search": self.slug,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.scale)
|
return str(self.scale)
|
||||||
|
|
||||||
|
|
||||||
class Tag(models.Model):
|
@receiver(models.signals.pre_save, sender=Scale)
|
||||||
name = models.CharField(max_length=128, unique=True)
|
def scale_save(sender, instance, **kwargs):
|
||||||
slug = models.CharField(max_length=128, unique=True)
|
instance.ratio_int = calculate_ratio(instance.ratio)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(models.signals.pre_save, sender=Tag)
|
|
||||||
def tag_pre_save(sender, instance, **kwargs):
|
|
||||||
instance.slug = slugify(instance.name)
|
|
||||||
|
|
||||||
|
|
||||||
class RollingStockType(models.Model):
|
class RollingStockType(models.Model):
|
||||||
@@ -113,10 +178,68 @@ class RollingStockType(models.Model):
|
|||||||
category = models.CharField(
|
category = models.CharField(
|
||||||
max_length=64, choices=settings.ROLLING_STOCK_TYPES
|
max_length=64, choices=settings.ROLLING_STOCK_TYPES
|
||||||
)
|
)
|
||||||
|
slug = models.CharField(max_length=128, unique=True, editable=False)
|
||||||
|
|
||||||
class Meta(object):
|
class Meta:
|
||||||
unique_together = ("category", "type")
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["category", "type"],
|
||||||
|
name="unique_category_type"
|
||||||
|
)
|
||||||
|
]
|
||||||
ordering = ["order"]
|
ordering = ["order"]
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"filtered",
|
||||||
|
kwargs={
|
||||||
|
"_filter": "type",
|
||||||
|
"search": self.slug,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{0} {1}".format(self.type, self.category)
|
return "{0} {1}".format(self.type, self.category)
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(models.Model):
|
||||||
|
name = models.CharField(max_length=128, unique=True)
|
||||||
|
slug = models.CharField(max_length=128, unique=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["slug"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
"filtered",
|
||||||
|
kwargs={
|
||||||
|
"_filter": "tag",
|
||||||
|
"search": self.slug,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Shop(models.Model):
|
||||||
|
name = models.CharField(max_length=128, unique=True)
|
||||||
|
country = CountryField(blank=True)
|
||||||
|
website = models.URLField(blank=True)
|
||||||
|
on_line = models.BooleanField(default=True)
|
||||||
|
active = models.BooleanField(default=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = [models.functions.Lower("name"),]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.pre_save, sender=Manufacturer)
|
||||||
|
@receiver(models.signals.pre_save, sender=Company)
|
||||||
|
@receiver(models.signals.pre_save, sender=Scale)
|
||||||
|
@receiver(models.signals.pre_save, sender=RollingStockType)
|
||||||
|
@receiver(models.signals.pre_save, sender=Tag)
|
||||||
|
def slug_pre_save(sender, instance, **kwargs):
|
||||||
|
instance.slug = slugify(instance.__str__())
|
||||||
|
@@ -1,6 +1,93 @@
|
|||||||
|
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 portal.models import SiteConfiguration
|
from ram.admin import publish, unpublish
|
||||||
|
from portal.models import SiteConfiguration, Flatpage
|
||||||
|
|
||||||
admin.site.register(SiteConfiguration, SingletonModelAdmin)
|
|
||||||
|
@admin.register(SiteConfiguration)
|
||||||
|
class SiteConfigurationAdmin(SingletonModelAdmin):
|
||||||
|
readonly_fields = ("site_name", "rest_api", "version")
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"site_name",
|
||||||
|
"site_author",
|
||||||
|
"about",
|
||||||
|
"items_per_page",
|
||||||
|
"items_ordering",
|
||||||
|
"currency",
|
||||||
|
"footer",
|
||||||
|
"footer_extended",
|
||||||
|
"disclaimer",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Advanced",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"show_version",
|
||||||
|
"use_cdn",
|
||||||
|
"extra_head",
|
||||||
|
"rest_api",
|
||||||
|
"version",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.display(description="REST API enabled", boolean=True)
|
||||||
|
def rest_api(self, obj):
|
||||||
|
return settings.REST_ENABLED
|
||||||
|
|
||||||
|
@admin.display()
|
||||||
|
def version(self, obj):
|
||||||
|
return "{} (Django {})".format(obj.version, obj.django_version)
|
||||||
|
|
||||||
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
|
if db_field.name in ("footer", "footer_extended", "disclaimer"):
|
||||||
|
return db_field.formfield(
|
||||||
|
widget=TinyMCE(
|
||||||
|
mce_attrs={"height": "200"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return super().formfield_for_dbfield(db_field, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Flatpage)
|
||||||
|
class FlatpageAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ("path", "creation_time", "updated_time")
|
||||||
|
list_display = ("name", "path", "published", "get_link")
|
||||||
|
list_filter = ("published",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"name",
|
||||||
|
"path",
|
||||||
|
"content",
|
||||||
|
"published",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Audit",
|
||||||
|
{
|
||||||
|
"classes": ("collapse",),
|
||||||
|
"fields": (
|
||||||
|
"creation_time",
|
||||||
|
"updated_time",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
actions = [publish, unpublish]
|
||||||
|
5
ram/portal/context_processors.py
Normal file
5
ram/portal/context_processors.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def default_card_image(request):
|
||||||
|
return {"DEFAULT_CARD_IMAGE": settings.DEFAULT_CARD_IMAGE}
|
22
ram/portal/migrations/0008_flatpage.py
Normal file
22
ram/portal/migrations/0008_flatpage.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.0.6 on 2022-08-07 15:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('portal', '0007_siteconfiguration_items_ordering'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Flatpage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=256, unique=True)),
|
||||||
|
('draft', models.BooleanField(default=True)),
|
||||||
|
('content', models.TextField(blank=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
19
ram/portal/migrations/0009_flatpage_path.py
Normal file
19
ram/portal/migrations/0009_flatpage_path.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.0.6 on 2022-08-07 15:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('portal', '0008_flatpage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='flatpage',
|
||||||
|
name='path',
|
||||||
|
field=models.CharField(default='', max_length=256, unique=True),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.0.6 on 2022-08-07 15:46
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('portal', '0009_flatpage_path'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='flatpage',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='flatpage',
|
||||||
|
name='updated_time',
|
||||||
|
field=models.DateTimeField(auto_now=True),
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 4.1 on 2022-08-23 15:54
|
||||||
|
|
||||||
|
# ckeditor dependency removal
|
||||||
|
# import ckeditor.fields
|
||||||
|
# import ckeditor_uploader.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("portal", "0010_flatpage_creation_time_flatpage_updated_time"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# migrations.AlterField(
|
||||||
|
# model_name="flatpage",
|
||||||
|
# name="content",
|
||||||
|
# field=ckeditor_uploader.fields.RichTextUploadingField(),
|
||||||
|
# ),
|
||||||
|
# migrations.AlterField(
|
||||||
|
# model_name="siteconfiguration",
|
||||||
|
# name="about",
|
||||||
|
# field=ckeditor.fields.RichTextField(blank=True),
|
||||||
|
# ),
|
||||||
|
# migrations.AlterField(
|
||||||
|
# model_name="siteconfiguration",
|
||||||
|
# name="footer",
|
||||||
|
# field=ckeditor.fields.RichTextField(blank=True),
|
||||||
|
# ),
|
||||||
|
# migrations.AlterField(
|
||||||
|
# model_name="siteconfiguration",
|
||||||
|
# name="footer_extended",
|
||||||
|
# field=ckeditor.fields.RichTextField(blank=True),
|
||||||
|
# ),
|
||||||
|
]
|
38
ram/portal/migrations/0012_md_to_html.py
Normal file
38
ram/portal/migrations/0012_md_to_html.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 4.1 on 2022-08-24 15:00
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def md_to_html(apps, schema_editor):
|
||||||
|
fields = {
|
||||||
|
"SiteConfiguration": ["about", "footer", "footer_extended"],
|
||||||
|
"Flatpage": ["content"]
|
||||||
|
}
|
||||||
|
|
||||||
|
for m in fields.items():
|
||||||
|
model = apps.get_model("portal", m[0])
|
||||||
|
|
||||||
|
for row in model.objects.all():
|
||||||
|
for field in m[1]:
|
||||||
|
html = markdown.markdown(getattr(row, field))
|
||||||
|
row.__dict__[field] = html
|
||||||
|
|
||||||
|
row.save(update_fields=m[1])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"portal",
|
||||||
|
"0011_alter_flatpage_content_alter_siteconfiguration_about_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
md_to_html,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 4.1 on 2022-08-25 10:18
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_bool(apps, schema_editor):
|
||||||
|
model = apps.get_model("portal", "Flatpage")
|
||||||
|
|
||||||
|
for row in model.objects.all():
|
||||||
|
row.published = not row.draft
|
||||||
|
row.save(update_fields=["published"])
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_bool_back(apps, schema_editor):
|
||||||
|
model = apps.get_model("portal", "Flatpage")
|
||||||
|
|
||||||
|
for row in model.objects.all():
|
||||||
|
row.draft = not row.published
|
||||||
|
row.save(update_fields=["draft"])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("portal", "0012_md_to_html"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="flatpage",
|
||||||
|
name="published",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
reverse_bool,
|
||||||
|
reverse_code=reverse_bool_back
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="flatpage",
|
||||||
|
name="draft",
|
||||||
|
),
|
||||||
|
]
|
18
ram/portal/migrations/0014_siteconfiguration_extra_head.py
Normal file
18
ram/portal/migrations/0014_siteconfiguration_extra_head.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.3 on 2022-12-28 22:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("portal", "0013_remove_flatpage_draft_flatpage_published"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="siteconfiguration",
|
||||||
|
name="extra_head",
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
]
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user