getting there

This commit is contained in:
Andras Schmelczer 2026-05-30 15:40:53 +01:00
parent f87480e79d
commit 5ac6633d40
20 changed files with 898 additions and 112 deletions

View file

@ -94,7 +94,7 @@ async def get_data(
block_rows = conn.execute(
"""
SELECT id, tag, description, is_done, created_at
SELECT id, tag, description, is_done, difficulty, created_at
FROM blocks
WHERE tower_id = ?
ORDER BY position
@ -108,6 +108,7 @@ async def get_data(
tag=b["tag"],
description=b["description"],
is_done=bool(b["is_done"]),
difficulty=b["difficulty"],
created_at=b["created_at"],
)
for b in block_rows
@ -213,8 +214,9 @@ async def put_data(
"""
INSERT INTO blocks
(id, tower_id, user_id, position, tag,
description, is_done, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
description, is_done, difficulty,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
block.id,
@ -224,6 +226,7 @@ async def put_data(
block.tag,
block.description,
1 if block.is_done else 0,
block.difficulty,
created_at,
now,
),

View file

@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS pages (
position INTEGER NOT NULL,
name TEXT NOT NULL,
hide_create_tower_button INTEGER NOT NULL DEFAULT 0 CHECK (hide_create_tower_button IN (0, 1)),
keep_tasks_open INTEGER NOT NULL DEFAULT 0 CHECK (keep_tasks_open IN (0, 1)),
default_date_from INTEGER,
default_date_to INTEGER,
created_at INTEGER NOT NULL,
@ -47,6 +48,7 @@ CREATE TABLE IF NOT EXISTS blocks (
tag TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
is_done INTEGER NOT NULL DEFAULT 0 CHECK (is_done IN (0, 1)),
difficulty INTEGER NOT NULL DEFAULT 1 CHECK (difficulty >= 1),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
) STRICT;

View file

@ -1,2 +0,0 @@
ALTER TABLE pages ADD COLUMN keep_tasks_open INTEGER NOT NULL DEFAULT 0
CHECK (keep_tasks_open IN (0, 1));

View file

@ -27,6 +27,7 @@ class BlockIn(BaseModel):
tag: str = Field(max_length=200)
description: str = Field(max_length=10_000)
is_done: bool
difficulty: int = Field(default=1, ge=1, le=100)
created_at: Optional[int] = None
@field_validator("id")
@ -42,6 +43,7 @@ class BlockOut(BaseModel):
tag: str
description: str
is_done: bool
difficulty: int
created_at: int

View file

@ -215,6 +215,7 @@ def _make_tree() -> dict:
"tag": f"tag-{pi}-{ti}-{bi}",
"description": f"desc-{pi}-{ti}-{bi}",
"is_done": False,
"difficulty": bi + 1,
"created_at": 1700000000 + bi,
}
)
@ -266,6 +267,42 @@ async def test_round_trip(client: AsyncClient) -> None:
for bi, block in enumerate(tower["blocks"]):
assert block["id"] == tree["pages"][pi]["towers"][ti]["blocks"][bi]["id"]
assert block["tag"] == tree["pages"][pi]["towers"][ti]["blocks"][bi]["tag"]
assert (
block["difficulty"]
== tree["pages"][pi]["towers"][ti]["blocks"][bi]["difficulty"]
)
@pytest.mark.asyncio
async def test_difficulty_defaults_to_one_when_omitted(client: AsyncClient) -> None:
"""A block sent without `difficulty` is stored and returned as 1."""
token = make_uuidv4()
await client.post("/api/v1/register", json={"token": token})
headers = {"Authorization": f"Bearer {token}"}
tree = _make_tree()
# Drop difficulty from the very first block.
del tree["pages"][0]["towers"][0]["blocks"][0]["difficulty"]
put_resp = await client.put("/api/v1/data", json=tree, headers=headers)
assert put_resp.status_code == 204
data = (await client.get("/api/v1/data", headers=headers)).json()
assert data["pages"][0]["towers"][0]["blocks"][0]["difficulty"] == 1
@pytest.mark.asyncio
async def test_difficulty_must_be_positive(client: AsyncClient) -> None:
"""difficulty < 1 is rejected by validation."""
token = make_uuidv4()
await client.post("/api/v1/register", json={"token": token})
headers = {"Authorization": f"Bearer {token}"}
tree = _make_tree()
tree["pages"][0]["towers"][0]["blocks"][0]["difficulty"] = 0
resp = await client.put("/api/v1/data", json=tree, headers=headers)
assert resp.status_code == 400
# ---------------------------------------------------------------------------