feat: make calendar capacity recurrence-aware (Closes #1208)
This commit is contained in:
1 parent
359d4337eb
commit
f1adffef6b
4 files changed
+195
-11
No files matched your search
@@ -103,10 +103,12 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
expect(capacity_import).to_be_visible()
|
||||
private_title = "Private customer planning"
|
||||
tomorrow = (date.today() + timedelta(days=1)).strftime("%Y%m%d")
|
||||
excluded = (date.today() + timedelta(days=2)).strftime("%Y%m%d")
|
||||
calendar = (
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n"
|
||||
f"SUMMARY:{private_title}\r\nDTSTART:{tomorrow}T100000\r\n"
|
||||
f"DTEND:{tomorrow}T110000\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
||||
f"DTEND:{tomorrow}T110000\r\nRRULE:FREQ=DAILY;COUNT=3\r\n"
|
||||
f"EXDATE:{excluded}T100000\r\nEND:VEVENT\r\nEND:VCALENDAR"
|
||||
)
|
||||
page.locator("#week-capacity-file").set_input_files({
|
||||
"name": "availability.ics", "mimeType": "text/calendar", "buffer": calendar.encode()
|
||||
@@ -115,6 +117,12 @@ def test_release_artifact_plans_seven_touch_safe_mobile_dates(
|
||||
expect(page.locator("#week-capacity-days .week-capacity-day").first).to_contain_text(
|
||||
"420 min available"
|
||||
)
|
||||
expect(page.locator("#week-capacity-days .week-capacity-day").nth(1)).to_contain_text(
|
||||
"480 min available"
|
||||
)
|
||||
expect(page.locator("#week-capacity-days .week-capacity-day").nth(2)).to_contain_text(
|
||||
"420 min available"
|
||||
)
|
||||
expect(capacity_import).not_to_contain_text(private_title)
|
||||
for control in (
|
||||
page.locator("#cancel-week-capacity-import"),
|
||||
|
||||
@@ -37,6 +37,72 @@ console.log(JSON.stringify({days,serialized:JSON.stringify(days)}));
|
||||
assert private_value not in result["serialized"]
|
||||
|
||||
|
||||
def test_calendar_import_expands_bounded_daily_recurrence_and_honors_exdate():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:standup\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nRRULE:FREQ=DAILY;COUNT=5\r\nEXDATE:20260823T100000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'});
|
||||
console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [60, 60, 0, 60, 60, 0, 0]
|
||||
|
||||
|
||||
def test_calendar_import_adds_rdates_and_excludes_matching_occurrences():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nRDATE:20260823T100000,20260825T100000\r\nEXDATE:20260823T100000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates});
|
||||
console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [60, 0, 0, 0, 60, 0, 0]
|
||||
|
||||
|
||||
def test_calendar_import_stops_recurrence_at_until_boundary():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nRRULE:FREQ=DAILY;UNTIL=20260823T100000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates});
|
||||
console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [60, 60, 60, 0, 0, 0, 0]
|
||||
|
||||
|
||||
def test_calendar_import_expands_weekly_recurrence_on_selected_weekdays():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=5\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates});
|
||||
console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [60, 0, 0, 60, 0, 60, 0]
|
||||
|
||||
|
||||
def test_calendar_import_treats_all_day_events_as_busy_for_working_hours():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART;VALUE=DATE:20260822\r\nDTEND;VALUE=DATE:20260823\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates,workdayStart:'09:00',workdayEnd:'17:00'});
|
||||
console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [0, 480, 0, 0, 0, 0, 0]
|
||||
|
||||
|
||||
def test_calendar_import_ignores_cancelled_and_transparent_events():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nSTATUS:CANCELLED\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nEND:VEVENT\r\nBEGIN:VEVENT\r\nTRANSP:TRANSPARENT\r\nDTSTART:20260822T100000\r\nDTEND:20260822T110000\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates});
|
||||
console.log(JSON.stringify({busy:days.map(day=>day.busy_minutes)}));
|
||||
""")
|
||||
|
||||
assert result["busy"] == [0] * 7
|
||||
|
||||
|
||||
def test_calendar_import_rejects_invalid_and_oversized_files_without_returning_capacity():
|
||||
result = run_import("""
|
||||
const errors=[];
|
||||
@@ -53,6 +119,45 @@ console.log(JSON.stringify({errors}));
|
||||
]
|
||||
|
||||
|
||||
def test_calendar_import_blocks_apply_when_recurrence_cannot_be_counted_truthfully():
|
||||
result = run_import("""
|
||||
const elements=new Map();
|
||||
const element=(value='')=>({value,hidden:false,textContent:'',innerHTML:'',disabled:false,files:[],listeners:{},
|
||||
addEventListener(name,listener){this.listeners[name]=listener;},focus(){}});
|
||||
for(const selector of ['#week-capacity-import','#week-capacity-review','#week-capacity-days','#week-capacity-status',
|
||||
'#week-capacity-file','#week-capacity-start','#week-capacity-end','#apply-week-capacities','#cancel-week-capacity-import'])
|
||||
elements.set(selector,element());
|
||||
elements.get('#week-capacity-start').value='09:00';elements.get('#week-capacity-end').value='17:00';
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
let staged=0;
|
||||
const controller={dates:()=>dates.map(date=>({date,label:date})),stageCapacities:()=>{staged+=1;return true;},flush:async()=>{}};
|
||||
const workflow=calendarImport.createWorkflow({controller,qs:selector=>elements.get(selector)});
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nSUMMARY:Private board review\r\nATTENDEE:mailto:secret@example.com\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nRRULE:FREQ=MONTHLY;BYDAY=1FR\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
workflow.review(source);const applied=await workflow.apply();
|
||||
console.log(JSON.stringify({disabled:elements.get('#apply-week-capacities').disabled,status:elements.get('#week-capacity-status').textContent,
|
||||
markup:elements.get('#week-capacity-days').innerHTML,applied,staged}));
|
||||
""")
|
||||
|
||||
assert result["disabled"] is True
|
||||
assert result["applied"] is False
|
||||
assert result["staged"] == 0
|
||||
assert "1 recurring event could not be counted" in result["status"]
|
||||
assert "Apply is unavailable" in result["status"]
|
||||
assert "Private board review" not in json.dumps(result)
|
||||
assert "secret@example.com" not in json.dumps(result)
|
||||
|
||||
|
||||
def test_calendar_import_marks_unsupported_daily_rule_parts_instead_of_ignoring_them():
|
||||
result = run_import("""
|
||||
const source=`BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nDTSTART:20260821T100000\r\nDTEND:20260821T110000\r\nRRULE:FREQ=DAILY;BYHOUR=10\r\nEND:VEVENT\r\nEND:VCALENDAR`;
|
||||
const dates=['2026-08-21','2026-08-22','2026-08-23','2026-08-24','2026-08-25','2026-08-26','2026-08-27'];
|
||||
const days=calendarImport.review(source,{dates});
|
||||
console.log(JSON.stringify({unsupported:days.unsupported_count}));
|
||||
""")
|
||||
|
||||
assert result["unsupported"] == 1
|
||||
|
||||
|
||||
def test_calendar_import_workflow_reviews_then_applies_once_without_persisting_calendar_text():
|
||||
result = run_import("""
|
||||
const elements=new Map();
|
||||
|
||||
Reference in new issue
Block a user