mirror of
https://github.com/daniviga/django-ram.git
synced 2026-02-03 17:40:39 +01:00
Compare commits
5 Commits
44a965eb62
...
4f136b91d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
4f136b91d0
|
|||
|
84cec29b5b
|
|||
|
ad40f7fb0c
|
|||
|
4dde4225eb
|
|||
|
6ab9db4ed4
|
@@ -128,6 +128,7 @@ python manage.py runserver --noreload # With pyinstrument middleware
|
|||||||
- **Long lines**: Use `# noqa: E501` comment when necessary (see settings.py)
|
- **Long lines**: Use `# noqa: E501` comment when necessary (see settings.py)
|
||||||
- **Indentation**: 4 spaces (no tabs)
|
- **Indentation**: 4 spaces (no tabs)
|
||||||
- **Encoding**: UTF-8
|
- **Encoding**: UTF-8
|
||||||
|
- **Blank lines**: Must not contain any whitespace (spaces or tabs)
|
||||||
|
|
||||||
### Import Organization
|
### Import Organization
|
||||||
Follow Django's import style (as seen in models.py, views.py, admin.py):
|
Follow Django's import style (as seen in models.py, views.py, admin.py):
|
||||||
|
|||||||
71
Makefile
Normal file
71
Makefile
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# 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 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) -c -m -o $(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"
|
||||||
@@ -309,3 +309,284 @@ roster = (
|
|||||||
*Generated: 2026-01-17*
|
*Generated: 2026-01-17*
|
||||||
*Updated: 2026-01-18*
|
*Updated: 2026-01-18*
|
||||||
*Project: Django Railroad Assets Manager (django-ram)*
|
*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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Updated: 2026-01-18 - Added Database Indexing and Aggregation Optimization sections*
|
||||||
|
*Project: Django Railroad Assets Manager (django-ram)*
|
||||||
|
|||||||
@@ -194,6 +194,12 @@ class BookAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
data = []
|
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:
|
for obj in queryset:
|
||||||
properties = settings.CSV_SEPARATOR_ALT.join(
|
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||||
"{}:{}".format(property.property.name, property.value)
|
"{}:{}".format(property.property.name, property.value)
|
||||||
@@ -360,6 +366,12 @@ class CatalogAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
data = []
|
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:
|
for obj in queryset:
|
||||||
properties = settings.CSV_SEPARATOR_ALT.join(
|
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||||
"{}:{}".format(property.property.name, property.value)
|
"{}:{}".format(property.property.name, property.value)
|
||||||
|
|||||||
@@ -122,12 +122,27 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"Item ID",
|
"Item ID",
|
||||||
]
|
]
|
||||||
data = []
|
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:
|
for obj in queryset:
|
||||||
for item in obj.consist_item.all():
|
# Cache the type count to avoid recalculating for each item
|
||||||
types = " + ".join(
|
types = " + ".join(
|
||||||
"{}x {}".format(t["count"], t["type"])
|
"{}x {}".format(t["count"], t["type"])
|
||||||
for t in obj.get_type_count()
|
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(
|
data.append(
|
||||||
[
|
[
|
||||||
obj.uuid,
|
obj.uuid,
|
||||||
@@ -139,9 +154,7 @@ class ConsistAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
obj.scale.scale,
|
obj.scale.scale,
|
||||||
obj.era,
|
obj.era,
|
||||||
html.unescape(strip_tags(obj.description)),
|
html.unescape(strip_tags(obj.description)),
|
||||||
settings.CSV_SEPARATOR_ALT.join(
|
tags_str,
|
||||||
t.name for t in obj.tags.all()
|
|
||||||
),
|
|
||||||
obj.length,
|
obj.length,
|
||||||
types,
|
types,
|
||||||
item.rolling_stock.__str__(),
|
item.rolling_stock.__str__(),
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ class Consist(BaseModel):
|
|||||||
def length(self):
|
def length(self):
|
||||||
return self.consist_item.filter(load=False).count()
|
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):
|
def get_type_count(self):
|
||||||
return self.consist_item.filter(load=False).annotate(
|
return self.consist_item.filter(load=False).annotate(
|
||||||
type=models.F("rolling_stock__rolling_class__type__type")
|
type=models.F("rolling_stock__rolling_class__type__type")
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm install terser
|
$ npm install terser
|
||||||
$ npx terser theme_selector.js tabs_selector.js -c -m -o ../main.min.js
|
$ npx terser theme_selector.js tabs_selector.js validators.js -c -m -o ../main.min.js
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Composition</th>
|
<th scope="row">Composition</th>
|
||||||
<td>{% for t in consist.get_type_count %}{{ t.count }}x {{ t.type }} {{t.category }}{% if not forloop.last %} » {% endif %}{% endfor %}{% if loads %} | <i class="bi bi-download"></i> {{ loads|length }}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 %} » {% endif %}{% endfor %}{% if loads %} | <i class="bi bi-download"></i> {{ loads_count }}x Load{{ loads|pluralize }}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class PortalTestBase(TestCase):
|
|||||||
self.catalog.scales.add(self.scale_ho)
|
self.catalog.scales.add(self.scale_ho)
|
||||||
|
|
||||||
self.magazine = Magazine.objects.create(
|
self.magazine = Magazine.objects.create(
|
||||||
name="Model Railroader", published=True
|
name="Model Railroader", publisher=self.publisher, published=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self.magazine_issue = MagazineIssue.objects.create(
|
self.magazine_issue = MagazineIssue.objects.create(
|
||||||
|
|||||||
@@ -579,6 +579,12 @@ class GetConsist(View):
|
|||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise Http404
|
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
|
# Fetch consist items with related rolling stock in one query
|
||||||
consist_items = consist.consist_item.select_related(
|
consist_items = consist.consist_item.select_related(
|
||||||
'rolling_stock',
|
'rolling_stock',
|
||||||
@@ -589,21 +595,17 @@ class GetConsist(View):
|
|||||||
'rolling_stock__scale',
|
'rolling_stock__scale',
|
||||||
).prefetch_related('rolling_stock__image')
|
).prefetch_related('rolling_stock__image')
|
||||||
|
|
||||||
# Filter items and loads
|
# Filter items and loads efficiently
|
||||||
data = list(
|
data = [
|
||||||
item.rolling_stock
|
item.rolling_stock
|
||||||
for item in consist_items.filter(load=False)
|
for item in consist_items.filter(load=False)
|
||||||
if RollingStock.objects.get_published(request.user)
|
if item.rolling_stock.uuid in published_ids
|
||||||
.filter(uuid=item.rolling_stock_id)
|
]
|
||||||
.exists()
|
loads = [
|
||||||
)
|
|
||||||
loads = list(
|
|
||||||
item.rolling_stock
|
item.rolling_stock
|
||||||
for item in consist_items.filter(load=True)
|
for item in consist_items.filter(load=True)
|
||||||
if RollingStock.objects.get_published(request.user)
|
if item.rolling_stock.uuid in published_ids
|
||||||
.filter(uuid=item.rolling_stock_id)
|
]
|
||||||
.exists()
|
|
||||||
)
|
|
||||||
|
|
||||||
paginator = Paginator(data, get_items_per_page())
|
paginator = Paginator(data, get_items_per_page())
|
||||||
data = paginator.get_page(page)
|
data = paginator.get_page(page)
|
||||||
@@ -619,6 +621,7 @@ class GetConsist(View):
|
|||||||
"consist": consist,
|
"consist": consist,
|
||||||
"data": data,
|
"data": data,
|
||||||
"loads": loads,
|
"loads": loads,
|
||||||
|
"loads_count": len(loads),
|
||||||
"page_range": page_range,
|
"page_range": page_range,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -273,6 +273,18 @@ class RollingStockAdmin(SortableAdminBase, admin.ModelAdmin):
|
|||||||
"Properties",
|
"Properties",
|
||||||
]
|
]
|
||||||
data = []
|
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:
|
for obj in queryset:
|
||||||
properties = settings.CSV_SEPARATOR_ALT.join(
|
properties = settings.CSV_SEPARATOR_ALT.join(
|
||||||
"{}:{}".format(property.property.name, property.value)
|
"{}:{}".format(property.property.name, property.value)
|
||||||
|
|||||||
Reference in New Issue
Block a user