feat(v4.4.0): Tasks, Sub-items & Dependencies
- Add is_task flag on collections (toggle-task API) - New page_dependencies table with auto_shift config (blocks/blocked_by/related) - API: GET/POST/DELETE page dependencies - API: POST auto-shift dates based on blocking dependencies (skip_weekends option) - 5 new tests (171 total)
This commit is contained in:
+118
-1
@@ -2451,4 +2451,121 @@ def test_view_unknown_falls_back_to_table(client):
|
||||
coll_id = resp.json()["id"]
|
||||
resp = client.get(f"/db/{coll_id}/view/unknown_view_type")
|
||||
assert resp.status_code == 200
|
||||
assert "<table>" in resp.text
|
||||
assert "<table>" in resp.text
|
||||
|
||||
|
||||
# ── v4.4.0: Tasks & Dependencies ──
|
||||
|
||||
|
||||
def test_toggle_task_flag(client):
|
||||
"""Toggle is_task on a collection."""
|
||||
resp = client.post("/db/api", json={"name": "Task DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
|
||||
resp = client.put(f"/db/{coll_id}/toggle-task/api")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["is_task"] is True
|
||||
assert resp.json()["mode"] == "tasks"
|
||||
|
||||
resp = client.get(f"/db/{coll_id}/api")
|
||||
assert resp.json()["collection"]["is_task"] == 1
|
||||
|
||||
# Toggle back
|
||||
resp = client.put(f"/db/{coll_id}/toggle-task/api")
|
||||
assert resp.json()["is_task"] is False
|
||||
assert resp.json()["mode"] == "standard"
|
||||
|
||||
|
||||
def test_page_dependencies_crud(client):
|
||||
"""Add, list, and remove page dependencies."""
|
||||
resp = client.post("/db/api", json={"name": "Dep DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
|
||||
p1 = client.post(f"/db/{coll_id}/pages/api", json={"title": "Task A"})
|
||||
p2 = client.post(f"/db/{coll_id}/pages/api", json={"title": "Task B"})
|
||||
pid1 = p1.json()["id"]
|
||||
pid2 = p2.json()["id"]
|
||||
|
||||
# Add dependency: Task A blocks Task B
|
||||
resp = client.post(f"/db/{coll_id}/pages/{pid2}/dependencies/api", json={
|
||||
"dependency_id": pid1,
|
||||
"dependency_type": "blocks",
|
||||
"auto_shift": "overlap",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "added"
|
||||
dep_id = resp.json()["id"]
|
||||
|
||||
# List dependencies
|
||||
resp = client.get(f"/db/{coll_id}/pages/{pid2}/dependencies/api")
|
||||
deps = resp.json()["dependencies"]
|
||||
assert len(deps) == 1
|
||||
assert deps[0]["dependency_type"] == "blocks"
|
||||
assert deps[0]["dependency_title"] == "Task A"
|
||||
|
||||
# Remove dependency
|
||||
resp = client.delete(f"/db/{coll_id}/pages/{pid2}/dependencies/{dep_id}/api")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "removed"
|
||||
|
||||
resp = client.get(f"/db/{coll_id}/pages/{pid2}/dependencies/api")
|
||||
assert len(resp.json()["dependencies"]) == 0
|
||||
|
||||
|
||||
def test_page_dependencies_duplicate_rejected(client):
|
||||
"""Duplicate dependency returns 409."""
|
||||
resp = client.post("/db/api", json={"name": "Dup Dep DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
p1 = client.post(f"/db/{coll_id}/pages/api", json={"title": "A"})
|
||||
p2 = client.post(f"/db/{coll_id}/pages/api", json={"title": "B"})
|
||||
|
||||
client.post(f"/db/{coll_id}/pages/{p2.json()['id']}/dependencies/api", json={
|
||||
"dependency_id": p1.json()["id"],
|
||||
})
|
||||
resp = client.post(f"/db/{coll_id}/pages/{p2.json()['id']}/dependencies/api", json={
|
||||
"dependency_id": p1.json()["id"],
|
||||
})
|
||||
assert resp.status_code == 409
|
||||
|
||||
|
||||
def test_auto_shift_dates(client):
|
||||
"""Auto-shift dates when a blocking task is completed."""
|
||||
resp = client.post("/db/api", json={"name": "Shift DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
|
||||
# Blocking task with end date July 10
|
||||
p1 = client.post(f"/db/{coll_id}/pages/api", json={
|
||||
"title": "Blocker",
|
||||
"properties": {"Timeline": "2026-07-01...2026-07-10"},
|
||||
})
|
||||
# Blocked task
|
||||
p2 = client.post(f"/db/{coll_id}/pages/api", json={
|
||||
"title": "Dependent",
|
||||
"properties": {"Timeline": "2026-07-11...2026-07-20"},
|
||||
})
|
||||
|
||||
# Add dependency
|
||||
client.post(f"/db/{coll_id}/pages/{p2.json()['id']}/dependencies/api", json={
|
||||
"dependency_id": p1.json()["id"],
|
||||
"dependency_type": "blocks",
|
||||
})
|
||||
|
||||
# Auto-shift
|
||||
resp = client.post(f"/db/{coll_id}/pages/{p2.json()['id']}/auto-shift/api", json={
|
||||
"skip_weekends": False,
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["shifted"] is True
|
||||
assert data["new_start"] == "2026-07-11" # day after blocker ends
|
||||
|
||||
|
||||
def test_auto_shift_no_blockers(client):
|
||||
"""Auto-shift with no blocking dependencies returns shifted=False."""
|
||||
resp = client.post("/db/api", json={"name": "NoBlock DB"})
|
||||
coll_id = resp.json()["id"]
|
||||
p = client.post(f"/db/{coll_id}/pages/api", json={"title": "Lone Task"})
|
||||
|
||||
resp = client.post(f"/db/{coll_id}/pages/{p.json()['id']}/auto-shift/api")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["shifted"] is False
|
||||
Reference in New Issue
Block a user