# AGENTS.md - AI Coding Agent Guidelines This document provides guidelines for AI coding agents working in the labhelper repository. ## Project Overview - **Type**: Django web application (lab inventory management system) - **Python**: 3.13.7 - **Django**: 5.2.9 - **Database**: SQLite (development) - **Virtual Environment**: `.venv/` ## Build/Run Commands ### Development Server ```bash python manage.py runserver # Start dev server on port 8000 python manage.py runserver 0.0.0.0:8000 # Bind to all interfaces ``` ### Database Operations ```bash python manage.py makemigrations # Create migration files python manage.py makemigrations boxes # Create migrations for specific app python manage.py migrate # Apply all migrations python manage.py showmigrations # List migration status ``` ### Testing ```bash # Run all tests python manage.py test # Run tests for a specific app python manage.py test boxes # Run a specific test class python manage.py test boxes.tests.TestClassName # Run a single test method python manage.py test boxes.tests.TestClassName.test_method_name # Run tests with verbosity python manage.py test -v 2 # Run tests with coverage coverage run manage.py test coverage report coverage html # Generate HTML report ``` ### Django Shell ```bash python manage.py shell # Interactive Django shell python manage.py createsuperuser # Create admin user python manage.py collectstatic # Collect static files ``` ### Production ```bash gunicorn labhelper.wsgi:application # Run with Gunicorn ``` ### Custom Management Commands ```bash # Create default users and groups python manage.py create_default_users # Clean up orphaned files from deleted things python manage.py clean_orphaned_files python manage.py clean_orphaned_files --dry-run # Clean up orphaned images and thumbnails python manage.py clean_orphaned_images python manage.py clean_orphaned_images --dry-run ``` ## Code Style Guidelines ### Python Style - Follow PEP 8 conventions - Use 4-space indentation (no tabs) - Maximum line length: 79 characters (PEP 8 standard) - Use single quotes for strings: `'string'` - Use double quotes for docstrings: `"""Docstring."""` ### Import Order Organize imports in this order, with blank lines between groups: ```python # 1. Standard library imports import os import sys from pathlib import Path # 2. Django imports from django.db import models from django.contrib import admin from django.shortcuts import render, redirect from django.http import HttpResponse, JsonResponse # 3. Third-party imports import requests from markdown import markdown # 4. Local application imports from .models import MyModel from .forms import MyForm ``` ### Naming Conventions | Type | Convention | Example | |------|------------|---------| | Modules | lowercase_with_underscores | `user_profile.py` | | Classes | PascalCase | `UserProfile` | | Functions | lowercase_with_underscores | `get_user_data()` | | Constants | UPPERCASE_WITH_UNDERSCORES | `MAX_CONNECTIONS` | | Variables | lowercase_with_underscores | `user_count` | | Django Models | PascalCase (singular) | `Box`, `UserProfile` | | Django Apps | lowercase (short) | `boxes`, `users` | ### Django-Specific Conventions **Models:** ```python from django.db import models class Box(models.Model): """A storage box in the lab.""" name = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name_plural = 'boxes' ordering = ['-created_at'] def __str__(self): return self.name ``` **Views:** ```python from django.shortcuts import render, get_object_or_404 from django.http import Http404 def box_detail(request, box_id): """Display details for a specific box.""" box = get_object_or_404(Box, pk=box_id) return render(request, 'boxes/detail.html', {'box': box}) ``` ### Error Handling ```python # Use specific exceptions try: result = some_operation() except SpecificError as exc: raise CustomError('Descriptive message') from exc # Django: Use get_object_or_404 for model lookups box = get_object_or_404(Box, pk=box_id) # Log errors appropriately import logging logger = logging.getLogger(__name__) logger.error('Error message: %s', error_detail) ``` ### Type Hints (Recommended) ```python from typing import Optional from django.http import HttpRequest, HttpResponse def get_box(request: HttpRequest, box_id: int) -> HttpResponse: """Retrieve a box by ID.""" ... ``` ## Project Structure ``` labhelper/ ├── .gitea/ │ └── workflows/ │ └── build-containers-on-demand.yml # CI/CD workflow ├── argocd/ # Kubernetes deployment manifests │ ├── 001_pvc.yaml # PersistentVolumeClaim │ ├── deployment.yaml # Deployment + Service │ ├── ingress.yaml # Traefik ingress │ ├── nfs-pv.yaml # NFS PersistentVolume │ ├── nfs-storageclass.yaml # NFS StorageClass │ └── secret.yaml # Django secret key template ├── boxes/ # Main Django app │ ├── management/ │ │ └── commands/ │ │ ├── clean_orphaned_files.py # Cleanup orphaned ThingFile attachments │ │ └── clean_orphaned_images.py # Cleanup orphaned Thing images │ ├── migrations/ # Database migrations │ ├── templates/ │ │ └── boxes/ │ │ ├── add_things.html # Form to add multiple things │ │ ├── box_detail.html # Box contents view │ │ ├── box_management.html # Box/BoxType CRUD management │ │ ├── index.html # Home page │ │ ├── search.html # Search page with AJAX │ │ ├── thing_detail.html # Thing details view │ │ └── thing_type_detail.html # Thing type hierarchy view │ ├── templatetags/ │ │ └── dict_extras.py # Custom template filter: get_item │ ├── admin.py # Admin configuration │ ├── apps.py # App configuration │ ├── forms.py # All forms and formsets │ ├── models.py # Data models │ ├── tests.py # Test cases │ └── views.py # View functions ├── data-loader/ # Init container for database preload │ ├── Dockerfile # Alpine-based init container │ └── preload.sqlite3 # Preloaded database for deployment ├── labhelper/ # Project configuration │ ├── management/ │ │ └── commands/ │ │ └── create_default_users.py # Create default users/groups │ ├── templates/ │ │ ├── base.html # Base template with navigation │ │ └── login.html # Login page │ ├── asgi.py # ASGI configuration │ ├── settings.py # Django settings │ ├── urls.py # Root URL configuration │ └── wsgi.py # WSGI configuration ├── scripts/ │ ├── deploy_secret.sh # Generate and deploy Django secret │ ├── full_deploy.sh # Bump both container versions + copy DB │ └── partial_deploy.sh # Bump main container version only ├── .gitignore ├── AGENTS.md # This file ├── Dockerfile # Multi-stage build for main container ├── manage.py # Django CLI entry point └── requirements.txt # Python dependencies ``` ## Data Models ### boxes app | Model | Description | Key Fields | |-------|-------------|------------| | **BoxType** | Type of storage box with dimensions | `name`, `width`, `height`, `length` (in mm) | | **Box** | A storage box in the lab | `id` (CharField PK, max 10), `box_type` (FK) | | **ThingType** | Hierarchical category (MPTT) | `name`, `parent` (TreeForeignKey to self) | | **Thing** | An item stored in a box | `name`, `thing_type` (FK), `box` (FK), `description`, `picture` | | **ThingFile** | File attachment for a Thing | `thing` (FK), `file`, `title`, `uploaded_at` | | **ThingLink** | Hyperlink for a Thing | `thing` (FK), `url`, `title`, `uploaded_at` | **Model Relationships:** - BoxType -> Box (1:N via `boxes` related_name, PROTECT on delete) - Box -> Thing (1:N via `things` related_name, PROTECT on delete) - ThingType -> Thing (1:N via `things` related_name, PROTECT on delete) - ThingType -> ThingType (self-referential tree via MPTT) - Thing -> ThingFile (1:N via `files` related_name, CASCADE on delete) - Thing -> ThingLink (1:N via `links` related_name, CASCADE on delete) ## Available Django Extensions The project includes these pre-installed packages: - **django-mptt**: Tree structures (categories, hierarchies) - **django-mptt-admin**: Admin interface for MPTT models - **django-admin-sortable2**: Drag-and-drop ordering in admin - **django-nested-admin**: Nested inline forms in admin - **django-nested-inline**: Additional nested inline support - **django-revproxy**: Reverse proxy functionality - **sorl-thumbnail**: Image thumbnailing - **Pillow**: Image processing - **gunicorn**: Production WSGI server - **Markdown**: Markdown processing - **bleach**: HTML sanitization - **coverage**: Test coverage - **Font Awesome**: Icon library (loaded via CDN) - **jQuery**: JavaScript library (loaded via CDN) ## Frontend/CSS Guidelines ### Base Template The project uses a base template system at `labhelper/templates/base.html`. All page templates should extend this base: ```django {% extends "base.html" %} {% block title %}Page Title - LabHelper{% endblock %} {% block page_header %} {% endblock %} {% block content %} {% endblock %} {% block extra_css %}{% endblock %} {% block extra_js %}{% endblock %} ``` ### Design System **Color Scheme:** - Primary gradient: `#667eea` (purple) to `#764ba2` (purple-blue) - Success: Green gradient - Error: Red gradient - Background: Light gray `#f5f5f5` with gradient overlays - Cards: White with subtle shadows **Components:** - **Navigation**: Glassmorphism effect with blur backdrop - **Buttons**: Gradient backgrounds with hover lift effect - **Cards**: White with rounded corners and box shadows - **Tables**: Gradient headers with hover row effects - **Alerts**: Gradient backgrounds with icons - **Form Inputs**: Focused states with color transitions **Typography:** - System fonts: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif` - Headings: Bold, colored - Body: Regular, dark gray **Icons:** - Font Awesome 6.5.1 (CDN) - Use semantic icons for actions - Color: Match context or inherit from parent **Responsive Design:** - Mobile-first approach - Grid layouts with `repeat(auto-fill, minmax(250px, 1fr))` - Flexbox for component layouts - Breakpoints handled by grid and flex-wrap ### CSS Guidelines **Naming:** - Use descriptive class names - BEM pattern encouraged for complex components - Inline styles allowed for template-specific styling **Styles:** - Use base template styles when possible - Template-specific styles in `{% block extra_css %}` - JavaScript in `{% block extra_js %}` - Smooth transitions (0.2s - 0.3s) - Hover effects with transform and box-shadow **jQuery Usage:** - Loaded in base template - Use for interactive elements (toggles, hovers) - Event delegation for dynamically added elements - Focus/blur events for form inputs ### Available Pages/Views | View Function | URL Pattern | Name | Description | |---------------|-------------|------|-------------| | `index` | `/` | `index` | Home page with boxes grid and thing types tree | | `box_management` | `/box-management/` | `box_management` | Manage boxes and box types | | `add_box_type` | `/box-type/add/` | `add_box_type` | Add new box type | | `edit_box_type` | `/box-type//edit/` | `edit_box_type` | Edit box type | | `delete_box_type` | `/box-type//delete/` | `delete_box_type` | Delete box type | | `add_box` | `/box/add/` | `add_box` | Add new box | | `edit_box` | `/box//edit/` | `edit_box` | Edit box | | `delete_box` | `/box//delete/` | `delete_box` | Delete box | | `box_detail` | `/box//` | `box_detail` | View box contents | | `thing_detail` | `/thing//` | `thing_detail` | View/edit thing (move, picture, files, links) | | `thing_type_detail` | `/thing-type//` | `thing_type_detail` | View thing type hierarchy | | `add_things` | `/box//add/` | `add_things` | Add multiple things to a box | | `search` | `/search/` | `search` | Search page | | `search_api` | `/search/api/` | `search_api` | AJAX search endpoint | | `LoginView` | `/login/` | `login` | Django auth login | | `LogoutView` | `/logout/` | `logout` | Django auth logout | | `admin.site` | `/admin/` | - | Django admin | **All views except login require authentication via `@login_required`.** ### Template Best Practices 1. **Always extend base template** ```django {% extends "base.html" %} ``` 2. **Use block system for content injection** - `title`: Page title tag - `page_header`: Page header with breadcrumbs - `content`: Main page content - `extra_css`: Additional styles - `extra_js`: Additional JavaScript 3. **Load required template tags** ```django {% load static %} {% load mptt_tags %} {% load thumbnail %} ``` 4. **Use URL names for links** ```django ``` 5. **Use icons with Font Awesome** ```django ``` 6. **Add breadcrumbs for navigation** ```django ``` ## Forms | Form | Model | Purpose | |------|-------|---------| | `ThingForm` | Thing | Add/edit a thing (name, type, description, picture) | | `ThingPictureForm` | Thing | Upload/change thing picture only | | `ThingFileForm` | ThingFile | Add file attachment | | `ThingLinkForm` | ThingLink | Add link | | `BoxTypeForm` | BoxType | Add/edit box type | | `BoxForm` | Box | Add/edit box | | `ThingFormSet` | Thing | Formset for adding multiple things | ## Management Commands ### boxes app | Command | Description | Options | |---------|-------------|---------| | `clean_orphaned_files` | Clean up orphaned files from deleted things | `--dry-run` | | `clean_orphaned_images` | Clean up orphaned images and thumbnails | `--dry-run` | ### labhelper project | Command | Description | |---------|-------------| | `create_default_users` | Create default users and groups (admin/admin123, staff/staff123, viewer/viewer123) | ## Testing Guidelines - Use `django.test.TestCase` for database tests - Use `django.test.SimpleTestCase` for tests without database - Name test files `test_*.py` or `*_tests.py` - Name test methods `test_*` - Use descriptive test method names ```python from django.test import TestCase from .models import Box class BoxModelTests(TestCase): """Tests for the Box model.""" def setUp(self): """Set up test fixtures.""" self.box = Box.objects.create(name='Test Box') def test_box_str_returns_name(self): """Box __str__ should return the box name.""" self.assertEqual(str(self.box), 'Test Box') ``` ## Files to Never Commit Per `.gitignore`: - `__pycache__/`, `*.pyc` - Python bytecode - `.venv/` - Virtual environment - `.env` - Environment variables - `data/db.sqlite3` - Database file - `keys/` - Secret keys ## Common Pitfalls 1. **NEVER commit or push without explicit permission**: Always ask the user before running `git commit` or `git push`. The user will explicitly say "commit and push" when they want you to do this. Do NOT automatically commit/push after making changes unless instructed to do so. 2. **Always activate venv**: `source .venv/bin/activate` 3. **Run migrations after model changes**: `makemigrations` then `migrate` 4. **Add new apps to INSTALLED_APPS** in `settings.py` 5. **Templates in labhelper/templates/**: The base template and shared templates are in `labhelper/templates/`. App-specific templates remain in `app_name/templates/`. 6. **Use get_object_or_404** instead of bare `.get()` calls 7. **Never commit SECRET_KEY** - use environment variables in production ## Deployment Commands ### Prepare a Full Deployment When instructed to "Prepare a full deployment", perform the following steps: 1. **Bump container versions**: In `argocd/deployment.yaml`, increment the version numbers by 0.001 for both containers: - `labhelper-data-loader` (initContainer) - `labhelper` (main container) 2. **Copy database**: Copy the current development database to the data-loader preload location: ```bash cp data/db.sqlite3 data-loader/preload.sqlite3 ``` ### Prepare a Partial Deployment When instructed to "Prepare a partial deployment", perform the following step: 1. **Bump main container version only**: In `argocd/deployment.yaml`, increment the version number by 0.001 for the main container only: - `labhelper` (main container) Do NOT bump the data-loader version or copy the database.