Typorithm · medium

Flask capstone: owned notes from request to selected readback

pythonflaskindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Flask capstone: owned notes from request to selected readback

0%

Concept and contract

Combine session identity, CSRF, validation, persistence and scoped reads.

A small complete vertical flow connects more responsibilities than a single route decorator. A browser write should establish identity, pass its CSRF policy, validate domain input, assign ownership from the trusted session and commit before reporting success.

Reading the resulting collection should use the same ownership boundary. This capstone combines those steps using a tiny fictional notes application.

Mechanism and worked example

The fixture creates an in-memory table and one foreign note, then signs in through a real password-hash check using a session-bound CSRF token.

The fixture creates an in-memory table and one foreign note, then signs in through a real password-hash check using a session-bound CSRF token. A valid title is trimmed, assigned to the authenticated fixture owner and committed.

A following GET selects only that owner’s notes. Observing the selected titles ties the response to actual persistence rather than a local echo of submitted text.

Complete worked example

python

from flask import Flask, request, session, abort
from flask_wtf.csrf import CSRFProtect, generate_csrf
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import select, String
from sqlalchemy.orm import Mapped, mapped_column

def owned_note(title, token_mode='correct'):
    app = Flask(__name__)
    app.config.update(TESTING=True, SECRET_KEY='owned-capstone-key', SQLALCHEMY_DATABASE_URI='sqlite:///:memory:')
    CSRFProtect(app)
    db = SQLAlchemy(app)
    class Note(db.Model):
        id: Mapped[int] = mapped_column(primary_key=True)
        owner: Mapped[int] = mapped_column(nullable=False)
        title: Mapped[str] = mapped_column(String(20), nullable=False)
    password_hash = generate_password_hash('course-password')
    @app.get('/token')
    def token():
        return {'token': generate_csrf()}
    @app.post('/login')
    def login():
        if not check_password_hash(password_hash, request.form.get('password', '')):
            abort(401)
        session['user_id'] = 7
        return {'ok': True}
    @app.route('/notes', methods=['GET', 'POST'])
    def notes():
        owner = session.get('user_id')
        if owner is None:
            abort(401)
        if request.method == 'GET':
            rows = db.session.scalars(select(Note).where(Note.owner == owner).order_by(Note.id))
            return {'titles': [row.title for row in rows]}
        value = request.form.get('title', '').strip()
        if not value or len(value) > 20:
            return {'error': 'title invalid'}, 400
        db.session.add(Note(owner=owner, title=value))
        db.session.commit()
        return {'saved': True}, 201
    with app.app_context():
        db.create_all()
        db.session.add(Note(owner=8, title='Private'))
        db.session.commit()
    client = app.test_client()
    token = client.get('/token').json['token']
    client.post('/login', data={'password': 'course-password'}, headers={'X-CSRFToken': token})
    headers = {'X-CSRFToken': token} if token_mode == 'correct' else {}
    response = client.post('/notes', data={'title': title}, headers=headers)
    listed = client.get('/notes').json['titles']
    return {'status': response.status_code, 'titles': listed}
Reasoning
The fixture creates an in-memory table and one foreign note, then signs in through a real password-hash check using a session-bound CSRF token. A valid title is trimmed, assigned to the authenticated fixture owner and committed.

Counterexamples and common mistakes

The counterexample drops the owner predicate from the collection query, exposing the pre-existing Private note.

The counterexample drops the owner predicate from the collection query, exposing the pre-existing Private note. Authentication and a valid token do not prevent that authorization failure.

The example also deliberately avoids accepting owner from the submitted form. In production, use a real user model, robust login protections and secret management rather than extending the hard-coded fictional identity scheme.

Counterexample: a runnable framework mistake

python

from flask import Flask, request, session, abort
from flask_wtf.csrf import CSRFProtect, generate_csrf
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import select, String
from sqlalchemy.orm import Mapped, mapped_column

def owned_note(title, token_mode='correct'):
    app = Flask(__name__)
    app.config.update(TESTING=True, SECRET_KEY='owned-capstone-key', SQLALCHEMY_DATABASE_URI='sqlite:///:memory:')
    CSRFProtect(app)
    db = SQLAlchemy(app)
    class Note(db.Model):
        id: Mapped[int] = mapped_column(primary_key=True)
        owner: Mapped[int] = mapped_column(nullable=False)
        title: Mapped[str] = mapped_column(String(20), nullable=False)
    password_hash = generate_password_hash('course-password')
    @app.get('/token')
    def token():
        return {'token': generate_csrf()}
    @app.post('/login')
    def login():
        if not check_password_hash(password_hash, request.form.get('password', '')):
            abort(401)
        session['user_id'] = 7
        return {'ok': True}
    @app.route('/notes', methods=['GET', 'POST'])
    def notes():
        owner = session.get('user_id')
        if owner is None:
            abort(401)
        if request.method == 'GET':
            rows = db.session.scalars(select(Note).order_by(Note.id))
            return {'titles': [row.title for row in rows]}
        value = request.form.get('title', '').strip()
        if not value or len(value) > 20:
            return {'error': 'title invalid'}, 400
        db.session.add(Note(owner=owner, title=value))
        db.session.commit()
        return {'saved': True}, 201
    with app.app_context():
        db.create_all()
        db.session.add(Note(owner=8, title='Private'))
        db.session.commit()
    client = app.test_client()
    token = client.get('/token').json['token']
    client.post('/login', data={'password': 'course-password'}, headers={'X-CSRFToken': token})
    headers = {'X-CSRFToken': token} if token_mode == 'correct' else {}
    response = client.post('/notes', data={'title': title}, headers=headers)
    listed = client.get('/notes').json['titles']
    return {'status': response.status_code, 'titles': listed}
Reasoning
The counterexample drops the owner predicate from the collection query, exposing the pre-existing Private note. Authentication and a valid token do not prevent that authorization failure.

Independent practice: one bounded framework change

Add an own_count field to the helper result based on the already selected owner-scoped title list.

Add an own_count field to the helper result based on the already selected owner-scoped title list. Preserve every request guard and transaction step.

A failed validation or missing-token request should leave an empty own collection with count zero. This task is intentionally small: it reinforces one coherent population instead of introducing a separate expensive count query or a new endpoint.

Step 1: Return own_count from the same owner-scoped selected collection. Step 2: Use len on the observed title list.

Practice starter: solve before reading the solution

python

from flask import Flask, request, session, abort
from flask_wtf.csrf import CSRFProtect, generate_csrf
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import select, String
from sqlalchemy.orm import Mapped, mapped_column

def owned_note(title, token_mode='correct'):
    # Remove this placeholder, then adapt the retained baseline:
    # Return own_count from the same owner-scoped selected collection.
    raise NotImplementedError
    app = Flask(__name__)
    app.config.update(TESTING=True, SECRET_KEY='owned-capstone-key', SQLALCHEMY_DATABASE_URI='sqlite:///:memory:')
    CSRFProtect(app)
    db = SQLAlchemy(app)
    class Note(db.Model):
        id: Mapped[int] = mapped_column(primary_key=True)
        owner: Mapped[int] = mapped_column(nullable=False)
        title: Mapped[str] = mapped_column(String(20), nullable=False)
    password_hash = generate_password_hash('course-password')
    @app.get('/token')
    def token():
        return {'token': generate_csrf()}
    @app.post('/login')
    def login():
        if not check_password_hash(password_hash, request.form.get('password', '')):
            abort(401)
        session['user_id'] = 7
        return {'ok': True}
    @app.route('/notes', methods=['GET', 'POST'])
    def notes():
        owner = session.get('user_id')
        if owner is None:
            abort(401)
        if request.method == 'GET':
            rows = db.session.scalars(select(Note).where(Note.owner == owner).order_by(Note.id))
            return {'titles': [row.title for row in rows]}
        value = request.form.get('title', '').strip()
        if not value or len(value) > 20:
            return {'error': 'title invalid'}, 400
        db.session.add(Note(owner=owner, title=value))
        db.session.commit()
        return {'saved': True}, 201
    with app.app_context():
        db.create_all()
        db.session.add(Note(owner=8, title='Private'))
        db.session.commit()
    client = app.test_client()
    token = client.get('/token').json['token']
    client.post('/login', data={'password': 'course-password'}, headers={'X-CSRFToken': token})
    headers = {'X-CSRFToken': token} if token_mode == 'correct' else {}
    response = client.post('/notes', data={'title': title}, headers=headers)
    listed = client.get('/notes').json['titles']
    return {'status': response.status_code, 'titles': listed}
Reasoning
Return own_count from the same owner-scoped selected collection. Use len on the observed title list.
  • Hint 1: Preserve rejected-write behavior.
  • Hint 2: Keep the foreign row excluded.

Solution, tests and trade-offs

The cases create a cleaned title, reject an empty title and reject a missing CSRF token.

The cases create a cleaned title, reject an empty title and reject a missing CSRF token. Each also checks that foreign data never enters the selected result.

Practice compares own_count with that same list. All rows, cookies, keys and hashes belong to this helper’s disposable application; the lab never opens production data or calls a public vacancy feed.

Practice solution and acceptance checks

python

from flask import Flask, request, session, abort
from flask_wtf.csrf import CSRFProtect, generate_csrf
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import select, String
from sqlalchemy.orm import Mapped, mapped_column

def owned_note(title, token_mode='correct'):
    app = Flask(__name__)
    app.config.update(TESTING=True, SECRET_KEY='owned-capstone-key', SQLALCHEMY_DATABASE_URI='sqlite:///:memory:')
    CSRFProtect(app)
    db = SQLAlchemy(app)
    class Note(db.Model):
        id: Mapped[int] = mapped_column(primary_key=True)
        owner: Mapped[int] = mapped_column(nullable=False)
        title: Mapped[str] = mapped_column(String(20), nullable=False)
    password_hash = generate_password_hash('course-password')
    @app.get('/token')
    def token():
        return {'token': generate_csrf()}
    @app.post('/login')
    def login():
        if not check_password_hash(password_hash, request.form.get('password', '')):
            abort(401)
        session['user_id'] = 7
        return {'ok': True}
    @app.route('/notes', methods=['GET', 'POST'])
    def notes():
        owner = session.get('user_id')
        if owner is None:
            abort(401)
        if request.method == 'GET':
            rows = db.session.scalars(select(Note).where(Note.owner == owner).order_by(Note.id))
            return {'titles': [row.title for row in rows]}
        value = request.form.get('title', '').strip()
        if not value or len(value) > 20:
            return {'error': 'title invalid'}, 400
        db.session.add(Note(owner=owner, title=value))
        db.session.commit()
        return {'saved': True}, 201
    with app.app_context():
        db.create_all()
        db.session.add(Note(owner=8, title='Private'))
        db.session.commit()
    client = app.test_client()
    token = client.get('/token').json['token']
    client.post('/login', data={'password': 'course-password'}, headers={'X-CSRFToken': token})
    headers = {'X-CSRFToken': token} if token_mode == 'correct' else {}
    response = client.post('/notes', data={'title': title}, headers=headers)
    listed = client.get('/notes').json['titles']
    return {'status': response.status_code, 'titles': listed, 'own_count': len(listed)}
Reasoning
The cases create a cleaned title, reject an empty title and reject a missing CSRF token. Each also checks that foreign data never enters the selected result.

Interview reasoning and next step

To extend the application, add selected-object ownership checks for updates and deletes, migration revisions, pagination and request tests that exercise both valid and rejected paths..

To extend the application, add selected-object ownership checks for updates and deletes, migration revisions, pagination and request tests that exercise both valid and rejected paths.

Keep database commit and external notifications coordinated through an explicit delivery design. This capstone is a learning baseline with truthful boundaries, not a claim of complete production hardening or a reason to skip deployment and recovery checks.

Optional stretch — after the core task works: Add owner-authorized update and delete routes with regression cases.

Computer practice: https://applydjinn.com/typorithm/python-frameworks-lab-20261105.zip. Install the pinned dependencies in an isolated environment, run verify.py --track flask, then edit the retained exercise baseline and check it with --practice fl-capstone. The lab uses owned in-memory databases, local framework clients and tiny CPU tensors. It never reads the working ApplyDjinn database, contacts production feeds or downloads models.

Combine session identity, CSRF, validation, persistence and scoped reads.