diff --git a/onedrive_planner_http.py b/onedrive_planner_http.py index ce3769a..a466776 100644 --- a/onedrive_planner_http.py +++ b/onedrive_planner_http.py @@ -6,8 +6,8 @@ Architecture: authenticated request to graph.microsoft.com, then closes immediately. 2. All subsequent work uses httpx against the Microsoft Graph API: - Discover plans via user's group memberships - - List Not-Started tasks per facility plan, skipping any task that is - already assigned to someone (their work — not ours to download) + - List Not-Started tasks per facility plan, skipping only + "Facesheets Needed (GG)" tasks (assigned or not, all others download) - Resolve SharePoint file references via Graph shares endpoint - Stream-download each PDF 3. Identical output structure to the Selenium bot: @@ -893,6 +893,12 @@ def _task_assignee_ids(task: dict) -> list[str]: return [uid for uid in (task.get("assignments") or {}) if uid] +def _is_gg_task(task: dict) -> bool: + """'Facesheets Needed (GG)' tasks are the only ones the bot must not download.""" + # ponytail: title match only; also check task details notes if GG ever appears there + return "facesheets needed (gg)" in " ".join((task.get("title") or "").lower().split()) + + def _resolve_user_names( client: httpx.Client, user_ids: list[str], @@ -1030,7 +1036,7 @@ def _build_workable_files( output_root: Path, today_iso: str, ) -> tuple[list[Path], Path]: - ddmmyyyy = datetime.strptime(today_iso, "%Y-%m-%d").strftime("%d-%m-%Y") + mmddyyyy = datetime.strptime(today_iso, "%Y-%m-%d").strftime("%m-%d-%Y") fac_safe = _sanitize_folder_name(facility_name) workable_root = output_root / today_iso / "Workable" medicare_root = output_root / today_iso / "Workable_Medicare" @@ -1045,9 +1051,9 @@ def _build_workable_files( for i, src in enumerate(downloaded, start=1): ext = src.suffix or ".pdf" base = ( - f"{ddmmyyyy}_{fac_safe}" + f"{mmddyyyy}_{fac_safe}" if len(downloaded) == 1 - else f"{ddmmyyyy}_{fac_safe}_{i}" + else f"{mmddyyyy}_{fac_safe}_{i}" ) # Check if this workable file already exists (previous interrupted run) dst_first = workable_root / f"{base}{ext}" @@ -1094,14 +1100,14 @@ def _write_summary( tasks_att = int(payload.get("tasks_with_attachments") or 0) tasks_dl = int(payload.get("tasks_with_downloads") or 0) tasks_skip = int(payload.get("tasks_skipped_no_attachment") or 0) - tasks_assigned = int(payload.get("tasks_skipped_assigned") or 0) + tasks_gg = int(payload.get("tasks_skipped_gg") or 0) status_changed = int(payload.get("status_changed") or 0) task_rows.extend(payload.get("task_rows") or []) if tasks_total <= 0: status = "No tasks" - elif tasks_att <= 0 and tasks_assigned > 0: - status = "All assigned — skipped" + elif tasks_att <= 0 and tasks_gg > 0: + status = "All GG — skipped" elif tasks_att <= 0: status = "No attachments" elif tasks_dl <= 0 or file_count <= 0: @@ -1126,7 +1132,7 @@ def _write_summary( "Files Downloaded": file_count, "Tasks Not Started": tasks_total, "Tasks With Attachments": tasks_att, - "Tasks Skipped (Assigned)": tasks_assigned, + "Tasks Skipped (GG)": tasks_gg, "Tasks Skipped (No File)": tasks_skip, "Status": status, "Status Changed": status_changed_label, @@ -1139,7 +1145,7 @@ def _write_summary( "Files Downloaded": 0, "Tasks Not Started": 0, "Tasks With Attachments": 0, - "Tasks Skipped (Assigned)": 0, + "Tasks Skipped (GG)": 0, "Tasks Skipped (No File)": 0, "Status": status, "Status Changed": "N/A", @@ -1191,14 +1197,14 @@ def _process_facility( tasks_with_att = 0 tasks_with_dl = 0 tasks_no_att = 0 # Not Started but no file attachment → skip - tasks_assigned = 0 # Not Started but already assigned to someone → skip + tasks_gg = 0 # "Facesheets Needed (GG)" → skip status_changed = 0 downloaded: list[Path] = [] task_rows: list[dict] = [] name_cache: dict[str, str] = {} _log.info( - " Processing ONLY unassigned Not Started tasks with attachments " + " Processing Not Started tasks with attachments, assigned or not " "(%d Not Started found)", tasks_total, ) @@ -1207,22 +1213,23 @@ def _process_facility( tid = task["id"] title = task.get("title", "") assignee_ids = _task_assignee_ids(task) + assigned_to = ( + ", ".join(_resolve_user_names(client, assignee_ids, cfg, name_cache)) + if assignee_ids else "" + ) - # A task someone has already picked up is theirs to work — never - # download it and never touch its status. Checked before the - # attachment lookup so an assigned task costs no extra Graph call. - if assignee_ids: - names = _resolve_user_names(client, assignee_ids, cfg, name_cache) - assigned_to = ", ".join(names) - _log.info(" SKIP '%s': assigned to %s", title[:60], assigned_to) - tasks_assigned += 1 + # GG tasks are never downloaded and their status is never touched. + # Checked before the attachment lookup so they cost no extra Graph call. + if _is_gg_task(task): + _log.info(" SKIP '%s': Facesheets Needed (GG)", title[:60]) + tasks_gg += 1 task_rows.append({ "Facility Name": facility_name, "Task Name": title, "Assigned To": assigned_to, "Attachments": "", "Downloaded": "No", - "Reason": f"Skipped — assigned to {assigned_to}", + "Reason": "Skipped — Facesheets Needed (GG)", }) continue @@ -1235,14 +1242,14 @@ def _process_facility( task_rows.append({ "Facility Name": facility_name, "Task Name": title, - "Assigned To": "", + "Assigned To": assigned_to, "Attachments": 0, "Downloaded": "No", "Reason": "Skipped — no attachment", }) continue - # Unassigned, Not Started, WITH attachment(s) — download ALL of them + # Not Started, WITH attachment(s) — download ALL of them tasks_with_att += 1 total_urls = len(file_urls) task_dl = 0 @@ -1271,7 +1278,7 @@ def _process_facility( task_rows.append({ "Facility Name": facility_name, "Task Name": title, - "Assigned To": "", + "Assigned To": assigned_to, "Attachments": total_urls, "Downloaded": "Yes" if task_dl else "No", "Reason": reason, @@ -1305,10 +1312,10 @@ def _process_facility( ) _log.info( - " Summary: %d Not Started | %d assigned (skipped) | " + " Summary: %d Not Started | %d GG (skipped) | " "%d with attachments (processed) | %d no attachment (skipped) | " "%d file(s) downloaded", - tasks_total, tasks_assigned, tasks_with_att, tasks_no_att, len(downloaded), + tasks_total, tasks_gg, tasks_with_att, tasks_no_att, len(downloaded), ) return { @@ -1317,7 +1324,7 @@ def _process_facility( "tasks_with_attachments": tasks_with_att, "tasks_with_downloads": tasks_with_dl, "tasks_skipped_no_attachment": tasks_no_att, - "tasks_skipped_assigned": tasks_assigned, + "tasks_skipped_gg": tasks_gg, "task_rows": task_rows, "status_changed": status_changed, "status_via_task_details": 0, @@ -1460,7 +1467,7 @@ def run_planner_http_workflow( "original": [], "workable": [], "comment": err_msg[:500], "tasks_total": 0, "tasks_with_attachments": 0, "tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0, - "tasks_skipped_assigned": 0, "task_rows": [], + "tasks_skipped_gg": 0, "task_rows": [], "status_changed": 0, "status_via_task_details": 0, } save_checkpoint( diff --git a/tests/test_assigned_task_skip.py b/tests/test_assigned_task_skip.py index 2b78d69..5115582 100644 --- a/tests/test_assigned_task_skip.py +++ b/tests/test_assigned_task_skip.py @@ -1,7 +1,7 @@ -"""Only unassigned Not-Started tasks may be downloaded. +"""Assigned tasks download like any other; only "Facesheets Needed (GG)" is skipped. -A task carrying an 'Assigned to' avatar in the Planner grid has been picked up -by a person; the bot must leave it — and its status — alone. +A task assigned to someone (BT, SM, ...) with a facesheet attached must still be +downloaded. GG tasks are never downloaded and their status is never touched. """ from __future__ import annotations @@ -16,7 +16,7 @@ for _p in (str(ROOT), str(SRC)): if _p not in sys.path: sys.path.insert(0, _p) -from onedrive_planner_http import _task_assignee_ids, _write_summary # noqa: E402 +from onedrive_planner_http import _is_gg_task, _task_assignee_ids, _write_summary # noqa: E402 class TestTaskAssigneeIds: @@ -42,6 +42,20 @@ class TestTaskAssigneeIds: assert _task_assignee_ids({"assignments": {"": {}}}) == [] +class TestGgSkip: + def test_gg_title_is_skipped(self): + assert _is_gg_task({"title": "Facesheets Needed (GG)"}) + assert _is_gg_task({"title": " facesheets needed (gg) "}) + + def test_assigned_non_gg_task_is_downloaded(self): + # Reo Vista / Balboa misses: assigned to BT / SM, must NOT be skipped. + assert not _is_gg_task({"title": "10.26.26", "assignments": {"u1": {}}}) + assert not _is_gg_task( + {"title": "Melzer, Gilbert 8/23/63-Need Medi-Cal ID", "assignments": {"u2": {}}} + ) + assert not _is_gg_task({"title": None}) + + class TestSummaryWorkbook: def _write(self, tmp_path: Path, payload: dict) -> Path: out = tmp_path / "Download_Summary_HTTP_test.xlsx" @@ -49,18 +63,18 @@ class TestSummaryWorkbook: return out def _balboa_payload(self) -> dict: - """The real Balboa plan: 2 unassigned with files, 2 assigned.""" + """The real Balboa plan: 2 downloaded (one assigned), 2 GG skipped.""" return { "original": [Path("a.pdf"), Path("b.pdf")], "tasks_total": 4, "tasks_with_attachments": 2, "tasks_with_downloads": 2, "tasks_skipped_no_attachment": 0, - "tasks_skipped_assigned": 2, + "tasks_skipped_gg": 2, "status_changed": 2, "task_rows": [ - {"Facility Name": "Balboa Nursing and Rehab", "Task Name": "8.24.26", - "Assigned To": "", "Attachments": 1, "Downloaded": "Yes", + {"Facility Name": "Balboa Nursing and Rehab", "Task Name": "10.26.26", + "Assigned To": "B T", "Attachments": 1, "Downloaded": "Yes", "Reason": "Downloaded 1/1 attachment(s)"}, {"Facility Name": "Balboa Nursing and Rehab", "Task Name": "8.21.26", "Assigned To": "", "Attachments": 1, "Downloaded": "Yes", @@ -68,18 +82,18 @@ class TestSummaryWorkbook: {"Facility Name": "Balboa Nursing and Rehab", "Task Name": "Facesheets Needed (GG)", "Assigned To": "A S", "Attachments": "", "Downloaded": "No", - "Reason": "Skipped — assigned to A S"}, + "Reason": "Skipped — Facesheets Needed (GG)"}, {"Facility Name": "Balboa Nursing and Rehab", "Task Name": "Facesheets Needed (GG)", "Assigned To": "D M", "Attachments": "", "Downloaded": "No", - "Reason": "Skipped — assigned to D M"}, + "Reason": "Skipped — Facesheets Needed (GG)"}, ], } - def test_download_sheet_reports_assigned_skips(self, tmp_path): + def test_download_sheet_reports_gg_skips(self, tmp_path): out = self._write(tmp_path, self._balboa_payload()) df = pd.read_excel(out, sheet_name="Download") - assert df.loc[0, "Tasks Skipped (Assigned)"] == 2 + assert df.loc[0, "Tasks Skipped (GG)"] == 2 assert df.loc[0, "Files Downloaded"] == 2 assert df.loc[0, "Status"] == "Success" @@ -94,27 +108,27 @@ class TestSummaryWorkbook: facesheets = df[df["Task Name"] == "Facesheets Needed (GG)"] assert len(facesheets) == 2 assert all(facesheets["Downloaded"] == "No") - assert all("assigned to" in r for r in facesheets["Reason"]) + assert all("GG" in r for r in facesheets["Reason"]) - def test_facility_whose_tasks_are_all_assigned(self, tmp_path): + def test_facility_whose_tasks_are_all_gg(self, tmp_path): out = self._write(tmp_path, { "original": [], "tasks_total": 3, "tasks_with_attachments": 0, "tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0, - "tasks_skipped_assigned": 3, + "tasks_skipped_gg": 3, "status_changed": 0, "task_rows": [], }) df = pd.read_excel(out, sheet_name="Download") - assert df.loc[0, "Status"] == "All assigned — skipped" + assert df.loc[0, "Status"] == "All GG — skipped" def test_tasks_sheet_exists_even_with_no_task_rows(self, tmp_path): out = self._write(tmp_path, { "original": [], "tasks_total": 0, "tasks_with_attachments": 0, "tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0, - "tasks_skipped_assigned": 0, "status_changed": 0, "task_rows": [], + "tasks_skipped_gg": 0, "status_changed": 0, "task_rows": [], }) df = pd.read_excel(out, sheet_name="Tasks") assert df.empty