fix: return extension_date regardless of server UTC date
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 4m14s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 4m14s
list_child_tasks used date_type.today() (server UTC) to look up the TaskExtension record. When the client's local date differed from the server's UTC date (e.g. user is UTC-5 and it's past midnight UTC), the lookup returned None and extension_date was incorrectly null. Fix: - Add get_extension_for_child_task() to db/task_extensions.py that queries by child_id + task_id only, without a date filter, and returns the entry with the latest date when multiple exist. - Update list_child_tasks to use the new function. The comment already states 'client does all time math', so returning the stored extension_date is correct — the frontend compares it to local date. - In extend_chore_time, delete any existing extension for the child+task before inserting the new one to prevent stale records from accumulating across days. - Add regression test that inserts an extension for yesterday's date and asserts extension_date is non-null in the list-tasks response. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
from datetime import date as date_type, datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from tinydb import Query
|
from tinydb import Query
|
||||||
@@ -31,7 +31,7 @@ from models.tracking_event import TrackingEvent
|
|||||||
from utils.tracking_logger import log_tracking_event
|
from utils.tracking_logger import log_tracking_event
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from db.chore_schedules import get_schedule
|
from db.chore_schedules import get_schedule
|
||||||
from db.task_extensions import get_extension
|
from db.task_extensions import get_extension_for_child_task
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
child_api = Blueprint('child_api', __name__)
|
child_api = Blueprint('child_api', __name__)
|
||||||
@@ -288,8 +288,7 @@ def list_child_tasks(id):
|
|||||||
if task_obj.type == 'chore':
|
if task_obj.type == 'chore':
|
||||||
schedule = get_schedule(id, tid)
|
schedule = get_schedule(id, tid)
|
||||||
ct_dict['schedule'] = schedule.to_dict() if schedule else None
|
ct_dict['schedule'] = schedule.to_dict() if schedule else None
|
||||||
today_str = date_type.today().isoformat()
|
ext = get_extension_for_child_task(id, tid)
|
||||||
ext = get_extension(id, tid, today_str)
|
|
||||||
ct_dict['extension_date'] = ext.date if ext else None
|
ct_dict['extension_date'] = ext.date if ext else None
|
||||||
|
|
||||||
# Attach pending confirmation status for chores
|
# Attach pending confirmation status for chores
|
||||||
|
|||||||
@@ -160,11 +160,14 @@ def extend_chore_time(child_id, task_id):
|
|||||||
if not date or not isinstance(date, str):
|
if not date or not isinstance(date, str):
|
||||||
return jsonify({'error': 'date is required (ISO date string)', 'code': ErrorCodes.MISSING_FIELD}), 400
|
return jsonify({'error': 'date is required (ISO date string)', 'code': ErrorCodes.MISSING_FIELD}), 400
|
||||||
|
|
||||||
# 409 if already extended for this date
|
# 409 if already extended for this exact date
|
||||||
existing = get_extension(child_id, task_id, date)
|
existing = get_extension(child_id, task_id, date)
|
||||||
if existing:
|
if existing:
|
||||||
return jsonify({'error': 'Chore already extended for this date', 'code': 'ALREADY_EXTENDED'}), 409
|
return jsonify({'error': 'Chore already extended for this date', 'code': 'ALREADY_EXTENDED'}), 409
|
||||||
|
|
||||||
|
# Clear any prior extension for this child+task before inserting the new one
|
||||||
|
# so stale records from previous dates do not accumulate.
|
||||||
|
delete_extension_for_child_task(child_id, task_id)
|
||||||
extension = TaskExtension(child_id=child_id, task_id=task_id, date=date)
|
extension = TaskExtension(child_id=child_id, task_id=task_id, date=date)
|
||||||
add_extension(extension)
|
add_extension(extension)
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,20 @@ def delete_extensions_for_task(task_id: str) -> None:
|
|||||||
def delete_extension_for_child_task(child_id: str, task_id: str) -> None:
|
def delete_extension_for_child_task(child_id: str, task_id: str) -> None:
|
||||||
q = Query()
|
q = Query()
|
||||||
task_extensions_db.remove((q.child_id == child_id) & (q.task_id == task_id))
|
task_extensions_db.remove((q.child_id == child_id) & (q.task_id == task_id))
|
||||||
|
|
||||||
|
|
||||||
|
def get_extension_for_child_task(child_id: str, task_id: str) -> TaskExtension | None:
|
||||||
|
"""Return the extension for a child+task without filtering by date.
|
||||||
|
|
||||||
|
Avoids timezone mismatches between the server (UTC) and the client's local
|
||||||
|
date. The caller (or the frontend) is responsible for deciding whether the
|
||||||
|
returned extension_date is still applicable.
|
||||||
|
"""
|
||||||
|
q = Query()
|
||||||
|
results = task_extensions_db.search(
|
||||||
|
(q.child_id == child_id) & (q.task_id == task_id)
|
||||||
|
)
|
||||||
|
if not results:
|
||||||
|
return None
|
||||||
|
latest = max(results, key=lambda r: r.get('date', ''))
|
||||||
|
return TaskExtension.from_dict(latest)
|
||||||
|
|||||||
@@ -439,6 +439,29 @@ def test_list_child_tasks_returns_extension_date_when_set(client):
|
|||||||
assert tasks[TASK_GOOD_ID]['extension_date'] == today
|
assert tasks[TASK_GOOD_ID]['extension_date'] == today
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_child_tasks_returns_extension_date_for_yesterday(client):
|
||||||
|
"""Regression: extension inserted for yesterday's date must still be returned.
|
||||||
|
|
||||||
|
Previously, list-tasks used date_type.today() on the server to look up
|
||||||
|
extensions, causing a timezone mismatch when the client's local date
|
||||||
|
differed from the server's UTC date. The fix removes the date filter so
|
||||||
|
any stored extension is returned regardless of which date it was created
|
||||||
|
for.
|
||||||
|
"""
|
||||||
|
_setup_sched_child_and_tasks(task_db, child_db)
|
||||||
|
from datetime import date as date_type_local, timedelta
|
||||||
|
yesterday = (date_type_local.today() - timedelta(days=1)).isoformat()
|
||||||
|
task_extensions_db.insert({
|
||||||
|
'id': 'ext-yesterday',
|
||||||
|
'child_id': CHILD_SCHED_ID,
|
||||||
|
'task_id': TASK_GOOD_ID,
|
||||||
|
'date': yesterday,
|
||||||
|
})
|
||||||
|
resp = client.get(f'/child/{CHILD_SCHED_ID}/list-tasks')
|
||||||
|
tasks = {t['id']: t for t in resp.get_json()['tasks']}
|
||||||
|
assert tasks[TASK_GOOD_ID]['extension_date'] == yesterday
|
||||||
|
|
||||||
|
|
||||||
def test_list_child_tasks_extension_date_null_when_not_set(client):
|
def test_list_child_tasks_extension_date_null_when_not_set(client):
|
||||||
"""Good chore with no extension returns extension_date=null."""
|
"""Good chore with no extension returns extension_date=null."""
|
||||||
_setup_sched_child_and_tasks(task_db, child_db)
|
_setup_sched_child_and_tasks(task_db, child_db)
|
||||||
|
|||||||
Reference in New Issue
Block a user