Student Management System for Five a Day English Academy
Albacete, Spain
Built to centralize student records, automate billing cycles, and streamline parent communication for a small and lovely English academy.
| Environment | Branch | Hosting | CI Status |
|---|---|---|---|
| Production | main |
https://example.com/ | |
| Testing (QA) | testing |
https://example.com/ | |
| Development | development |
Local Docker development: http://localhost:8000/ |
| Version | Date | Description |
|---|---|---|
| v1.0.10 | 2026-04-21 | Branded admin theme, white-bg favicon, social meta |
| v1.0.9 | 2026-04-16 | Test suite restructure to unit/integration, 96% coverage, CI gates |
| v1.0.8 | 2026-04-15 | README trim, docs/ purge, flaky CI test removed |
- Project Status
- Version History & Roadmap
- Roadmap
- v1.1 — Waiting List & Group Capacity
- v1.2 — Google Sheets Integration
- v1.3 — PDF Invoice Generation
- v1.4 — Celery + Redis Deployment
- v1.5 — Expense Tracking
- v1.6 — Multi-User Permissions
- v1.7 — Advanced Reporting & Analytics
- v1.8 — SMS Notifications (Twilio)
- v1.9 — Parent Portal
- v1.10 — Audit Log & Security Hardening
- v1.11 — Stripe Payment Integration
- v1.12 — Mobile Optimization & PWA
- Roadmap
- Tech Stack
- Database Schema
- Development & Docker
- Project Structure & Architecture
- Features by View
- Testing
- Migrations
- Security
- Testing Environment (QA)
- CI/CD & GitHub Actions
- Contributing
- License
v1.0.10 — Branded Admin Theme, White-Bg Favicon & Social Meta (current)
Social sharing & branding
- Logo changed to
logo_white_bg.pngacross README,base.htmlfavicon, apple-touch-icon, Open Graph, and Twitter Card — white background improves rendering in light-themed link preview cards - Favicon regenerated from
logo_white_bg.png: multi-size ICO (16/32/48/64px),favicon-32x32.png, andapple-touch-icon.png(180×180) — dropped in bothproject/static/andproject/core/static/ og:image:secure_urladded alongsideog:imagefor Facebook's HTTPS-explicit crawlertwitter:image:altadded to Twitter Card block- Schema.org JSON-LD block added to
base.html(@type: WebApplication, provider asEducationalOrganization) — covers Google Search previews, Gmail, and Google Chat link unfurling
Django admin — Five a Day theme
project/templates/admin/created;TEMPLATES.DIRSnow points toproject/templates/so project-level overrides take priority over Django's built-insadmin/base_site.html— violet gradient header (#4c1d95→#7c3aed) withlogo_white_bg.png, loadsadmin_custom.csson every admin pageadmin/login.html— card-style login page: logo, "Gestión Académica · Albacete" subtitle, Spanish field labels (Usuario / Contraseña / Entrar)admin/index.html— welcome banner with logo + Spanish action labels (Añadir / Editar / Ver / Acciones recientes)project/static/css/admin_custom.css— full CSS-variable override of Django admin (violet/purple palette, Trebuchet MS font, styled login card, fieldset headers, welcome banner)core/admin.py—site_title→ "Five a Day · Admin",index_title→ "Panel de administración"
Dependencies (Dependabot)
gunicornupgraded 22.0.0 → 23.0.0 (constraint widened<23→<24)pandasupgraded 2.3.3 → 3.0.2 (constraint widened<3→<4)
v1.0.9 — Test Suite Restructure, 96% Coverage & CI Coverage Gates
Testing
- Reorganised flat
project/tests/into two subdirectories:unit/(direct calls, no HTTP stack) andintegration/(Django test client through middleware) - Expanded test suite from ~280 tests to 574 tests across 30 files — coverage raised from ~70% to 96%
- New unit test files:
test_tasks.py,test_student_view_internals.py,test_decorators.py,test_error_handlers.py,test_qa_error_middleware.py,test_payment_helpers.py,test_testing_tools_helpers.py, expandedtest_email_functions.py,test_email_service.py,test_context_processors.py,test_models.py,test_services.py - New integration test files:
test_app_form_views.py,test_payment_views.py,test_student_views.py,test_management_views.py,test_testing_tools.py,test_auth_oauth.py,test_dashboard_views.py,test_parent_views.py,test_schedule_views.py,test_fun_friday_attendance_views.py,test_todo_views.py,test_support_views.py
CI/CD — Coverage enforcement
- CI coverage step: hard floor ≥ 75% (fails CI), warning annotation < 90% (CI still passes)
- Pre-commit hook:
pytest-coveragehook blocks commits when coverage drops below 75% make test-cov-gatetarget added for pre-commit integration- Coverage threshold
fail_under = 75set inpyproject.toml
Dashboard
- Quote-of-the-day rewritten: in-memory batch cache fetches up to 50 quotes from zenquotes.io per API call; each page load pops one; cookie stores the last served quote as ASCII fallback
- Thread-safety comment added to
_quotesmodule-level list
Developer tooling
/pc-runClaude skill: runs pre-commit in a loop, fixes failures, then asksy/X.Y.Z/nfor version bumpingupdate-readmeskill: added Coverage Report subsection generation (§ k.1)- Makefile: all em dashes and right-arrow Unicode replaced with ASCII equivalents
comms/tasks.py:raise Exception(...)tightened toraise RuntimeError(...)across all four failure paths- Mid-file imports with
# noqa: E402removed from all test files — moved to file-top imports
v1.0.8 — Lean README, docs/ Purge & Test-Suite Hygiene
README / docs
- Removed the "Key objectives" bullet list and the "Live status for each environment…" subtitle — the header + intro sentence + Project Status table already communicate that
- Shortened the project intro line
- Recent Versions table rewritten to ≤10-word headline phrases; the dense per-version writeups now live only in the Version History
<details>blocks below (where you're reading this) - README header image sourced from
project/static/images/logo.pngnow that the olddocs/resources/logo.pngis gone
docs/ asset cleanup
- Deleted every tracked binary under
docs/— UI screenshots, Gantt PNG/SVGs, legacy logos.docs/is already in.gitignore, so nothing gets re-tracked - No remaining references to the deleted paths; header image is the only asset the README needed from there
Documentation convention
update-readmeskill (Step 3.1.c) and the README-maintenance checklist inCLAUDE.mdnow mandate extremely brief Recent Versions rows (≤10 words, headline only). Long-form content belongs in the Version History block. Future runs of the skill will enforce this.
Test-suite hygiene
- Removed
test_google_oauth_prefix_public— the CI was failing becausecore/views/auth.py::google_oauth_redirectgracefully returnsredirect("login")whenGOOGLE_CLIENT_IDis unset (CI's state), which the test's assertion couldn't distinguish from a middleware-level block. The remainingTestPublicPathscases (static, health, login) still cover the middleware's exemption logic.
Developer tooling
make pc-runlog line for the auto-stageduv.locktrimmed from"Staged updated uv.lock — next git commit will not be blocked by it"to"Staged updated uv.lock"— the explanatory tail was redundant in practice
v1.0.7 — Favicon, Social Metadata & CI Test Fixes
Social sharing & branding
- Multi-resolution
favicon.ico(16/32/48/64/128/256) generated fromproject/static/images/logo.png— dropped in bothproject/static/andproject/core/static/so both STATICFILES_DIRS paths serve it base.htmlnow includes full social-sharing metadata:<meta name="description">,<meta name="author">,<meta name="theme-color" content="#6d28d9">(matches the violet palette),apple-touch-icon, full Open Graph set (og:type,og:site_name,og:title,og:description,og:image,og:image:alt,og:url,og:locale), and Twitter Card summary tags- Every content field is wrapped in an overridable Django block (
meta_description,og_title,og_description,og_image,twitter_title,twitter_description,twitter_image) so per-page templates can tailor link previews without touchingbase.html
Test-suite fixes
settings_test.pynow explicitly setsSECURE_SSL_REDIRECT = False,SECURE_HSTS_SECONDS = 0,SESSION_COOKIE_SECURE = False,CSRF_COOKIE_SECURE = False— the CI environment runs withDJANGO_DEBUG=False, which activated the production SSL redirect and turned every test request into a 301 tohttps://testserver/.... The test settings are now self-contained and correct regardless ofDJANGO_DEBUG.pytest.iniaddsfilterwarnings = ignore:No directory at:UserWarningto silence the 142 WhiteNoise warnings that were emitted once per test request (thestaticfiles/directory only exists aftercollectstatic, which isn't run before tests)
Dashboard reliability
- Zenquotes fetch in
core/views/dashboard.pynow targetshttps://zenquotes.io/api/quotes(no trailing slash — the old URL was getting 301-redirected) withfollow_redirects=Trueas a guard against future URL changes - Silent
except Exception: passreplaced with properlogger.warning(...)calls — failures are still non-fatal but now visible in logs
CI tooling
mypyjob inci.ymlnow setsDJANGO_SETTINGS_MODULE=project.settings,DJANGO_DEBUG=True, a dummyDJANGO_SECRET_KEY, andPYTHONPATH=project—django-stubsimportssettings.pyat load time, which previously raised the production secret-key guardmake version x.y.znow also updates the README version badge viased, regeneratesuv.lockviauv lock --quiet, and prints a reminder to run theupdate-readmeskill afterwards; runningmake versionwith no arg now shows bothpyproject.tomland the README badge side-by-side and warns if they've driftedmake pc-run's auto patch-bump now also rewrites the README badge and regeneratesuv.lock— the existinggit add uv.locktail stages the refreshed lockfile automatically
v1.0.6 — Documentation Skill & Doc Overhaul
Documentation agent
- New
update-readmeClaude skill at.claude/skills/update-readme/SKILL.md— routes staged files to the right docs (main README, CLAUDE.md, DEPLOYMENT.md, docs/, per-app READMEs), applies per-file checklists, and sweeps for stale references across the full documentation tree
README overhaul
readme.md→README.mdrename (case-sensitive file systems matter on GCP)- Major reorganization of sections; expanded Environment Variables Reference; tightened Recent Versions table to 3 rows; populated Developer Tooling and Make Commands tables
.env templateis now the single authoritative source for local env-var structure, lives inline in the README as a fencedbashblock
Secrets hygiene
- Removed
.env.testing.example(its content now lives only inline in the README.env templateblock) .gitignoretightened:.env*matches everything, no!.env.exampleexception, no.env*.examplecarve-outs
CI workflow refinements
auto-merge.yml— improved commit detection and PR creation for thedevelopment→testing→maincascadenotify-production.yml— richer production deployment notification email with commit info and next-stepgcloudcommands
Per-app docs
project/core/README.mdandproject/comms/README.mdtouched up to match post-refactor structure
v1.0.5 — CI/CD Pipeline & Public Repo Hardening
GitHub Actions CI/CD (new — see docs/GITHUB.md)
ci.yml— three parallel jobs on every push/PR: Ruff + Bandit lint, mypy type check, pytest against a PostgreSQL 16 service container with coverage uploaded to Codecovauto-merge.yml— hourly cron that mergesdevelopment→testingafter 24 h of inactivity and CI passing, then auto-creates a PRtesting→maincodeql.yml— weekly Python security analysis (OWASP Top 10, Django-specific queries)notify-production.yml— emailshellofiveaday@gmail.comon every push tomainwith commit info andgclouddeploy instructions- Owner email notifications when
development→testingmerge lands and a PR is opened tomain dependabot.yml— grouped weekly Python and GitHub Actions updates targetingdevelopmentCODEOWNERS— auto-request reviews from both owner accounts
Public-repo hardening
- Branch protection rules documented for
main(14 protections) andtesting(minimal) - Secret scanning + push protection + CodeQL enabled (all free for public repos)
- Fork PR workflow restriction, read-only default workflow permissions, block-approvals-from-Actions
SECURITY.md+CODEOWNERS+LICENSErequired-file checklist in docs/GITHUB.md
Developer tooling
make pc-runauto-stages regenerateduv.lockas the final step — nextgit commitis no longer blocked by the lock file
v1.0.4 — GCP Migration Plan, Quote Generator, Celery
GCP migration plan (new — see DEPLOYMENT.md)
- Full Cloud Run + Cloud SQL architecture documented
- Three environments: local Docker (dev), Compute Engine e2-micro free tier (testing), Cloud Run + Cloud SQL (production)
- Cost estimate: ~$15-27/month for production, $0/month for testing
- Celery replacement strategy using Cloud Scheduler + Cloud Run Jobs
- Cleaned legacy Render config —
render.yamlremoved; commented nginx and pgAdmin services removed fromdocker-compose.yml
Dashboard enhancement
- Inspirational quote generator on
/home— fetches two daily quotes fromzenquotes.io, stores them in a 48 h cookie, rotates daily (day 0 shows quote 1, day 1+ shows quote 2), graceful fallback to the default Spanish subtitle on API failure
Developer tooling
make version x.y.z— positional argument (replacesV=x.y.z) with confirmation guard before writingmake pc-run— renamed frompre-commit-run; after a clean pass, prompts to auto-increment the patch version inpyproject.tomlandproject/settings.py
Celery
- Celery worker and beat containers added to
docker-compose.ymlwith correct permissions and health checks - Several payment and enrollment issues fixed
v1.0.3 — Test Coverage Expansion (70%)
Testing
- 40+ new tests added across 13 new test files — overall suite around 280+ tests
- Coverage raised to 70% across
core,students,billing,comms - New test files:
test_auth_views.py,test_app_form_views.py,test_constants.py,test_create_payment_views.py,test_exports.py,test_forms.py,test_parent_views.py,test_payment_views.py,test_schedule_views.py,test_student_forms.py,test_student_views.py,test_transactions.py - Additional parametrized test cases for email-form views and error pages
Coverage tooling
- Coverage badge pulled dynamically from Codecov (CI workflow uploads
coverage.xmlon every run) make coverage-badgeretained for offline SVG generation
v1.0.2 — UV Migration & Developer Tooling
Dependency management
- Replaced Poetry with UV (see docs/UV.md)
uv.lockreplacespoetry.lock- All Make commands updated to use
uv run
Developer tooling
- Ruff — unified lint + format (replaces flake8, isort, black)
- mypy with
django-stubs— static type checking - bandit — Python security linter
- pip-audit — dependency CVE scanning
- pytest-xdist — parallel test execution (
-n auto) - pytest-randomly — randomized test order with reproducible seeds
- pytest-cov — coverage reports (HTML + XML + terminal)
- pre-commit hooks — Ruff, mypy, bandit on every commit
All tools configured in pyproject.toml — single source of truth.
v1.0.1t — QA Testing Environment
Testing infrastructure
- QA Docker Compose overlay (
docker-compose.testing.yml) — Gunicorn,DEBUG=False, separate DB volume .env.testingwith dedicated credentials andDJANGO_ENV=testing- Database seeding command (
seed_testdata) — 15+ students, parents, enrollments, payments - HTTPS documentation (
HTTPS.md) — local Docker (Nginx + self-signed cert) and GCP Cloud Run
Testing dashboard (/testing/)
- Project info card — version, environment, last commit (branch, hash, author, date)
- Error reporting toggle — sends unhandled exceptions to SUPPORT_EMAIL with full traceback
- Database seeding UI — seed or wipe-and-reseed via AJAX
- Backlog — create tasks with priority, each emailed to support automatically
Access control
qa_access_requireddecorator incore/decorators.py- Gated by
DJANGO_ENV=testing+DEBUG=False+ session username matchesQA_TESTING_USERNAME - Returns 404 (not 403) for unauthorized users — page appears not to exist
- Sidebar icon hidden for all non-QA users via context processor
Bug fixes
- Added
STATICFILES_DIRSforproject/static/— email CSS was missing from collectstatic manifest - Added
SECURE_PROXY_SSL_HEADERfor HTTPS behind reverse proxies QAErrorEmailMiddlewarefor automated error reporting to support email
v1.0.0 — Architecture Refactor & Test Suite
Architecture
- Split monolithic
coreapp into 4 apps:students,billing,comms,core - Created service layer: EnrollmentService, PaymentService, PricingService
- Split 3,648-line views.py into 12 focused modules
- Fixed module-level querysets, wildcard imports, dual pricing source of truth
Frontend
- Replaced 1,178-line pre-compiled Tailwind with CDN + custom violet palette config
- Extracted ~1,400 lines of inline JS into 13 static modules
- Removed
#webcrumbsCSS scoping wrapper - base.html: 610 lines reduced to 305 lines
Testing
- 132 pytest tests: 41 model, 26 service, 65 view tests
- Tests run against PostgreSQL (same as production)
- Found and fixed Payment
activefield bug
Templates
- Renamed all Spanish-named email templates to English (e.g.,
matricula_niño.html->enrollment_child.html)
Documentation
- Comprehensive README with all sections
- Per-app README.md files (core, students, billing, comms)
- CLAUDE.md for AI-assisted development
- DEPLOYMENT.md for Google Cloud Platform
v0.30.2 — Docker & History System
- Docker Compose with PostgreSQL 16 + Django
- Makefile with 40+ commands for development workflow
- HistoryLog system for tracking user actions (capped at 1,000 entries)
- GDPR tracking for adult students
- Improved entrypoint script for Docker
v0.29.0 — Enrollment & Email System
- Enrollment system with 3 plans (monthly full/part-time, quarterly)
- Discount engine: language cheque, sibling, quarterly, June end-of-year
- Adult student support with separate pricing
- 12 email templates with preview and test-send
- Fun Friday attendance tracking
- Support ticket system
Click to expand full roadmap (v1.1 — v1.12)
Students can be created with a waiting_list flag instead of being immediately enrolled. When a group has capacity (a student leaves), waiting list students are surfaced for assignment.
- New
is_waitingboolean on Student model max_studentssoft limit on Group model withstudent_counttracking- Notification when a student is deactivated and a group drops below capacity
- Waiting list management view: filter by group preference, priority by creation date
- Quick-assign flow: from waiting list or student creation, assign to group with one click
- Dashboard widget showing groups with available spots and waiting students
Automatic export of student/payment data to Google Sheets for existing spreadsheet workflows. Read and write via gspread using already-configured Google OAuth credentials.
Proper PDF generation using WeasyPrint. Invoice/receipt PDFs for individual payments and quarterly summaries. Replace the current HTML-fallback tax certificate.
Full async task processing with Redis broker. Move all email sends to background tasks. Add Celery Beat for scheduled jobs: daily birthday emails at 8:00 AM, monthly payment generation on the 1st, monthly reports on the 28th.
Track academy expenses (rent, supplies, salaries) with categories, recurring templates, and monthly totals. Income-vs-expense dashboard widget showing profitability.
Replace SimpleAuthMiddleware with Django's built-in auth. Roles: admin (full access), teacher (read-only students + schedule), assistant (everything except configuration).
Monthly and yearly financial reports with charts. Student retention analytics. Payment collection rates. Group utilization metrics. Exportable to PDF.
SMS as an alternative notification channel for payment reminders and urgent communications. Opt-in per parent. Fallback to email when SMS fails.
Read-only web portal for parents to view enrollment status, payment history, upcoming events, and download receipts/certificates. Separate authentication from admin panel.
Full audit trail for all data changes (who changed what, when). Rate limiting on login and API endpoints. Two-factor authentication for admin users.
Online payment via Stripe. Parents receive payment links by email. Automatic reconciliation with pending payments. Receipts generated on completion.
Progressive Web App support: installable on mobile, offline-capable dashboard, push notifications for overdue payments and birthdays.
| Technology | Version | Purpose |
|---|---|---|
| Python | 3.12+ | Runtime |
| Django | 5.2.5 | Web framework |
| PostgreSQL | 16 (Alpine) | Database (production, development, and testing) |
| Celery | 5.5.3 | Async task queue (eager mode without Redis, full async with Redis in v1.4) |
| Celery Beat | (bundled with Celery) | Scheduled task execution (birthday emails, payment generation — v1.4) |
| Redis | 7 (Alpine) | Message broker for Celery (planned, v1.4) |
| Gunicorn | 23.0.0 | Production WSGI server |
| WhiteNoise | 6.11.0 | Static file serving in production |
| Technology | Purpose |
|---|---|
| Tailwind CSS (CDN) | Utility-first CSS with custom violet primary palette |
| Google Fonts | Material Symbols Outlined (icons), Montserrat Alternates (login), Parisienne (login accent) |
| Vanilla JavaScript | 13 static modules — zero build tools, no framework |
| Technology | Purpose |
|---|---|
| Docker | Multi-stage build, non-root django user |
| Docker Compose | Service orchestration (PostgreSQL + Django) |
| Google Cloud Platform | Production hosting: Cloud Run + Cloud SQL |
| Gmail SMTP | Email sending (app password authentication) |
| Google OAuth 2.0 | Optional admin authentication |
| Make | 45+ development commands |
| Package | Purpose |
|---|---|
django-cors-headers |
CORS handling for future API consumers |
django-filter |
Query filtering utilities |
django-extensions |
Development utilities (shell_plus, graph_models) |
django-gsheets + gspread |
Google Sheets integration (v1.2) |
django-redis |
Redis cache backend (v1.4) |
django-storages |
Cloud storage backends (future) |
pandas |
Data processing for exports |
openpyxl |
Excel file generation (.xlsx) |
httpx |
HTTP client for external API calls |
psycopg2-binary |
PostgreSQL database adapter |
dj-database-url |
Database URL parsing for cloud deployments |
python-dotenv |
Environment variable loading from .env |
markdown |
Markdown rendering |
pytest + pytest-django |
Testing framework |
pytest-xdist |
Parallel test execution (-n auto) |
pytest-randomly |
Randomized test ordering (catches order-dependent bugs) |
pytest-cov + coverage-badge |
Coverage reporting + SVG badge generation |
| Tool | Purpose |
|---|---|
| UV | Dependency management (replaces Poetry). PEP 621, uv.lock. See docs/UV.md |
| Ruff | Linting + formatting (replaces flake8, black, isort). Config in pyproject.toml |
mypy + django-stubs |
Static type checking with Django ORM support |
| bandit | Security linter (hardcoded secrets, SQL injection, etc.) |
| pip-audit | Dependency vulnerability scanning against PyPI CVE database |
| pre-commit | Git hooks: ruff, ruff-format, mypy, bandit |
erDiagram
Teacher {
int id PK
string first_name
string last_name
string email UK
string phone
bool active
bool admin
}
Group {
int id PK
string group_name UK
string color
int teacher_id FK
bool active
}
Parent {
int id PK
string first_name
string last_name
string dni UK
string phone
string email
string iban
}
Student {
int id PK
string first_name
string last_name
date birth_date
bool is_adult
string email
string phone
string school
text allergies
bool gdpr_signed
int group_id FK
bool active
date withdrawal_date
text withdrawal_reason
}
StudentParent {
int id PK
int student_id FK
int parent_id FK
}
SiteConfiguration {
int id PK
decimal children_enrollment_fee
decimal adult_enrollment_fee
decimal full_time_monthly_fee
decimal part_time_monthly_fee
decimal adult_group_monthly_fee
decimal language_cheque_discount
decimal quarterly_enrollment_discount
decimal sibling_discount
decimal june_discount
decimal full_year_bonus
}
EnrollmentType {
int id PK
string name UK
string display_name
decimal base_amount_full_time
decimal base_amount_part_time
bool active
}
Enrollment {
int id PK
int student_id FK
int enrollment_type_id FK
date enrollment_period_start
date enrollment_period_end
string academic_year
string schedule_type
string payment_modality
bool has_language_cheque
bool is_sibling_discount
decimal enrollment_amount
decimal discount_percentage
decimal final_amount
string status
date enrollment_date
}
Payment {
int id PK
int student_id FK
int parent_id FK
int enrollment_id FK
string payment_type
string payment_method
decimal amount
string payment_status
date due_date
date payment_date
string concept
string reference_number
}
TodoItem {
int id PK
string text
date due_date
}
HistoryLog {
int id PK
string action
string message
string icon
datetime created_at
}
ScheduleSlot {
int id PK
int row
int day
int col
int group_id FK
}
FunFridayAttendance {
int id PK
int student_id FK
date date
}
Teacher ||--o{ Group : "teaches"
Group ||--o{ Student : "contains"
Student }o--o{ Parent : "has parents"
Student ||--o{ StudentParent : ""
Parent ||--o{ StudentParent : ""
Student ||--o{ Enrollment : "enrolls in"
EnrollmentType ||--o{ Enrollment : "type of"
Student ||--o{ Payment : "pays"
Parent ||--o{ Payment : "responsible for"
Enrollment ||--o{ Payment : "covers"
Group ||--o{ ScheduleSlot : "assigned to"
Student ||--o{ FunFridayAttendance : "attends"
| Constraint | Model | Rule |
|---|---|---|
| Singleton | SiteConfiguration | Always pk=1, cannot be deleted |
| Unique active | Enrollment | Only one active enrollment per student |
| Unique pair | StudentParent | (student, parent) |
| Unique pair | FunFridayAttendance | (student, date) |
| Unique triple | ScheduleSlot | (row, day, col) |
| Unique | Teacher.email, Group.group_name, Parent.dni, EnrollmentType.name |
# Clone the repository
git clone https://github.com/starseeker-code-public/five-a-day.git
cd five-a-day
# Create the .env file — copy the template below into `.env` and fill in the blanks
touch .envPaste the template from .env template into your new .env file and fill in the empty values.
Docker (recommended):
make build # Build images
make up # Start PostgreSQL + Redis + Django + Celery → http://localhost:8000
make migrate # Apply migrations (first time only)Local development (no Docker):
uv sync # Install dependencies
cd project
python manage.py migrate
python manage.py runserverImportant: The
.envfile controls whether the app runs in production or development mode. Before starting, set at minimum:
DJANGO_ENV=development— enables development behaviors (auto superuser, no collectstatic)DJANGO_DEBUG=true— enables Django debug mode, detailed error pagesPOSTGRES_PASSWORD— required for database connectionDJANGO_SECRET_KEY— generate withpython -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
.env is gitignored and never committed. The template below is the authoritative structure — copy it into your new .env file, then fill in the empty values with your own secrets. Defaults that are safe to keep as-is are already filled in.
# ============================================================================
# DJANGO SETTINGS
# ============================================================================
DJANGO_ENV=development # production | development
DJANGO_DEBUG=True
SECURE_SSL_REDIRECT=False
# Generate with: python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
DJANGO_SECRET_KEY=
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# ============================================================================
# LOGGING
# ============================================================================
LOG_LEVEL=INFO
DJANGO_LOG_LEVEL=INFO
# ============================================================================
# DATABASE CONFIGURATION
# ============================================================================
DATABASE=postgres # sqlite | postgres
POSTGRES_DB=fiveaday_db
POSTGRES_USER=fiveaday_user
# Generate with: openssl rand -base64 32
POSTGRES_PASSWORD=
POSTGRES_HOST=db # `db` in Docker, `localhost` outside
POSTGRES_PORT=5432
# ============================================================================
# SUPERUSER (auto-created on first boot if all three are set)
# ============================================================================
DJANGO_SUPERUSER_USERNAME=
DJANGO_SUPERUSER_EMAIL=
DJANGO_SUPERUSER_PASSWORD=
# ============================================================================
# EMAIL CONFIGURATION (Gmail SMTP + App Password)
# ============================================================================
EMAIL_HOST_USER= # your-academy@gmail.com
EMAIL_SECRET= # 16-char Gmail App Password
SUPPORT_EMAIL= # where support tickets are sent
EMAIL_TEST_1= # dev test recipient 1
EMAIL_TEST_2= # dev test recipient 2
# ============================================================================
# CELERY / REDIS
# ============================================================================
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0
# ============================================================================
# AUTHENTICATION (session-based, until the Django User model is adopted in v1.6)
# ============================================================================
LOGIN_USERNAME=fiveaday
LOGIN_PASSWORD=
# ============================================================================
# GOOGLE OAUTH
# ============================================================================
# Create at https://console.cloud.google.com/ → APIs & Services → Credentials
# Authorised redirect URI: http://localhost:8000/auth/google/callback/
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback/
# ============================================================================
# ACADEMY BUSINESS INFO (prefilled in payment-reminder email forms)
# ============================================================================
ACADEMY_IBAN=
ACADEMY_IBAN_HOLDER=
ACADEMY_PHONE=
ACADEMY_WHATSAPP=Note: do not include VERSION= in your .env — it is deprecated. The app version is derived from pyproject.toml (and overridable via APP_VERSION).
Run make or make help for the full list. Key commands:
| Command | Description |
|---|---|
| Lifecycle | |
make up |
Start all services (detached) |
make down |
Stop and remove containers |
make dev |
Start in foreground (logs visible) |
make rebuild |
Full rebuild (no cache) + start |
| Django | |
make shell |
Django shell in container |
make migrate |
Apply migrations |
make makemigrations |
Create migrations (all 4 apps) |
make check |
Django system checks |
| Database | |
make dbshell |
PostgreSQL shell |
make backup |
Dump DB to backups/ |
make reset-db |
Recreate database (destructive!) |
| Testing | |
make test |
Run all tests in Docker (PostgreSQL) |
make test-local |
Run tests locally against Docker PostgreSQL |
make test-sqlite |
Run tests with SQLite (no Docker needed) |
make test-coverage |
Tests with HTML coverage report |
make test-models |
Only model tests |
make test-services |
Only service tests |
make test-views |
Only view tests |
make test-fast |
Stop on first failure |
make test-k K=payment |
Run tests matching keyword |
| Versioning | |
make version 1.1.0 |
Update version in pyproject.toml, settings.py, the README badge, and regenerate uv.lock (with y/N confirmation); reminds you to run the update-readme skill to refresh Version History |
make version |
Show current version from pyproject.toml + README badge; warns if they've drifted |
| Developer Tooling | |
make lint / make lint-fix |
Run Ruff linter (optionally auto-fix) |
make format / make format-check |
Run Ruff formatter |
make mypy |
Run mypy type checker |
make bandit |
Run bandit security linter |
make audit |
pip-audit — scan deps for CVEs |
make pc-run |
Run pre-commit on all files; on clean pass, offer to auto-bump patch version; auto-stages regenerated uv.lock |
make pre-commit-install |
Install the git pre-commit hook |
| Email & Payments | |
make send-test-email |
Send test birthday email |
make test-all-emails |
List all email templates |
make generate-payments |
Generate current month's payments |
make generate-payments-dry |
Preview without creating |
| Health | |
make health |
Full health check (Django + DB + HTTP) |
make check-deploy |
Django deployment checklist |
The project supports three environments, controlled by DJANGO_ENV and DJANGO_DEBUG:
| Environment | DJANGO_ENV |
DJANGO_DEBUG |
Database | Static Files | Use Case |
|---|---|---|---|---|---|
| Production | production |
false |
PostgreSQL (Cloud SQL) | WhiteNoise + collectstatic | Live deployment |
| Testing | (via settings_test.py) | false |
PostgreSQL (Docker) | Simple storage | make test |
| Development | development |
true |
PostgreSQL (Docker) | Django dev server | Local coding |
Defaults are production-safe:
DJANGO_DEBUGdefaults tofalseandDJANGO_ENVdefaults todevelopment. In production, always setDJANGO_ENV=productionand ensureDJANGO_SECRET_KEYis a strong random value.
The database is always PostgreSQL — in Docker development, in tests, and in production. Tests run against the same Docker PostgreSQL container to ensure realistic behavior. For quick local test runs without Docker, use make test-sqlite.
The table below describes every variable in the .env template above, plus a few advanced overrides not included in the template. See the template for the full .env structure.
| Variable | Description | Required | Default |
|---|---|---|---|
| Django core | |||
DJANGO_ENV |
Environment: development / production / testing |
No | development |
DJANGO_DEBUG |
Debug mode: true / false |
No | false |
DJANGO_SECRET_KEY |
Secret key | Yes in production | dev fallback |
DJANGO_ALLOWED_HOSTS |
Comma-separated hosts | No | localhost,127.0.0.1 |
SECURE_SSL_REDIRECT |
Force HTTPS redirects | No | True when DEBUG=False |
| Database | |||
DATABASE |
Set to postgres for PostgreSQL |
No | postgres |
DATABASE_URL |
Full URL (Cloud deployments) | No | — |
POSTGRES_DB |
Database name | No | fiveaday_db |
POSTGRES_USER |
Database user | No | fiveaday_user |
POSTGRES_PASSWORD |
Database password | Yes | — |
POSTGRES_HOST |
Database host | No | db (Docker) |
POSTGRES_PORT |
Database port | No | 5432 |
| Superuser (auto-created on first boot when all three are set) | |||
DJANGO_SUPERUSER_USERNAME |
Superuser name | No | — |
DJANGO_SUPERUSER_EMAIL |
Superuser email | No | — |
DJANGO_SUPERUSER_PASSWORD |
Superuser password | No | — |
EMAIL_HOST_USER |
Gmail address | For email features | — |
EMAIL_SECRET |
Gmail app password | For email features | — |
SUPPORT_EMAIL |
Support ticket recipient | No | — |
EMAIL_TEST_1 / EMAIL_TEST_2 |
Test email recipients | No | — |
| Auth | |||
LOGIN_USERNAME |
Admin username | Yes | — (login refused if missing) |
LOGIN_PASSWORD |
Admin password | Yes | — (login refused if missing) |
QA_TESTING_USERNAME |
Extra user allowed to see /testing/ dashboard |
No (QA only) | — |
GOOGLE_CLIENT_ID |
OAuth client ID | For Google login | — |
GOOGLE_CLIENT_SECRET |
OAuth client secret | For Google login | — |
GOOGLE_REDIRECT_URI |
OAuth callback URL | For Google login | auto-detected |
GOOGLE_ALLOWED_EMAIL |
Restrict Google login to one email | No | EMAIL_HOST_USER |
| Celery / Redis | |||
CELERY_BROKER_URL |
Redis URL for Celery | No | eager mode (tasks run inline) |
CELERY_RESULT_BACKEND |
Redis URL for results | No | same as broker |
| Academy business info (prefills payment-reminder email forms) | |||
ACADEMY_IBAN |
Bank account for payment reminders | No | — |
ACADEMY_IBAN_HOLDER |
IBAN account holder | No | — |
ACADEMY_PHONE |
Phone for Bizum payments | No | — |
ACADEMY_WHATSAPP |
WhatsApp number for reminders | No | — |
| Logging / misc | |||
LOG_LEVEL |
App log level | No | DEBUG in dev, INFO in prod |
DJANGO_LOG_LEVEL |
Django framework log level | No | inherits LOG_LEVEL |
APP_VERSION |
Version string override | No | from settings.py default |
SESSION_COOKIE_AGE |
Session duration (seconds) | No | 86400 (24 h) |
The app version is defined in two places and should be updated together:
pyproject.tomlline 3:version = "x.y.z"— package metadataproject/settings.pyline 17:APP_VERSION = os.getenv("APP_VERSION", "x.y.z")— runtime fallback
Use make version x.y.z (positional) to update both at once — it prompts Version A will become the new version B, are you sure? before writing. make pc-run also auto-bumps the patch digit on successful pre-commit if you answer y when asked.
The version appears in:
/health/endpoint response- Support ticket emails
- Can be overridden at runtime via the
APP_VERSIONenvironment variable (do not leave a legacy value like0.x.yin.env— remove the line so the default insettings.pytakes effect)
graph TB
Browser[Browser] --> Django[Django / Gunicorn :8000]
Django --> PG[(PostgreSQL :5432)]
Django --> SMTP[Gmail SMTP]
Django --> OAuth[Google OAuth]
subgraph "Django Apps"
Core["<b>core</b><br/>Dashboard, Auth<br/>Schedule, Utilities<br/><i>4 models</i>"]
Students["<b>students</b><br/>Student, Parent<br/>Teacher, Group<br/><i>5 models</i>"]
Billing["<b>billing</b><br/>Payment, Enrollment<br/>Pricing, Exports<br/><i>4 models, 3 services</i>"]
Comms["<b>comms</b><br/>Email Service<br/>Tasks, Commands<br/><i>0 models</i>"]
end
Core --> Students
Core --> Billing
Core --> Comms
Billing --> Students
Comms --> Students
Comms --> Billing
graph LR
students["<b>students</b><br/>(foundation — no dependencies)"] --> billing["<b>billing</b><br/>(FK to Student, Parent)"]
students --> core["<b>core</b><br/>(FK to Student, Group)"]
students --> comms["<b>comms</b><br/>(email recipients)"]
billing --> comms
five-a-day/
├── project/
│ ├── project/ Django settings module
│ │ ├── settings.py Main settings
│ │ ├── settings_test.py Test overrides (PostgreSQL or SQLite)
│ │ ├── urls.py Root URL conf → includes 4 app URL files
│ │ ├── celery.py Celery configuration
│ │ └── wsgi.py / asgi.py
│ │
│ ├── core/ Dashboard, Auth, Schedule, Utilities
│ │ ├── models.py TodoItem, HistoryLog, FunFridayAttendance, ScheduleSlot
│ │ ├── views/ 13 view modules (dashboard, auth, students, parents,
│ │ │ payments, management, app_forms, schedule,
│ │ │ fun_friday_attendance, todos, support, errors,
│ │ │ testing_tools)
│ │ ├── constants.py DIAS_ES, MESES_ES, SCHEDULED_APPS
│ │ ├── middleware.py SimpleAuthMiddleware, QAErrorEmailMiddleware
│ │ ├── decorators.py qa_access_required (testing env gate)
│ │ ├── context_processors.py Notifications injected into all templates
│ │ ├── transactions.py Optimized queryset builders
│ │ ├── templates/ ALL HTML templates (base, pages, emails)
│ │ └── static/ CSS (app.css) + JS (13 modules) + images
│ │
│ ├── students/ People Management
│ │ ├── models.py Student, Parent, StudentParent, Teacher, Group
│ │ ├── forms.py StudentForm, ParentForm, ParentFormSet
│ │ ├── admin.py Custom admin with inlines
│ │ └── urls.py 12 URL patterns
│ │
│ ├── billing/ Financial Management
│ │ ├── models.py SiteConfiguration, EnrollmentType, Enrollment, Payment
│ │ ├── forms.py EnrollmentForm (delegates to service)
│ │ ├── constants.py Pricing seeds, choice tuples
│ │ ├── services/ EnrollmentService, PaymentService, PricingService
│ │ ├── exports.py Excel/CSV builders
│ │ ├── admin.py Payment + Enrollment admin with actions
│ │ ├── urls.py 20 URL patterns
│ │ └── management/commands/ generate_payments, seed_testdata
│ │
│ ├── comms/ Communications
│ │ ├── services/ EmailService + 12 email functions + PDF gen
│ │ ├── tasks.py 6 Celery tasks
│ │ ├── urls.py 10 URL patterns
│ │ └── management/commands/ send_email, test_all_emails
│ │
│ ├── tests/ pytest suite (574 tests, 96 % coverage) — unit/ + integration/
│ └── conftest.py Shared fixtures (models + authenticated_client)
│
├── .github/ CI/CD — see docs/GITHUB.md
│ ├── workflows/
│ │ ├── ci.yml Lint + typecheck + tests on every push/PR
│ │ ├── auto-merge.yml Hourly development → testing merge + PR to main
│ │ ├── codeql.yml Weekly Python security scan
│ │ └── notify-production.yml Email on push to main
│ ├── dependabot.yml Weekly dependency updates
│ └── CODEOWNERS Auto-request reviews from owner accounts
│
├── docs/
│ ├── GITHUB.md Full CI/CD + branch protection reference
│ ├── HTTPS.md HTTPS setup (Docker Nginx + Cloud Run)
│ ├── UV.md UV dependency management guide
│ ├── CELERY.md Celery worker/beat reference
│ └── TODO.md Open tasks
│
├── scripts/ Dev helpers (docker_smoke_test, etc.)
├── backups/ DB dumps from `make backup` (gitignored)
│
├── Dockerfile Multi-stage build (builder + runtime)
├── docker-compose.yml PostgreSQL + Redis + Django + Celery worker + beat
├── docker-compose.testing.yml QA override (Gunicorn, DEBUG=False)
├── Makefile 75+ commands (`make help`)
├── pyproject.toml Dependencies (uv-managed) + tool config
├── uv.lock Reproducible dependency lock
├── entrypoint.sh Docker entrypoint (migrate, collectstatic, start)
├── .env / .env.testing Gitignored — never committed
├── CLAUDE.md AI development context (project rules)
├── DEPLOYMENT.md GCP deployment guide (all 3 environments)
└── README.md This file
Dashboard, authentication, scheduling, and shared utilities. Owns all views and templates.
| Component | Details |
|---|---|
| Models | TodoItem, HistoryLog (1000-entry cap), FunFridayAttendance, ScheduleSlot |
| Views | 13 modules: auth, dashboard, students, parents, payments, management, app_forms, schedule, fun_friday_attendance, todos, support, errors, testing_tools |
| Middleware | SimpleAuthMiddleware — session-based, protects all routes except /login/, /health/, /static/ |
| Templates | base.html (layout), 15+ page templates, 12 email templates, error pages |
| Static | app.css (sidebar/icons), 13 JS modules, logo |
See core/README.md for details.
People management — the foundation app with no external dependencies.
| Component | Details |
|---|---|
| Models | Student (with age calc, withdrawal tracking), Parent (DNI unique), Teacher, Group, StudentParent (M2M through) |
| Forms | StudentForm (birth_date validation), ParentForm (DNI validation), ParentFormSet |
| Admin | StudentAdmin with StudentParentInline, ParentAdmin with ParentStudentInline |
| URLs | 12 patterns: CRUD + search + fun friday attendance |
See students/README.md for details.
Financial management with a dedicated service layer.
| Component | Details |
|---|---|
| Models | SiteConfiguration (singleton pricing), EnrollmentType (plan types), Enrollment (with discount flags), Payment (with overdue detection) |
| Services | EnrollmentService (creation + discounts), PaymentService (generation + calculations), PricingService (centralized config access) |
| Constants | Pricing seeds, ENROLLMENT_TYPE_CHOICES, SCHEDULE_TYPE_CHOICES, PAYMENT_METHOD_CHOICES, etc. |
| Exports | build_database_workbook() → multi-sheet .xlsx |
| Commands | generate_payments --month X --year Y [--dry-run] |
| URLs | 20 patterns: payment CRUD, enrollment API, management, exports |
See billing/README.md for details.
Email communications — no database models, pure service layer.
| Component | Details |
|---|---|
| EmailService | Generic HTML email sender with inline images and attachments |
| Email functions | 12 convenience functions (birthday, welcome, enrollment, payment reminder, receipts, tax cert, etc.) |
| Celery tasks | 6 tasks with retry logic: welcome, birthday (single + batch), payment reminders, generic, enrollment confirmation |
| Commands | send_email --template X [--test], test_all_emails [--only X,Y] |
| URLs | 10 patterns: all email app form views |
See comms/README.md for details.
| Decision | Rationale |
|---|---|
| Views stay in core | Models split across apps, but all views in core/views/ avoids template/URL fragmentation. Each app's urls.py imports from core. |
| Service layer in billing | Business logic (pricing, discounts, payment generation) extracted from forms/views into testable services. |
| SiteConfiguration singleton | All pricing editable from UI. Auto-creates with defaults. No hardcoded prices in views. |
| Session-based auth | SimpleAuthMiddleware with env var credentials. Sufficient for 3-10 users until v1.6. |
| Tailwind CDN | Zero build tools. All utilities available instantly. Custom violet palette in config block. |
| PostgreSQL everywhere | Same database engine in development, testing, and production. Avoids SQLite behavioral differences. |
The main landing page. Shows real-time operational data for the current month.
- Pending payments card — count + student names with amounts. Click count to expand modal with full student list and individual amounts.
- Birthdays card — monthly count with today's birthdays highlighted by name.
- Upcoming events — Fun Fridays and scheduled email sends for the rest of the month, linked to their form views.
- Monthly revenue — expected total (all due this month) vs completed total (paid this month), with payment count.
- Todo list — create tasks with date selector (today / this week's Friday / custom date picker). Overdue items shown in red. Check to complete (deletes + logs to HistoryLog). Sorted by due date.
- History dropdown — lazy-loaded, paginated (20 per page) log of all actions: payments completed, students enrolled, emails sent, config changes.
- Notification bell — badge count of today's due tasks + today's scheduled email sends.
Student management with toolbar, inline actions, and real-time filtering.
- Student table — columns: name, group (color badge), enrollment type, Fun Friday status icon. Rows have
data-*attributes for client-side filtering. - Search — real-time filter by name (client-side, no server round-trip).
- Sort — 4-state cycle: date ascending → date descending → name A-Z → name Z-A.
- Fun Friday toggle — per-row button. States: green check (registered this week), amber check (this + last week), amber X (only last week), grey X (neither). AJAX POST to
/api/students/{id}/fun-friday/toggle/. - Fun Friday filter — 3-state cycle: all → not this week → this week only.
- Type filter — 4-state cycle: all → children only → adults only → language cheque students.
- New student dropdown — choose creation flow: new parent → new student, existing parent → new student, or adult student (no parent).
Multi-step creation form with live price calculator.
- Parent selection — either create new (name, DNI, phone, email, IBAN) or search existing parents with pagination (6 per page).
- Student fields — first name, last name, birth date (validated: not future), school, allergies, GDPR consent, group selector.
- Enrollment plan — dropdown: monthly full-time (2 days/week), monthly part-time (1 day/week), quarterly. Checkboxes: language cheque discount, sibling discount (with sibling search), special/manual price.
- Live price calculator — updates as you change plan/discounts. Shows base price, strikethrough, final price, and breakdown text (e.g., "trimestral incl. -5%, -20 cheque").
- Adult mode — no parent needed, email/phone on student, fixed adult_group pricing.
- On submit — atomic transaction creates: Student → StudentParent link → Enrollment (active) → Payment (enrollment fee, pending) → HistoryLog entry → Celery welcome email task.
- Success page — shows student name, enrollment fee amount. Auto-redirects to student list after 4 seconds. Option to "create sibling" (pre-fills same parent).
- Detail view — personal info, linked parents with contact details, enrollment history (all enrollments, active highlighted), payment history, Fun Friday dates with add/remove.
- Enrollment modality toggle — switch monthly ↔ quarterly via AJAX.
- Update view — same form as create, pre-filled. Saves student changes + finishes old enrollment + creates new enrollment.
Payment management with search, filtering, pagination, and quick-complete.
- Stats bar — 4 cards: expected total, completed total, pending total, overdue total. All for the current period.
- Payment table — columns: student, parent, concept, amount, method, status badge, due date, payment date. Client-side pagination (10 per page).
- Search — real-time filter by student name, parent name, concept, or reference number.
- Status filter — 4-state cycle: all → pending → completed → overdue.
- Type filter — 5-state: all → enrollment → monthly → quarterly → other.
- Quick complete — click a pending status badge → dropdown with 3 payment methods (cash / transfer / card) → one click marks as completed with today's date, logs to history.
- Create payment — autocomplete student search → autocomplete parent search → validates student-parent relationship → select type, method, amount, dates, concept.
- Detail view — read-only display of all payment fields.
- Export — CSV download (all payments) and Excel download (full database: students + enrollments + payments as multi-sheet .xlsx).
Weekly class timetable with drag-and-drop group assignment.
- Grid — 5 columns (Mon-Fri) × 3 time rows × 2 sub-columns. Time slots: 16:10-17:30, 17:40-19:00, 19:10-20:30. Friday: 16:00-17:20.
- Edit mode — toggle button. In edit mode, click any cell → dropdown to assign a group. Saves via AJAX to
/api/schedule/slot/save/. - Cell display — group color, group name, teacher first name, student first names.
Dedicated attendance management for the weekly Fun Friday event.
- Student list — all non-adult active students, grouped by class group.
- Toggle buttons — same icon system as student list. AJAX toggles.
- This week / Last week panels — lists of registered students for each Friday.
- Search, sort, filter — same tools as student list.
Hub page listing all 10 email communication tools. Each follows a consistent pattern:
- Form — fields specific to the email type (dates, activity description, year, etc.)
- Email preview — collapsible panel showing the rendered email HTML. "Refresh" button fetches live preview with current form data via AJAX.
- Test send — sends to
EMAIL_TEST_1/EMAIL_TEST_2env vars for verification before bulk send. - Send — iterates over qualifying parent emails, sends individually, counts success/failures, logs to HistoryLog, shows flash messages.
| App | Email Template | Recipients | Trigger |
|---|---|---|---|
| Fun Friday | fun_friday.html |
Parents with active non-adult students | Weekly, manual |
| Payment Reminder | payment_reminder.html |
Parents with active students | Monthly, manual |
| Vacation Closure | vacation_closure.html |
All parents | Manual |
| Tax Certificate | tax_certificate.html |
Parents with completed payments in year | Yearly (April) |
| Monthly Report | monthly_report.html |
All parents (personalized per parent) | Monthly, manual |
| Birthday | happy_birthday.html |
Parents of today's birthday students | Daily, manual |
| Receipts (child) | receipt_quarterly_child.html |
Parents with active children | Quarterly, manual |
| Receipts (adult) | receipt_adult.html |
Adult students | Monthly, manual |
| Welcome | welcome_student.html |
Parent of new student | On creation (auto) |
| Enrollment | enrollment_child.html / enrollment_adult.html |
Parent of enrolled student | On enrollment |
Admin configuration panel with live editing.
- Pricing config — all fees and discounts from SiteConfiguration. Toggle edit mode → modify values → save via AJAX. Fields: children/adult enrollment fees, full-time/part-time/adult monthly fees, 8 discount types.
- Teachers — create via modal (name, email, phone, admin flag). Validates unique email. Lists active teachers.
- Groups — create via modal (name, color picker, teacher dropdown). Teacher list populated via AJAX from
/api/teachers/. Validates unique name. - Language cheque API —
GET /api/students/language-cheque/returns all students with active language cheque for government reporting.
Paginated read-only tables of all data.
- Students tab — sortable by creation date, ID, first name, last name. Paginated (20 per page).
- Payments tab — sortable by creation date or student name. Paginated (20 per page).
- Excel export button — downloads complete database as
five_a_day_YYYYMMDD.xlsx.
Standalone page with custom styling (does not extend base.html).
- Credentials login — username/password from
LOGIN_USERNAME/LOGIN_PASSWORDenv vars. - Google OAuth — optional. Button shown if
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETare configured. Validates email matchesGOOGLE_ALLOWED_EMAIL. Stores Google credentials in session for Gmail/Sheets API access. - Session — sets
is_authenticated=Trueandusernamein Django session. 24-hour expiry.
| Metric | Value |
|---|---|
| Total tests | 574 |
| Test files | 32 (17 unit + 15 integration) |
| Coverage | 96% |
| Coverage thresholds | ≥ 90% (target, no warning) / 75-89% (CI warning, pre-commit still blocks below 75) / < 75% (CI fails, pre-commit rejects the commit) |
| Runtime | ~19 seconds (8 parallel workers via pytest-xdist) |
| Database | PostgreSQL (same as production) — always use make test |
| Framework | pytest 9 + pytest-django + pytest-cov + pytest-xdist + pytest-randomly |
| Type checking | mypy + django-stubs (pre-commit hook) |
| Security | bandit security linter (pre-commit hook) |
| Dependency audit | pip-audit for CVE scanning |
| Linting | Ruff (check + format) via pre-commit hooks |
| Settings | project/settings_test.py |
| Fixtures | conftest.py — 15 shared fixtures |
make test # Inside Docker (PostgreSQL, parallel, with coverage)
make test-unit # Only unit tests (tests/unit/)
make test-integration # Only integration tests (tests/integration/)
make test-cov-gate # Same as make test + fails if coverage < 75%
# (invoked by the pytest-coverage pre-commit hook)
make test-local # Local against Docker PostgreSQL
make test-sqlite # Local with SQLite (no Docker)
make test-coverage # Generate HTML coverage report
make test-fast # Stop on first failure
make test-k K=payment # Run tests matching keywordCoverage gates at every stage of the pipeline:
| Stage | What enforces it | Behavior |
|---|---|---|
| Pre-commit | pytest-coverage hook in .pre-commit-config.yaml → make test-cov-gate |
Runs full suite inside Docker; rejects commit if coverage < 75%. Bypass with git commit --no-verify if containers are down (CI will still catch it). |
| CI (GitHub Actions) | Check coverage threshold step in ci.yml after Run tests |
Parses coverage.xml. < 75% fails the job with an ::error:: annotation. 75-89% passes with a ::warning:: annotation (visible in the PR checks UI). ≥ 90% silent pass. |
| Local dev | pyproject.toml [tool.coverage.report].fail_under = 75 |
Applies to any tool that reads the coverage config (e.g. coverage report standalone). Same 75% floor as the other stages. |
Raise fail_under in pyproject.toml and the FLOOR / TARGET values in ci.yml as coverage grows.
Tests split cleanly into two directories, each with a 1:1 file-to-source-module mapping:
- project/tests/unit/ — direct-call tests. No HTTP stack, no URL resolver, no template rendering. Service-layer, pure-function, Celery-task, model, and helper tests live here. Tests that exercise view-object internals via
RequestFactoryalso belong here. - project/tests/integration/ — full HTTP-stack tests. Django's test client sends real requests through
SimpleAuthMiddleware→ URL resolver → view → template renderer and back. Uses theauthenticated_clientfixture.
Within each file, related tests are grouped into classes. Where a large file absorbed "extra" content or gap-filling edge cases, a # === comment divider marks the section and a separate class name (e.g. TestEmailServiceExtra, TestStudentCreateViewErrors) keeps the cohesion visible at a glance. Shared fixtures live in project/conftest.py; pytest discovers both subdirectories automatically.
| File | Count | Coverage |
|---|---|---|
unit/test_models.py |
48 | Every model across students, billing, core — properties (full_name, age, is_overdue, remaining_amount, is_paid), __str__, unique constraints, FK behavior, academic-year helpers (current_academic_year, academic_year_start_date, academic_year_end_date), SiteConfiguration singleton, HistoryLog cap + debounce |
unit/test_services.py |
27 | PricingService (all fee + discount combos), EnrollmentService (all plans, language cheque, sibling, both, minimum-amount floor, adult enrollment, edge cases), PaymentService (monthly + quarterly amounts, June bonus, academic month/quarter validation, payment completion), service error paths |
unit/test_student_view_internals.py |
25 | StudentUpdateView view-object method branches (quarterly, part-time, no enrollment, exception-handling) via RequestFactory to sidestep missing template, plus unreferenced helper functions handle_student_form, student_detail, update_student called directly |
unit/test_tasks.py |
24 | Celery tasks called synchronously with email_service mocked: send_welcome_email_task (parent + adult-student + missing + failure paths), send_birthday_email_task, send_birthday_emails_task, send_payment_reminders, send_generic_email_task, send_enrollment_confirmation_task (success + missing + attachments + failure) |
unit/test_email_service.py |
18 | EmailService.send_email: string + list recipients, CC/BCC, attachments, inline images (existing + missing path), fail_silently on and off, exception-raises-when-not-silent, send_bulk_emails mixed success/failure, get_email_config |
unit/test_email_functions.py |
17 | All convenience wrappers (send_birthday_email, send_welcome_email, send_payment_reminder, send_monthly_report, send_enrollment_confirmation_email, send_quarterly_receipt_email, send_fun_friday_email, send_vacation_closure_email, send_tax_certificate_email, send_all_tax_certificates) plus tax-certificate PDF generation branches |
unit/test_context_processors.py |
13 | today_notifications: expected keys, todos due today vs other day, scheduled apps on Friday vs Monday, monthly apps excluded on day 15, history count, unauthenticated early-return |
unit/test_constants.py |
13 | Pure functions: calculate_discount (flat/percentage/invalid/edge), get_monthly_fee_by_schedule, get_enrollment_fee |
unit/test_transactions.py |
10 | Query helpers: get_active_students, get_payments_for_last_two_school_years, get_all_payments_unrestricted — ordering, select_related, school-year filtering |
unit/test_forms.py |
9 | EnrollmentForm validation + create_enrollment() delegation to EnrollmentService (quarterly, monthly full/part, manual amount, sibling checkbox, adult, below-minimum rejection) |
unit/test_student_forms.py |
7 | StudentForm + ParentForm validation: future birth date rejected, DNI minimum length, required fields, both date formats |
unit/test_exports.py |
7 | Excel workbook generation via openpyxl: Students, Enrollments, Payments sheets + combined workbook; empty-database edge case |
unit/test_payment_helpers.py |
7 | parse_date_value (6 formats including invalid) + payment_detail AJAX helper called directly via RequestFactory |
unit/test_decorators.py |
5 | @qa_access_required: allow when IS_TESTING_ENV + QA_TESTING_USERNAME + session.username all match, 404 on any missing condition |
unit/test_error_handlers.py |
5 | handler400/handler403/handler404/handler405/handler500 render with correct status codes |
unit/test_qa_error_middleware.py |
5 | QAErrorEmailMiddleware.process_exception via RequestFactory: pass-through, disabled config, no support email, send success, send failure swallowed |
unit/test_testing_tools_helpers.py |
2 | _git_info helper: success path + non-zero returncode branch with subprocess.run mocked |
| File | Count | Coverage |
|---|---|---|
integration/test_app_form_views.py |
97 | Every email form GET page, POST action=preview (JSON HTML), test_send with/without EMAIL_TEST_* env vars, main send-to-parents for every form (fun_friday, payment_reminder, vacation_closure, tax_certificate, monthly_report, birthday, receipts × 3, newsletter, enrollment/welcome), invalid-date fallbacks, missing-field errors, no-parents-with-email edge cases, per-recipient exception swallowing, welcome_form redirect |
integration/test_views.py |
54 | Cross-cutting top-level HTTP coverage: auth flow, dashboard, all_info, student/parent list + detail + create + search, payment list + create + detail + CRUD + stats + CSV + validation, todos + history API, management admin, email form pages (parametrized), enrollment API, error pages (parametrized), schedule, Fun Friday, support |
integration/test_payment_views.py |
37 | All HTTP payment endpoints: list (search, stats), create (+ invalid parent + unexpected exception), detail-view (+ 404), update (JSON + FormData + all error branches), delete (success + exception 500), deactivate (success + exception 400), quick-complete (success + invalid method + broken JSON), get-details (success + exception), search payments/parents (short query + hits), validate student-parent (all branches), export DB to Excel |
integration/test_student_views.py |
22 | StudentListView (search, exclude inactive, context), StudentDetailView (parents visible, 404), StudentCreateView (form + adult mode + success + full POST + error paths including invalid parent, existing-parent mode, create_sibling flag, email-task swallow), search_students FBV |
integration/test_testing_tools.py |
20 | QA dashboard /testing/ gated by @qa_access_required (via override_settings): dashboard renders + git failure handled, api_seed_database (success + reset + command error 500 + non-QA 404), api_create_backlog_task (all branches + email send/swallow), api_update_backlog_task (success + invalid status + 404), api_toggle_error_email (on + off + bad JSON) |
integration/test_management_views.py |
19 | gestion_view + update_site_config (all fields + bad JSON), create_teacher (success + duplicate + missing field + bad JSON), create_group (success + missing fields + duplicate + nonexistent teacher + bad JSON), api_get_teachers, update_enrollment_modality (success + invalid + no enrollment + student not found), language_cheque_students |
integration/test_schedule_views.py |
13 | Schedule page (groups + slots in context), save_schedule_slot (assign + clear + reject GET + invalid JSON), Fun Friday page (loads, excludes adults, with attendance) |
integration/test_auth_oauth.py |
13 | OAuth callback flow with google_auth_oauthlib.flow.Flow mocked: state missing, state mismatch, fetch_token failure, id-token verification failure, email whitelist mismatch, successful session establishment; login view extras (already-auth redirect, missing env, OAuth-available flag); logout clears session |
integration/test_dashboard_views.py |
11 | home view quote-cookie branches (valid cookie, corrupt cookie → API, API failure, API empty, [AUTH] placeholder filtered, with pending payments), all_info sort variants (default, first_name, last_name, id_asc, payments_sort=student_asc) |
integration/test_parent_views.py |
8 | ParentCreateView: GET renders, POST new + existing DNI + invalid + exception-triggers-form-invalid |
integration/test_todo_views.py |
8 | create_todo (missing text + missing date + invalid date + success), complete_todo, history_list (default + offset + invalid offset) |
integration/test_middleware.py |
8 | SimpleAuthMiddleware: public paths (login, health, static, media, OAuth prefix), protected paths redirect to login, authenticated requests pass |
integration/test_auth_views.py |
7 | Login view: render for unauth'd, redirect for authenticated, valid + invalid credentials, logout, OAuth redirect (no creds → login) |
integration/test_fun_friday_attendance_views.py |
6 | toggle_fun_friday_this_week (adult rejected + toggle on/off), add_fun_friday_attendance (success + invalid), remove_fun_friday_attendance (success + invalid) |
integration/test_support_views.py |
5 | submit_support_ticket: success (send_mail called), short message rejected, no support email configured → 500, bad JSON, unexpected exception |
| File | Stmts | Miss | Cover | Missing lines |
|---|---|---|---|---|
billing/models.py |
143 | 7 | 95% | 282-292, 361, 366 |
billing/services/enrollment_service.py |
69 | 5 | 93% | 97, 107-110, 140 |
billing/services/payment_service.py |
55 | 3 | 95% | 38, 41, 48 |
comms/services/email_functions.py |
97 | 5 | 95% | 514-516, 559-560 |
comms/services/email_service.py |
59 | 6 | 90% | 55, 118-122 |
core/context_processors.py |
25 | 5 | 80% | 15-16, 22, 33-34 |
core/middleware.py |
50 | 2 | 96% | 51-52 |
core/models.py |
91 | 4 | 96% | 48, 61, 147, 167 |
core/transactions.py |
19 | 1 | 95% | 28 |
core/views/app_forms.py |
615 | 46 | 93% | 51, 138-140, 152-154, 168-171, 188-189, 211, 267-268, 291-292, 342-344, 375, 527-529, 535, 673, 695, 714, 788, 792, 814, 825, 901, 927, 940, 953, 965, 976, 987, 1098, 1144, 1172-1173, 1201-1202 |
core/views/auth.py |
103 | 11 | 89% | 65, 69-85, 100, 129 |
core/views/dashboard.py |
111 | 6 | 95% | 104-111, 152, 168 |
core/views/parents.py |
26 | 3 | 88% | 24-29 |
core/views/payments.py |
226 | 4 | 98% | 320-321, 349-350 |
core/views/schedule.py |
59 | 1 | 98% | 45 |
core/views/students.py |
298 | 11 | 96% | 53-54, 96, 181-183, 289, 506, 509-511 |
students/models.py |
88 | 1 | 99% | 135 |
42 files have 100% coverage (skipped above). Total coverage: 96% across 2,809 statements. Coverage is very good. Coverage is enforced at three levels: pre-commit hook (≥ 75%), CI hard floor (≥ 75%), and CI warning (< 90%).
All migrations were regenerated from scratch during the v1.0.0 multi-app split.
| App | Migration | Changes | Depends On |
|---|---|---|---|
students |
0001_initial |
Teacher, Group, Parent, Student, StudentParent | — |
students |
0002 |
Student gender field, StudentParent UniqueConstraint | students.0001 |
billing |
0001_initial |
SiteConfiguration, EnrollmentType, Enrollment, Payment | students.0001 |
billing |
0002 |
Enrollment academic_year index | billing.0001, students.0002 |
core |
0001_initial |
TodoItem, HistoryLog, FunFridayAttendance, ScheduleSlot | students.0001 |
core |
0002 |
UniqueConstraint for FunFridayAttendance and ScheduleSlot (replaces unique_together) | core.0001, students.0002 |
comms |
— | (no models) | — |
# After modifying models:
make makemigrations # Creates migrations for all 4 apps
make migrate # Applies themThis section documents every security decision, mechanism, and configuration in the project.
Mechanism: Custom session-based authentication with two backends — environment credentials and Google OAuth 2.0.
| Component | File | How it works |
|---|---|---|
| Login view | core/views/auth.py |
Validates username/password against LOGIN_USERNAME/LOGIN_PASSWORD env vars. Sets request.session["is_authenticated"] = True. No hardcoded fallbacks — if env vars are missing, login is refused with an error message. |
| Google OAuth | core/views/auth.py |
Full OAuth 2.0 code flow via google-auth-oauthlib. State token stored in session and verified on callback. ID token verified server-side via Google's public keys. Only the email matching GOOGLE_ALLOWED_EMAIL (or EMAIL_HOST_USER / DJANGO_SUPERUSER_EMAIL) is authorized. |
| Auth middleware | core/middleware.py |
SimpleAuthMiddleware protects all routes. Public URLs use exact match for /login/ and prefix match for /health/, /static/, /media/, /auth/google/ (covers /callback/). All other paths require session["is_authenticated"]. |
| OAuth credentials | core/views/auth.py |
Google tokens (access, refresh) are stored in session server-side. client_secret is never sent to the frontend. Allowed email check is backend-only. |
Design decisions:
- No Django User model — the system has 3-10 trusted admin users, so session-based auth with env var credentials is simpler and sufficient.
- Google OAuth is optional — if
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETare not set, the OAuth button is hidden. OAUTHLIB_INSECURE_TRANSPORTis only set whenDEBUG=True(for local HTTP testing).
All cookie flags are enforced via settings.py with environment-aware defaults:
| Setting | Development | Production | Purpose |
|---|---|---|---|
SESSION_COOKIE_AGE |
86400 (24h) | 86400 (24h) | Session lifetime |
SESSION_COOKIE_HTTPONLY |
True |
True |
Prevents JavaScript access to session cookie |
SESSION_COOKIE_SAMESITE |
Lax |
Strict |
Prevents cross-site request forgery via session cookies |
SESSION_COOKIE_SECURE |
False |
True |
Requires HTTPS for cookie transmission |
CSRF_COOKIE_HTTPONLY |
False |
True |
Prevents JavaScript access to CSRF cookie in production |
CSRF_COOKIE_SAMESITE |
Lax |
Strict |
Prevents cross-site CSRF cookie leakage |
CSRF_COOKIE_SECURE |
False |
True |
Requires HTTPS for CSRF cookie |
Production defaults are applied automatically when DEBUG=False — no manual override needed in env vars.
- Django's
CsrfViewMiddlewareis active in the middleware stack. - All POST endpoints receive CSRF validation. JavaScript AJAX requests use
getCsrfToken()(reads from cookies) and send viaX-CSRFTokenheader. CSRF_TRUSTED_ORIGINSis configured per deployment via theCSRF_TRUSTED_ORIGINSenv var (see DEPLOYMENT.md).- Only exception:
@csrf_exempton/health/endpoint (GET-only, returns{"status": "healthy"}).
When DEBUG=False, the following are enforced via settings.py:
| Setting | Value | Effect |
|---|---|---|
SECURE_SSL_REDIRECT |
True |
All HTTP requests redirected to HTTPS |
SECURE_HSTS_SECONDS |
31536000 (1 year) |
Browser remembers to use HTTPS |
SECURE_HSTS_INCLUDE_SUBDOMAINS |
True |
HSTS applies to all subdomains |
SECURE_HSTS_PRELOAD |
True |
Eligible for browser HSTS preload lists |
All settings are environment-controlled and only activate when DEBUG=False.
| Header | Setting | Value | Effect |
|---|---|---|---|
X-Frame-Options |
X_FRAME_OPTIONS |
DENY |
Prevents clickjacking — page cannot be embedded in iframes |
X-Content-Type-Options |
SECURE_CONTENT_TYPE_NOSNIFF |
True |
Prevents MIME type sniffing attacks |
X-XSS-Protection |
SECURE_BROWSER_XSS_FILTER |
True |
Enables browser XSS filter (legacy, supplementary) |
| Decision | Implementation |
|---|---|
| Non-root container | Dockerfile creates user django (uid 1000) and runs as USER django |
| Multi-stage build | Builder stage compiles dependencies; runtime stage uses python:3.12-slim without build tools |
| No secrets in image | .dockerignore excludes .env*, scripts/, .git/ |
| DB port restricted | docker-compose.yml binds PostgreSQL to 127.0.0.1:5432 only (not exposed to network) |
| Health checks | Database has auth-checking healthcheck; web service uses /health/ endpoint |
| Seed script guard | scripts/reset_seed_dev_data.py aborts if DJANGO_ENV=production or DEBUG=False |
Full deployment walkthrough in DEPLOYMENT.md. Security-relevant decisions:
| Decision | Implementation |
|---|---|
| Secret Manager | All credentials (DJANGO_SECRET_KEY, LOGIN_*, EMAIL_SECRET, POSTGRES_*, GOOGLE_*) injected at startup from GCP Secret Manager |
| Cloud SQL Auth Proxy | PostgreSQL connection goes through the proxy — no public IP on the database |
| Autoscaling | min=0 (cold starts acceptable) or min=1 (~$7/mo) for always-warm, max=2 instances |
| Probes | Startup probe + liveness probe on /health/ |
| TLS | Managed automatically by Cloud Run (custom domain + Google-managed certificate) |
| SSL enforced | SECURE_SSL_REDIRECT=True, all cookie secure flags enabled when DEBUG=False |
| Strict cookies | SESSION_COOKIE_SAMESITE=Strict, CSRF_COOKIE_SAMESITE=Strict, CSRF_COOKIE_HTTPONLY=True |
| Rule | Implementation |
|---|---|
| No hardcoded credentials | auth.py requires LOGIN_USERNAME/LOGIN_PASSWORD env vars — refuses login if missing |
| No secrets in YAML | Production credentials live in GCP Secret Manager, injected into Cloud Run at startup — never in the repo |
| No secrets in GitHub Actions for deploy | CI uses only non-production Gmail SMTP + Codecov upload token. Production deploy runs manually with the operator's gcloud credentials |
| No secrets in Docker image | .dockerignore excludes all .env* files |
.gitignore coverage |
.env* pattern excludes all env file variants |
| Production startup validation | settings.py raises ValueError if SECRET_KEY is the dev default and DEBUG=False |
| Decision | Implementation |
|---|---|
| TLS enforced | EMAIL_USE_TLS=True, port 587 (STARTTLS) |
| App Password | Uses Gmail App Password (not account password) via EMAIL_SECRET env var |
fail_silently |
Defaults to False for single sends (raises on failure); True for bulk sends (logs failures) |
| No PII in logs | Celery tasks log by ID (student_id=X) not by name/email/DNI |
| Template auto-escaping | All email templates use Django's default auto-escaping — {{ variable }} is HTML-safe |
| Inline images | Attached via MIME Content-ID headers, not external URLs |
| Layer | Mechanism |
|---|---|
| Models | DecimalField with MinValueValidator for all money fields. UniqueConstraint for enrollment/schedule/attendance integrity. PROTECT on foreign keys prevents orphaned records. |
| Forms | Django ModelForm with clean_*() validators. Date fields accept %Y-%m-%d and %d/%m/%Y. DNI validated for minimum length. |
| Views | get_object_or_404 for safe lookups. @require_http_methods on all AJAX endpoints. Decimal(str(...)) for safe numeric conversion. json.JSONDecodeError caught explicitly. |
| Services | transaction.atomic() wraps multi-model writes (enrollment creation, payment completion). ValueError raised for missing config. |
| GDPR | gdpr_signed field on Student. No student data exposed without authentication. PII removed from log messages. |
- Console logging via
StreamHandlerwith configurableLOG_LEVELenv var. - Separate loggers for
djangoframework and project modules. HistoryLogmodel tracks user actions (payment completed, student enrolled, config updated) — capped at 1000 entries with automatic cleanup.- Celery tasks log by entity ID, not PII.
These are not blockers but would strengthen the system for scale or compliance:
| Priority | Improvement | Why |
|---|---|---|
| High | Rate limiting on login (django-ratelimit, 5 attempts/15 min per IP) |
Prevents brute force. Currently no protection. |
| High | Content-Security-Policy header | Prevents XSS. Currently absent — Tailwind CDN requires unsafe-inline for styles, but scripts can be locked down. |
| High | Referrer-Policy header (strict-origin-when-cross-origin) |
Prevents referrer leakage to external links. Currently absent. |
| Medium | Session rotation on OAuth login (request.session.create()) |
Prevents session fixation. Currently session ID persists through OAuth flow. |
| Medium | Inactivity timeout (30 min idle logout) | 24h session is long for sensitive student data. |
| Medium | Security event audit log (failed logins with IP, CSRF failures) | Currently no visibility into attack attempts. |
| Medium | Permissions-Policy header | Disables camera, microphone, geolocation APIs the app doesn't need. |
| Medium | Argon2 password hasher (if Django User model is ever adopted) |
Stronger than default PBKDF2. |
| Low | Request ID tracking (X-Request-ID middleware) |
Enables log correlation across services. |
| Low | detect-secrets pre-commit hook |
Prevents accidental secret commits in the future. |
| Low | Migrate to OAuth-only (deprecate password login) | Reduces credential attack surface to zero. |
| Low | Web Application Firewall (WAF) rules at cloud provider level | Blocks common attack patterns before they reach Django. |
This section is for testers, teachers, and anyone helping us try out the application before it goes live. You do not need to be a programmer to use the testing environment. If something looks wrong or confusing, that is exactly the kind of feedback we need.
The testing environment is a copy of the real application that runs on the internet, just like the final version will. It looks and works exactly the same, but it uses fake data — fake students, fake parents, fake payments. Nothing you do here affects real people or real money.
Think of it as a rehearsal stage: you can click anything, try any feature, and even break things. We can always reset it.
| Web address | (will be provided once deployed on GCP) |
| Username | See .env.testing → LOGIN_USERNAME |
| Password | See .env.testing → LOGIN_PASSWORD |
The login credentials are stored in the .env.testing file and are never committed to the repository. Ask the development team if you need them.
- Open the web address in your browser (Chrome, Firefox, Safari, or Edge all work).
- You will see a login page. Type the username and password you were given.
- After logging in you will see the Dashboard — the home screen with today's tasks, pending payments, and birthdays.
Here is a quick checklist of things to try. If anything does not work, take note of what happened and tell the development team.
- Dashboard — Does it load? Do the numbers make sense?
- Students — Can you see the list of students? Open a student's profile? Search by name?
- Create a student — Fill in the form and save. Does the new student appear in the list?
- Payments — Open the payments page. Try marking a payment as completed. Try filtering by status.
- Schedule — Open the weekly schedule. Can you see groups assigned to time slots?
- Fun Friday — Toggle a student's attendance on or off.
- Email forms (Apps section) — Open each email form. You do not need to send real emails; just verify the forms load correctly.
- Management — Can you update the site configuration (pricing)? Create a teacher or group?
- General navigation — Does the sidebar work? Do all links go to the right page? Is the text readable?
- Testing Tools (the blue "info" icon at the bottom of the sidebar) — This is your QA control panel:
- Project Info — shows the current software version, last commit, server status
- Error Reporting toggle — turn this ON so every server error is automatically emailed to the development team with full details
- Database Seeding — click to populate the database with test data, or wipe and start fresh
- QA Backlog — report bugs and suggestions directly from this page; each new task is emailed to the development team
When something goes wrong, please note:
- What page you were on — copy the web address from your browser's address bar, or describe the page ("I was on the payments list").
- What you did — "I clicked the green Complete button on a payment" or "I searched for a student named Sofia".
- What happened — "The page showed an error" or "Nothing happened" or "It showed the wrong information".
- Screenshot — If possible, take a screenshot (press the Print Screen key or use the Snipping Tool on Windows).
Send this information to the development team. Even a short message like "The payments page shows an error when I click Export" is helpful.
| Page | What it means |
|---|---|
| Login page (you are sent back to login) | Your session expired. Just log in again. |
| Page not found (404) | You followed a link that does not exist. Go back to the Dashboard. |
| Server error (500) | Something broke inside the application. This is a bug — please report it. |
| Forbidden (403) | The application blocked your action for security reasons. Try logging in again. |
The testing environment mirrors production:
| Setting | Value | Why |
|---|---|---|
DEBUG |
False |
Hides technical details from error pages, same as production |
DJANGO_ENV |
testing |
Like production (collectstatic, Gunicorn, secure cookies) but enables the /testing/ dashboard |
| Server | Gunicorn (2 workers) | Same as production (not Django's development server) |
| HTTPS cookies | Secure=True, SameSite=Strict |
Same cookie policy as production |
| HTTPS | Via Nginx reverse proxy (local) or Cloud Run (GCP) | See HTTPS.md for full setup guide |
SECURE_PROXY_SSL_HEADER |
Trusts X-Forwarded-Proto from reverse proxy |
Enables Django to detect HTTPS behind Nginx/Cloud Run |
| Database | PostgreSQL 16 (separate volume) | Isolated from the development database |
| Login | Credentials in .env.testing |
Dedicated QA credentials, never committed to git |
| Admin panel | /admin/ — credentials in .env.testing |
Django admin for inspecting raw data |
Configuration files:
| File | Purpose |
|---|---|
.env.testing |
All environment variables for QA (credentials, database, security flags) |
docker-compose.testing.yml |
Docker override that switches to Gunicorn and uses a separate database volume |
seed_testdata command |
Populates the database with realistic fake data |
HTTPS.md |
Full guide for HTTPS setup with Docker (Nginx + self-signed cert) and GCP Cloud Run |
/testing/ |
In-app QA dashboard with project info, seeding, backlog, and error reporting toggle |
core/decorators.py |
qa_access_required decorator — reusable access gate for QA-only views |
The testing dashboard and all its API endpoints are protected by three conditions that must all be true:
| Condition | Setting | Where it's checked |
|---|---|---|
Environment is testing |
DJANGO_ENV=testing |
settings.IS_TESTING_ENV |
| Debug is off | DJANGO_DEBUG=False |
settings.IS_TESTING_ENV |
| User matches QA username | QA_TESTING_USERNAME in .env.testing |
core/decorators.py via session |
If any condition fails, the page returns 404 Not Found (not 403) so the URL appears not to exist. The sidebar icon is also hidden — controlled by the show_testing_tools context variable injected by core/context_processors.py.
This means:
- In development (
DEBUG=True): the page doesn't exist, no sidebar icon. - In production (
DJANGO_ENV=production): the page doesn't exist, no sidebar icon. - In testing with a non-QA user: the page doesn't exist, no sidebar icon.
- In testing with the QA user (
manitas): full access, sidebar icon visible.
The QA username is configured in .env.testing (never hardcoded) via QA_TESTING_USERNAME. To grant another user access, change the value in the env file.
Running locally (for developers):
# Start the QA environment
make testing-up
# Populate with test data (students, parents, payments, etc.)
make testing-seed
# Wipe everything and re-seed from scratch
make testing-reset
# View logs
make testing-logs
# Stop the environment
make testing-down
# Full rebuild (after code changes)
make testing-rebuildThe seed_testdata command creates:
- 3 teachers, 5 groups
- 6 parents, 12 child students, 3 adult students, 1 inactive student
- Active enrollments with monthly and quarterly payment plans
- Payments in various states (completed, pending, overdue)
- Schedule slots, todo items, and history log entries
Use --reset to wipe and re-seed, or --small for a minimal dataset (6 children only).
Deploying the QA environment — see DEPLOYMENT.md for the full GCP plan. Testing runs on a Compute Engine e2-micro (free tier) with Docker Compose, while production uses Cloud Run + Cloud SQL.
The project runs a fully automated CI/CD pipeline on GitHub Actions. Every push is tested, every merge is audited, and production is reached only through a protected pull request. The full configuration reference is in docs/GITHUB.md — this section is the overview.
Push to development
│
▼
CI runs (lint + typecheck + tests) + CodeQL
│
│ hourly cron
▼
Auto-merge check
• development ahead of testing?
• last commit ≥ 24 h old?
• CI passing on that commit?
• version bumped in pyproject.toml (dev > testing)?
│ all yes
▼
git merge development → testing
(commit: "YYYY-MM-DD - <last commit message>")
│
├── CI re-runs on testing
└── PR created: testing → main
│
▼
Email to owners (OWNER_EMAILS)
│
▼
Manual review + Code Owner approval
│
▼
Merge to main (protected — all checks required)
│
▼
Email to hellofiveaday@gmail.com
(production deploy ready)
| Branch | Purpose | Protected | Direct push |
|---|---|---|---|
main |
Production. Every commit is deployable. | Full protection | No (PR + review only) |
testing |
Staging. Auto-merged from development. | Minimal (no force/delete) | Only from auto-merge flow |
development |
Active development. Day-to-day work. | None | Yes |
Feature branches off development are welcome for non-trivial work, but the expected flow is: work on development → wait 24 h → auto-promoted to testing → manual merge to main.
| Workflow | File | Triggers | Purpose |
|---|---|---|---|
| CI | ci.yml |
Push to development/testing/main; PRs to testing/main |
Three parallel jobs — Lint (Ruff + Bandit), Type check (mypy), Tests (pytest + PostgreSQL 16 service container + Codecov upload) |
| Auto-merge | auto-merge.yml |
Hourly cron + manual dispatch | Merges development → testing when conditions pass, creates PR to main, emails owners |
| CodeQL | codeql.yml |
Push to main/testing/development; PRs to main; Monday 04:30 UTC |
Python static security analysis (OWASP Top 10, Django-specific queries) |
| Notify production | notify-production.yml |
Push to main |
Emails hellofiveaday@gmail.com with commit info and gcloud deploy instructions |
| Dependabot | dependabot.yml |
Weekly (Mondays 08:00 Madrid) | Grouped Python and GitHub Actions updates targeting development |
Concurrent CI runs on the same branch cancel each other automatically — new pushes always produce a fresh run.
1. You push to development
- CI triggers immediately (lint, typecheck, tests run in parallel, ~2-4 min)
- CodeQL triggers immediately (weekly scan also runs independently)
- The hourly auto-merge cron promotes to
testingonly when all four conditions hold: dev is ahead of testing, the last commit is ≥ 24 h old, CI is green, and the version inpyproject.tomlhas been bumped (strictly higher thantesting's version). Without a version bump the merge is skipped even with 24 h of new commits on dev — runmake pc-run(answer yes) ormake version x.y.zbefore the next tick to unlock it.
2. Auto-merge fires
- Creates a
--no-ffmerge commit ontestingtitledYYYY-MM-DD - <your last commit message> - Pushes to
testing(which triggers CI ontesting) - Creates and pushes an annotated staging tag
testing-vX.Y.Zon the new testing merge commit - Opens PR
testing → mainif one is not already open (title matches the merge commit) - Sends an HTML email to
OWNER_EMAILSwith version bump, staging tag, and a "Review PR" button
2b. You merge the PR → release tag on main
notify-production.ymlreadsversionfrompyproject.tomlonmain's new HEAD- Creates and pushes an annotated release tag
vX.Y.Zon that commit (skipped if tag already exists) - Sends an HTML email to
hellofiveaday@gmail.comwith the release tag andgclouddeploy steps
The two tag namespaces (testing-vX.Y.Z and vX.Y.Z) are fully independent — the testing → main PR can use any merge strategy (merge commit, squash, or rebase) because the release tag is derived from pyproject.toml, not from commit SHA continuity.
3. You review and merge the PR
- All required checks must pass (Lint, Type check, Tests, CodeQL alerts, Code Owner approval)
- You cannot approve your own PR — the second owner account approves
- On merge,
mainis updated
4. Production notification fires
notify-production.ymlsends an email tohellofiveaday@gmail.com- Email contains commit info, file-change summary, and the exact
gcloudcommands to deploy to Cloud Run
Configure at Settings → Branches → Add ruleset, target main:
Required status checks (names must match CI job names exactly):
| Check | Workflow |
|---|---|
Lint |
ci.yml |
Type check |
ci.yml |
Tests |
ci.yml |
Analyze Python |
codeql.yml |
Protection rules (every item below enabled):
| Rule | Setting |
|---|---|
| Require a pull request before merging | ✓ |
| Required approvals | 1 (higher if you add collaborators) |
| Dismiss stale reviews when new commits are pushed | ✓ |
| Require review from Code Owners | ✓ |
| Require status checks to pass | ✓ |
| Require branches to be up to date before merging | ✓ |
| Require conversation resolution before merging | ✓ |
| Require signed commits | ✓ (strongly recommended for a public repo) |
| Require linear history | ✓ (enforces squash/rebase merges) |
| Restrict who can push to matching branches | ✓ |
| Do not allow bypassing the above settings | ✓ (admins follow the same rules) |
| Allow force pushes | ✗ |
| Allow deletions | ✗ |
testing needs direct pushes from the auto-merge workflow, so PR requirements are not enforced. Apply only safety rails:
| Rule | Setting |
|---|---|
| Require a pull request before merging | ✗ |
| Allow force pushes | ✗ |
| Allow deletions | ✗ |
| Require status checks to pass (optional) | ✓ — lets CI block a broken auto-merge from polluting testing further |
Because this repository is public, extra care is taken to prevent accidental secret leaks, abuse of the CI, and unreviewed contributions:
| Control | Where | Why |
|---|---|---|
| GitHub Secret Scanning | Settings → Code security | Free for public repos — detects committed secrets across history |
| Push Protection | Settings → Code security | Free for public repos — blocks pushes that contain secrets before they land |
| CodeQL | codeql.yml + Settings → Code security |
Free for public repos — weekly security analysis |
| Dependabot alerts + security updates | Settings → Code security | Free for public repos — fixes known CVEs in dependencies |
| Require 2FA for all contributors | Organization settings (if in an org) | Prevents compromised account pushes |
| Restrict fork PRs from running CI with secrets | Settings → Actions → Fork PR workflows: require approval for first-time contributors | Prevents secret exfiltration via malicious PRs from forks |
| Actions allow-list | Settings → Actions → Allow specific actions | Prevents supply-chain attacks — pin to verified creators only |
| Workflow permissions default: read-only | Settings → Actions → Workflow permissions | Individual workflows explicitly request write where needed |
| Block workflows from approving PRs | Settings → Actions → Allow GitHub Actions to create and approve pull requests: only allow create, not approve | Humans must approve, even automated PRs |
| SECURITY.md | Root of the repo | Public disclosure policy so researchers know how to report vulnerabilities privately |
| License file | Root of the repo | Required for a public repo — defines what others can legally do with the code |
The .env file is gitignored and never committed. Production secrets live in GCP Secret Manager (see DEPLOYMENT.md), not in the repository or in GitHub Secrets. GitHub Secrets are used only for CI operations (sending notification emails, uploading coverage).
Configure at Settings → Secrets and variables → Actions:
| Secret | Required by | Purpose |
|---|---|---|
GH_PAT |
auto-merge.yml | Fine-grained Personal Access Token. Pushes to testing and creates PRs while triggering downstream CI (which the default GITHUB_TOKEN cannot do). Permissions: Contents RW, Pull requests RW, Checks R, Metadata R |
EMAIL_HOST_USER |
auto-merge.yml, notify-production.yml | Gmail address used to send notification emails |
EMAIL_SECRET |
auto-merge.yml, notify-production.yml | Gmail App Password — can be the same one the application uses for transactional email |
OWNER_EMAILS |
auto-merge.yml | Comma-separated recipient list for the development → testing merge notification |
CODECOV_TOKEN |
ci.yml | Optional — only needed for private repos. Public repos push coverage anonymously |
Rotate GH_PAT annually. Without it, the auto-merge falls back to the default GITHUB_TOKEN, which cannot trigger CI on PRs it creates — breaking the pipeline silently.
| Event | Recipient | Sent by |
|---|---|---|
development → testing merged + PR opened to main |
OWNER_EMAILS (secret) |
auto-merge.yml |
New commit on main (production ready to deploy) |
hellofiveaday@gmail.com (hardcoded) |
notify-production.yml |
Both use Gmail SMTP via the dawidd6/action-send-mail@v3 action. Emails include HTML formatting, links to the commit/PR, and actionable next steps.
Dependabot opens weekly PRs on development (Mondays, 08:00 Europe/Madrid) for:
- Python packages — minor and patch updates grouped into a single PR. Django major version bumps are intentionally ignored (require manual upgrade planning).
- GitHub Actions — updates to
actions/*,astral-sh/setup-uv,dawidd6/action-send-mail, etc.
PRs are labelled dependencies + python or github-actions for easy filtering. The normal 24 h cycle carries merged updates to testing and then to main.
Runs on every push and PR to main, plus a full scan every Monday at 04:30 UTC. Uses the security-and-quality query suite — covers OWASP Top 10, CWE Top 25, and Django-specific queries (SQL injection, path traversal, hardcoded credentials, insecure deserialization, etc.).
Results appear in Security → Code scanning alerts. A new alert on main does not auto-block future merges unless branch protection is configured to require the CodeQL check.
# First-time setup
uv sync --no-install-project # Install all dependencies (UV — see docs/UV.md)
make pre-commit-install # Install the git pre-commit hook
make up # Start Docker (PostgreSQL + Redis + Django + Celery)- Work on
development(or a short-lived branch offdevelopment) - Make changes following the conventions below
- Run
make pc-run— Ruff + mypy + bandit all pass, offers to auto-bump the patch version on success, and auto-stagesuv.lockif regenerated - Run
make test— all 283 tests must pass (PostgreSQL via Docker, parallel, with coverage) git commitwith a message likev1.0.6 - Short description(version comes first — conventions match every other commit in the project)git push origin development- CI runs automatically on your push (see CI/CD)
- ~24 h later, the auto-merge pipeline promotes your commit to
testingand opens a PR tomainfor your review
Pre-commit hooks run Ruff (lint + format), mypy (type checking), and bandit (security) automatically on every git commit. If a hook modifies files (e.g. mypy regenerates uv.lock), the commit aborts — running make pc-run once resolves this by staging the regenerated lock file.
| Tool | Purpose | Command |
|---|---|---|
| UV | Dependency management | uv sync, uv add, uv lock |
| Ruff | Lint + format | make lint, make format |
| mypy | Type checking | make mypy |
| bandit | Security linting | make bandit |
| pip-audit | Dependency CVE scanning | make audit |
| pytest-xdist | Parallel test execution | Built into make test (-n auto) |
| pytest-randomly | Randomized test ordering | Built into make test (seed printed) |
| pytest-cov | Coverage reporting + badge | make test, make coverage-badge |
| pre-commit | Git hooks: ruff, ruff-format, mypy, bandit | make pre-commit-install (first-time), make pc-run (dry-run all hooks + auto bump) |
| make version | Bump version in both pyproject.toml and settings.py |
make version x.y.z (positional, with y/N confirmation) |
All tools are configured in pyproject.toml and installed as dev dependencies via uv sync.
| Area | Convention |
|---|---|
| Language | Code in English, UI/templates in Spanish, comments mixed |
| Models | Explicit db_table, created_at/updated_at timestamps, BigAutoField PKs |
| Views | CBVs for CRUD, FBVs for everything else. AJAX returns {"success": bool, ...} |
| Forms | ModelForms for data entry. Business logic delegates to services. |
| Templates | Extend base.html. Blocks: title, page_title, content, extra_js |
| JS | External files in core/static/js/. Django data via data-* attrs or window.CONFIG |
| Services | Pure business logic in billing/services/. No request/response objects. |
| Tests | pytest with fixtures in conftest.py. authenticated_client for view tests. |
| Imports | Always explicit — no from app.models import * |
| Pricing | Always from SiteConfiguration.get_config(), never hardcoded |
| Template names | Always in English (e.g., enrollment_child.html, not matricula_niño.html) |
- Model → correct app (students/billing/core), explicit
db_table - Service →
billing/services/or new service if it has business logic - View → appropriate
core/views/module, add to__init__.pyre-exports - URL → correct app's
urls.py - Template →
core/templates/, extendbase.html - Tests → fixtures in
conftest.py, tests in correct test file - Admin → correct app's
admin.py - Docs → update this README, app README, CLAUDE.md if needed
Private project — all rights reserved.
Developed for Five a Day English Academy, Albacete, Spain.