Files
django-ram/AGENTS.md
Daniele Viganò 4f136b91d0 docs: add blank line whitespace rule to AGENTS.md
Specify that blank lines must not contain any whitespace (spaces or tabs) to maintain code cleanliness and PEP 8 compliance
2026-01-18 23:34:08 +01:00

12 KiB

Django Railroad Assets Manager - Agent Guidelines

This document provides coding guidelines and command references for AI coding agents working on the Django-RAM project.

Project Overview

Django Railroad Assets Manager (django-ram) is a Django 6.0+ application for managing model railroad collections with DCC++ EX integration. The project manages rolling stock, consists, metadata, books/magazines, and provides an optional REST API for DCC control.

Environment Setup

Python Requirements

  • Python 3.11+ (tested on 3.13, 3.14)
  • Django >= 6.0
  • Working directory: ram/ (Django project root)
  • Virtual environment recommended: python3 -m venv venv && source venv/bin/activate

Installation

pip install -r requirements.txt           # Core dependencies
pip install -r requirements-dev.txt       # Development tools
cd ram && python manage.py migrate        # Initialize database
python manage.py createsuperuser          # Create admin user

Frontend Assets

npm install                               # Install clean-css-cli, terser

Project Structure

ram/                      # Django project root
├── ram/                 # Core settings, URLs, base models
├── portal/              # Public-facing frontend (Bootstrap 5)
├── roster/              # Rolling stock management (main app)
├── metadata/            # Manufacturers, companies, scales, decoders
├── bookshelf/           # Books and magazines
├── consist/             # Train consists (multiple locomotives)
├── repository/          # Document repository
├── driver/              # DCC++ EX API gateway (optional, disabled by default)
└── storage/             # Runtime data (SQLite DB, media, cache)

Build/Lint/Test Commands

Running the Development Server

cd ram
python manage.py runserver                # Runs on http://localhost:8000

Database Management

python manage.py makemigrations           # Create new migrations
python manage.py migrate                  # Apply migrations
python manage.py showmigrations           # Show migration status

Testing

# Run all tests (comprehensive test suite with 75+ tests)
python manage.py test

# Run tests for a specific app
python manage.py test roster                  # Rolling stock tests
python manage.py test metadata                # Metadata tests  
python manage.py test bookshelf               # Books/magazines tests
python manage.py test consist                 # Consist tests

# Run a specific test case class
python manage.py test roster.tests.RollingStockTestCase
python manage.py test metadata.tests.ScaleTestCase

# Run a single test method
python manage.py test roster.tests.RollingStockTestCase.test_road_number_int_extraction
python manage.py test bookshelf.tests.TocEntryTestCase.test_toc_entry_page_validation_exceeds_book

# Run with verbosity for detailed output
python manage.py test --verbosity=2

# Keep test database for inspection
python manage.py test --keepdb

# Run tests matching a pattern
python manage.py test --pattern="test_*.py"

Linting and Formatting

# Run flake8 (configured in requirements-dev.txt)
flake8 .                                  # Lint entire project
flake8 roster/                            # Lint specific app
flake8 roster/models.py                   # Lint specific file

# Note: No .flake8 config exists; uses PEP 8 defaults
# Long lines use # noqa: E501 comments in settings.py

# Run black formatter with 79 character line length
black -l 79 .                             # Format entire project
black -l 79 roster/                       # Format specific app
black -l 79 roster/models.py              # Format specific file
black -l 79 --check .                     # Check formatting without changes
black -l 79 --diff .                      # Show formatting changes

Admin Commands

python manage.py createsuperuser          # Create admin user
python manage.py purge_cache              # Custom: purge cache
python manage.py loaddata <fixture>       # Load sample data

Debugging & Profiling

# Use pdbpp for debugging (installed via requirements-dev.txt)
import pdb; pdb.set_trace()               # Set breakpoint in code

# Use pyinstrument for profiling
python manage.py runserver --noreload     # With pyinstrument middleware

Code Style Guidelines

General Python Style

  • PEP 8 compliant - Follow standard Python style guide
  • Line length: 79 characters preferred; 119 acceptable for complex lines
  • 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):

# 1. Standard library imports
import os
import re
from itertools import chain
from functools import reduce

# 2. Related third-party imports
from django.db import models
from django.conf import settings
from django.contrib import admin
from tinymce import models as tinymce

# 3. Local application imports
from ram.models import BaseModel, Image
from ram.utils import DeduplicatedStorage, slugify
from metadata.models import Scale, Manufacturer

Key points:

  • Group imports by category with blank lines between
  • Use from module import specific for commonly used items
  • Avoid import *
  • Use as for aliasing when needed (e.g., tinymce.models as tinymce)

Naming Conventions

  • Classes: PascalCase (e.g., RollingStock, BaseModel)
  • Functions/methods: snake_case (e.g., get_items_per_page(), image_thumbnail())
  • Variables: snake_case (e.g., road_number, item_number_slug)
  • Constants: UPPER_SNAKE_CASE (e.g., BASE_DIR, ALLOWED_HOSTS)
  • Private methods: Prefix with _ (e.g., _internal_method())
  • Model Meta options: Use verbose_name, verbose_name_plural, ordering

Django Model Patterns

class MyModel(BaseModel):  # Inherit from BaseModel for common fields
    # Field order: relationships first, then data fields, then metadata
    foreign_key = models.ForeignKey(OtherModel, on_delete=models.CASCADE)
    name = models.CharField(max_length=128)
    slug = models.SlugField(max_length=128, unique=True)
    created = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ["name"]
        verbose_name = "My Model"
        verbose_name_plural = "My Models"
    
    def __str__(self):
        return self.name
    
    @property
    def computed_field(self):
        """Document properties with docstrings."""
        return self.calculate_something()

Model field conventions:

  • Use null=True, blank=True for optional fields
  • Use help_text for user-facing field descriptions
  • Use limit_choices_to for filtered ForeignKey choices
  • Use related_name for reverse relations
  • Set on_delete=models.CASCADE explicitly
  • Use default=None with null=True for nullable fields

Admin Customization

@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
    list_display = ("name", "created", "custom_method")
    list_filter = ("category", "created")
    search_fields = ("name", "slug")
    autocomplete_fields = ("foreign_key",)
    readonly_fields = ("created", "updated")
    save_as = True  # Enable "Save as new" button
    
    @admin.display(description="Custom Display")
    def custom_method(self, obj):
        return format_html('<strong>{}</strong>', obj.name)

Error Handling

# Use Django's exception classes
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.http import Http404
from django.db.utils import OperationalError, ProgrammingError

# Handle database errors gracefully
try:
    config = get_site_conf()
except (OperationalError, ProgrammingError):
    config = default_config  # Provide fallback

Type Hints

  • Not currently used in this project
  • Follow existing patterns without type hints unless explicitly adding them

Django-Specific Patterns

Using BaseModel

All major models inherit from ram.models.BaseModel:

from ram.models import BaseModel

class MyModel(BaseModel):
    # Automatically includes: uuid, description, notes, creation_time, 
    # updated_time, published, obj_type, obj_label properties
    pass

Using PublicManager

Models use PublicManager for filtering published items:

from ram.managers import PublicManager

objects = PublicManager()  # Only returns items where published=True

Image and Document Patterns

from ram.models import Image, Document, PrivateDocument

class MyImage(Image):
    my_model = models.ForeignKey(MyModel, on_delete=models.CASCADE)
    # Inherits: order, image, image_thumbnail()

class MyDocument(PrivateDocument):
    my_model = models.ForeignKey(MyModel, on_delete=models.CASCADE)
    # Inherits: description, file, private, creation_time, updated_time

Using DeduplicatedStorage

For media files that should be deduplicated:

from ram.utils import DeduplicatedStorage

image = models.ImageField(upload_to="images/", storage=DeduplicatedStorage)

Testing Practices

Test Coverage

The project has comprehensive test coverage:

  • roster/tests.py: RollingStock, RollingClass models (~340 lines, 19+ tests)
  • metadata/tests.py: Scale, Manufacturer, Company, etc. (~378 lines, 29+ tests)
  • bookshelf/tests.py: Book, Magazine, Catalog, TocEntry (~436 lines, 25+ tests)
  • consist/tests.py: Consist, ConsistItem (~315 lines, 15+ tests)
  • ram/tests.py: BaseModel, utility functions (~140 lines, 11+ tests)

Writing Tests

from django.test import TestCase
from django.core.exceptions import ValidationError
from roster.models import RollingStock

class RollingStockTestCase(TestCase):
    def setUp(self):
        """Set up test data."""
        # Create necessary related objects
        self.company = Company.objects.create(name="RGS", country="US")
        self.scale = Scale.objects.create(scale="HO", ratio="1:87", tracks=16.5)
        # ...
    
    def test_road_number_int_extraction(self):
        """Test automatic extraction of integer from road number."""
        stock = RollingStock.objects.create(
            rolling_class=self.rolling_class,
            road_number="RGS-42",
            scale=self.scale,
        )
        self.assertEqual(stock.road_number_int, 42)
    
    def test_validation_error(self):
        """Test that validation errors are raised correctly."""
        with self.assertRaises(ValidationError):
            # Test validation logic
            pass

Testing best practices:

  • Use descriptive test method names with test_ prefix
  • Include docstrings explaining what each test verifies
  • Create necessary test data in setUp() method
  • Test both success and failure cases
  • Use assertRaises() for exception testing
  • Test model properties, methods, and validation logic

Git & Version Control

  • Branch: master (main development branch)
  • CI runs on push and PR to master
  • Follow conventional commit messages
  • No pre-commit hooks configured (consider adding)

Additional Notes

  • Settings override: Use ram/local_settings.py for local configuration
  • Debug mode: DEBUG = True in settings.py (change for production)
  • Database: SQLite by default (in storage/db.sqlite3)
  • Static files: Bootstrap 5.3.8, Bootstrap Icons 1.13.1
  • Rich text: TinyMCE for HTMLField content
  • REST API: Disabled by default (REST_ENABLED = False)
  • Security: CSP middleware enabled, secure cookies in production