feat: Refactor ScheduleModal to support interval scheduling with date input and deadline toggle
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m30s
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m30s
- Updated ChoreSchedule model to include anchor_date and interval_has_deadline. - Refactored interval scheduling logic in scheduleUtils to use anchor_date. - Introduced DateInputField component for selecting anchor dates in ScheduleModal. - Enhanced ScheduleModal to include a stepper for interval days and a toggle for deadline. - Updated tests for ScheduleModal and scheduleUtils to reflect new interval scheduling logic. - Added DateInputField tests to ensure proper functionality and prop handling.
This commit is contained in:
@@ -70,19 +70,23 @@ def set_chore_schedule(child_id, task_id):
|
||||
)
|
||||
else:
|
||||
interval_days = data.get('interval_days', 2)
|
||||
anchor_weekday = data.get('anchor_weekday', 0)
|
||||
anchor_date = data.get('anchor_date', '')
|
||||
interval_has_deadline = data.get('interval_has_deadline', True)
|
||||
interval_hour = data.get('interval_hour', 0)
|
||||
interval_minute = data.get('interval_minute', 0)
|
||||
if not isinstance(interval_days, int) or not (2 <= interval_days <= 7):
|
||||
return jsonify({'error': 'interval_days must be an integer between 2 and 7', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
if not isinstance(anchor_weekday, int) or not (0 <= anchor_weekday <= 6):
|
||||
return jsonify({'error': 'anchor_weekday must be an integer between 0 and 6', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
if not isinstance(interval_days, int) or not (1 <= interval_days <= 7):
|
||||
return jsonify({'error': 'interval_days must be an integer between 1 and 7', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
if not isinstance(anchor_date, str):
|
||||
return jsonify({'error': 'anchor_date must be a string', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
if not isinstance(interval_has_deadline, bool):
|
||||
return jsonify({'error': 'interval_has_deadline must be a boolean', 'code': ErrorCodes.INVALID_VALUE}), 400
|
||||
schedule = ChoreSchedule(
|
||||
child_id=child_id,
|
||||
task_id=task_id,
|
||||
mode='interval',
|
||||
interval_days=interval_days,
|
||||
anchor_weekday=anchor_weekday,
|
||||
anchor_date=anchor_date,
|
||||
interval_has_deadline=interval_has_deadline,
|
||||
interval_hour=interval_hour,
|
||||
interval_minute=interval_minute,
|
||||
)
|
||||
|
||||
@@ -37,8 +37,9 @@ class ChoreSchedule(BaseModel):
|
||||
default_minute: int = 0 # master deadline minute for 'days' mode
|
||||
|
||||
# mode='interval' fields
|
||||
interval_days: int = 2 # 2–7
|
||||
anchor_weekday: int = 0 # 0=Sun–6=Sat
|
||||
interval_days: int = 2 # 1–7
|
||||
anchor_date: str = "" # ISO date string e.g. "2026-02-25"; "" = use today
|
||||
interval_has_deadline: bool = True # False = "Anytime" (no deadline)
|
||||
interval_hour: int = 0
|
||||
interval_minute: int = 0
|
||||
|
||||
@@ -52,7 +53,8 @@ class ChoreSchedule(BaseModel):
|
||||
default_hour=d.get('default_hour', 8),
|
||||
default_minute=d.get('default_minute', 0),
|
||||
interval_days=d.get('interval_days', 2),
|
||||
anchor_weekday=d.get('anchor_weekday', 0),
|
||||
anchor_date=d.get('anchor_date', ''),
|
||||
interval_has_deadline=d.get('interval_has_deadline', True),
|
||||
interval_hour=d.get('interval_hour', 0),
|
||||
interval_minute=d.get('interval_minute', 0),
|
||||
id=d.get('id'),
|
||||
@@ -70,7 +72,8 @@ class ChoreSchedule(BaseModel):
|
||||
'default_hour': self.default_hour,
|
||||
'default_minute': self.default_minute,
|
||||
'interval_days': self.interval_days,
|
||||
'anchor_weekday': self.anchor_weekday,
|
||||
'anchor_date': self.anchor_date,
|
||||
'interval_has_deadline': self.interval_has_deadline,
|
||||
'interval_hour': self.interval_hour,
|
||||
'interval_minute': self.interval_minute,
|
||||
})
|
||||
|
||||
@@ -117,7 +117,8 @@ def test_set_schedule_interval_mode(client):
|
||||
payload = {
|
||||
"mode": "interval",
|
||||
"interval_days": 3,
|
||||
"anchor_weekday": 2,
|
||||
"anchor_date": "2026-03-01",
|
||||
"interval_has_deadline": True,
|
||||
"interval_hour": 14,
|
||||
"interval_minute": 30,
|
||||
}
|
||||
@@ -126,25 +127,57 @@ def test_set_schedule_interval_mode(client):
|
||||
data = resp.get_json()
|
||||
assert data["mode"] == "interval"
|
||||
assert data["interval_days"] == 3
|
||||
assert data["anchor_weekday"] == 2
|
||||
assert data["anchor_date"] == "2026-03-01"
|
||||
assert data["interval_has_deadline"] is True
|
||||
assert data["interval_hour"] == 14
|
||||
assert data["interval_minute"] == 30
|
||||
|
||||
|
||||
def test_set_schedule_interval_bad_days(client):
|
||||
# interval_days = 1 is out of range [2–7]
|
||||
payload = {"mode": "interval", "interval_days": 1, "anchor_weekday": 0}
|
||||
def test_set_schedule_interval_days_1_valid(client):
|
||||
"""interval_days=1 is now valid (range changed to [1, 7])."""
|
||||
payload = {"mode": "interval", "interval_days": 1, "anchor_date": ""}
|
||||
resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()["interval_days"] == 1
|
||||
|
||||
|
||||
def test_set_schedule_interval_days_0_invalid(client):
|
||||
"""interval_days=0 is still out of range."""
|
||||
payload = {"mode": "interval", "interval_days": 0, "anchor_date": ""}
|
||||
resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_set_schedule_interval_bad_weekday(client):
|
||||
# anchor_weekday = 7 is out of range [0–6]
|
||||
payload = {"mode": "interval", "interval_days": 2, "anchor_weekday": 7}
|
||||
def test_set_schedule_interval_days_8_invalid(client):
|
||||
"""interval_days=8 is still out of range."""
|
||||
payload = {"mode": "interval", "interval_days": 8, "anchor_date": ""}
|
||||
resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_set_schedule_interval_has_deadline_false(client):
|
||||
"""interval_has_deadline=False is accepted and persisted."""
|
||||
payload = {
|
||||
"mode": "interval",
|
||||
"interval_days": 2,
|
||||
"anchor_date": "",
|
||||
"interval_has_deadline": False,
|
||||
"interval_hour": 0,
|
||||
"interval_minute": 0,
|
||||
}
|
||||
resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()["interval_has_deadline"] is False
|
||||
|
||||
|
||||
def test_set_schedule_interval_anchor_date_empty_string(client):
|
||||
"""anchor_date empty string is valid (means use today)."""
|
||||
payload = {"mode": "interval", "interval_days": 2, "anchor_date": ""}
|
||||
resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()["anchor_date"] == ""
|
||||
|
||||
|
||||
def test_set_schedule_invalid_mode(client):
|
||||
payload = {"mode": "weekly", "day_configs": []}
|
||||
resp = client.put(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule', json=payload)
|
||||
@@ -160,13 +193,56 @@ def test_set_schedule_upserts_existing(client):
|
||||
# Overwrite with interval mode
|
||||
client.put(
|
||||
f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule',
|
||||
json={"mode": "interval", "interval_days": 2, "anchor_weekday": 0, "interval_hour": 9, "interval_minute": 0},
|
||||
json={"mode": "interval", "interval_days": 2, "anchor_date": "", "interval_hour": 9, "interval_minute": 0},
|
||||
)
|
||||
resp = client.get(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()["mode"] == "interval"
|
||||
|
||||
|
||||
def test_old_record_missing_new_fields_loads_with_defaults(client):
|
||||
"""Old DB records without anchor_date/interval_has_deadline load with correct defaults."""
|
||||
from db.chore_schedules import upsert_schedule
|
||||
from models.chore_schedule import ChoreSchedule
|
||||
|
||||
# Insert a schedule as if it was created before phase 2 (missing new fields)
|
||||
old_style = ChoreSchedule(
|
||||
child_id=TEST_CHILD_ID,
|
||||
task_id=TEST_TASK_ID,
|
||||
mode='interval',
|
||||
interval_days=3,
|
||||
anchor_date='', # default value
|
||||
interval_has_deadline=True, # default value
|
||||
interval_hour=8,
|
||||
interval_minute=0,
|
||||
)
|
||||
upsert_schedule(old_style)
|
||||
|
||||
# Manually wipe the new fields from the raw stored record to simulate a pre-phase-2 record
|
||||
from db.db import chore_schedules_db
|
||||
from tinydb import Query
|
||||
ScheduleQ = Query()
|
||||
record = chore_schedules_db.search(
|
||||
(ScheduleQ.child_id == TEST_CHILD_ID) & (ScheduleQ.task_id == TEST_TASK_ID)
|
||||
)[0]
|
||||
doc_id = chore_schedules_db.get(
|
||||
(ScheduleQ.child_id == TEST_CHILD_ID) & (ScheduleQ.task_id == TEST_TASK_ID)
|
||||
).doc_id
|
||||
chore_schedules_db.update(
|
||||
lambda rec: (
|
||||
rec.pop('anchor_date', None),
|
||||
rec.pop('interval_has_deadline', None),
|
||||
),
|
||||
doc_ids=[doc_id],
|
||||
)
|
||||
|
||||
resp = client.get(f'/child/{TEST_CHILD_ID}/task/{TEST_TASK_ID}/schedule')
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data['anchor_date'] == ''
|
||||
assert data['interval_has_deadline'] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DELETE schedule
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user