# 03 | Models and Python migrations

## Goal

Define all five application tables and apply the first Python migration.

## What you are building

User owns projects; Project owns tasks. The ForeignKeyField names use the root ORM label models. Cascading a project deletion removes its tasks. Session stores a digest of an opaque cookie and an expiry; AuthAttempt persists login attempt counts. Define the schema once in models.py. makemigrations compares models with migration history and generates Python code; migrate applies that code. Commit the generated file. No JSON migration is needed. We define the complete schema now so later API chapters focus on behavior rather than repeated table changes.

## Build this chapter

Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.

## Step 1: CREATE - make parent folders, then create this file - models.py

These five models are the complete schema. Relationships define ownership and deletion behavior; handlers add authorization.

Create the parent directories, then save this complete file.

```python
"""Application tables. Generate Python migrations whenever these models change."""

from flaxon.db import Model, fields


class User(Model):
    id = fields.IntField(primary_key=True)
    name = fields.CharField(max_length=80)
    email = fields.CharField(max_length=254, unique=True)
    password_hash = fields.TextField()

    class Meta:
        table = "users"

    def __str__(self):
        return self.name


class Project(Model):
    id = fields.IntField(primary_key=True)
    owner = fields.ForeignKeyField(
        "models.User", related_name="projects", on_delete=fields.CASCADE
    )
    name = fields.CharField(max_length=120)
    description = fields.TextField(default="")
    created_at = fields.DatetimeField(auto_now_add=True)

    class Meta:
        table = "projects"

    def __str__(self):
        return self.name


class Task(Model):
    id = fields.IntField(primary_key=True)
    project = fields.ForeignKeyField(
        "models.Project", related_name="tasks", on_delete=fields.CASCADE
    )
    title = fields.CharField(max_length=200)
    status = fields.CharField(max_length=10, default="todo")
    due_date = fields.DateField(null=True)
    created_at = fields.DatetimeField(auto_now_add=True)

    class Meta:
        table = "tasks"

    def __str__(self):
        return self.title


class Session(Model):
    token_hash = fields.CharField(max_length=64, primary_key=True)
    user = fields.ForeignKeyField("models.User", null=True, on_delete=fields.CASCADE)
    csrf = fields.CharField(max_length=64)
    expires_at = fields.BigIntField(db_index=True)

    class Meta:
        table = "sessions"


class AuthAttempt(Model):
    key = fields.CharField(max_length=64, primary_key=True)
    attempts = fields.IntField(default=0)
    started_at = fields.BigIntField(db_index=True)

    class Meta:
        table = "auth_attempts"
```

**Say:** "These five models are the complete schema. Relationships define ownership and deletion behavior; handlers add authorization."

**Type:** Follow the action above in `models.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 2: EDIT - replace the entire file - admin.py

Keep explicit registration in this file. At chapter 3 it is empty; chapter 12 adds the domain models.

Select all existing contents of this file and replace them with the complete code below.

```python
"""Staff model registration is added in chapter 12."""
def register(admin):
    pass
```

**Say:** "Keep explicit registration in this file. At chapter 3 it is empty; chapter 12 adds the domain models."

**Type:** Follow the action above in `admin.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 3: CREATE - make parent folders, then create this file - migrations/__init__.py

This file owns the feature named by its module or component. Read the route/form flow before continuing.

Create the parent directories, then save this complete file.

```python

```

**Say:** "This file owns the feature named by its module or component. Read the route/form flow before continuing."

**Type:** Follow the action above in `migrations/__init__.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Step 4: EDIT - replace the marked blocks - app.py

The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact.

### Edit 1: find this exact block

Open `app.py`. Locate this block once. The unchanged surrounding lines identify its position; do not use line numbers from another revision.

```python

    @app.get("/health/course")
    async def health():
        return {"data": {"status": "ok"}}

    @app.get("/")
```
### Replace that block with

```python

    @app.get("/health/course")
    async def health():
        from models import User

        await User.all().limit(1)
        return {"data": {"status": "ok"}}

    @app.get("/")
```

Leave the rest of the file unchanged.

**Say:** "The application factory mounts only the features already introduced. Follow the action label: a marked-block edit keeps the surrounding factory intact."

**Type:** Follow the action above in `app.py`; explain each new function or binding as you add it.

**Viewers should see:** The saved file matches this step. After all steps, run the chapter checks below and show the expected result.

## Run and check

```bash
python management.py check
python management.py makemigrations --name initial
python management.py migrate --plan
python management.py migrate
python management.py migrate --status
python management.py runserver
```

## Flaxon API and ORM: the commands you will teach

This course uses the current Flaxon public integration rather than JSON migrations or hand-written SQL. Flaxon's `Model` and `fields` come from Tortoise; queries are asynchronous. These APIs are available in Flaxon 0.3.0.

**Say:** "Models describe tables. Settings choose the database. Flaxon opens and closes connections. Migrations record schema changes, and management.py applies them explicitly."

In `models.py`, the exact imports are:

```python
from flaxon.db import Model, fields
```

You have already created `Project` in this chapter. After migrating, use these queries in an async handler or the management shell. These examples explain the API; do not paste them at module scope, and do not add an unsecured demonstration route:

```python
project = await Project.create(
    owner_id=user["id"], name="Record the course", description="First project"
)
projects = await Project.filter(owner_id=user["id"]).order_by("-created_at")
project = await Project.filter(id=project_id, owner_id=user["id"]).first()
await Project.filter(id=project_id, owner_id=user["id"]).update(name="New name")
await Project.filter(id=project_id, owner_id=user["id"]).delete()
```

`user` is the authenticated customer dictionary and `project_id` is the validated route parameter introduced in chapters 4-5. Never trust an owner ID supplied by the browser. A model relationship does not perform authorization automatically.

For an operation that must commit or roll back together:

```python
from flaxon.db import in_transaction

async with in_transaction() as connection:
    project = await Project.create(
        owner_id=user["id"], name="Atomic project", using_db=connection
    )
    await Task.create(
        project_id=project.id, title="First task", using_db=connection
    )
```

The server manages normal connection startup and shutdown through `Flaxon.from_settings(...)`. The course tests create disposable schemas explicitly; normal application startup never silently changes tables.

Keep these responsibilities separate:

- `settings.py`: `DATABASE_URL`, debug mode, secrets and deployment configuration.
- `models.py`: fields, foreign keys, indexes and readable model names.
- `app.py`: `Flaxon.from_settings(...)`, middleware and `app.mount_module(...)`.
- `management.py`: one short entry point backed by `flaxon.management.execute`.
- `admin.py`: explicit staff model registration; chapter 12 adds controlled adapters.

Run these from `project_manager`:

```bash
python management.py check
python management.py makemigrations --name initial
python management.py migrate --plan
python management.py migrate
python management.py migrate --status
python management.py shell
python management.py setup-admin
python management.py runserver
```

Only run `makemigrations --name initial` once for this initial schema. Later changes use a descriptive new name. `createsuperuser` is an alias for `setup-admin`; migration files are Python and belong in Git. Stop the server before changing configuration. Staff credentials and customer accounts are separate.

The later chapters show actual `FlaxonModule` definitions, JSON responses, request parsing, middleware, Teloce compilation and Admin/CMS integration in their complete files. Optional SSR is outside this course's client-rendered SPA; use the dedicated framework guide when adding it.


## Expected result

check reports valid settings and registrations. makemigrations creates migrations/0001_initial.py. migrate applies it; status lists it as applied. A second migrate performs no new schema changes.

## Common errors

Use a fresh learner project. Do not apply the starter ProjectNote migration before replacing models.py. If you already migrated the starter, select a NEW DATA_DIR in .env and start the course migration history in a separate project. Do not delete or reset a database containing data you need. No such table means the selected database has not been migrated.

## Short exercise

Add a priority field to Task in a disposable copy. Generate and inspect a second migration without applying it to your course database.

## What to say and show

Say: "Define all five application tables and apply the first Python migration. The server remains responsible for persistence and authorization; the browser presents the result."

Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.

## Save your checkpoint

```bash
git add .
git commit -m "Complete chapter 03: Models and Python migrations"
```
