Skip to content

starseeker-code-public/five-a-day

Repository files navigation

Five a Day eVolution

Five a Day Logo
Student Management System for Five a Day English Academy
Albacete, Spain

Version Python Django PostgreSQL CI Coverage


Built to centralize student records, automate billing cycles, and streamline parent communication for a small and lovely English academy.

Project Status

Environment Branch Hosting CI Status
Production main https://example.com/ Production CI
Testing (QA) testing https://example.com/ Testing CI
Development development Local Docker development: http://localhost:8000/ Development CI
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

Table of Contents


Version History & Roadmap

v1.0.10 — Branded Admin Theme, White-Bg Favicon & Social Meta (current)

Social sharing & branding

  • Logo changed to logo_white_bg.png across README, base.html favicon, 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, and apple-touch-icon.png (180×180) — dropped in both project/static/ and project/core/static/
  • og:image:secure_url added alongside og:image for Facebook's HTTPS-explicit crawler
  • twitter:image:alt added to Twitter Card block
  • Schema.org JSON-LD block added to base.html (@type: WebApplication, provider as EducationalOrganization) — covers Google Search previews, Gmail, and Google Chat link unfurling

Django admin — Five a Day theme

  • project/templates/admin/ created; TEMPLATES.DIRS now points to project/templates/ so project-level overrides take priority over Django's built-ins
  • admin/base_site.html — violet gradient header (#4c1d95#7c3aed) with logo_white_bg.png, loads admin_custom.css on every admin page
  • admin/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.pysite_title → "Five a Day · Admin", index_title → "Panel de administración"

Dependencies (Dependabot)

  • gunicorn upgraded 22.0.0 → 23.0.0 (constraint widened <23<24)
  • pandas upgraded 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) and integration/ (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, expanded test_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-coverage hook blocks commits when coverage drops below 75%
  • make test-cov-gate target added for pre-commit integration
  • Coverage threshold fail_under = 75 set in pyproject.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 _quotes module-level list

Developer tooling

  • /pc-run Claude skill: runs pre-commit in a loop, fixes failures, then asks y / X.Y.Z / n for version bumping
  • update-readme skill: 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 to raise RuntimeError(...) across all four failure paths
  • Mid-file imports with # noqa: E402 removed 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.png now that the old docs/resources/logo.png is 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-readme skill (Step 3.1.c) and the README-maintenance checklist in CLAUDE.md now 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 because core/views/auth.py::google_oauth_redirect gracefully returns redirect("login") when GOOGLE_CLIENT_ID is unset (CI's state), which the test's assertion couldn't distinguish from a middleware-level block. The remaining TestPublicPaths cases (static, health, login) still cover the middleware's exemption logic.

Developer tooling

  • make pc-run log line for the auto-staged uv.lock trimmed 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 from project/static/images/logo.png — dropped in both project/static/ and project/core/static/ so both STATICFILES_DIRS paths serve it
  • base.html now 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 touching base.html

Test-suite fixes

  • settings_test.py now explicitly sets SECURE_SSL_REDIRECT = False, SECURE_HSTS_SECONDS = 0, SESSION_COOKIE_SECURE = False, CSRF_COOKIE_SECURE = False — the CI environment runs with DJANGO_DEBUG=False, which activated the production SSL redirect and turned every test request into a 301 to https://testserver/.... The test settings are now self-contained and correct regardless of DJANGO_DEBUG.
  • pytest.ini adds filterwarnings = ignore:No directory at:UserWarning to silence the 142 WhiteNoise warnings that were emitted once per test request (the staticfiles/ directory only exists after collectstatic, which isn't run before tests)

Dashboard reliability

  • Zenquotes fetch in core/views/dashboard.py now targets https://zenquotes.io/api/quotes (no trailing slash — the old URL was getting 301-redirected) with follow_redirects=True as a guard against future URL changes
  • Silent except Exception: pass replaced with proper logger.warning(...) calls — failures are still non-fatal but now visible in logs

CI tooling

  • mypy job in ci.yml now sets DJANGO_SETTINGS_MODULE=project.settings, DJANGO_DEBUG=True, a dummy DJANGO_SECRET_KEY, and PYTHONPATH=projectdjango-stubs imports settings.py at load time, which previously raised the production secret-key guard
  • make version x.y.z now also updates the README version badge via sed, regenerates uv.lock via uv lock --quiet, and prints a reminder to run the update-readme skill afterwards; running make version with no arg now shows both pyproject.toml and the README badge side-by-side and warns if they've drifted
  • make pc-run's auto patch-bump now also rewrites the README badge and regenerates uv.lock — the existing git add uv.lock tail stages the refreshed lockfile automatically
v1.0.6 — Documentation Skill & Doc Overhaul

Documentation agent

  • New update-readme Claude 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.mdREADME.md rename (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 template is now the single authoritative source for local env-var structure, lives inline in the README as a fenced bash block

Secrets hygiene

  • Removed .env.testing.example (its content now lives only inline in the README .env template block)
  • .gitignore tightened: .env* matches everything, no !.env.example exception, no .env*.example carve-outs

CI workflow refinements

  • auto-merge.yml — improved commit detection and PR creation for the developmenttestingmain cascade
  • notify-production.yml — richer production deployment notification email with commit info and next-step gcloud commands

Per-app docs

  • project/core/README.md and project/comms/README.md touched 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 Codecov
  • auto-merge.yml — hourly cron that merges developmenttesting after 24 h of inactivity and CI passing, then auto-creates a PR testingmain
  • codeql.yml — weekly Python security analysis (OWASP Top 10, Django-specific queries)
  • notify-production.yml — emails hellofiveaday@gmail.com on every push to main with commit info and gcloud deploy instructions
  • Owner email notifications when developmenttesting merge lands and a PR is opened to main
  • dependabot.yml — grouped weekly Python and GitHub Actions updates targeting development
  • CODEOWNERS — auto-request reviews from both owner accounts

Public-repo hardening

  • Branch protection rules documented for main (14 protections) and testing (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 + LICENSE required-file checklist in docs/GITHUB.md

Developer tooling

  • make pc-run auto-stages regenerated uv.lock as the final step — next git commit is 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.yaml removed; commented nginx and pgAdmin services removed from docker-compose.yml

Dashboard enhancement

  • Inspirational quote generator on /home — fetches two daily quotes from zenquotes.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 (replaces V=x.y.z) with confirmation guard before writing
  • make pc-run — renamed from pre-commit-run; after a clean pass, prompts to auto-increment the patch version in pyproject.toml and project/settings.py

Celery

  • Celery worker and beat containers added to docker-compose.yml with 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.xml on every run)
  • make coverage-badge retained for offline SVG generation
v1.0.2 — UV Migration & Developer Tooling

Dependency management

  • Replaced Poetry with UV (see docs/UV.md)
  • uv.lock replaces poetry.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.testing with dedicated credentials and DJANGO_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_required decorator in core/decorators.py
  • Gated by DJANGO_ENV=testing + DEBUG=False + session username matches QA_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_DIRS for project/static/ — email CSS was missing from collectstatic manifest
  • Added SECURE_PROXY_SSL_HEADER for HTTPS behind reverse proxies
  • QAErrorEmailMiddleware for automated error reporting to support email
v1.0.0 — Architecture Refactor & Test Suite

Architecture

  • Split monolithic core app 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 #webcrumbs CSS 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 active field 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

Roadmap

Click to expand full roadmap (v1.1 — v1.12)

v1.1 — Waiting List & Group Capacity

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_waiting boolean on Student model
  • max_students soft limit on Group model with student_count tracking
  • 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

v1.2 — Google Sheets Integration

Automatic export of student/payment data to Google Sheets for existing spreadsheet workflows. Read and write via gspread using already-configured Google OAuth credentials.

v1.3 — PDF Invoice Generation

Proper PDF generation using WeasyPrint. Invoice/receipt PDFs for individual payments and quarterly summaries. Replace the current HTML-fallback tax certificate.

v1.4 — Celery + Redis Deployment

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.

v1.5 — Expense Tracking

Track academy expenses (rent, supplies, salaries) with categories, recurring templates, and monthly totals. Income-vs-expense dashboard widget showing profitability.

v1.6 — Multi-User Permissions

Replace SimpleAuthMiddleware with Django's built-in auth. Roles: admin (full access), teacher (read-only students + schedule), assistant (everything except configuration).

v1.7 — Advanced Reporting & Analytics

Monthly and yearly financial reports with charts. Student retention analytics. Payment collection rates. Group utilization metrics. Exportable to PDF.

v1.8 — SMS Notifications (Twilio)

SMS as an alternative notification channel for payment reminders and urgent communications. Opt-in per parent. Fallback to email when SMS fails.

v1.9 — Parent Portal

Read-only web portal for parents to view enrollment status, payment history, upcoming events, and download receipts/certificates. Separate authentication from admin panel.

v1.10 — Audit Log & Security Hardening

Full audit trail for all data changes (who changed what, when). Rate limiting on login and API endpoints. Two-factor authentication for admin users.

v1.11 — Stripe Payment Integration

Online payment via Stripe. Parents receive payment links by email. Automatic reconciliation with pending payments. Receipts generated on completion.

v1.12 — Mobile Optimization & PWA

Progressive Web App support: installable on mobile, offline-capable dashboard, push notifications for overdue payments and birthdays.


Tech Stack

Backend

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

Frontend

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

Infrastructure & Deployment

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

Python Dependencies

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

Developer Tooling

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

Database Schema

ER Diagram

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"
Loading

Key Constraints

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

Development & Docker

Quick Start

# 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 .env

Paste 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 runserver

Important: The .env file 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 pages
  • POSTGRES_PASSWORD — required for database connection
  • DJANGO_SECRET_KEY — generate with python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'

.env template

.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).

Make Commands

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

Environment Configuration

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_DEBUG defaults to false and DJANGO_ENV defaults to development. In production, always set DJANGO_ENV=production and ensure DJANGO_SECRET_KEY is 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.

Environment Variables Reference

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
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)

App Versioning

The app version is defined in two places and should be updated together:

  1. pyproject.toml line 3: version = "x.y.z" — package metadata
  2. project/settings.py line 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_VERSION environment variable (do not leave a legacy value like 0.x.y in .env — remove the line so the default in settings.py takes effect)

Project Structure & Architecture

Architecture Overview

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
Loading

App Dependency Flow

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
Loading

Directory Layout

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

App: core

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.

App: students

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.

App: billing

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.

App: comms

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.

Design Decisions

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.

Features by View

Home (Dashboard)

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.

Students

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).

Student Create

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).

Student Detail & Update

  • 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.

Payments

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).

Schedule

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.

Fun Friday

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.

Apps (Email Tools)

Hub page listing all 10 email communication tools. Each follows a consistent pattern:

  1. Form — fields specific to the email type (dates, activity description, year, etc.)
  2. Email preview — collapsible panel showing the rendered email HTML. "Refresh" button fetches live preview with current form data via AJAX.
  3. Test send — sends to EMAIL_TEST_1 / EMAIL_TEST_2 env vars for verification before bulk send.
  4. 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

Management

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 APIGET /api/students/language-cheque/ returns all students with active language cheque for government reporting.

Database (All Info)

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.

Login

Standalone page with custom styling (does not extend base.html).

  • Credentials login — username/password from LOGIN_USERNAME / LOGIN_PASSWORD env vars.
  • Google OAuth — optional. Button shown if GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are configured. Validates email matches GOOGLE_ALLOWED_EMAIL. Stores Google credentials in session for Gmail/Sheets API access.
  • Session — sets is_authenticated=True and username in Django session. 24-hour expiry.

Testing

Testing Overview

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 keyword

Coverage gates at every stage of the pipeline:

Stage What enforces it Behavior
Pre-commit pytest-coverage hook in .pre-commit-config.yamlmake 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 RequestFactory also 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 the authenticated_client fixture.

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.

Unit Tests

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

Integration Tests

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

Coverage Report

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%).


Migrations

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 them

Security

This section documents every security decision, mechanism, and configuration in the project.

Authentication

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_SECRET are not set, the OAuth button is hidden.
  • OAUTHLIB_INSECURE_TRANSPORT is only set when DEBUG=True (for local HTTP testing).

Session & Cookie Configuration

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.

CSRF Protection

  • Django's CsrfViewMiddleware is active in the middleware stack.
  • All POST endpoints receive CSRF validation. JavaScript AJAX requests use getCsrfToken() (reads from cookies) and send via X-CSRFToken header.
  • CSRF_TRUSTED_ORIGINS is configured per deployment via the CSRF_TRUSTED_ORIGINS env var (see DEPLOYMENT.md).
  • Only exception: @csrf_exempt on /health/ endpoint (GET-only, returns {"status": "healthy"}).

Transport Security (HTTPS)

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.

Security Headers

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)

Infrastructure & Deployment

Docker

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

Google Cloud Run

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

Secrets Management

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

Email Security

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

Data Protection & Input Validation

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.

Logging & Monitoring

  • Console logging via StreamHandler with configurable LOG_LEVEL env var.
  • Separate loggers for django framework and project modules.
  • HistoryLog model tracks user actions (payment completed, student enrolled, config updated) — capped at 1000 entries with automatic cleanup.
  • Celery tasks log by entity ID, not PII.

Future Security Improvements

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.

Testing Environment (QA)

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.

What is the testing environment?

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.

How to access it

Web address (will be provided once deployed on GCP)
Username See .env.testingLOGIN_USERNAME
Password See .env.testingLOGIN_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.

  1. Open the web address in your browser (Chrome, Firefox, Safari, or Edge all work).
  2. You will see a login page. Type the username and password you were given.
  3. After logging in you will see the Dashboard — the home screen with today's tasks, pending payments, and birthdays.

What you can test

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

How to report a problem

When something goes wrong, please note:

  1. 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").
  2. What you did — "I clicked the green Complete button on a payment" or "I searched for a student named Sofia".
  3. What happened — "The page showed an error" or "Nothing happened" or "It showed the wrong information".
  4. 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.

Error pages you might see

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.

For developers: how the QA environment works

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

Access control for /testing/

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-rebuild

The 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.


CI/CD & GitHub Actions

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.

Pipeline 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 Strategy

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.

Workflows

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 developmenttesting 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.

Automated Flows

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 testing only when all four conditions hold: dev is ahead of testing, the last commit is ≥ 24 h old, CI is green, and the version in pyproject.toml has been bumped (strictly higher than testing's version). Without a version bump the merge is skipped even with 24 h of new commits on dev — run make pc-run (answer yes) or make version x.y.z before the next tick to unlock it.

2. Auto-merge fires

  • Creates a --no-ff merge commit on testing titled YYYY-MM-DD - <your last commit message>
  • Pushes to testing (which triggers CI on testing)
  • Creates and pushes an annotated staging tag testing-vX.Y.Z on the new testing merge commit
  • Opens PR testing → main if one is not already open (title matches the merge commit)
  • Sends an HTML email to OWNER_EMAILS with version bump, staging tag, and a "Review PR" button

2b. You merge the PR → release tag on main

  • notify-production.yml reads version from pyproject.toml on main's new HEAD
  • Creates and pushes an annotated release tag vX.Y.Z on that commit (skipped if tag already exists)
  • Sends an HTML email to hellofiveaday@gmail.com with the release tag and gcloud deploy 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, main is updated

4. Production notification fires

  • notify-production.yml sends an email to hellofiveaday@gmail.com
  • Email contains commit info, file-change summary, and the exact gcloud commands to deploy to Cloud Run

Branch Protection — main

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

Branch Protection — testing

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

Public Repository Hardening

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).

Required GitHub Secrets

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.

Email Notifications

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

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.

CodeQL Security Scanning

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.


Contributing

Development Workflow

# 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)
  1. Work on development (or a short-lived branch off development)
  2. Make changes following the conventions below
  3. Run make pc-run — Ruff + mypy + bandit all pass, offers to auto-bump the patch version on success, and auto-stages uv.lock if regenerated
  4. Run make test — all 283 tests must pass (PostgreSQL via Docker, parallel, with coverage)
  5. git commit with a message like v1.0.6 - Short description (version comes first — conventions match every other commit in the project)
  6. git push origin development
  7. CI runs automatically on your push (see CI/CD)
  8. ~24 h later, the auto-merge pipeline promotes your commit to testing and opens a PR to main for 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.

Make Commands (Developer Tooling)

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.

Code Conventions

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)

Adding a Feature

  1. Model → correct app (students/billing/core), explicit db_table
  2. Servicebilling/services/ or new service if it has business logic
  3. View → appropriate core/views/ module, add to __init__.py re-exports
  4. URL → correct app's urls.py
  5. Templatecore/templates/, extend base.html
  6. Tests → fixtures in conftest.py, tests in correct test file
  7. Admin → correct app's admin.py
  8. Docs → update this README, app README, CLAUDE.md if needed

License

Private project — all rights reserved.

Developed for Five a Day English Academy, Albacete, Spain.

About

Django-based student management system for a Spanish English academy. Django 5.2, PostgreSQL 16, Celery, deployed on GCP Cloud Run. Automated CI/CD with typed, tested code.

Topics

Resources

Stars

Watchers

Forks

Contributors