Merge pull request 'Remind from due Week Ahead plans' (#1193) from timmy/1192-week-ahead-start-day-reminder into main
This commit is contained in:
commit
5c74051aab
|
|
@ -127,7 +127,7 @@ async def _start_day_plan_snapshot() -> dict:
|
||||||
login = user.get("login") if isinstance(user, dict) else None
|
login = user.get("login") if isinstance(user, dict) else None
|
||||||
if not isinstance(login, str) or not login:
|
if not isinstance(login, str) or not login:
|
||||||
return {}
|
return {}
|
||||||
return await asyncio.to_thread(_today_store().get_tomorrow, login)
|
return await asyncio.to_thread(_today_store().get_start_day_plan, login)
|
||||||
|
|
||||||
|
|
||||||
async def _push_poll_loop() -> None:
|
async def _push_poll_loop() -> None:
|
||||||
|
|
|
||||||
|
|
@ -450,6 +450,32 @@ class TodayStore:
|
||||||
raise PrivateStateEncryptionError("private state could not be decrypted")
|
raise PrivateStateEncryptionError("private state could not be decrypted")
|
||||||
return {"revision": int(row[0]), **payload}
|
return {"revision": int(row[0]), **payload}
|
||||||
|
|
||||||
|
def get_start_day_plan(self, login: str) -> dict:
|
||||||
|
"""Return the next private planned day without changing either plan."""
|
||||||
|
login = self._normalize_login(login)
|
||||||
|
with self._connect() as connection:
|
||||||
|
week_row = connection.execute(
|
||||||
|
"SELECT revision, payload FROM week_plans WHERE login = ?", (login,)
|
||||||
|
).fetchone()
|
||||||
|
if week_row is not None:
|
||||||
|
week = self._week_snapshot(week_row, login)
|
||||||
|
local_date = datetime.fromtimestamp(
|
||||||
|
self.clock(), ZoneInfo(week["timezone"])
|
||||||
|
).date().isoformat()
|
||||||
|
for day in week["days"]:
|
||||||
|
if (
|
||||||
|
isinstance(day, dict)
|
||||||
|
and isinstance(day.get("ids"), list)
|
||||||
|
and day["ids"]
|
||||||
|
and day.get("plan_date", "") <= local_date
|
||||||
|
):
|
||||||
|
return {**day, "timezone": week.get("timezone")}
|
||||||
|
tomorrow_row = connection.execute(
|
||||||
|
"SELECT revision, payload FROM tomorrow_plans WHERE login = ?", (login,)
|
||||||
|
).fetchone()
|
||||||
|
tomorrow = self._tomorrow_snapshot(tomorrow_row, login)
|
||||||
|
return tomorrow if tomorrow.get("ids") and tomorrow.get("plan_date") else {}
|
||||||
|
|
||||||
def get_week(self, login: str) -> dict:
|
def get_week(self, login: str) -> dict:
|
||||||
login = self._normalize_login(login)
|
login = self._normalize_login(login)
|
||||||
with self._connect() as connection:
|
with self._connect() as connection:
|
||||||
|
|
|
||||||
|
|
@ -281,15 +281,18 @@ async def test_authenticated_device_can_enable_start_day_reminders(tmp_path, mon
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_worker_snapshot_reads_the_confirmed_accounts_tomorrow_plan(monkeypatch):
|
async def test_worker_snapshot_reads_the_confirmed_accounts_next_planned_day(monkeypatch):
|
||||||
async def current_user():
|
async def current_user():
|
||||||
return {"login": "Timmy"}
|
return {"login": "Timmy"}
|
||||||
|
|
||||||
class Store:
|
class Store:
|
||||||
def get_tomorrow(self, login):
|
def get_start_day_plan(self, login):
|
||||||
assert login == "Timmy"
|
assert login == "Timmy"
|
||||||
return {"ids": ["one"], "plan_date": "2026-08-20"}
|
return {"ids": ["one"], "plan_date": "2026-08-20"}
|
||||||
|
|
||||||
|
def get_tomorrow(self, _login):
|
||||||
|
raise AssertionError("the worker must use Week Ahead discovery")
|
||||||
|
|
||||||
monkeypatch.setattr(main, "current_user", current_user)
|
monkeypatch.setattr(main, "current_user", current_user)
|
||||||
monkeypatch.setattr(main, "_today_store", lambda: Store())
|
monkeypatch.setattr(main, "_today_store", lambda: Store())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,72 @@ def test_week_plan_is_encrypted_account_scoped_revisioned_and_preserves_other_da
|
||||||
assert conflict.value.snapshot == updated
|
assert conflict.value.snapshot == updated
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_day_plan_selects_earliest_nonempty_week_date_without_mutation(tmp_path):
|
||||||
|
store = TodayStore(
|
||||||
|
tmp_path / "today.sqlite3", encryption_key=b"r" * 32,
|
||||||
|
clock=lambda: datetime(2026, 8, 21, 13, tzinfo=timezone.utc).timestamp(),
|
||||||
|
)
|
||||||
|
days = [
|
||||||
|
{
|
||||||
|
"plan_date": "2026-08-20", "ids": [],
|
||||||
|
"capacity_minutes": 60, "estimates": {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plan_date": "2026-08-21", "ids": ["issue:private/repo:7:"],
|
||||||
|
"capacity_minutes": 90,
|
||||||
|
"estimates": {"issue:private/repo:7:": 45},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plan_date": "2026-08-22", "ids": ["issue:private/repo:8:"],
|
||||||
|
"capacity_minutes": 120,
|
||||||
|
"estimates": {"issue:private/repo:8:": 30},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
week = store.replace_week(
|
||||||
|
"timmy", base_revision=0, days=days, timezone="America/New_York"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert store.get_start_day_plan("Timmy") == {
|
||||||
|
**days[1], "timezone": "America/New_York",
|
||||||
|
}
|
||||||
|
assert store.get_week("timmy") == week
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_day_plan_falls_back_to_tomorrow_without_migrating_it(tmp_path):
|
||||||
|
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"r" * 32)
|
||||||
|
tomorrow = store.replace_tomorrow(
|
||||||
|
"timmy", base_revision=0, ids=["issue:private/repo:9:"],
|
||||||
|
capacity_minutes=75, estimates={"issue:private/repo:9:": 45},
|
||||||
|
plan_date="2026-08-21", timezone="UTC",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert store.get_start_day_plan("timmy") == tomorrow
|
||||||
|
assert store.get_tomorrow("timmy") == tomorrow
|
||||||
|
with store._connect() as connection:
|
||||||
|
assert connection.execute(
|
||||||
|
"SELECT COUNT(*) FROM week_plans WHERE login = ?", ("timmy",)
|
||||||
|
).fetchone()[0] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_day_plan_uses_due_tomorrow_fallback_before_future_week_day(tmp_path):
|
||||||
|
store = TodayStore(
|
||||||
|
tmp_path / "today.sqlite3", encryption_key=b"r" * 32,
|
||||||
|
clock=lambda: datetime(2026, 8, 20, 13, tzinfo=timezone.utc).timestamp(),
|
||||||
|
)
|
||||||
|
future = [{
|
||||||
|
"plan_date": "2026-08-21", "ids": ["issue:private/repo:10:"],
|
||||||
|
"capacity_minutes": 90, "estimates": {"issue:private/repo:10:": 45},
|
||||||
|
}]
|
||||||
|
store.replace_week("timmy", base_revision=0, days=future, timezone="UTC")
|
||||||
|
tomorrow = store.replace_tomorrow(
|
||||||
|
"timmy", base_revision=0, ids=["issue:private/repo:9:"],
|
||||||
|
capacity_minutes=75, estimates={"issue:private/repo:9:": 45},
|
||||||
|
plan_date="2026-08-20", timezone="UTC",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert store.get_start_day_plan("timmy") == tomorrow
|
||||||
|
|
||||||
|
|
||||||
def test_week_plan_rejects_the_same_work_on_multiple_dates_without_advancing_revision(tmp_path):
|
def test_week_plan_rejects_the_same_work_on_multiple_dates_without_advancing_revision(tmp_path):
|
||||||
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"w" * 32)
|
store = TodayStore(tmp_path / "today.sqlite3", encryption_key=b"w" * 32)
|
||||||
original = store.replace_week(
|
original = store.replace_week(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user