Week 12 · The project: putting it all together
The one-minute story
Building a small restaurant, not just cooking one dish. Until now you practised single dishes: a Flask route, a Vue component, a Celery task, a cache. The project is the whole restaurant: a kitchen (Flask API and database), a dining room (Vue front end), a waiter who checks reservations (token login and roles), a clock that rings at closing time to tidy up (Celery beat), a pantry with ready-made sauces (Redis cache), and a place to test the fire alarm without a real fire (MailHog).
A restaurant is not judged by how fancy one dish is. It is judged by whether guests are seated, served and billed correctly, and whether the owner can explain how it all works. That last part is your viva.
Honest note on scope. This lesson is a guide, not an official rulebook. The official site lists only the single word "Project" for this week. The project statement of your term (published on the course page) is the only authority on what is required and what is optional. The lists below come from the course material and what students report; if the two differ, follow the statement and ask on the course forum.
By the end of this week you can:
- draw the architecture of a MAD2 project and say what each part is for;
- plan the folders, database tables, API routes and Vue structure for a given statement;
- explain the token flow and implement a role guard on the server and in the router;
- schedule Celery jobs (daily reminder, monthly report, async CSV export) and cache with expiry;
- run everything locally on Windows, test it, and prepare the README, report and submission;
- check your own project against a rubric and avoid the common mistakes.
Words you need
- Architecture - the big picture: which programs exist and how they talk.
- API-driven - the backend only serves data (JSON); the front end renders it.
- SPA (single page application) - one HTML page; Vue swaps components without full reloads.
- Token - a string the server gives after login; the client sends it with every request.
- RBAC (role-based access control) - what you may do depends on your role (admin, user...).
- Guard - a check that blocks access: a decorator on the server,
beforeEachin the router. - Blueprint - a way to split a Flask app into modules.
- OpenAPI / Swagger - a standard file describing your API routes (YAML), used as documentation.
- CORS - the browser rule that blocks a page from one origin from calling another unless the server allows it.
- Celery worker / beat - the process that runs tasks / the scheduler that starts them on time.
- MailHog - a fake mail server for local testing.
- WSL - Windows Subsystem for Linux: a Linux inside Windows, convenient for Redis and Celery.
- Seed data - starter rows (the admin user) created on first run.
- Viva - the oral examination on your own project.
Idea 1: The architecture of a typical project
Browser (Vue 2 SPA: components, Vue Router, Vuex)
| HTTP + JSON, header Authorization: Bearer <token>
v
Flask API (routes, auth, roles, validation) ---- SQLite (SQLAlchemy models)
| \
| cache get/set \ task.delay()
v v
Redis <------------------ Celery worker ----> SMTP: MailHog (dev) or real server
(cache + broker) ^
| schedule
Celery beat (daily / monthly)
| Part | Job | Typical tools |
|---|---|---|
| Front end | pages, forms, calls the API | Vue 2, Vue Router 3, Vuex 3, fetch |
| Backend | business rules, auth, JSON | Flask, Flask-RESTful or plain routes, Flask-SQLAlchemy |
| Database | durable data | SQLite |
| Redis | cache and Celery broker | Redis, Flask-Caching |
| Celery workers | slow or scheduled jobs | Celery, delay(), beat |
| reminders, reports | Flask-Mail, MailHog |
Core versus optional (as students report)
Typical features reported by students for MAD2 projects:
| Usually core | Often optional or bonus |
|---|---|
| Role-based access (for example admin and user) | Scheduled jobs: daily reminder, monthly report |
| Token login (not only server sessions) | CSV export (async, with a download) |
| CRUD for the main entities | Caching with expiry |
| Backend as an API, a Vue front end | PDF report, extra charts, search |
The point to remember: read your term's project statement. Marks follow it. If it makes a feature mandatory, it is mandatory, whatever this table says.
A good MAD2 project is small but complete: every required feature works end to end, the code is organised, secrets are not in the repository, and you can explain every line. Ten half-working features lose to five solid ones.
Your statement says: "Librarians add books; members request books; members get a reminder mail every evening if a book is due tomorrow; librarians can export all issued books as CSV." List the processes you need to run on your laptop to demo this.
Need a hint?
Think: web, data, background, schedule, mail, broker.
Show the full solution
(1) Flask app (API). (2) Vue front end (dev server or built files). (3) Redis (broker, and cache if used). (4) Celery worker. (5) Celery beat. (6) MailHog. SQLite is just a file, no process. That is why a demo needs several terminals: write a README run order.
Idea 2: Folders, database and API plan
Folder structure
project/
backend/
app.py # create_app(), registers extensions and blueprints
config.py # settings read from environment
extensions.py # db, jwt, cache, mail, celery objects
models.py # SQLAlchemy models
routes/
auth.py # /api/login, /api/register
books.py # CRUD
admin.py # admin-only routes
tasks.py # Celery tasks and beat schedule
templates/email/ # reminder.html, report.html
requirements.txt
frontend/
src/
main.js
router/index.js
store/index.js # Vuex
api.js # fetch wrapper that adds the token
components/ views/
package.json
README.md
openapi.yaml # API description
report.pdf
Why: extensions.py avoids circular imports; routes in blueprints keep each file short; templates for emails keep HTML out of Python code. A simpler layout (a single app.py) is fine for a small project, as long as it is tidy.
Database design: design from the statement
Underline nouns in the statement: they become tables. Underline "has many": they become foreign keys. Take the library statement above.
| Table | Columns | Notes |
|---|---|---|
user | id, username (unique), email, password_hash, role, last_visit | role is admin or member |
book | id, title, author, available (bool) | index on title if you search |
issue | id, user_id (FK), book_id (FK), issued_on, due_on, returned_on (nullable) | one row per loan; a many-to-many relation turned into a table |
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
email = db.Column(db.String(120), nullable=False)
password_hash = db.Column(db.String(200), nullable=False)
role = db.Column(db.String(10), nullable=False, default="member")
class Issue(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
book_id = db.Column(db.Integer, db.ForeignKey("book.id"), nullable=False)
due_on = db.Column(db.Date, nullable=False)
returned_on = db.Column(db.Date)
Store a hash (werkzeug.security.generate_password_hash) never the password. Create the admin by seeding on first run, not through a public register page.
API route plan
Use nouns, the right verb, the right status code (the earlier weeks).
| Method and path | Who | Success | Errors |
|---|---|---|---|
POST /api/login | anyone | 200 + token | 401 |
POST /api/register | anyone | 201 | 400, 409 |
GET /api/books?page=1 | logged in | 200 | 401 |
POST /api/books | admin | 201 | 400, 401, 403 |
PUT /api/books/<id> | admin | 200 | 404 |
DELETE /api/books/<id> | admin | 204 | 404 |
POST /api/books/<id>/issue | member | 201 | 409 if unavailable |
POST /api/export | admin | 202 + task id | |
GET /api/export/<task_id> | admin | 200 state / file |
OpenAPI. Describe the same table in openapi.yaml; it doubles as documentation for the examiner:
openapi: 3.0.0
info: {title: Library API, version: "1.0"}
paths:
/api/books:
get:
summary: List books (paginated)
parameters:
- {name: page, in: query, schema: {type: integer}}
responses:
"200": {description: A page of books}
"401": {description: Missing or bad token}
Statement. "Doctors add available slots; patients book a slot; a patient gets a mail the evening before the appointment."
Solution. Nouns: user (role doctor or patient), slot, booking. Tables: user, slot(id, doctor_id FK, starts_at, booked bool), booking(id, slot_id FK unique, patient_id FK). Unique slot_id in booking stops double booking; a second attempt returns 409. Routes: POST /api/slots (doctor, 201), GET /api/slots?page=1 (patient), POST /api/slots/<id>/book (patient, 201 or 409). Job: beat at 18:00 mails patients whose slot starts tomorrow.
Statement: "A quiz platform. Admin creates quizzes with questions. Students attempt a quiz and see their score. Admin sees all scores. Students get a daily mail if they have not attempted any quiz today." Give the tables with keys and eight routes.
Need a hint?
Nouns: user, quiz, question, attempt. An attempt links a user and a quiz and holds a score.
Show the full solution
Tables: user(id, username, email, password_hash, role); quiz(id, title, created_by FK user); question(id, quiz_id FK, text, options JSON/text, correct_index); attempt(id, user_id FK, quiz_id FK, score, taken_at).
Routes: POST /api/login (200/401); GET /api/quizzes (any user); POST /api/quizzes (admin, 201); POST /api/quizzes/<id>/questions (admin, 201); GET /api/quizzes/<id> (questions without answers, user); POST /api/quizzes/<id>/attempts (student, body answers, returns score, 201); GET /api/me/attempts (student); GET /api/admin/attempts (admin). Daily reminder: Celery beat task queries users with no attempt today and mails them.
Idea 3: Token login, roles and guards
Token flow.
- The Vue login form sends
POST /api/loginwith username and password (HTTPS in real life). - Flask checks the password hash and replies with a token (for example a JWT that carries the user id, role and an expiry).
- Vue stores the token (Vuex, and
localStorageif the user should stay logged in after reload). - Every later request sends the header
Authorization: Bearer <token>. - Flask checks the token on each protected route; a missing or bad token gives 401; a valid token with the wrong role gives 403.
- Logout: delete the token in the client. A token also expires on its own.
401 vs 403: 401 means "I do not know who you are" (no or invalid token). 403 means "I know who you are, and you may not do this".
Server side with Flask-JWT-Extended, including a role decorator (I ran this with the test client: no token gives 401, a student token on the admin route gives 403, an admin token gives 200):
from datetime import timedelta
from functools import wraps
from flask import Flask, jsonify, request
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt
app = Flask(__name__)
app.config["JWT_SECRET_KEY"] = "change-me-and-read-from-environment"
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
JWTManager(app)
@app.post("/api/login")
def login():
d = request.get_json()
user = find_user(d.get("username")) # your own lookup
if not user or not check_password(user, d.get("password")):
return jsonify(error="bad credentials"), 401
token = create_access_token(identity=user.username, additional_claims={"role": user.role})
return jsonify(token=token, role=user.role)
def role_required(role):
def deco(fn):
@wraps(fn)
@jwt_required()
def wrapper(*a, **k):
if get_jwt().get("role") != role:
return jsonify(error="forbidden"), 403
return fn(*a, **k)
return wrapper
return deco
@app.get("/api/admin/stats")
@role_required("admin")
def stats():
return jsonify(users=2)
Your course may use a different token library (for example Flask-Security token authentication). The flow is the same: use what the lectures and the statement ask for.
Vue side: Vuex store, API helper, router guard
// store/index.js (Vuex 3)
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
token: localStorage.getItem("token") || null,
role: localStorage.getItem("role") || null
},
mutations: {
setAuth(state, { token, role }) {
state.token = token; state.role = role;
localStorage.setItem("token", token); localStorage.setItem("role", role);
},
clearAuth(state) {
state.token = null; state.role = null;
localStorage.removeItem("token"); localStorage.removeItem("role");
}
},
actions: {
async login({ commit }, creds) {
const res = await fetch("http://localhost:5000/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(creds)
});
if (!res.ok) throw new Error("Login failed");
commit("setAuth", await res.json());
}
}
});
// api.js
import store from "./store";
export function api(path, options = {}) {
return fetch("http://localhost:5000" + path, {
...options,
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + store.state.token,
...(options.headers || {})
}
});
}
// router/index.js (Vue Router 3)
import VueRouter from "vue-router";
import store from "../store";
const routes = [
{ path: "/login", component: Login },
{ path: "/books", component: Books, meta: { requiresAuth: true } },
{ path: "/admin", component: Admin, meta: { requiresAuth: true, role: "admin" } }
];
const router = new VueRouter({ routes });
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.token) return next("/login");
if (to.meta.role && to.meta.role !== store.state.role) return next("/books");
next();
});
export default router;
- A client-side guard is only for convenience. A user can edit
localStorageand open/admin. The server must check the role on every protected route. This is the most asked viva question. - Do not keep the signing secret, the SMTP password or webhook URLs in Vue code, or in Git.
- A token is not encrypted by default (a JWT is only signed): do not put the password in it.
- Do not use
GETfor login with a password in the URL. - A guard that checks only on page load misses expiry: handle a
401response by clearing the token and redirecting to login.
Question. An admin clicks "Export CSV". Trace what happens.
Solution. (1) Vue calls POST /api/export with the Bearer token. (2) Flask checks the token and the admin role (else 401 or 403). (3) It calls export_csv.delay(), so a message goes into Redis, and replies 202 with a task id. (4) A Celery worker takes the message and writes the file. (5) The page polls GET /api/export/<id> every few seconds. (6) On SUCCESS the page shows a download link.
(a) No Authorization header on /api/books. (b) A student token on POST /api/books (admin only). (c) Admin token on GET /api/books/9999 which does not exist. (d) A student edits localStorage.role to admin and opens /admin in Vue; what stops the data from leaking?
Need a hint?
Is it identity, permission, or existence?
Show the full solution
(a) 401. (b) 403. (c) 404. (d) The Vue guard is fooled, but the API still checks the role claim inside the signed token, which the student cannot change, so the admin API returns 403 and no data leaks.
Idea 4: Celery jobs, caching and CORS in the project
Celery jobs
Three typical jobs (details in week 10):
| Job | Trigger | Notes |
|---|---|---|
| Daily reminder | beat, crontab(hour=18, minute=0) | query who needs it, send an HTML mail |
| Monthly report | beat, crontab(day_of_month=1, hour=8, minute=0) | Jinja2 to HTML (optionally PDF), mail it |
| Async CSV export | user clicks a button, export.delay() | API returns 202 with a task id; page polls GET /api/export/<task_id>; then offers the download |
Two design questions from past papers: for 1000 users, should the report job be one task per user or one task that loops? One task per user can spread over many workers; one big task is simpler but runs in series. Either can be right; state the trade-off. And for a CSV that takes 20 to 40 seconds, the front end should poll the task state (say every 3 to 5 seconds), not wait a guessed fixed time.
from celery.result import AsyncResult
@app.post("/api/export")
@role_required("admin")
def start_export():
task = export_csv.delay()
return jsonify(task_id=task.id), 202
@app.get("/api/export/<task_id>")
@role_required("admin")
def export_status(task_id):
r = AsyncResult(task_id, app=celery)
return jsonify(state=r.state, file="/static/export.csv" if r.state == "SUCCESS" else None)
Caching with expiry
Cache the read endpoints that many users repeat and that tolerate a little staleness, such as a summary or a book list. Use a TTL and delete the key when data changes (see week 11). Say your numbers: "book list, 60 seconds, deleted on add or delete".
CORS
The Vue dev server (http://localhost:8080) and Flask (http://localhost:5000) are different origins (different port), so the browser blocks the call unless Flask allows it. Install flask-cors:
from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": ["http://localhost:8080"]}})
I tested that a request carrying Origin: http://localhost:8080 receives Access-Control-Allow-Origin: http://localhost:8080. Allow only your front end origin, not * in the final version. If the browser sends an Authorization header, it first sends an OPTIONS preflight; flask-cors answers it.
Idea 5: Run it, test it, document it
Running everything on Windows
| Piece | Windows way |
|---|---|
| Flask | python -m venv venv, venv\Scripts\activate, pip install -r requirements.txt, flask run |
| Vue | npm install, npm run serve |
| Redis | not native on Windows: install WSL (wsl --install), then sudo apt install redis-server and redis-server; reachable from Windows on localhost:6379 in most setups |
| Celery worker | in WSL: celery -A app.celery worker -l info. On plain Windows it needs -P solo (or -P threads) |
| Celery beat | celery -A app.celery beat -l info |
| MailHog | run the downloaded .exe; SMTP 1025, inbox http://localhost:8025 |
If one of them does not start, the demo breaks, so rehearse the start-up order: Redis, MailHog, Flask, worker, beat, Vue. Write it in the README as numbered steps. Keep a requirements.txt (pip freeze > requirements.txt).
Testing
Test the backend with pytest and Flask's test client (no server needed):
def test_admin_route_needs_admin(client, student_token, admin_token):
h = lambda t: {"Authorization": "Bearer " + t}
assert client.get("/api/admin/stats").status_code == 401
assert client.get("/api/admin/stats", headers=h(student_token)).status_code == 403
assert client.get("/api/admin/stats", headers=h(admin_token)).status_code == 200
Test login (good and bad password), one CRUD path, one 404, and one validation error (400). Test the Celery task by calling the function directly (export_csv()), without a worker.
README and report checklist
- README: what the project is, how to install, exact run order, default admin login, ports, how to run tests, optional features and where they are.
- Report (short; use the template your term gives): statement, architecture diagram, database ER diagram, API list (or
openapi.yaml), key design decisions, screenshots, how features map to the statement. - Never commit
venv,node_modules,.env, real passwords or webhook URLs.
Submission checklist
- Project statement requirements ticked one by one.
- Fresh clone works: new virtual environment,
pip install -r requirements.txt,npm install, run order in the README. - Seed admin exists; sample data loads.
- The
.zipor repository is within the stated size and format; correct file names. - Report and screen-recording or links (if the term asks) in the right place before the deadline.
- You can start every process within two minutes.
A project has: the admin check only in Vue, the SMTP password in app.py, the Celery task sending mail for 1000 users inside the /send route, no requirements.txt, and CORS origins="*". Fix each in one line.
Need a hint?
One line each; think server, secrets, async, reproducibility, scope.
Show the full solution
(1) Check the role on the server too (decorator). (2) Move the password to an environment variable. (3) Call task.delay() and return 202. (4) pip freeze > requirements.txt. (5) Allow only the front-end origin.
Self-review rubric (score yourself 0, 1 or 2 per row; below 14 of 20 means fix before submitting).
| Row | 2 means |
|---|---|
| Requirements | every item of the statement works end to end |
| Auth and roles | token login; server checks roles; sensible 401 and 403 |
| API and data | clean routes, status codes, validation, hashed passwords, indexes where used |
| Front end | components, router, store; loading and error states shown |
| Background jobs | at least the required jobs run and can be shown on demand |
| Performance | pagination; cache with TTL and invalidation (if required) |
| Code quality | folders, naming, no secrets, comments where it is not obvious |
| Tests | key paths covered; tests pass |
| Documentation | README, report, API description |
| Viva readiness | you can explain every file and change something live |
Idea 6: A six-week timeline, common mistakes and the viva
A project timeline of 6 weeks
| Week | Goal | Done when |
|---|---|---|
| 1 | Read the statement, list features, design tables and routes | a one-page plan; a repository with folders |
| 2 | Backend: models, seed, login, token, roles | login works in a REST client |
| 3 | Backend: CRUD, validation, errors, pagination | all required routes pass tests |
| 4 | Vue: router, store, login, main pages | a user can log in and use the core flow |
| 5 | Redis cache, Celery jobs, mail, CSV export | jobs shown with MailHog; cache has TTL |
| 6 | Tests, README, report, rehearse the viva, submit early | fresh-clone test passes |
Start week 5 early if scheduled jobs are mandatory; they need the most set-up time.
Common mistakes
- Building features not in the statement before the required ones.
- Secrets in Git (token key, SMTP password, webhook URL).
- Role checks only in the front end.
- Plain-text passwords; SQL built by string formatting.
- Heavy work inside a request (mail, PDF, CSV).
- Cache with no expiry and no invalidation; or caching personalised data under a shared key.
- Works only on your laptop (missing
requirements.txt, hard-coded paths and ports). - Forgetting CORS, then discovering it on demo day.
- Not understanding code taken from tutorials or generated by tools. The viva will find out.
How this connects to the viva
The project viva is you walking the examiner through your own work. Questions follow your code: the architecture, why this table, how login works, what the cache stores, how the job is scheduled, and "change X live". Practise with the question banks: project viva and MAD2 viva.
Write five questions an examiner could ask about your own project, and the one-sentence answer for each.
Need a hint?
Use: architecture, auth, database, job, cache.
Show the full solution
Example set: (1) "Draw your architecture." (Flask API, SQLite, Vue, Redis, Celery worker and beat, MailHog.) (2) "How does a request prove who it is?" (Bearer token on every call, checked by the server.) (3) "Why is the report generated in Celery?" (Slow, must not block the request, scheduled by beat.) (4) "What happens to your cache when a book is added?" (The list key is deleted so the next read refreshes it.) (5) "Add a field isbn to books now." (Add the column, update the serializer and form; delete or migrate the old database file.)
Exam radar
The evidence for this week is thin. About 4 distinct questions matched; keyword counts are roughly 11 of 1471 End Term, 6 of 848 Quiz 1 and 0 of 608 Quiz 2 questions. Weeks 10 to 12 have few matched questions, so this is not a topic with its own question bank: the project is assessed in the viva and report, and the written papers test the ingredients from other weeks.
What the few matched questions looked like:
- A Vue
v-forlist from the project-style data (predict what is rendered). - A UI state question after a series of clicks on tasks.
- Celery design: generating 1000 reports with one task per user versus one task looping over users.
- CSV generation in Celery taking 20 to 40 seconds, and how the front end should wait: poll the task state, not a guessed
setTimeout.
So revise the project ingredients: token auth, roles, Celery and beat, Redis cache, CORS (an origin is scheme, host and port: a script from http://example.com cannot call http://new.example.com unless allowed), plus week 10 and week 11.
- Treating the project as "extra". It carries its own marks and the viva.
- Using a fixed delay instead of polling for an async job.
- Forgetting that different subdomains and different ports are different origins.
- Mixing up 401 and 403.
Quick quiz
A student edits localStorage so the Vuex role is admin and opens the admin page. The admin API route checks the role claim in the signed token. What happens when the page calls the API?
- The data is shown, because the front end said admin.
- The API returns 403, because the signed token still carries the role member.
- The API returns 404.
- The API returns 200 but with empty data.
Which design is best for an export that takes 20 to 40 seconds?
- Compute it inside the request and increase the browser timeout.
- Wait 41 seconds with
setTimeoutand then fetch the file. - Start a Celery task, return 202 with a task id, and poll the status until it is SUCCESS.
- Ask the user to refresh until it works.
Which processes does a demo of "daily reminder mail" need running, in addition to Flask?
- Only MailHog.
- Redis, a Celery worker, Celery beat and MailHog.
- Only the Vue server.
- Only SQLite.
Select all statements that are true. (select all that apply)
- A route guard in Vue improves the user experience but is not real security.
- The Vue dev server on port 8080 and Flask on 5000 are different origins, so CORS must be configured.
- A 403 means the token is missing.
- A password may be stored in plain text if the database is local.
Which belongs in .gitignore (not in the repository)?
requirements.txtREADME.md.envwith the SMTP password andvenv/models.py
Which statement about the project is the most accurate?
- This lesson's list of optional features overrides the project statement.
- The project statement of the term is the authority on what is required.
- Only the front end is marked.
- Features beyond the statement always earn extra marks.
Viva: say it out loud
Draw and explain the architecture of your project.
The Vue single page app calls the Flask JSON API with a bearer token. Flask uses SQLAlchemy with SQLite for data and Redis for caching. Slow and scheduled work runs in Celery workers, started by Celery beat, with Redis as the broker. In development MailHog catches the emails.
How does login work?
The form sends the username and password to the login route. Flask checks the password hash and returns a signed token with the user and role and an expiry. Vue keeps it in the Vuex store and localStorage, and sends it as a Bearer header on every call. The server verifies it on each protected route.
How do you stop a normal user from using admin features?
The router guard hides the pages, but the real check is on the server: an admin-only decorator reads the role from the signed token and returns 403 otherwise. The client cannot change the signed token.
Why did you design the tables this way?
Each noun in the statement became a table, and the relations became foreign keys, such as an issue table linking user and book. Passwords are hashed, the role is a column, and I added indexes on columns I filter by.
What does your Celery beat do, and how can you show it now?
It runs the daily reminder at 6 pm and the monthly report on the first of the month. I can trigger the same task by hand with delay(), and open MailHog to show the mail, then show the schedule entry in the code.
What do you cache and how do you invalidate it?
The book list and dashboard summary, in Redis through Flask-Caching with a 60 second timeout. When a book is added or changed, the key is deleted so the next request rebuilds it.
What would you change with more time?
I would add more tests and a refresh-token flow, move from SQLite to a server database for many users, and improve error messages. Being honest about limits is better than pretending the project is perfect.
Change something live: add a field to a model.
Add the column in models.py, update the serializer and the form, and recreate or migrate the database (for a small project, delete the SQLite file and re-seed). Then show it in the API response and the page.
Recap
- The project is the whole system: Vue SPA, Flask API, SQLite, Redis, Celery worker and beat, MailHog.
- Your term's project statement decides what is required. Typical core: roles, token login, CRUD, API backend. Typical optional: scheduled jobs, CSV export, caching.
- Design from the statement: nouns to tables, relations to foreign keys, routes with verbs and status codes, an OpenAPI file.
- Server-side role checks always; client guards are convenience. 401 is unknown, 403 is forbidden.
- Heavy work goes to Celery; the page polls task state; caches have TTL and invalidation; CORS allows only your front end.
- Prepare a README with run order, report, tests, a fresh-clone check and a 6 week plan. Rehearse the viva.
- Header:
Authorization: Bearer <token>; 401 = no or bad token, 403 = not allowed, 409 = conflict, 202 = accepted for background work - Flask:
create_access_token(identity=..., additional_claims={"role": ...}),@jwt_required(),get_jwt() - Vue router guard:
router.beforeEach((to, from, next) => { ... next("/login") }) - Vuex:
state,mutations(sync),actions(async),commit,dispatch - CORS:
CORS(app, resources={r"/api/*": {"origins": ["http://localhost:8080"]}}) - Beat:
crontab(hour=18, minute=0),crontab(day_of_month=1, hour=8, minute=0) - Celery on Windows:
celery -A app.celery worker -l info -P solo; beat:celery -A app.celery beat -l info - MailHog: SMTP 1025, inbox
http://localhost:8025 - Run order: Redis, MailHog, Flask, worker, beat, Vue
pip freeze > requirements.txt; keep secrets in.env, never in Git- Related: week 10, week 11, project viva, MAD2 viva
- MAD1 week 10 - Testing web applications with pytest: Test the API with pytest before the Vue app depends on it.
- MAD1 week 12 - Deployment: Git, WSGI, servers and the cloud: Deploying the MAD1 app is the starting point for deploying the full MAD2 project (Flask + Vue + Redis + Celery).
Practise with real exam questions
These come straight from past papers (with the real figures and the official answer key). Open 34 real questions on this week All End Term papers