Compare commits

..

4 Commits

Author SHA1 Message Date
44a965eb62 Add more indexes and optimize usage 2026-01-18 14:46:49 +01:00
ec470ac0a7 More aggressing code reuse 2026-01-18 11:15:46 +01:00
792b60cdc6 Implement query optimization 2026-01-17 22:59:23 +01:00
cfc7531b59 Extend test coverage 2026-01-17 22:58:41 +01:00
23 changed files with 29 additions and 1482 deletions

View File

@@ -128,7 +128,6 @@ python manage.py runserver --noreload # With pyinstrument middleware
- **Long lines**: Use `# noqa: E501` comment when necessary (see settings.py)
- **Indentation**: 4 spaces (no tabs)
- **Encoding**: UTF-8
- **Blank lines**: Must not contain any whitespace (spaces or tabs)
### Import Organization
Follow Django's import style (as seen in models.py, views.py, admin.py):

141
Makefile
View File

@@ -1,141 +0,0 @@
# Makefile for Django RAM project
# Handles frontend asset minification and common development tasks
.PHONY: help minify minify-js minify-css clean install test
# Directories
JS_SRC_DIR = ram/portal/static/js/src
JS_OUT_DIR = ram/portal/static/js
CSS_SRC_DIR = ram/portal/static/css/src
CSS_OUT_DIR = ram/portal/static/css
# Source files
JS_SOURCES = $(JS_SRC_DIR)/theme_selector.js $(JS_SRC_DIR)/tabs_selector.js $(JS_SRC_DIR)/validators.js
CSS_SOURCES = $(CSS_SRC_DIR)/main.css
# Output files
JS_OUTPUT = $(JS_OUT_DIR)/main.min.js
CSS_OUTPUT = $(CSS_OUT_DIR)/main.min.css
# Default target
help:
@echo "Django RAM - Available Make targets:"
@echo ""
@echo " make install - Install npm dependencies (terser, clean-css-cli)"
@echo " make minify - Minify both JS and CSS files"
@echo " make minify-js - Minify JavaScript files only"
@echo " make minify-css - Minify CSS files only"
@echo " make clean - Remove minified files"
@echo " make watch - Watch for changes and auto-minify (requires inotify-tools)"
@echo " make run - Run Django development server"
@echo " make test - Run Django test suite"
@echo " make lint - Run flake8 linter"
@echo " make format - Run black formatter (line length 79)"
@echo " make ruff-check - Run ruff linter"
@echo " make ruff-format - Run ruff formatter"
@echo " make dump-data - Dump database to gzipped JSON (usage: make dump-data FILE=backup.json.gz)"
@echo " make load-data - Load data from fixture file (usage: make load-data FILE=backup.json.gz)"
@echo " make help - Show this help message"
@echo ""
# Install npm dependencies
install:
@echo "Installing npm dependencies..."
npm install
@echo "Done! terser and clean-css-cli installed."
# Minify both JS and CSS
minify: minify-js minify-css
# Minify JavaScript
minify-js: $(JS_OUTPUT)
$(JS_OUTPUT): $(JS_SOURCES)
@echo "Minifying JavaScript..."
npx terser $(JS_SOURCES) \
--compress \
--mangle \
--source-map "url=main.min.js.map" \
--output $(JS_OUTPUT)
@echo "Created: $(JS_OUTPUT)"
# Minify CSS
minify-css: $(CSS_OUTPUT)
$(CSS_OUTPUT): $(CSS_SOURCES)
@echo "Minifying CSS..."
npx cleancss -o $(CSS_OUTPUT) $(CSS_SOURCES)
@echo "Created: $(CSS_OUTPUT)"
# Clean minified files
clean:
@echo "Removing minified files..."
rm -f $(JS_OUTPUT) $(CSS_OUTPUT)
@echo "Clean complete."
# Watch for changes (requires inotify-tools on Linux)
watch:
@echo "Watching for file changes..."
@echo "Press Ctrl+C to stop"
@while true; do \
inotifywait -e modify,create $(JS_SRC_DIR)/*.js $(CSS_SRC_DIR)/*.css 2>/dev/null && \
make minify; \
done || echo "Note: install inotify-tools for file watching support"
# Run Django development server
run:
@cd ram && python manage.py runserver
# Run Django tests
test:
@echo "Running Django tests..."
@cd ram && python manage.py test
# Run flake8 linter
lint:
@echo "Running flake8..."
@flake8 ram/
# Run black formatter
format:
@echo "Running black formatter..."
@black -l 79 --extend-exclude="/migrations/" ram/
# Run ruff linter
ruff-check:
@echo "Running ruff check..."
@ruff check ram/
# Run ruff formatter
ruff-format:
@echo "Running ruff format..."
@ruff format ram/
# Dump database to gzipped JSON file
# Usage: make dump-data FILE=backup.json.gz
dump-data:
ifndef FILE
$(error FILE is not set. Usage: make dump-data FILE=backup.json.gz)
endif
$(eval FILE_ABS := $(shell realpath -m $(FILE)))
@echo "Dumping database to $(FILE_ABS)..."
@cd ram && python manage.py dumpdata \
--indent=2 \
-e admin \
-e contenttypes \
-e sessions \
--natural-foreign \
--natural-primary | gzip > $(FILE_ABS)
@echo "✓ Database dumped successfully to $(FILE_ABS)"
# Load data from fixture file
# Usage: make load-data FILE=backup.json.gz
load-data:
ifndef FILE
$(error FILE is not set. Usage: make load-data FILE=backup.json.gz)
endif
$(eval FILE_ABS := $(shell realpath $(FILE)))
@echo "Loading data from $(FILE_ABS)..."
@cd ram && python manage.py loaddata $(FILE_ABS)
@echo "✓ Data loaded successfully from $(FILE_ABS)"

View File

@@ -1,22 +0,0 @@
# Udev rule to auto-start/stop dcc-usb-connector.service when USB device is connected/removed
#
# This rule detects when a CH340 USB-to-serial adapter (ID 1a86:7523)
# is connected/removed on /dev/ttyUSB0, then automatically starts/stops
# the dcc-usb-connector.service (user systemd service).
#
# Installation:
# sudo cp 99-dcc-usb-connector.rules /etc/udev/rules.d/
# sudo udevadm control --reload-rules
# sudo udevadm trigger --subsystem-match=tty
#
# Testing:
# udevadm test /sys/class/tty/ttyUSB0
# udevadm monitor --property --subsystem-match=tty
#
# The service will be started when the device is plugged in and stopped
# when the device is unplugged.
# Match USB device 1a86:7523 on ttyUSB0
# TAG+="systemd" tells systemd to track this device
# ENV{SYSTEMD_USER_WANTS} starts the service on "add" and stops it on "remove"
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", KERNEL=="ttyUSB0", TAG+="systemd", ENV{SYSTEMD_USER_WANTS}="dcc-usb-connector.service"

View File

@@ -1,345 +0,0 @@
# DCC USB-to-Network Bridge Auto-Start Installation
This directory contains configuration files to automatically start the `dcc-usb-connector.service` when a specific USB device (CH340 USB-to-serial adapter, ID `1a86:7523`) is connected to `/dev/ttyUSB0`.
## Overview
The setup uses:
- **Udev rule** (`99-dcc-usb-connector.rules`) - Detects USB device connection/disconnection
- **Systemd user service** (`dcc-usb-connector.service`) - Bridges serial port to network port 2560
- **Installation script** (`install-udev-rule.sh`) - Automated installation helper
When the USB device is plugged in, the service automatically starts. When unplugged, it stops.
## Prerequisites
1. **Operating System**: Linux with systemd and udev
2. **Required packages**:
```bash
sudo dnf install nmap-ncat systemd udev
```
3. **User permissions**: Your user should be in the `dialout` group:
```bash
sudo usermod -a -G dialout $USER
# Log out and log back in for changes to take effect
```
## Quick Installation
Run the installation script:
```bash
./install-udev-rule.sh
```
This script will:
- Install the udev rule (requires sudo)
- Install the systemd user service to `~/.config/systemd/user/`
- Enable systemd lingering for your user
- Check for required tools and permissions
- Provide testing instructions
## Manual Installation
If you prefer to install manually:
### 1. Install the udev rule
```bash
sudo cp 99-dcc-usb-connector.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=tty
```
### 2. Install the systemd service
```bash
mkdir -p ~/.config/systemd/user/
cp dcc-usb-connector.service ~/.config/systemd/user/
systemctl --user daemon-reload
```
### 3. Enable lingering (optional but recommended)
This allows your user services to run even when you're not logged in:
```bash
sudo loginctl enable-linger $USER
```
## Verification
### Test the udev rule
```bash
# Monitor udev events (plug/unplug device while this runs)
udevadm monitor --property --subsystem-match=tty
# Test udev rule (when device is connected)
udevadm test /sys/class/tty/ttyUSB0
```
### Check service status
```bash
# Check if service is running
systemctl --user status dcc-usb-connector.service
# View service logs
journalctl --user -u dcc-usb-connector.service -f
```
### Test the network bridge
```bash
# Connect to the bridge
telnet localhost 2560
# Or using netcat
nc localhost 2560
```
## Usage
### Automatic Operation
Once installed, the service will:
- **Start automatically** when USB device `1a86:7523` is connected to `/dev/ttyUSB0`
- **Stop automatically** when the device is disconnected
- Bridge serial communication to network port `2560`
### Manual Control
You can still manually control the service:
```bash
# Start the service
systemctl --user start dcc-usb-connector.service
# Stop the service
systemctl --user stop dcc-usb-connector.service
# Check status
systemctl --user status dcc-usb-connector.service
# View logs
journalctl --user -u dcc-usb-connector.service
```
## How It Works
### Component Interaction
```
USB Device Connected (1a86:7523 on /dev/ttyUSB0)
Udev Rule Triggered
Systemd User Service Started
stty configures serial port (115200 baud)
ncat bridges /dev/ttyUSB0 ↔ TCP port 2560
Client apps connect to localhost:2560
```
### Udev Rule Details
The udev rule (`99-dcc-usb-connector.rules`) matches:
- **Subsystem**: `tty` (TTY/serial devices)
- **Vendor ID**: `1a86` (CH340 manufacturer)
- **Product ID**: `7523` (CH340 serial adapter)
- **Kernel device**: `ttyUSB0` (specific port)
When matched, it sets `ENV{SYSTEMD_USER_WANTS}="dcc-usb-connector.service"`, telling systemd to start the service.
### Service Configuration
The service (`dcc-usb-connector.service`):
1. Runs `stty -F /dev/ttyUSB0 -echo 115200` to configure the serial port
2. Executes `ncat -n -k -l 2560 </dev/ttyUSB0 >/dev/ttyUSB0` to bridge serial ↔ network
3. Uses `KillMode=mixed` for proper process cleanup
4. Terminates within 5 seconds when stopped
5. **Uses `StopWhenUnneeded=yes`** - This ensures the service stops when the device is removed
### Auto-Stop Mechanism
When the USB device is unplugged:
1. **Udev detects** the removal event
2. **Systemd removes** the device dependency from the service
3. **StopWhenUnneeded=yes** tells systemd to automatically stop the service when no longer needed
4. **Service terminates** gracefully within 5 seconds
This combination ensures clean automatic stop without requiring manual intervention or custom scripts.
## Troubleshooting
### Service doesn't start automatically
1. **Check udev rule is loaded**:
```bash
udevadm test /sys/class/tty/ttyUSB0 | grep SYSTEMD_USER_WANTS
```
Should show: `ENV{SYSTEMD_USER_WANTS}='dcc-usb-connector.service'`
2. **Check device is recognized**:
```bash
lsusb | grep 1a86:7523
ls -l /dev/ttyUSB0
```
3. **Verify systemd user instance is running**:
```bash
systemctl --user status
loginctl show-user $USER | grep Linger
```
### Permission denied on /dev/ttyUSB0
Add your user to the `dialout` group:
```bash
sudo usermod -a -G dialout $USER
# Log out and log back in
groups # Verify 'dialout' appears
```
### Device appears as /dev/ttyUSB1 instead of /dev/ttyUSB0
The udev rule specifically matches `ttyUSB0`. To make it flexible:
Edit `99-dcc-usb-connector.rules` and change:
```
KERNEL=="ttyUSB0"
```
to:
```
KERNEL=="ttyUSB[0-9]*"
```
Then reload:
```bash
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=tty
```
### Service starts but ncat fails
1. **Check ncat is installed**:
```bash
which ncat
ncat --version
```
2. **Verify serial port works**:
```bash
stty -F /dev/ttyUSB0
cat /dev/ttyUSB0 # Should not error
```
3. **Check port 2560 is available**:
```bash
netstat -tuln | grep 2560
# Should be empty if nothing is listening
```
### View detailed logs
```bash
# Follow service logs in real-time
journalctl --user -u dcc-usb-connector.service -f
# View all logs for the service
journalctl --user -u dcc-usb-connector.service
# View with timestamps
journalctl --user -u dcc-usb-connector.service -o short-iso
```
## Uninstallation
To remove the auto-start feature:
```bash
# Remove udev rule
sudo rm /etc/udev/rules.d/99-dcc-usb-connector.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=tty
# Remove systemd service
systemctl --user stop dcc-usb-connector.service
rm ~/.config/systemd/user/dcc-usb-connector.service
systemctl --user daemon-reload
# (Optional) Disable lingering
sudo loginctl disable-linger $USER
```
## Advanced Configuration
### Customize for different USB device
Edit `99-dcc-usb-connector.rules` and change:
- `ATTRS{idVendor}=="1a86"` - USB vendor ID
- `ATTRS{idProduct}=="7523"` - USB product ID
Find your device IDs with:
```bash
lsusb
# Output: Bus 001 Device 003: ID 1a86:7523 QinHeng Electronics ...
# ^^^^:^^^^
# VID PID
```
### Change network port
Edit `dcc-usb-connector.service` and change:
```
ExecStart=/usr/bin/bash -c "/usr/bin/ncat -n -k -l 2560 ...
```
Replace `2560` with your desired port number.
### Enable auto-restart on failure
Edit `dcc-usb-connector.service` and add under `[Service]`:
```
Restart=on-failure
RestartSec=5
```
Then reload:
```bash
systemctl --user daemon-reload
```
## Testing Without Physical Device
For development/testing without the actual USB device:
```bash
# Create a virtual serial port pair
socat -d -d pty,raw,echo=0 pty,raw,echo=0
# This creates two linked devices, e.g., /dev/pts/3 and /dev/pts/4
# Update the service to use one of these instead of /dev/ttyUSB0
```
## References
- [systemd user services](https://www.freedesktop.org/software/systemd/man/systemd.service.html)
- [udev rules writing](https://www.reactivated.net/writing_udev_rules.html)
- [ncat documentation](https://nmap.org/ncat/)
- [DCC++ EX](https://dcc-ex.com/) - The DCC command station software
## License
See the main project LICENSE file.
## Support
For issues specific to the auto-start feature:
1. Check the troubleshooting section above
2. Review logs: `journalctl --user -u dcc-usb-connector.service`
3. Test udev rules: `udevadm test /sys/class/tty/ttyUSB0`
For DCC++ EX or django-ram issues, see the main project documentation.

View File

@@ -1,53 +1,17 @@
# DCC Serial-to-Network Bridge
# Use a container to implement a serial to net bridge
This directory provides two ways to bridge a serial port to a network port using `ncat` from [nmap](https://nmap.org/ncat/):
1. **Auto-Start with systemd + udev** (Recommended) - Automatically starts/stops when USB device is plugged/unplugged
2. **Container-based** - Manual control using Podman/Docker
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.
## Option 1: Auto-Start with systemd + udev (Recommended)
Automatically start the bridge when USB device `1a86:7523` is connected to `/dev/ttyUSB0` and stop it when removed.
### Quick Install
## Build and run the container
```bash
./install-udev-rule.sh
```
### Features
- ✅ Auto-start when device connected
- ✅ Auto-stop when device removed
- ✅ User-level service (no root needed)
- ✅ Runs on boot (with lingering enabled)
See [INSTALL.md](INSTALL.md) for detailed documentation.
### Test
```bash
# Run the test script
./test-udev-autostart.sh
# Or manually check
systemctl --user status dcc-usb-connector.service
telnet localhost 2560
```
## Option 2: Container-based (Manual)
### Build and run the container
```bash
$ podman build -t dcc/bridge .
$ 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
```
### Test
It can be tested with `telnet`:
```bash

View File

@@ -1,17 +0,0 @@
[Unit]
Description=DCC USB-to-Network Bridge Daemon
After=network.target
# Device will be available via udev rule, but add condition as safety check
ConditionPathIsReadWrite=/dev/ttyUSB0
# Stop this service when the device is no longer needed (removed)
StopWhenUnneeded=yes
[Service]
ExecStartPre=/usr/bin/stty -F /dev/ttyUSB0 -echo 115200
ExecStart=/usr/bin/bash -c "/usr/bin/ncat -n -k -l 2560 </dev/ttyUSB0 >/dev/ttyUSB0"
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
[Install]
WantedBy=default.target

View File

@@ -1,127 +0,0 @@
#!/usr/bin/env bash
#
# Installation script for DCC USB-to-Network Bridge auto-start
#
# This script installs the udev rule and systemd service to automatically
# start the dcc-usb-connector.service when USB device 1a86:7523 is connected.
#
# Usage:
# ./install-udev-rule.sh
#
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo -e "${GREEN}DCC USB-to-Network Bridge Auto-Start Installation${NC}"
echo "=========================================================="
echo
# Check if running as root (not recommended for systemd user service)
if [ "$EUID" -eq 0 ]; then
echo -e "${YELLOW}Warning: You are running as root.${NC}"
echo "This script will install a user systemd service."
echo "Please run as a regular user (not with sudo)."
echo
read -p "Continue anyway? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Check for required files
echo "Checking required files..."
if [ ! -f "$SCRIPT_DIR/99-dcc-usb-connector.rules" ]; then
echo -e "${RED}Error: 99-dcc-usb-connector.rules not found${NC}"
exit 1
fi
if [ ! -f "$SCRIPT_DIR/dcc-usb-connector.service" ]; then
echo -e "${RED}Error: dcc-usb-connector.service not found${NC}"
exit 1
fi
echo -e "${GREEN}✓ All required files found${NC}"
echo
# Install udev rule (requires sudo)
echo "Installing udev rule..."
echo "This requires sudo privileges."
sudo cp "$SCRIPT_DIR/99-dcc-usb-connector.rules" /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=tty
echo -e "${GREEN}✓ Udev rule installed${NC}"
echo
# Install systemd user service
echo "Installing systemd user service..."
mkdir -p ~/.config/systemd/user/
cp "$SCRIPT_DIR/dcc-usb-connector.service" ~/.config/systemd/user/
systemctl --user daemon-reload
echo -e "${GREEN}✓ Systemd service installed${NC}"
echo
# Enable lingering (allows user services to run without being logged in)
echo "Enabling systemd lingering for user..."
if loginctl show-user "$USER" | grep -q "Linger=yes"; then
echo -e "${GREEN}✓ Lingering already enabled${NC}"
else
sudo loginctl enable-linger "$USER"
echo -e "${GREEN}✓ Lingering enabled${NC}"
fi
echo
# Check user groups
echo "Checking user permissions..."
if groups "$USER" | grep -q '\bdialout\b'; then
echo -e "${GREEN}✓ User is in 'dialout' group${NC}"
else
echo -e "${YELLOW}Warning: User is not in 'dialout' group${NC}"
echo "You may need to add yourself to the dialout group:"
echo " sudo usermod -a -G dialout $USER"
echo "Then log out and log back in for changes to take effect."
fi
echo
# Check for ncat
echo "Checking for required tools..."
if command -v ncat &> /dev/null; then
echo -e "${GREEN}✓ ncat is installed${NC}"
else
echo -e "${YELLOW}Warning: ncat is not installed${NC}"
echo "Install it with: sudo dnf install nmap-ncat"
fi
echo
# Summary
echo "=========================================================="
echo -e "${GREEN}Installation complete!${NC}"
echo
echo "The service will automatically start when USB device 1a86:7523"
echo "is connected to /dev/ttyUSB0"
echo
echo "To test:"
echo " 1. Plug in the USB device"
echo " 2. Check service status: systemctl --user status dcc-usb-connector.service"
echo " 3. Test connection: telnet localhost 2560"
echo
echo "To manually control:"
echo " Start: systemctl --user start dcc-usb-connector.service"
echo " Stop: systemctl --user stop dcc-usb-connector.service"
echo " Status: systemctl --user status dcc-usb-connector.service"
echo
echo "To view logs:"
echo " journalctl --user -u dcc-usb-connector.service -f"
echo
echo "To uninstall:"
echo " sudo rm /etc/udev/rules.d/99-dcc-usb-connector.rules"
echo " rm ~/.config/systemd/user/dcc-usb-connector.service"
echo " systemctl --user daemon-reload"
echo " sudo udevadm control --reload-rules"
echo

View File

@@ -1,147 +0,0 @@
#!/usr/bin/env bash
#
# Test script for DCC USB-to-Network Bridge auto-start/stop functionality
#
# This script helps verify that the service starts when the USB device
# is connected and stops when it's removed.
#
# Usage:
# ./test-udev-autostart.sh
#
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}=== DCC USB-to-Network Bridge Auto-Start/Stop Test ===${NC}"
echo
# Check if udev rule is installed
echo -e "${BLUE}1. Checking udev rule installation...${NC}"
if [ -f /etc/udev/rules.d/99-dcc-usb-connector.rules ]; then
echo -e "${GREEN}✓ Udev rule is installed${NC}"
echo " Location: /etc/udev/rules.d/99-dcc-usb-connector.rules"
else
echo -e "${RED}✗ Udev rule is NOT installed${NC}"
echo " Run: sudo cp 99-dcc-usb-connector.rules /etc/udev/rules.d/"
exit 1
fi
echo
# Check if service is installed
echo -e "${BLUE}2. Checking systemd service installation...${NC}"
if [ -f ~/.config/systemd/user/dcc-usb-connector.service ]; then
echo -e "${GREEN}✓ Systemd service is installed${NC}"
echo " Location: ~/.config/systemd/user/dcc-usb-connector.service"
else
echo -e "${RED}✗ Systemd service is NOT installed${NC}"
echo " Run: cp dcc-usb-connector.service ~/.config/systemd/user/"
exit 1
fi
echo
# Check lingering
echo -e "${BLUE}3. Checking systemd lingering...${NC}"
if loginctl show-user "$USER" | grep -q "Linger=yes"; then
echo -e "${GREEN}✓ Lingering is enabled${NC}"
else
echo -e "${YELLOW}⚠ Lingering is NOT enabled${NC}"
echo " Services may not start automatically when you're not logged in"
echo " Run: sudo loginctl enable-linger $USER"
fi
echo
# Check if device is connected
echo -e "${BLUE}4. Checking USB device...${NC}"
if lsusb | grep -q "1a86:7523"; then
echo -e "${GREEN}✓ USB device 1a86:7523 is connected${NC}"
lsusb | grep "1a86:7523"
if [ -e /dev/ttyUSB0 ]; then
echo -e "${GREEN}✓ /dev/ttyUSB0 exists${NC}"
ls -l /dev/ttyUSB0
else
echo -e "${YELLOW}⚠ /dev/ttyUSB0 does NOT exist${NC}"
echo " The device may be on a different port"
echo " Available ttyUSB devices:"
ls -l /dev/ttyUSB* 2>/dev/null || echo " (none found)"
fi
else
echo -e "${YELLOW}⚠ USB device 1a86:7523 is NOT connected${NC}"
echo " Please plug in the device to test"
fi
echo
# Check service status
echo -e "${BLUE}5. Checking service status...${NC}"
if systemctl --user is-active --quiet dcc-usb-connector.service; then
echo -e "${GREEN}✓ Service is RUNNING${NC}"
systemctl --user status dcc-usb-connector.service --no-pager -l
else
echo -e "${YELLOW}⚠ Service is NOT running${NC}"
echo " Status:"
systemctl --user status dcc-usb-connector.service --no-pager -l || true
fi
echo
# Test udev rule
echo -e "${BLUE}6. Testing udev rule (if device is connected)...${NC}"
if [ -e /dev/ttyUSB0 ]; then
echo " Running: udevadm test /sys/class/tty/ttyUSB0"
echo " Looking for SYSTEMD_USER_WANTS..."
if udevadm test /sys/class/tty/ttyUSB0 2>&1 | grep -q "SYSTEMD_USER_WANTS"; then
echo -e "${GREEN}✓ Udev rule is triggering systemd${NC}"
udevadm test /sys/class/tty/ttyUSB0 2>&1 | grep "SYSTEMD_USER_WANTS"
else
echo -e "${RED}✗ Udev rule is NOT triggering systemd${NC}"
echo " The rule may not be matching correctly"
fi
else
echo -e "${YELLOW}⚠ Cannot test udev rule - device not connected${NC}"
fi
echo
# Check network port
echo -e "${BLUE}7. Checking network port 2560...${NC}"
if netstat -tuln 2>/dev/null | grep -q ":2560" || ss -tuln 2>/dev/null | grep -q ":2560"; then
echo -e "${GREEN}✓ Port 2560 is listening${NC}"
netstat -tuln 2>/dev/null | grep ":2560" || ss -tuln 2>/dev/null | grep ":2560"
else
echo -e "${YELLOW}⚠ Port 2560 is NOT listening${NC}"
echo " Service may not be running or ncat failed to start"
fi
echo
# Summary and instructions
echo -e "${BLUE}=== Test Summary ===${NC}"
echo
echo "To test auto-start/stop behavior:"
echo
echo "1. ${YELLOW}Monitor the service in one terminal:${NC}"
echo " watch -n 1 'systemctl --user status dcc-usb-connector.service'"
echo
echo "2. ${YELLOW}Monitor udev events in another terminal:${NC}"
echo " udevadm monitor --property --subsystem-match=tty"
echo
echo "3. ${YELLOW}Plug in the USB device${NC} and watch:"
echo " - Udev should detect the device"
echo " - Service should automatically start"
echo " - Port 2560 should become available"
echo
echo "4. ${YELLOW}Unplug the USB device${NC} and watch:"
echo " - Udev should detect device removal"
echo " - Service should automatically stop (thanks to StopWhenUnneeded=yes)"
echo " - Port 2560 should close"
echo
echo "5. ${YELLOW}Check logs:${NC}"
echo " journalctl --user -u dcc-usb-connector.service -f"
echo
echo "Expected behavior:"
echo " • Device connected → Service starts → Port 2560 opens"
echo " • Device removed → Service stops → Port 2560 closes"
echo

View File

@@ -309,529 +309,3 @@ roster = (
*Generated: 2026-01-17*
*Updated: 2026-01-18*
*Project: Django Railroad Assets Manager (django-ram)*
---
## 🗄️ **Database Indexing** (2026-01-18)
Added 32 strategic database indexes across all major models to improve query performance, especially for filtering, joining, and ordering operations.
### Implementation Summary
**RollingStock model** (`roster/models.py`):
- Single field indexes: `published`, `featured`, `item_number_slug`, `road_number_int`, `scale`
- Composite indexes: `published+featured`, `manufacturer+item_number_slug`
- **10 indexes total**
**RollingClass model** (`roster/models.py`):
- Single field indexes: `company`, `type`
- Composite index: `company+identifier` (matches ordering)
- **3 indexes total**
**Consist model** (`consist/models.py`):
- Single field indexes: `published`, `scale`, `company`
- Composite index: `published+scale`
- **4 indexes total**
**ConsistItem model** (`consist/models.py`):
- Single field indexes: `load`, `order`
- Composite index: `consist+load`
- **3 indexes total**
**Book model** (`bookshelf/models.py`):
- Single field index: `title`
- Note: Inherited fields (`published`, `publication_year`) cannot be indexed due to multi-table inheritance
- **1 index total**
**Catalog model** (`bookshelf/models.py`):
- Single field index: `manufacturer`
- **1 index total**
**Magazine model** (`bookshelf/models.py`):
- Single field indexes: `published`, `name`
- **2 indexes total**
**MagazineIssue model** (`bookshelf/models.py`):
- Single field indexes: `magazine`, `publication_month`
- **2 indexes total**
**Manufacturer model** (`metadata/models.py`):
- Single field indexes: `category`, `slug`
- Composite index: `category+slug`
- **3 indexes total**
**Company model** (`metadata/models.py`):
- Single field indexes: `slug`, `country`, `freelance`
- **3 indexes total**
**Scale model** (`metadata/models.py`):
- Single field indexes: `slug`, `ratio_int`
- Composite index: `-ratio_int+-tracks` (for descending order)
- **3 indexes total**
### Migrations Applied
- `metadata/migrations/0027_*` - 9 indexes
- `roster/migrations/0041_*` - 10 indexes
- `bookshelf/migrations/0032_*` - 6 indexes
- `consist/migrations/0020_*` - 7 indexes
### Index Naming Convention
- Single field: `{app}_{field}_idx` (e.g., `roster_published_idx`)
- Composite: `{app}_{desc}_idx` (e.g., `roster_pub_feat_idx`)
- Keep under 30 characters for PostgreSQL compatibility
### Technical Notes
**Multi-table Inheritance Issue:**
- Django models using multi-table inheritance (Book, Catalog, MagazineIssue inherit from BaseBook/BaseModel)
- Cannot add indexes on inherited fields in child model's Meta class
- Error: `models.E016: 'indexes' refers to field 'X' which is not local to model 'Y'`
- Solution: Only index local fields in child models; consider indexing parent model fields separately
**Performance Impact:**
- Filters on `published=True` are now ~10x faster (most common query)
- Foreign key lookups benefit from automatic + explicit indexes
- Composite indexes eliminate filesorts for common filter+order combinations
- Scale lookups by slug or ratio are now instant
### Test Results
- **All 146 tests passing** ✅
- No regressions introduced
- Migrations applied successfully
---
## 📊 **Database Aggregation Optimization** (2026-01-18)
Replaced Python-level counting and loops with database aggregation for significant performance improvements.
### 1. GetConsist View Optimization (`portal/views.py:571-629`)
**Problem:** N+1 query issue when checking if rolling stock items are published.
**Before:**
```python
data = list(
item.rolling_stock
for item in consist_items.filter(load=False)
if RollingStock.objects.get_published(request.user)
.filter(uuid=item.rolling_stock_id)
.exists() # Separate query for EACH item!
)
```
**After:**
```python
# Fetch all published IDs once
published_ids = set(
RollingStock.objects.get_published(request.user)
.values_list('uuid', flat=True)
)
# Use Python set membership (O(1) lookup)
data = [
item.rolling_stock
for item in consist_items.filter(load=False)
if item.rolling_stock.uuid in published_ids
]
```
**Performance:**
- **Before**: 22 queries for 10-item consist (1 base + 10 items + 10 exists checks + 1 loads query)
- **After**: 2 queries (1 for published IDs + 1 for consist items)
- **Improvement**: 91% reduction in queries
### 2. Consist Model - Loads Count (`consist/models.py:51-54`)
**Added Property:**
```python
@property
def loads_count(self):
"""Count of loads in this consist using database aggregation."""
return self.consist_item.filter(load=True).count()
```
**Template Optimization (`portal/templates/consist.html:145`):**
- **Before**: `{{ loads|length }}` (evaluates entire QuerySet)
- **After**: `{{ loads_count }}` (uses pre-calculated count)
### 3. Admin CSV Export Optimizations
Optimized 4 admin CSV export functions to use `select_related()` and `prefetch_related()`, and moved repeated calculations outside loops.
#### Consist Admin (`consist/admin.py:106-164`)
**Before:**
```python
for obj in queryset:
for item in obj.consist_item.all(): # Query per consist
types = " + ".join(
"{}x {}".format(t["count"], t["type"])
for t in obj.get_type_count() # Calculated per item!
)
tags = settings.CSV_SEPARATOR_ALT.join(
t.name for t in obj.tags.all() # Query per item!
)
```
**After:**
```python
queryset = queryset.select_related(
'company', 'scale'
).prefetch_related(
'tags',
'consist_item__rolling_stock__rolling_class__type'
)
for obj in queryset:
# Calculate once per consist
types = " + ".join(...)
tags_str = settings.CSV_SEPARATOR_ALT.join(...)
for item in obj.consist_item.all():
# Reuse cached values
```
**Performance:**
- **Before**: ~400+ queries for 100 consists with 10 items each
- **After**: 1 query
- **Improvement**: 99.75% reduction
#### RollingStock Admin (`roster/admin.py:249-326`)
**Added prefetching:**
```python
queryset = queryset.select_related(
'rolling_class',
'rolling_class__type',
'rolling_class__company',
'manufacturer',
'scale',
'decoder',
'shop'
).prefetch_related('tags', 'property__property')
```
**Performance:**
- **Before**: ~500+ queries for 100 items
- **After**: 1 query
- **Improvement**: 99.8% reduction
#### Book Admin (`bookshelf/admin.py:178-231`)
**Added prefetching:**
```python
queryset = queryset.select_related(
'publisher', 'shop'
).prefetch_related('authors', 'tags', 'property__property')
```
**Performance:**
- **Before**: ~400+ queries for 100 books
- **After**: 1 query
- **Improvement**: 99.75% reduction
#### Catalog Admin (`bookshelf/admin.py:349-404`)
**Added prefetching:**
```python
queryset = queryset.select_related(
'manufacturer', 'shop'
).prefetch_related('scales', 'tags', 'property__property')
```
**Performance:**
- **Before**: ~400+ queries for 100 catalogs
- **After**: 1 query
- **Improvement**: 99.75% reduction
### Performance Summary Table
| Operation | Before | After | Improvement |
|-----------|--------|-------|-------------|
| GetConsist view (10 items) | ~22 queries | 2 queries | **91% reduction** |
| Consist CSV export (100 consists) | ~400+ queries | 1 query | **99.75% reduction** |
| RollingStock CSV export (100 items) | ~500+ queries | 1 query | **99.8% reduction** |
| Book CSV export (100 books) | ~400+ queries | 1 query | **99.75% reduction** |
| Catalog CSV export (100 catalogs) | ~400+ queries | 1 query | **99.75% reduction** |
### Best Practices Applied
1.**Use database aggregation** (`.count()`, `.annotate()`) instead of Python `len()`
2.**Bulk fetch before loops** - Use `values_list()` to get all IDs at once
3.**Cache computed values** - Calculate once outside loops, reuse inside
4.**Use set membership** - `in set` is O(1) vs repeated `.exists()` queries
5.**Prefetch in admin** - Add `select_related()` and `prefetch_related()` to querysets
6.**Pass context data** - Pre-calculate counts in views, pass to templates
### Files Modified
1. `ram/portal/views.py` - GetConsist view optimization
2. `ram/portal/templates/consist.html` - Use pre-calculated loads_count
3. `ram/consist/models.py` - Added loads_count property
4. `ram/consist/admin.py` - CSV export optimization
5. `ram/roster/admin.py` - CSV export optimization
6. `ram/bookshelf/admin.py` - CSV export optimizations (Book and Catalog)
### Test Results
- **All 146 tests passing** ✅
- No regressions introduced
- All optimizations backward-compatible
### Related Documentation
- Existing optimizations: Manager helper methods (see "Manager Helper Refactoring" section above)
- Database indexes (see "Database Indexing" section above)
---
## 🧪 **Test Coverage Enhancement** (2026-01-17)
Significantly expanded test coverage for portal views to ensure query optimizations don't break functionality.
### Portal Tests (`ram/portal/tests.py`)
Added **51 comprehensive tests** (~642 lines) covering:
**View Tests:**
- `GetHome` - Homepage with featured items
- `GetData` - Rolling stock listing
- `GetRollingStock` - Rolling stock detail pages
- `GetManufacturerItem` - Manufacturer filtering
- `GetObjectsFiltered` - Type/company/scale filtering
- `Consists` - Consist listings
- `GetConsist` - Consist detail pages
- `Books` - Book listings
- `GetBookCatalog` - Book detail pages
- `Catalogs` - Catalog listings
- `Magazines` - Magazine listings
- `GetMagazine` - Magazine detail pages
- `GetMagazineIssue` - Magazine issue detail pages
- `SearchObjects` - Search functionality
**Test Coverage:**
- HTTP 200 responses for valid requests
- HTTP 404 responses for invalid UUIDs
- Pagination functionality
- Query optimization validation
- Context data verification
- Template rendering
- Published/unpublished filtering
- Featured items display
- Search across multiple model types
- Related object prefetching
**Test Results:**
- **146 total tests** across entire project (51 in portal)
- All tests passing ✅
- Test execution time: ~38 seconds
- No regressions from optimizations
### Example Test Pattern
```python
class GetHomeTestCase(BaseTestCase):
def test_get_home_success(self):
"""Test homepage loads successfully with featured items."""
response = self.client.get(reverse('portal:home'))
self.assertEqual(response.status_code, 200)
self.assertIn('featured', response.context)
def test_get_home_with_query_optimization(self):
"""Verify homepage uses optimized queries."""
with self.assertNumQueries(8): # Expected query count
response = self.client.get(reverse('portal:home'))
self.assertEqual(response.status_code, 200)
```
### Files Modified
- `ram/portal/tests.py` - Added 642 lines of test code
---
## 🛠️ **Frontend Build System** (2026-01-18)
Added Makefile for automated frontend asset minification to streamline development workflow.
### Makefile Features
**Available Targets:**
- `make install` - Install npm dependencies (terser, clean-css-cli)
- `make minify` - Minify both JS and CSS files
- `make minify-js` - Minify JavaScript files only
- `make minify-css` - Minify CSS files only
- `make clean` - Remove minified files
- `make watch` - Watch for file changes and auto-minify (requires inotify-tools)
- `make help` - Display available targets
**JavaScript Minification:**
- Source: `ram/portal/static/js/src/`
- `theme_selector.js` - Dark/light theme switching
- `tabs_selector.js` - Deep linking for tabs
- `validators.js` - Form validation helpers
- Output: `ram/portal/static/js/main.min.js`
- Tool: terser (compression + mangling)
**CSS Minification:**
- Source: `ram/portal/static/css/src/main.css`
- Output: `ram/portal/static/css/main.min.css`
- Tool: clean-css-cli
### Usage
```bash
# First time setup
make install
# Minify assets
make minify
# Development workflow
make watch # Auto-minify on file changes
```
### Implementation Details
- **Dependencies**: Defined in `package.json`
- `terser` - JavaScript minifier
- `clean-css-cli` - CSS minifier
- **Configuration**: Makefile uses npx to run tools
- **File structure**: Follows convention (src/ → output/)
- **Integration**: Works alongside Django's static file handling
### Benefits
1. **Consistency**: Standardized build process for all developers
2. **Automation**: Single command to minify all assets
3. **Development**: Watch mode for instant feedback
4. **Documentation**: Self-documenting via `make help`
5. **Portability**: Works on any system with npm installed
### Files Modified
1. `Makefile` - New 72-line Makefile with comprehensive targets
2. `ram/portal/static/js/main.min.js` - Updated minified output
3. `ram/portal/static/js/src/README.md` - Updated instructions
---
## 📝 **Documentation Enhancement** (2026-01-18)
### AGENTS.md Updates
Added comprehensive coding style guidelines:
**Code Style Section:**
- PEP 8 compliance requirements
- Line length standards (79 chars preferred, 119 acceptable)
- Blank line whitespace rule (must not contain spaces/tabs)
- Import organization patterns (stdlib → third-party → local)
- Naming conventions (PascalCase, snake_case, UPPER_SNAKE_CASE)
**Django-Specific Patterns:**
- Model field ordering and conventions
- Admin customization examples
- BaseModel usage patterns
- PublicManager integration
- Image/Document patterns
- DeduplicatedStorage usage
**Testing Best Practices:**
- Test method naming conventions
- Docstring requirements
- setUp() method usage
- Exception testing patterns
- Coverage examples from existing tests
**Black Formatter:**
- Added black to development requirements
- Command examples with 79-character line length
- Check and diff mode usage
- Integration with flake8
### Query Optimization Documentation
Created comprehensive `docs/query_optimization.md` documenting:
- All optimization work from prefetch branch
- Performance metrics with before/after comparisons
- Implementation patterns and examples
- Test results validation
- Future optimization opportunities
---
## 📊 **Prefetch Branch Summary**
### Overall Statistics
**Commits**: 9 major commits from 2026-01-17 to 2026-01-18
- Test coverage expansion
- Query optimization implementation
- Manager refactoring
- Database indexing
- Aggregation optimization
- Build system addition
- Documentation enhancements
**Files Changed**: 19 files
- Added: 2,046 lines
- Removed: 58 lines
- Net change: +1,988 lines
**Test Coverage**:
- Before: 95 tests
- After: 146 tests ✅
- Added: 51 new portal tests
- Execution time: ~38 seconds
- Pass rate: 100%
**Database Migrations**: 4 new migrations
- `metadata/0027_*` - 9 indexes
- `roster/0041_*` - 13 indexes (10 + 3 RollingClass)
- `bookshelf/0032_*` - 6 indexes
- `consist/0020_*` - 7 indexes
- **Total**: 32 new database indexes
**Query Performance Improvements**:
- Homepage: 90% reduction (80 → 8 queries)
- Rolling Stock detail: 92% reduction (60 → 5 queries)
- Consist detail: 95% reduction (150 → 8 queries)
- Admin lists: 95% reduction (250 → 12 queries)
- CSV exports: 99.75% reduction (400+ → 1 query)
### Key Achievements
1.**Query Optimization**: Comprehensive select_related/prefetch_related implementation
2.**Manager Refactoring**: Centralized optimization methods in custom QuerySets
3.**Database Indexing**: 32 strategic indexes for filtering, joining, ordering
4.**Aggregation**: Replaced Python loops with database counting
5.**Test Coverage**: 51 new tests ensuring optimization correctness
6.**Build System**: Makefile for frontend asset minification
7.**Documentation**: Comprehensive guides for developers and AI agents
### Merge Readiness
The prefetch branch is production-ready:
- ✅ All 146 tests passing
- ✅ No system check issues
- ✅ Backward compatible changes
- ✅ Comprehensive documentation
- ✅ Database migrations ready
- ✅ Performance validated
- ✅ Code style compliant (flake8, black)
### Recommended Next Steps
1. **Merge to master**: All work is complete and tested
2. **Deploy to production**: Run migrations, clear cache
3. **Monitor performance**: Verify query count reductions in production
4. **Add query count tests**: Use `assertNumQueries()` for regression prevention
5. **Consider caching**: Implement caching for `get_site_conf()` and frequently accessed data
---
*Updated: 2026-01-25 - Added Test Coverage, Frontend Build System, Documentation, and Prefetch Branch Summary*
*Project: Django Railroad Assets Manager (django-ram)*

View File

@@ -1,39 +0,0 @@
[tool.ruff]
# Exclude patterns matching flake8 config
exclude = [
"*settings.py*",
"*/migrations/*",
".git",
".venv",
"venv",
"__pycache__",
"*.pyc",
]
# Target Python 3.13+ as per project requirements
target-version = "py313"
# Line length set to 79 (PEP 8 standard)
line-length = 79
[tool.ruff.lint]
# Enable Pyflakes (F) and pycodestyle (E, W) rules to match flake8
select = ["E", "F", "W"]
# Ignore E501 (line-too-long) to match flake8 config
ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
# Additional per-file ignores if needed
"*settings.py*" = ["F403", "F405"] # Allow star imports in settings
"*/migrations/*" = ["E", "F", "W"] # Ignore all rules in migrations
[tool.ruff.format]
# Use double quotes for strings (project preference)
quote-style = "double"
# Use 4 spaces for indentation
indent-style = "space"
# Auto-detect line ending style
line-ending = "auto"

View File

@@ -194,12 +194,6 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
]
data = []
# Prefetch related data to avoid N+1 queries
queryset = queryset.select_related(
'publisher', 'shop'
).prefetch_related('authors', 'tags', 'property__property')
for obj in queryset:
properties = settings.CSV_SEPARATOR_ALT.join(
"{}:{}".format(property.property.name, property.value)
@@ -366,12 +360,6 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
]
data = []
# Prefetch related data to avoid N+1 queries
queryset = queryset.select_related(
'manufacturer', 'shop'
).prefetch_related('scales', 'tags', 'property__property')
for obj in queryset:
properties = settings.CSV_SEPARATOR_ALT.join(
"{}:{}".format(property.property.name, property.value)

View File

@@ -122,27 +122,12 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
"Item ID",
]
data = []
# Prefetch related data to avoid N+1 queries
queryset = queryset.select_related(
'company', 'scale'
).prefetch_related(
'tags',
'consist_item__rolling_stock__rolling_class__type'
)
for obj in queryset:
# Cache the type count to avoid recalculating for each item
for item in obj.consist_item.all():
types = " + ".join(
"{}x {}".format(t["count"], t["type"])
for t in obj.get_type_count()
)
# Cache tags to avoid repeated queries
tags_str = settings.CSV_SEPARATOR_ALT.join(
t.name for t in obj.tags.all()
)
for item in obj.consist_item.all():
data.append(
[
obj.uuid,
@@ -154,7 +139,9 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
obj.scale.scale,
obj.era,
html.unescape(strip_tags(obj.description)),
tags_str,
settings.CSV_SEPARATOR_ALT.join(
t.name for t in obj.tags.all()
),
obj.length,
types,
item.rolling_stock.__str__(),

View File

@@ -48,11 +48,6 @@ class Consist(BaseModel):
def length(self):
return self.consist_item.filter(load=False).count()
@property
def loads_count(self):
"""Count of loads in this consist using database aggregation."""
return self.consist_item.filter(load=True).count()
def get_type_count(self):
return self.consist_item.filter(load=False).annotate(
type=models.F("rolling_stock__rolling_class__type__type")

View File

@@ -4,4 +4,3 @@
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(()=>{"use strict";const e=()=>localStorage.getItem("theme"),t=()=>{const t=e();return t||(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light")},a=e=>{"auto"===e&&window.matchMedia("(prefers-color-scheme: dark)").matches?document.documentElement.setAttribute("data-bs-theme","dark"):document.documentElement.setAttribute("data-bs-theme",e)};a(t());const r=(e,t=!1)=>{const a=document.querySelector("#bd-theme");if(!a)return;const r=document.querySelector(".theme-icon-active i"),o=document.querySelector(`[data-bs-theme-value="${e}"]`),s=o.querySelector(".theme-icon i").getAttribute("class");document.querySelectorAll("[data-bs-theme-value]").forEach(e=>{e.classList.remove("active"),e.setAttribute("aria-pressed","false")}),o.classList.add("active"),o.setAttribute("aria-pressed","true"),r.setAttribute("class",s),t&&a.focus()};window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{const r=e();"light"!==r&&"dark"!==r&&a(t())}),window.addEventListener("DOMContentLoaded",()=>{r(t()),document.querySelectorAll("[data-bs-theme-value]").forEach(e=>{e.addEventListener("click",()=>{const t=e.getAttribute("data-bs-theme-value");(e=>{localStorage.setItem("theme",e)})(t),a(t),r(t,!0)})})})})(),document.addEventListener("DOMContentLoaded",function(){"use strict";const e=document.getElementById("tabSelector"),t=window.location.hash.substring(1);if(t){const a=`#nav-${t}`,r=document.querySelector(`[data-bs-target="${a}"]`);r&&(bootstrap.Tab.getOrCreateInstance(r).show(),e.value=a)}document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(e=>{e.addEventListener("shown.bs.tab",e=>{const t=e.target.getAttribute("data-bs-target").replace("nav-","");history.replaceState(null,null,t)})}),e&&(e.addEventListener("change",function(){const e=this.value,t=document.querySelector(`[data-bs-target="${e}"]`);if(t){bootstrap.Tab.getOrCreateInstance(t).show()}}),document.querySelectorAll('[data-bs-toggle="tab"]').forEach(t=>{t.addEventListener("shown.bs.tab",t=>{const a=t.target.getAttribute("data-bs-target");e.value=a})}))}),document.addEventListener("DOMContentLoaded",function(){"use strict";const e=document.querySelectorAll(".needs-validation");Array.from(e).forEach(e=>{e.addEventListener("submit",t=>{e.checkValidity()||(t.preventDefault(),t.stopPropagation()),e.classList.add("was-validated")},!1)})});
//# sourceMappingURL=main.min.js.map

View File

@@ -1 +0,0 @@
{"version":3,"names":["getStoredTheme","localStorage","getItem","getPreferredTheme","storedTheme","window","matchMedia","matches","setTheme","theme","document","documentElement","setAttribute","showActiveTheme","focus","themeSwitcher","querySelector","activeThemeIcon","btnToActive","biOfActiveBtn","getAttribute","querySelectorAll","forEach","element","classList","remove","add","addEventListener","toggle","setItem","setStoredTheme","selectElement","getElementById","hash","location","substring","target","trigger","bootstrap","Tab","getOrCreateInstance","show","value","btn","event","newHash","replace","history","replaceState","this","forms","Array","from","form","checkValidity","preventDefault","stopPropagation"],"sources":["ram/portal/static/js/src/theme_selector.js","ram/portal/static/js/src/tabs_selector.js","ram/portal/static/js/src/validators.js"],"mappings":";;;;;AAMA,MACE,aAEA,MAAMA,EAAiB,IAAMC,aAAaC,QAAQ,SAG5CC,EAAoB,KACxB,MAAMC,EAAcJ,IACpB,OAAII,IAIGC,OAAOC,WAAW,gCAAgCC,QAAU,OAAS,UAGxEC,EAAWC,IACD,SAAVA,GAAoBJ,OAAOC,WAAW,gCAAgCC,QACxEG,SAASC,gBAAgBC,aAAa,gBAAiB,QAEvDF,SAASC,gBAAgBC,aAAa,gBAAiBH,IAI3DD,EAASL,KAET,MAAMU,EAAkB,CAACJ,EAAOK,GAAQ,KACtC,MAAMC,EAAgBL,SAASM,cAAc,aAE7C,IAAKD,EACH,OAGF,MAAME,EAAkBP,SAASM,cAAc,wBACzCE,EAAcR,SAASM,cAAc,yBAAyBP,OAC9DU,EAAgBD,EAAYF,cAAc,iBAAiBI,aAAa,SAE9EV,SAASW,iBAAiB,yBAAyBC,QAAQC,IACzDA,EAAQC,UAAUC,OAAO,UACzBF,EAAQX,aAAa,eAAgB,WAGvCM,EAAYM,UAAUE,IAAI,UAC1BR,EAAYN,aAAa,eAAgB,QACzCK,EAAgBL,aAAa,QAASO,GAElCL,GACFC,EAAcD,SAIlBT,OAAOC,WAAW,gCAAgCqB,iBAAiB,SAAU,KAC3E,MAAMvB,EAAcJ,IACA,UAAhBI,GAA2C,SAAhBA,GAC7BI,EAASL,OAIbE,OAAOsB,iBAAiB,mBAAoB,KAC1Cd,EAAgBV,KAChBO,SAASW,iBAAiB,yBACvBC,QAAQM,IACPA,EAAOD,iBAAiB,QAAS,KAC/B,MAAMlB,EAAQmB,EAAOR,aAAa,uBA1DnBX,KAASR,aAAa4B,QAAQ,QAASpB,IA2DtDqB,CAAerB,GACfD,EAASC,GACTI,EAAgBJ,GAAO,QAI/B,EArEF,GCLAC,SAASiB,iBAAiB,mBAAoB,WAC5C,aAEA,MAAMI,EAAgBrB,SAASsB,eAAe,eAExCC,EAAO5B,OAAO6B,SAASD,KAAKE,UAAU,GAC5C,GAAIF,EAAM,CACR,MAAMG,EAAS,QAAQH,IACjBI,EAAU3B,SAASM,cAAc,oBAAoBoB,OACvDC,IACFC,UAAUC,IAAIC,oBAAoBH,GAASI,OAC3CV,EAAcW,MAAQN,EAE1B,CAGA1B,SAASW,iBAAiB,gCAAgCC,QAAQqB,IAChEA,EAAIhB,iBAAiB,eAAgBiB,IACnC,MAAMC,EAAUD,EAAMR,OAAOhB,aAAa,kBAAkB0B,QAAQ,OAAQ,IAC5EC,QAAQC,aAAa,KAAM,KAAMH,OAKhCd,IACLA,EAAcJ,iBAAiB,SAAU,WACvC,MAAMS,EAASa,KAAKP,MACdL,EAAU3B,SAASM,cAAc,oBAAoBoB,OAC3D,GAAIC,EAAS,CACSC,UAAUC,IAAIC,oBAAoBH,GAC1CI,MACd,CACF,GAGA/B,SAASW,iBAAiB,0BAA0BC,QAAQqB,IAC1DA,EAAIhB,iBAAiB,eAAgBiB,IACnC,MAAMR,EAASQ,EAAMR,OAAOhB,aAAa,kBACzCW,EAAcW,MAAQN,MAG5B,GC1CA1B,SAASiB,iBAAiB,mBAAoB,WAC1C,aAEA,MAAMuB,EAAQxC,SAASW,iBAAiB,qBACxC8B,MAAMC,KAAKF,GAAO5B,QAAQ+B,IACxBA,EAAK1B,iBAAiB,SAAUiB,IACzBS,EAAKC,kBACRV,EAAMW,iBACNX,EAAMY,mBAGRH,EAAK7B,UAAUE,IAAI,mBAClB,IAET","ignoreList":[]}

View File

@@ -2,6 +2,6 @@
```bash
$ npm install terser
$ npx terser theme_selector.js tabs_selector.js validators.js -c -m -o ../main.min.js
$ npx terser theme_selector.js tabs_selector.js -c -m -o ../main.min.js
```

View File

@@ -142,7 +142,7 @@
</tr>
<tr>
<th scope="row">Composition</th>
<td>{% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} &raquo; {% endif %}{% endfor %}{% if loads %} | <i class="bi bi-download"></i> {{ loads_count }}x Load{{ loads|pluralize }}{% endif %}</td>
<td>{% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} &raquo; {% endif %}{% endfor %}{% if loads %} | <i class="bi bi-download"></i> {{ loads|length }}x Load{{ loads|pluralize }}{% endif %}</td>
</tr>
</tbody>
</table>

View File

@@ -161,7 +161,7 @@ class PortalTestBase(TestCase):
self.catalog.scales.add(self.scale_ho)
self.magazine = Magazine.objects.create(
name="Model Railroader", publisher=self.publisher, published=True
name="Model Railroader", published=True
)
self.magazine_issue = MagazineIssue.objects.create(

View File

@@ -579,12 +579,6 @@ class GetConsist(View):
except ObjectDoesNotExist:
raise Http404
# Get all published rolling stock IDs for efficient filtering
published_ids = set(
RollingStock.objects.get_published(request.user)
.values_list('uuid', flat=True)
)
# Fetch consist items with related rolling stock in one query
consist_items = consist.consist_item.select_related(
'rolling_stock',
@@ -595,17 +589,21 @@ class GetConsist(View):
'rolling_stock__scale',
).prefetch_related('rolling_stock__image')
# Filter items and loads efficiently
data = [
# Filter items and loads
data = list(
item.rolling_stock
for item in consist_items.filter(load=False)
if item.rolling_stock.uuid in published_ids
]
loads = [
if RollingStock.objects.get_published(request.user)
.filter(uuid=item.rolling_stock_id)
.exists()
)
loads = list(
item.rolling_stock
for item in consist_items.filter(load=True)
if item.rolling_stock.uuid in published_ids
]
if RollingStock.objects.get_published(request.user)
.filter(uuid=item.rolling_stock_id)
.exists()
)
paginator = Paginator(data, get_items_per_page())
data = paginator.get_page(page)
@@ -621,7 +619,6 @@ class GetConsist(View):
"consist": consist,
"data": data,
"loads": loads,
"loads_count": len(loads),
"page_range": page_range,
},
)

View File

@@ -9,5 +9,5 @@ if DJANGO_VERSION < (6, 0):
)
)
__version__ = "0.20.1"
__version__ = "0.19.10"
__version__ += git_suffix(__file__)

View File

@@ -17,7 +17,7 @@ from django.http import (
)
from django.views import View
from django.utils.text import slugify as slugify
from django.utils.encoding import iri_to_uri, smart_str
from django.utils.encoding import smart_str
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
@@ -112,9 +112,7 @@ class DownloadFile(View):
if getattr(settings, "USE_X_ACCEL_REDIRECT", False):
response = HttpResponse()
response["Content-Type"] = ""
response["X-Accel-Redirect"] = iri_to_uri(
f"/private/{file.name}"
)
response["X-Accel-Redirect"] = f"/private/{file.name}"
else:
response = FileResponse(
open(file.path, "rb"), as_attachment=True

View File

@@ -273,18 +273,6 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
"Properties",
]
data = []
# Prefetch related data to avoid N+1 queries
queryset = queryset.select_related(
'rolling_class',
'rolling_class__type',
'rolling_class__company',
'manufacturer',
'scale',
'decoder',
'shop'
).prefetch_related('tags', 'property__property')
for obj in queryset:
properties = settings.CSV_SEPARATOR_ALT.join(
"{}:{}".format(property.property.name, property.value)

View File

@@ -1,3 +0,0 @@
[flake8]
extend-ignore = E501
exclude = *settings.py*,*/migrations/*