test_template_flask/CLAUDE.md
2026-07-20 20:14:41 +02:00

531 lines
16 KiB
Markdown

# CLAUDE.md
This file is project memory for Claude Code. Drop it in the repo root, `cd` into
the project, run `claude`, and Claude Code will read it automatically. Work
through the phases below **in order**, one at a time — commit after each phase.
## Project Overview
A multi-feature Flask app built with the **app-factory + blueprint** pattern.
Every feature (auth, main, and anything you add later) lives in its own
blueprint folder, so new features drop in without touching existing code.
Ships with a full authentication flow (register / login / logout) and a
public index page.
## Tech Stack
- Python 3.11+ (pyenv-managed)
- Flask 3.x — app factory pattern
- Flask-SQLAlchemy — ORM, SQLite by default
- Flask-Login — session-based auth
- Flask-WTF + WTForms — forms, CSRF protection
- Flask-Migrate — Alembic migrations
- python-dotenv — env config
- Jinja2 templates, Bootstrap 5 via CDN (swap for your own CI later)
## Target Project Structure
```
myapp/
├── app/
│ ├── __init__.py # app factory
│ ├── models.py # shared models (User)
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── forms.py
│ │ └── routes.py
│ └── main/
│ ├── __init__.py
│ └── routes.py
├── templates/
│ ├── base.html
│ ├── index.html
│ └── auth/
│ ├── login.html
│ └── register.html
├── static/
│ └── css/
├── config.py
├── run.py
├── requirements.txt
├── .env
└── .gitignore
```
## Conventions for Claude Code
- One blueprint per feature, under `app/<feature>/`.
- Each blueprint: `__init__.py`, `routes.py`, and `forms.py` / `models.py` only if that feature needs them.
- Templates mirror blueprint structure: feature-specific templates go in `templates/<feature>/`.
- Every template extends `templates/base.html`.
- No secrets hardcoded — config comes from environment variables via `.env`.
- Any model change → `flask db migrate` then `flask db upgrade`, never edit the DB by hand.
- When asked to "implement Phase N", only touch the files listed for that phase.
---
## Current Session Status (resume point)
Phase 0 is mostly done:
- `pyenv install 3.13 -s`, `pyenv local 3.13`, `pyenv virtualenv 3.13 venv`, and `git init` all completed.
- `.python-version` is set to `venv`.
- **Known issue:** inside the Claude Code sandboxed Bash tool, `$PATH` did not include `~/.pyenv/shims`, so the venv never actually activated in that tool — `python -V` kept resolving to the system Python.
- **Resolution in progress:** the user is activating the virtualenv manually in their own terminal (`pyenv activate venv` or equivalent), then quitting and restarting `claude` from inside that activated shell so the tool inherits the correct environment.
- **On resume:** verify with `which python` / `python -V` that the venv (Python 3.13, path containing `.pyenv/versions/3.13.14/envs/venv`) is active before running any `pip install` or `flask` commands. Once confirmed, proceed to Phase 1 (Skeleton).
---
## Build Tutorial
### Phase 0 — Environment
```bash
pyenv install 3.13 -s
pyenv local 3.13
pyenv virtualenv 3.13 venv
pyenv activate venv
git init
```
### Phase 1 — Skeleton
```bash
mkdir -p app/auth app/main templates/auth static/css
touch app/__init__.py app/models.py \
app/auth/__init__.py app/auth/routes.py app/auth/forms.py \
app/main/__init__.py app/main/routes.py \
templates/base.html templates/index.html \
templates/auth/login.html templates/auth/register.html \
config.py run.py requirements.txt .env .gitignore
```
### Phase 2 — Dependencies
`requirements.txt`:
```
Flask==3.0.3
Flask-SQLAlchemy==3.1.1
Flask-Login==0.6.3
Flask-WTF==1.2.1
Flask-Migrate==4.0.7
python-dotenv==1.0.1
email-validator==2.1.1
```
```bash
pip install -r requirements.txt
```
`.env`:
```
FLASK_APP=run.py
FLASK_DEBUG=1
SECRET_KEY=change-me-to-a-random-string
DATABASE_URL=sqlite:///app.db
```
`.gitignore`:
```
.venv/
__pycache__/
*.pyc
instance/
.env
app.db
```
**Prompt for Claude Code:**
```
claude "Implement Phase 2 exactly as specified in CLAUDE.md: create requirements.txt, .env, and .gitignore with the given content."
```
### Phase 3 — Config & App Factory
`config.py`:
```python
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-key-not-safe")
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///app.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
```
`app/__init__.py`:
```python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from config import Config
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)
login_manager.login_view = "auth.login"
login_manager.login_message_category = "info"
from app.auth.routes import auth_bp
from app.main.routes import main_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
app.register_blueprint(main_bp)
from app import models # noqa: F401 — registers models with SQLAlchemy
return app
```
`run.py`:
```python
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run()
```
### Phase 4 — User Model
`app/models.py`:
```python
from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db, login_manager
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f"<User {self.username}>"
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
```
### Phase 5 — Auth Blueprint
`app/auth/forms.py`:
```python
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError
from app.models import User
class RegistrationForm(FlaskForm):
username = StringField("Username", validators=[DataRequired(), Length(min=3, max=64)])
email = StringField("Email", validators=[DataRequired(), Email()])
password = PasswordField("Password", validators=[DataRequired(), Length(min=8)])
confirm_password = PasswordField(
"Confirm Password", validators=[DataRequired(), EqualTo("password")]
)
submit = SubmitField("Create Account")
def validate_username(self, username):
if User.query.filter_by(username=username.data).first():
raise ValidationError("That username is already taken.")
def validate_email(self, email):
if User.query.filter_by(email=email.data).first():
raise ValidationError("That email is already registered.")
class LoginForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
password = PasswordField("Password", validators=[DataRequired()])
remember_me = BooleanField("Remember Me")
submit = SubmitField("Log In")
```
`app/auth/routes.py`:
```python
from urllib.parse import urlparse
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, login_required, current_user
from app import db
from app.models import User
from app.auth.forms import RegistrationForm, LoginForm
auth_bp = Blueprint("auth", __name__, template_folder="../../templates/auth")
@auth_bp.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for("main.index"))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data, email=form.email.data)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash("Account created. You can log in now.", "success")
return redirect(url_for("auth.login"))
return render_template("auth/register.html", form=form)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("main.index"))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None or not user.check_password(form.password.data):
flash("Invalid email or password.", "danger")
return redirect(url_for("auth.login"))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get("next")
if not next_page or urlparse(next_page).netloc != "":
next_page = url_for("main.index")
return redirect(next_page)
return render_template("auth/login.html", form=form)
@auth_bp.route("/logout")
@login_required
def logout():
logout_user()
flash("You have been logged out.", "info")
return redirect(url_for("main.index"))
```
### Phase 6 — Main Blueprint
`app/main/routes.py`:
```python
from flask import Blueprint, render_template
main_bp = Blueprint("main", __name__)
@main_bp.route("/")
def index():
return render_template("index.html")
```
### Phase 7 — Templates
`templates/base.html`:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}MyApp{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.index') }}">MyApp</a>
<div class="d-flex gap-2">
{% if current_user.is_authenticated %}
<span class="navbar-text">Hi, {{ current_user.username }}</span>
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('auth.logout') }}">Log out</a>
{% else %}
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('auth.login') }}">Log in</a>
<a class="btn btn-primary btn-sm" href="{{ url_for('auth.register') }}">Register</a>
{% endif %}
</div>
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</body>
</html>
```
`templates/index.html`:
```html
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<h1>Welcome{% if current_user.is_authenticated %}, {{ current_user.username }}{% endif %}!</h1>
{% if not current_user.is_authenticated %}
<p>Please <a href="{{ url_for('auth.login') }}">log in</a> or
<a href="{{ url_for('auth.register') }}">create an account</a>.</p>
{% endif %}
{% endblock %}
```
`templates/auth/login.html`:
```html
{% extends "base.html" %}
{% block title %}Log In{% endblock %}
{% block content %}
<h2>Log In</h2>
<form method="POST" novalidate class="col-md-6">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
</div>
<div class="form-check mb-3">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
{% endblock %}
```
`templates/auth/register.html`:
```html
{% extends "base.html" %}
{% block title %}Register{% endblock %}
{% block content %}
<h2>Create Account</h2>
<form method="POST" novalidate class="col-md-6">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control") }}
{% for error in form.username.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control") }}
{% for error in form.confirm_password.errors %}<div class="text-danger small">{{ error }}</div>{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
{% endblock %}
```
### Phase 8 — Wire Up & Run
```bash
flask db init
flask db migrate -m "initial: User model"
flask db upgrade
flask run
```
Visit `http://127.0.0.1:5000/` → should show the index page.
Visit `/auth/register` → create an account → redirected to `/auth/login`.
Log in → index page now greets you by username and shows "Log out".
### Phase 9 — Adding a New Feature (this is the "multiple feature" part)
Every additional feature follows the same pattern as `auth`/`main`. Example —
adding a `dashboard` feature that only logged-in users can see:
```bash
mkdir -p app/dashboard templates/dashboard
touch app/dashboard/__init__.py app/dashboard/routes.py templates/dashboard/index.html
```
`app/dashboard/routes.py`:
```python
from flask import Blueprint, render_template
from flask_login import login_required
dashboard_bp = Blueprint("dashboard", __name__, template_folder="../../templates/dashboard")
@dashboard_bp.route("/dashboard")
@login_required
def index():
return render_template("dashboard/index.html")
```
Register it in `app/__init__.py` alongside the other blueprints:
```python
from app.dashboard.routes import dashboard_bp
app.register_blueprint(dashboard_bp)
```
Repeat this pattern for every new feature — that's the whole point of the
blueprint layout: no existing file changes when a feature is added, only new
ones.
---
## Using This File With the Claude Code CLI
1. Place `CLAUDE.md` at the project root before running `claude`.
2. Work phase by phase, e.g.:
```
claude "Implement Phase 3 exactly as specified in CLAUDE.md"
```
3. Review the diff Claude Code produces, run/test it, `git commit`.
4. Move to the next phase. Don't skip ahead — later phases assume earlier files exist exactly as specified.
## Testing Checklist
- [ ] Can register a new user
- [ ] Duplicate email/username is rejected with a clear error
- [ ] Password is hashed, never stored in plain text
- [ ] Can log in and log out
- [ ] Index page differs for logged-in vs. anonymous users
- [ ] CSRF token present in both forms (`form.hidden_tag()`)
- [ ] `flask db upgrade` runs clean on a fresh clone
## Common Pitfalls
- Forgetting to set `SECRET_KEY` / `FLASK_APP` in `.env`.
- Circular imports between `app/__init__.py` and blueprints — always import blueprints *inside* `create_app()`, not at module top level.
- Forgetting `login_manager.login_view = "auth.login"` → `@login_required` redirects to a 404 instead of the login page.
- Running the app before `flask db upgrade` → "no such table: user".