Add optional time ranges and a calendar day view
Tasks can now have a start/end time on their deadline date. The new Calendar view shows a day as a timeline (with a week strip and projected repeats of recurring tasks), and the task form lists what is already planned that day and flags overlaps. Existing databases are migrated to schema v2 automatically, with a backup copy taken first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -120,3 +120,63 @@ def test_export_import_roundtrip(client):
|
||||
assert cats[exported_task["category_id"]] == "Work"
|
||||
|
||||
assert client.post("/api/import", headers=H, json={"app": "other", "version": 1}).status_code == 400
|
||||
|
||||
|
||||
def test_times(client):
|
||||
t = client.post("/api/tasks", headers=H, json={
|
||||
"title": "Meeting", "due_date": "2026-10-08", "start_time": "10:00", "end_time": "12:00",
|
||||
"recurrence": "weekly",
|
||||
})
|
||||
assert t.status_code == 201, t.text
|
||||
t = t.json()
|
||||
assert (t["start_time"], t["end_time"]) == ("10:00", "12:00")
|
||||
|
||||
bad = [
|
||||
{"title": "x", "start_time": "10:00"}, # no date
|
||||
{"title": "x", "due_date": "2026-10-08", "end_time": "10:00"}, # end without start
|
||||
{"title": "x", "due_date": "2026-10-08", "start_time": "10:00", "end_time": "09:00"},
|
||||
]
|
||||
for body in bad:
|
||||
assert client.post("/api/tasks", headers=H, json=body).status_code == 422, body
|
||||
|
||||
# Clearing the date while a time is set is rejected; clearing all three works.
|
||||
assert client.patch(f"/api/tasks/{t['id']}", headers=H, json={"due_date": None}).status_code == 422
|
||||
assert client.patch(f"/api/tasks/{t['id']}", headers=H, json={"end_time": "09:30"}).status_code == 422
|
||||
assert client.patch(f"/api/tasks/{t['id']}", headers=H, json={"end_time": None}).json()["end_time"] is None
|
||||
|
||||
# The next occurrence of a recurring task keeps its time.
|
||||
nxt = client.post(f"/api/tasks/{t['id']}/complete", headers=H, json={"today": "2026-10-08"}).json()["next"]
|
||||
assert (nxt["due_date"], nxt["start_time"]) == ("2026-10-15", "10:00")
|
||||
|
||||
cleared = client.patch(f"/api/tasks/{nxt['id']}", headers=H,
|
||||
json={"due_date": None, "start_time": None, "end_time": None}).json()
|
||||
assert cleared["due_date"] is None and cleared["start_time"] is None
|
||||
|
||||
|
||||
def test_migration_from_v1(tmp_path, monkeypatch):
|
||||
import sqlite3
|
||||
from app import db
|
||||
path = tmp_path / "old.db"
|
||||
conn = sqlite3.connect(path)
|
||||
conn.executescript("""
|
||||
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
||||
INSERT INTO meta VALUES ('schema_version', '1');
|
||||
CREATE TABLE categories (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
color TEXT NOT NULL DEFAULT '#6b7280', position INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT NOT NULL, notes TEXT NOT NULL DEFAULT '',
|
||||
category_id INTEGER, priority INTEGER NOT NULL DEFAULT 0, due_date TEXT,
|
||||
tags TEXT NOT NULL DEFAULT '[]', recurrence TEXT NOT NULL DEFAULT 'none',
|
||||
recur_interval INTEGER NOT NULL DEFAULT 1, done INTEGER NOT NULL DEFAULT 0, done_at TEXT,
|
||||
created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
||||
CREATE TABLE subtasks (id INTEGER PRIMARY KEY, task_id INTEGER NOT NULL, title TEXT NOT NULL,
|
||||
done INTEGER NOT NULL DEFAULT 0, position INTEGER NOT NULL DEFAULT 0);
|
||||
INSERT INTO tasks (title, created_at, updated_at) VALUES ('old task', 'x', 'x');
|
||||
""")
|
||||
conn.close()
|
||||
monkeypatch.setattr(db, "DB_PATH", str(path))
|
||||
db.init()
|
||||
conn = sqlite3.connect(path)
|
||||
assert conn.execute("SELECT title, start_time FROM tasks").fetchall() == [("old task", None)]
|
||||
assert conn.execute("SELECT value FROM meta").fetchone()[0] == "2"
|
||||
conn.close()
|
||||
assert (tmp_path / "old.db.before-v2.bak").exists()
|
||||
|
||||
Reference in New Issue
Block a user