diff --git a/.gitignore b/.gitignore index ceeb3e7..03c86c9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ keys/ node_modules/ package-lock.json package.json -AGENT*.md + # Diagram cache directory .env data/db.sqlite3 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bdd23d6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,272 @@ +# 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 +- **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 +``` + +## 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/ +├── manage.py # Django CLI entry point +├── requirements.txt # Python dependencies +├── labhelper/ # Project configuration +│ ├── settings.py # Django settings +│ ├── urls.py # Root URL routing +│ ├── wsgi.py # WSGI application +│ └── asgi.py # ASGI application +└── boxes/ # Django app + ├── admin.py # Admin configuration + ├── apps.py # App configuration + ├── models.py # Data models + ├── views.py # View functions + ├── tests.py # Test cases + ├── migrations/ # Database migrations + └── templates/ # HTML templates +``` + +## 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 + +## 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. **Always activate venv**: `source .venv/bin/activate` +2. **Run migrations after model changes**: `makemigrations` then `migrate` +3. **Add new apps to INSTALLED_APPS** in `settings.py` +4. **Use get_object_or_404** instead of bare `.get()` calls +5. **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.