English
with Flor
A full-stack bilingual website for a Buenos Aires language school. Flask web app with student registration, course enrolment, a user dashboard, and an admin panel — paired with a FastAPI REST API sharing the same data layer.
Flor runs a small English school serving Argentine students at CEFR levels A1 through B2. She needed a proper web presence — not a page builder template — that reflected the school's conversational, student-first approach and handled the operational side: course information, enrolment, and direct contact.
Argentine students bring specific complexity: two student types (domestic and international) with different identity document requirements, and a primarily Spanish-speaking audience that the site needed to serve natively alongside English.
Built as a Flask application with a Jinja2 template layer — full server-side rendering, no JavaScript framework. Bilingual support implemented as a translation dictionary keyed on a ?lang= URL parameter, applied consistently across every page, form label, and validation message.
A companion FastAPI service sits alongside the Flask app, sharing the same SQLite database. It exposes a documented REST API for external integrations — JWT-authenticated, rate-limited, and fully tested against an in-memory database.
Hero section with rotating location imagery, a "Now enrolling for 2026" badge, bilingual EN/ES toggle, and clear CTAs into the course catalogue and registration flow.
Three-step learning method presented with a numbered layout — Immerse & Listen, Practice & Speak, Refine & Master — alongside an "Our Method" editorial section explaining the conversation-first approach.
Student testimonials in the students' own language (Spanish), followed by a high-contrast "Book your level test" call-to-action — the primary conversion event for prospective students.
Four active CEFR levels (A1–B2) presented as cards. C1 and C2 are built but hidden behind a feature flag for staged release — no code changes needed to enable them.
Each level has its own page with curriculum outcomes, format and schedule details, pricing, and a direct enrolment CTA. Content is structured for the student to self-qualify before signing up.
Bilingual UI
Full English and Spanish support driven by a translation dictionary keyed on a ?lang= URL parameter. Applied consistently across every page, form label, validation message, and error state — not just marketing copy.
Student-type-aware enrolment
Argentine students (CUIT/CUIL and DNI with format validation) and international students (passport) follow different enrolment paths. Both collect a full postal address. The form adapts dynamically based on the selected student type.
Authentication and account management
Registration and login with scrypt-hashed passwords via Werkzeug. Flask-Login session management. Students can view enrolment details, edit personal information, update their address, change their password, and delete their account (GDPR-style data removal).
Admin panel
Role-protected view behind an is_admin flag. Displays all registered users, enrolments, and contact form submissions. No separate admin application — it lives within the same Flask blueprint.
Word of the Day
Daily-rotating vocabulary card driven by a date-based index against a curated word list. No additional database queries — just a deterministic calculation at render time. Exposed as a REST API endpoint for external use.
Contact form
Submissions persisted to the database so nothing is lost to email. WhatsApp CTA for direct replies. CSRF-protected, rate-limited to prevent spam.
A FastAPI service runs alongside the Flask app on port 8000, connecting to the same SQLite database via a standalone SQLAlchemy session. It provides authenticated JSON access to the same data — no duplication, no synchronisation overhead.
JWT authentication
HS256-signed tokens issued by POST /auth/login via OAuth2PasswordRequestForm. Verified on protected endpoints via a FastAPI dependency. Integrates with Swagger UI's Authorize button — no manual header manipulation needed during development.
Pydantic validation
All request bodies and response schemas are Pydantic BaseModel subclasses. Malformed requests return 422 Unprocessable Entity automatically. Login is rate-limited to 5 requests/minute; enquiry submission to 10/minute — both return 429 on breach.
Test suite
20 pytest tests using FastAPI's TestClient. Covers all endpoints, authentication flows, 404 handling, and Pydantic validation errors. Isolated via an in-memory SQLite database with dependency_overrides — the real school.db is never touched.
Hardened by default
CSRF protection on all forms. Rate limiting on login, registration, and contact endpoints. Content-Security-Policy, X-Frame-Options, and X-Content-Type-Options headers. SameSite=Lax session cookies with the Secure flag set in production.
No information leakage
Generic error messages on registration to prevent account enumeration — a failed login or duplicate registration gives the same response regardless of which condition triggered it. Duplicate enrolment guard prevents re-submission.
- Flask 3 web application — bilingual EN/ES, server-side rendered with Jinja2
- Course catalogue with individual CEFR level pages (A1–B2); C1/C2 feature-flagged for staged release
- Student registration — Argentine (CUIT/CUIL, DNI) and international (passport) flows with full postal address collection
- Authentication — scrypt-hashed passwords via Werkzeug, Flask-Login session management
- User dashboard — view enrolment, edit personal details, address, and password, delete account
- Admin dashboard — all users, enrolments, and contact submissions in a role-protected view
- Contact form — persisted to database, WhatsApp CTA, CSRF-protected and rate-limited
- Word of the Day — date-index vocabulary card, zero extra database queries
- Security hardening — CSRF, rate limiting, CSP/X-Frame-Options/X-Content-Type-Options headers, SameSite cookies, account enumeration prevention
- FastAPI REST API — shared SQLite data layer, JWT authentication, Swagger UI documentation
- API endpoints — health check, Word of the Day, courses, auth, enquiries, user profile (GET/PUT/DELETE), enrolments
- 20-test pytest suite — full endpoint coverage, isolated in-memory database, dependency_overrides
- Deployment configuration for PythonAnywhere (Gunicorn WSGI, persistent filesystem)