Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
49b95253da
|
|||
| df392f7b7f | |||
| f4e9cea73b | |||
| c72869383b | |||
| c4946f8e16 | |||
|
a2dbcaf79a
|
|||
|
de4d3fc790
|
|||
| 20c468cca8 | |||
|
88c1dd704f
|
@@ -1,272 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -21,4 +21,4 @@ data:
|
|||||||
LOGOUT_REDIRECT_URL: "/login/"
|
LOGOUT_REDIRECT_URL: "/login/"
|
||||||
OIDC_AUTHENTICATION_FAILURE_REDIRECT_URL: "/login/"
|
OIDC_AUTHENTICATION_FAILURE_REDIRECT_URL: "/login/"
|
||||||
GUNICORN_OPTS: "--access-logfile -"
|
GUNICORN_OPTS: "--access-logfile -"
|
||||||
IMAGE_TAG: "0.083"
|
IMAGE_TAG: "0.086"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ spec:
|
|||||||
mountPath: /data
|
mountPath: /data
|
||||||
containers:
|
containers:
|
||||||
- name: web
|
- name: web
|
||||||
image: git.baumann.gr/adebaumann/labhelper:0.083
|
image: git.baumann.gr/adebaumann/labhelper:0.086
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8000
|
- containerPort: 8000
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ metadata:
|
|||||||
namespace: labhelper
|
namespace: labhelper
|
||||||
annotations:
|
annotations:
|
||||||
argocd.argoproj.io/ignore-healthcheck: "true"
|
argocd.argoproj.io/ignore-healthcheck: "true"
|
||||||
|
gethomepage.dev/enabled: "true"
|
||||||
|
gethomepage.dev/name: "Labhelper"
|
||||||
|
gethomepage.dev/description: "Laboratory inventory system"
|
||||||
|
gethomepage.dev/group: "Kubernetes"
|
||||||
|
gethomepage.dev/icon: "shield.png"
|
||||||
|
gethomepage.dev/href: "https://labhelper.adebaumann.com"
|
||||||
|
gethomepage.dev/ping: "https://labhelper.adebaumann.com/health/"
|
||||||
|
gethomepage.dev/pod-selector: "app=django"
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: traefik
|
ingressClassName: traefik
|
||||||
rules:
|
rules:
|
||||||
|
|||||||
+18
-1
@@ -1,14 +1,31 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, RequestFactory, SimpleTestCase, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from mozilla_django_oidc.middleware import SessionRefresh
|
||||||
|
|
||||||
from .admin import BoxAdmin, BoxTypeAdmin, ThingAdmin
|
from .admin import BoxAdmin, BoxTypeAdmin, ThingAdmin
|
||||||
from .models import Box, BoxType, Facet, Tag, Thing, ThingFile, ThingLink
|
from .models import Box, BoxType, Facet, Tag, Thing, ThingFile, ThingLink
|
||||||
|
|
||||||
|
|
||||||
|
class OIDCSessionRefreshTests(SimpleTestCase):
|
||||||
|
"""Tests for OIDC session refresh routing exclusions."""
|
||||||
|
|
||||||
|
def test_media_urls_do_not_trigger_oidc_session_refresh(self):
|
||||||
|
"""Uploaded files should be served directly, not redirected through OIDC."""
|
||||||
|
request = RequestFactory().get('/media/things/files/251/diag_6606.png')
|
||||||
|
request.user = SimpleNamespace(is_authenticated=True)
|
||||||
|
request.session = {}
|
||||||
|
|
||||||
|
middleware = SessionRefresh(lambda request: None)
|
||||||
|
|
||||||
|
self.assertFalse(middleware.is_refreshable_url(request))
|
||||||
|
|
||||||
|
|
||||||
class AuthTestCase(TestCase):
|
class AuthTestCase(TestCase):
|
||||||
"""Base test case that provides authenticated client."""
|
"""Base test case that provides authenticated client."""
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
|||||||
+12
-1
@@ -2,6 +2,7 @@
|
|||||||
# Full deployment script - bumps both container versions by 0.001 and copies database
|
# Full deployment script - bumps both container versions by 0.001 and copies database
|
||||||
|
|
||||||
DEPLOYMENT_FILE="argocd/deployment.yaml"
|
DEPLOYMENT_FILE="argocd/deployment.yaml"
|
||||||
|
CONFIGMAP_FILE="argocd/configmap.yaml"
|
||||||
DB_SOURCE="data/db.sqlite3"
|
DB_SOURCE="data/db.sqlite3"
|
||||||
DB_DEST="data-loader/preload.sqlite3"
|
DB_DEST="data-loader/preload.sqlite3"
|
||||||
|
|
||||||
@@ -11,6 +12,11 @@ if [ ! -f "$DEPLOYMENT_FILE" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIGMAP_FILE" ]; then
|
||||||
|
echo "Error: $CONFIGMAP_FILE not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Check if source database exists
|
# Check if source database exists
|
||||||
if [ ! -f "$DB_SOURCE" ]; then
|
if [ ! -f "$DB_SOURCE" ]; then
|
||||||
echo "Error: $DB_SOURCE not found"
|
echo "Error: $DB_SOURCE not found"
|
||||||
@@ -37,7 +43,12 @@ sed -i "s|image: git.baumann.gr/adebaumann/labhelper-data-loader:$LOADER_VERSION
|
|||||||
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$MAIN_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_MAIN_VERSION|" "$DEPLOYMENT_FILE"
|
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$MAIN_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_MAIN_VERSION|" "$DEPLOYMENT_FILE"
|
||||||
|
|
||||||
# Update ConfigMap with new main container version
|
# Update ConfigMap with new main container version
|
||||||
sed -i "s| IMAGE_TAG: \"$MAIN_VERSION\"| IMAGE_TAG: \"$NEW_MAIN_VERSION\"|" "argocd/configmap.yaml"
|
sed -i -E "s|^([[:space:]]*IMAGE_TAG: \")[^\"]*(\")|\\1$NEW_MAIN_VERSION\\2|" "$CONFIGMAP_FILE"
|
||||||
|
|
||||||
|
if ! grep -q "IMAGE_TAG: \"$NEW_MAIN_VERSION\"" "$CONFIGMAP_FILE"; then
|
||||||
|
echo "Error: Could not update IMAGE_TAG in $CONFIGMAP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Copy database
|
# Copy database
|
||||||
cp "$DB_SOURCE" "$DB_DEST"
|
cp "$DB_SOURCE" "$DB_DEST"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# Partial deployment script - bumps main container version by 0.001
|
# Partial deployment script - bumps main container version by 0.001
|
||||||
|
|
||||||
DEPLOYMENT_FILE="argocd/deployment.yaml"
|
DEPLOYMENT_FILE="argocd/deployment.yaml"
|
||||||
|
CONFIGMAP_FILE="argocd/configmap.yaml"
|
||||||
|
|
||||||
# Check if file exists
|
# Check if file exists
|
||||||
if [ ! -f "$DEPLOYMENT_FILE" ]; then
|
if [ ! -f "$DEPLOYMENT_FILE" ]; then
|
||||||
@@ -9,6 +10,11 @@ if [ ! -f "$DEPLOYMENT_FILE" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$CONFIGMAP_FILE" ]; then
|
||||||
|
echo "Error: $CONFIGMAP_FILE not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Extract current version of main container (labhelper, not labhelper-data-loader)
|
# Extract current version of main container (labhelper, not labhelper-data-loader)
|
||||||
CURRENT_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/labhelper:[0-9]" "$DEPLOYMENT_FILE" | grep -v "data-loader" | sed -E 's/.*:([0-9.]+)/\1/')
|
CURRENT_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/labhelper:[0-9]" "$DEPLOYMENT_FILE" | grep -v "data-loader" | sed -E 's/.*:([0-9.]+)/\1/')
|
||||||
|
|
||||||
@@ -24,7 +30,12 @@ NEW_VERSION=$(echo "$CURRENT_VERSION + 0.001" | bc | sed 's/^\./0./')
|
|||||||
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$CURRENT_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_VERSION|" "$DEPLOYMENT_FILE"
|
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$CURRENT_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_VERSION|" "$DEPLOYMENT_FILE"
|
||||||
|
|
||||||
# Update ConfigMap with new main container version
|
# Update ConfigMap with new main container version
|
||||||
sed -i "s| IMAGE_TAG: \"$CURRENT_VERSION\"| IMAGE_TAG: \"$NEW_VERSION\"|" "argocd/configmap.yaml"
|
sed -i -E "s|^([[:space:]]*IMAGE_TAG: \")[^\"]*(\")|\\1$NEW_VERSION\\2|" "$CONFIGMAP_FILE"
|
||||||
|
|
||||||
|
if ! grep -q "IMAGE_TAG: \"$NEW_VERSION\"" "$CONFIGMAP_FILE"; then
|
||||||
|
echo "Error: Could not update IMAGE_TAG in $CONFIGMAP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Partial deployment prepared:"
|
echo "Partial deployment prepared:"
|
||||||
echo " Main container: $CURRENT_VERSION -> $NEW_VERSION"
|
echo " Main container: $CURRENT_VERSION -> $NEW_VERSION"
|
||||||
|
|||||||
Reference in New Issue
Block a user