Skip Not-Started tasks that are already assigned to someone
A task carrying an "Assigned to" avatar in the Planner grid has been picked up by a person, so its attachments must not be downloaded and its status must not be moved to In Progress. Until now the only filter was percentComplete == 0, so an assigned task with an attachment was downloaded like any other — on the Balboa Nursing and Rehab plan that was one of the two "Facesheets Needed (GG)" tasks assigned to Dr. Mark Lukasik and Adina Smets. - _not_started_tasks() now selects `assignments` as well. - _task_assignee_ids()/_resolve_user_names(): the assignee check runs before the task-details call, so an assigned task costs no extra Graph request; display names are resolved once per user and cached for the run, falling back to the raw ID when the lookup fails. - Summary workbook gains a "Tasks Skipped (Assigned)" column and a second "Tasks" sheet with one row per task — facility, task name, assigned to, attachment count, Downloaded Yes/No and the reason — so a skip can be explained without reading the log. - New "All assigned — skipped" facility status, distinct from "No attachments". scripts/probe_assignments.py is the read-only probe used to confirm the rule against the live plan before changing behaviour. Verified on Balboa Nursing and Rehab: 4 Not Started -> 8.24.26 and 8.21.26 downloaded (unassigned), both Facesheets Needed (GG) skipped. 110 tests pass (tests/test_uipath_pilot.py and tests/test_config.py fail on this checkout for pre-existing, unrelated reasons). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd864e0b4c
commit
bfd72825b4
3 files changed
+317
-9
No files matched your search
+108
-9
@@ -6,7 +6,8 @@ Architecture:
|
|||||||
authenticated request to graph.microsoft.com, then closes immediately.
|
authenticated request to graph.microsoft.com, then closes immediately.
|
||||||
2. All subsequent work uses httpx against the Microsoft Graph API:
|
2. All subsequent work uses httpx against the Microsoft Graph API:
|
||||||
- Discover plans via user's group memberships
|
- Discover plans via user's group memberships
|
||||||
- List Not-Started tasks per facility plan
|
- List Not-Started tasks per facility plan, skipping any task that is
|
||||||
|
already assigned to someone (their work — not ours to download)
|
||||||
- Resolve SharePoint file references via Graph shares endpoint
|
- Resolve SharePoint file references via Graph shares endpoint
|
||||||
- Stream-download each PDF
|
- Stream-download each PDF
|
||||||
3. Identical output structure to the Selenium bot:
|
3. Identical output structure to the Selenium bot:
|
||||||
@@ -880,13 +881,42 @@ def _not_started_tasks(client: httpx.Client, plan_id: str, cfg: AppConfig) -> li
|
|||||||
client,
|
client,
|
||||||
f"planner/plans/{plan_id}/tasks",
|
f"planner/plans/{plan_id}/tasks",
|
||||||
cfg,
|
cfg,
|
||||||
**{"$select": "id,title,percentComplete"},
|
**{"$select": "id,title,percentComplete,assignments"},
|
||||||
)
|
)
|
||||||
ns = [t for t in tasks if (t.get("percentComplete") or 0) == 0]
|
ns = [t for t in tasks if (t.get("percentComplete") or 0) == 0]
|
||||||
_log.info("Plan %s: %d tasks total, %d not started", plan_id, len(tasks), len(ns))
|
_log.info("Plan %s: %d tasks total, %d not started", plan_id, len(tasks), len(ns))
|
||||||
return ns
|
return ns
|
||||||
|
|
||||||
|
|
||||||
|
def _task_assignee_ids(task: dict) -> list[str]:
|
||||||
|
"""User IDs a task is assigned to (the Planner grid's 'Assigned to' column)."""
|
||||||
|
return [uid for uid in (task.get("assignments") or {}) if uid]
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_user_names(
|
||||||
|
client: httpx.Client,
|
||||||
|
user_ids: list[str],
|
||||||
|
cfg: AppConfig,
|
||||||
|
cache: dict[str, str],
|
||||||
|
) -> list[str]:
|
||||||
|
"""Display names for assignee IDs, cached across the run.
|
||||||
|
|
||||||
|
A lookup failure is never fatal — the ID is shown instead so the summary
|
||||||
|
still records who the task was assigned to.
|
||||||
|
"""
|
||||||
|
names: list[str] = []
|
||||||
|
for uid in user_ids:
|
||||||
|
if uid not in cache:
|
||||||
|
try:
|
||||||
|
user = _get(client, f"users/{uid}", cfg, **{"$select": "displayName"})
|
||||||
|
cache[uid] = user.get("displayName") or uid
|
||||||
|
except Exception as exc:
|
||||||
|
_log.debug("user lookup %s failed: %s", uid, exc)
|
||||||
|
cache[uid] = uid
|
||||||
|
names.append(cache[uid])
|
||||||
|
return names
|
||||||
|
|
||||||
|
|
||||||
def _task_file_urls(client: httpx.Client, task_id: str, cfg: AppConfig) -> list[str]:
|
def _task_file_urls(client: httpx.Client, task_id: str, cfg: AppConfig) -> list[str]:
|
||||||
"""Return SharePoint file URLs from a task's references (details endpoint)."""
|
"""Return SharePoint file URLs from a task's references (details endpoint)."""
|
||||||
try:
|
try:
|
||||||
@@ -1054,6 +1084,7 @@ def _write_summary(
|
|||||||
) -> None:
|
) -> None:
|
||||||
failure_names = {f for f, _, _ in failures}
|
failure_names = {f for f, _, _ in failures}
|
||||||
rows: list[dict] = []
|
rows: list[dict] = []
|
||||||
|
task_rows: list[dict] = []
|
||||||
|
|
||||||
for facility, payload in results.items():
|
for facility, payload in results.items():
|
||||||
if facility in failure_names:
|
if facility in failure_names:
|
||||||
@@ -1063,10 +1094,14 @@ def _write_summary(
|
|||||||
tasks_att = int(payload.get("tasks_with_attachments") or 0)
|
tasks_att = int(payload.get("tasks_with_attachments") or 0)
|
||||||
tasks_dl = int(payload.get("tasks_with_downloads") or 0)
|
tasks_dl = int(payload.get("tasks_with_downloads") or 0)
|
||||||
tasks_skip = int(payload.get("tasks_skipped_no_attachment") or 0)
|
tasks_skip = int(payload.get("tasks_skipped_no_attachment") or 0)
|
||||||
|
tasks_assigned = int(payload.get("tasks_skipped_assigned") or 0)
|
||||||
status_changed = int(payload.get("status_changed") or 0)
|
status_changed = int(payload.get("status_changed") or 0)
|
||||||
|
task_rows.extend(payload.get("task_rows") or [])
|
||||||
|
|
||||||
if tasks_total <= 0:
|
if tasks_total <= 0:
|
||||||
status = "No tasks"
|
status = "No tasks"
|
||||||
|
elif tasks_att <= 0 and tasks_assigned > 0:
|
||||||
|
status = "All assigned — skipped"
|
||||||
elif tasks_att <= 0:
|
elif tasks_att <= 0:
|
||||||
status = "No attachments"
|
status = "No attachments"
|
||||||
elif tasks_dl <= 0 or file_count <= 0:
|
elif tasks_dl <= 0 or file_count <= 0:
|
||||||
@@ -1091,6 +1126,7 @@ def _write_summary(
|
|||||||
"Files Downloaded": file_count,
|
"Files Downloaded": file_count,
|
||||||
"Tasks Not Started": tasks_total,
|
"Tasks Not Started": tasks_total,
|
||||||
"Tasks With Attachments": tasks_att,
|
"Tasks With Attachments": tasks_att,
|
||||||
|
"Tasks Skipped (Assigned)": tasks_assigned,
|
||||||
"Tasks Skipped (No File)": tasks_skip,
|
"Tasks Skipped (No File)": tasks_skip,
|
||||||
"Status": status,
|
"Status": status,
|
||||||
"Status Changed": status_changed_label,
|
"Status Changed": status_changed_label,
|
||||||
@@ -1103,14 +1139,26 @@ def _write_summary(
|
|||||||
"Files Downloaded": 0,
|
"Files Downloaded": 0,
|
||||||
"Tasks Not Started": 0,
|
"Tasks Not Started": 0,
|
||||||
"Tasks With Attachments": 0,
|
"Tasks With Attachments": 0,
|
||||||
|
"Tasks Skipped (Assigned)": 0,
|
||||||
"Tasks Skipped (No File)": 0,
|
"Tasks Skipped (No File)": 0,
|
||||||
"Status": status,
|
"Status": status,
|
||||||
"Status Changed": "N/A",
|
"Status Changed": "N/A",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
task_columns = [
|
||||||
|
"Facility Name", "Task Name", "Assigned To",
|
||||||
|
"Attachments", "Downloaded", "Reason",
|
||||||
|
]
|
||||||
|
tasks_df = pd.DataFrame(task_rows, columns=task_columns)
|
||||||
|
|
||||||
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
summary_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
pd.DataFrame(rows).to_excel(summary_path, index=False, sheet_name="Download")
|
with pd.ExcelWriter(summary_path, engine="openpyxl") as writer:
|
||||||
_log.info("Summary -> %s", summary_path)
|
pd.DataFrame(rows).to_excel(writer, index=False, sheet_name="Download")
|
||||||
|
tasks_df.to_excel(writer, index=False, sheet_name="Tasks")
|
||||||
|
_log.info(
|
||||||
|
"Summary -> %s (%d facility row(s), %d task row(s))",
|
||||||
|
summary_path, len(rows), len(tasks_df),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1143,26 +1191,58 @@ def _process_facility(
|
|||||||
tasks_with_att = 0
|
tasks_with_att = 0
|
||||||
tasks_with_dl = 0
|
tasks_with_dl = 0
|
||||||
tasks_no_att = 0 # Not Started but no file attachment → skip
|
tasks_no_att = 0 # Not Started but no file attachment → skip
|
||||||
|
tasks_assigned = 0 # Not Started but already assigned to someone → skip
|
||||||
status_changed = 0
|
status_changed = 0
|
||||||
downloaded: list[Path] = []
|
downloaded: list[Path] = []
|
||||||
|
task_rows: list[dict] = []
|
||||||
|
name_cache: dict[str, str] = {}
|
||||||
|
|
||||||
_log.info(
|
_log.info(
|
||||||
" Processing ONLY Not Started tasks with attachments (%d Not Started found)",
|
" Processing ONLY unassigned Not Started tasks with attachments "
|
||||||
|
"(%d Not Started found)",
|
||||||
tasks_total,
|
tasks_total,
|
||||||
)
|
)
|
||||||
|
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
tid = task["id"]
|
tid = task["id"]
|
||||||
title = task.get("title", "")
|
title = task.get("title", "")
|
||||||
|
assignee_ids = _task_assignee_ids(task)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
task_rows.append({
|
||||||
|
"Facility Name": facility_name,
|
||||||
|
"Task Name": title,
|
||||||
|
"Assigned To": assigned_to,
|
||||||
|
"Attachments": "",
|
||||||
|
"Downloaded": "No",
|
||||||
|
"Reason": f"Skipped — assigned to {assigned_to}",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
file_urls = _task_file_urls(client, tid, cfg)
|
file_urls = _task_file_urls(client, tid, cfg)
|
||||||
|
|
||||||
if not file_urls:
|
if not file_urls:
|
||||||
# Not Started but no attachment — skip entirely
|
# Not Started but no attachment — skip entirely
|
||||||
_log.info(" SKIP '%s': Not Started but no attachment", title[:60])
|
_log.info(" SKIP '%s': Not Started but no attachment", title[:60])
|
||||||
tasks_no_att += 1
|
tasks_no_att += 1
|
||||||
|
task_rows.append({
|
||||||
|
"Facility Name": facility_name,
|
||||||
|
"Task Name": title,
|
||||||
|
"Assigned To": "",
|
||||||
|
"Attachments": 0,
|
||||||
|
"Downloaded": "No",
|
||||||
|
"Reason": "Skipped — no attachment",
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Not Started WITH attachment(s) — download ALL of them
|
# Unassigned, Not Started, WITH attachment(s) — download ALL of them
|
||||||
tasks_with_att += 1
|
tasks_with_att += 1
|
||||||
total_urls = len(file_urls)
|
total_urls = len(file_urls)
|
||||||
task_dl = 0
|
task_dl = 0
|
||||||
@@ -1182,6 +1262,21 @@ def _process_facility(
|
|||||||
downloaded.append(path)
|
downloaded.append(path)
|
||||||
task_dl += 1
|
task_dl += 1
|
||||||
|
|
||||||
|
if task_dl == total_urls:
|
||||||
|
reason = f"Downloaded {task_dl}/{total_urls} attachment(s)"
|
||||||
|
elif task_dl > 0:
|
||||||
|
reason = f"Partial — {task_dl}/{total_urls} attachment(s) downloaded"
|
||||||
|
else:
|
||||||
|
reason = f"Download failed — 0/{total_urls} attachment(s)"
|
||||||
|
task_rows.append({
|
||||||
|
"Facility Name": facility_name,
|
||||||
|
"Task Name": title,
|
||||||
|
"Assigned To": "",
|
||||||
|
"Attachments": total_urls,
|
||||||
|
"Downloaded": "Yes" if task_dl else "No",
|
||||||
|
"Reason": reason,
|
||||||
|
})
|
||||||
|
|
||||||
if task_dl == total_urls:
|
if task_dl == total_urls:
|
||||||
# Every attachment downloaded — mark In Progress
|
# Every attachment downloaded — mark In Progress
|
||||||
tasks_with_dl += 1
|
tasks_with_dl += 1
|
||||||
@@ -1210,9 +1305,10 @@ def _process_facility(
|
|||||||
)
|
)
|
||||||
|
|
||||||
_log.info(
|
_log.info(
|
||||||
" Summary: %d Not Started | %d with attachments (processed) | "
|
" Summary: %d Not Started | %d assigned (skipped) | "
|
||||||
"%d no attachment (skipped) | %d file(s) downloaded",
|
"%d with attachments (processed) | %d no attachment (skipped) | "
|
||||||
tasks_total, tasks_with_att, tasks_no_att, len(downloaded),
|
"%d file(s) downloaded",
|
||||||
|
tasks_total, tasks_assigned, tasks_with_att, tasks_no_att, len(downloaded),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1221,6 +1317,8 @@ def _process_facility(
|
|||||||
"tasks_with_attachments": tasks_with_att,
|
"tasks_with_attachments": tasks_with_att,
|
||||||
"tasks_with_downloads": tasks_with_dl,
|
"tasks_with_downloads": tasks_with_dl,
|
||||||
"tasks_skipped_no_attachment": tasks_no_att,
|
"tasks_skipped_no_attachment": tasks_no_att,
|
||||||
|
"tasks_skipped_assigned": tasks_assigned,
|
||||||
|
"task_rows": task_rows,
|
||||||
"status_changed": status_changed,
|
"status_changed": status_changed,
|
||||||
"status_via_task_details": 0,
|
"status_via_task_details": 0,
|
||||||
}
|
}
|
||||||
@@ -1362,6 +1460,7 @@ def run_planner_http_workflow(
|
|||||||
"original": [], "workable": [], "comment": err_msg[:500],
|
"original": [], "workable": [], "comment": err_msg[:500],
|
||||||
"tasks_total": 0, "tasks_with_attachments": 0,
|
"tasks_total": 0, "tasks_with_attachments": 0,
|
||||||
"tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0,
|
"tasks_with_downloads": 0, "tasks_skipped_no_attachment": 0,
|
||||||
|
"tasks_skipped_assigned": 0, "task_rows": [],
|
||||||
"status_changed": 0, "status_via_task_details": 0,
|
"status_changed": 0, "status_via_task_details": 0,
|
||||||
}
|
}
|
||||||
save_checkpoint(
|
save_checkpoint(
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Read-only probe: for one plan, list every Not-Started task with its assignee
|
||||||
|
count and attachment count, so the "skip assigned tasks" rule can be validated
|
||||||
|
against what the Planner grid shows before any behaviour is changed.
|
||||||
|
|
||||||
|
Downloads nothing and never PATCHes a task — safe against production Planner.
|
||||||
|
|
||||||
|
Run: .venv\\Scripts\\python scripts\\probe_assignments.py --facility "Balboa"
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SRC = ROOT / "src"
|
||||||
|
for p in (str(ROOT), str(SRC)):
|
||||||
|
if p not in sys.path:
|
||||||
|
sys.path.insert(0, p)
|
||||||
|
|
||||||
|
from config import read_app_config # noqa: E402
|
||||||
|
from onedrive_planner_http import ( # noqa: E402
|
||||||
|
_build_client,
|
||||||
|
_discover_all_plans,
|
||||||
|
_get_paged,
|
||||||
|
_login_with_retry,
|
||||||
|
_match_plan,
|
||||||
|
_task_file_urls,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--facility", required=True, help="facility / plan name fragment")
|
||||||
|
ap.add_argument("--headed", action="store_true")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)-7s %(message)s",
|
||||||
|
stream=sys.stdout,
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg = read_app_config()
|
||||||
|
token, _ = _login_with_retry(cfg, headless=not args.headed)
|
||||||
|
client = _build_client(cfg, token)
|
||||||
|
|
||||||
|
plans = _discover_all_plans(client, cfg)
|
||||||
|
plan = _match_plan(plans, args.facility)
|
||||||
|
if not plan:
|
||||||
|
print(f"no plan matched {args.facility!r}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"\nplan: {plan.get('title')!r}\n")
|
||||||
|
tasks = _get_paged(
|
||||||
|
client,
|
||||||
|
f"planner/plans/{plan['id']}/tasks",
|
||||||
|
cfg,
|
||||||
|
**{"$select": "id,title,percentComplete,assignments,orderHint"},
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"{'%':>4} {'assignees':>9} {'files':>5} title")
|
||||||
|
print("-" * 78)
|
||||||
|
would_download = would_skip = 0
|
||||||
|
for t in tasks:
|
||||||
|
pct = t.get("percentComplete") or 0
|
||||||
|
if pct != 0:
|
||||||
|
continue
|
||||||
|
assignees = len(t.get("assignments") or {})
|
||||||
|
n_files = len(_task_file_urls(client, t["id"], cfg))
|
||||||
|
print(f"{pct:>4} {assignees:>9} {n_files:>5} {(t.get('title') or '')[:50]}")
|
||||||
|
if n_files:
|
||||||
|
if assignees:
|
||||||
|
would_skip += 1
|
||||||
|
else:
|
||||||
|
would_download += 1
|
||||||
|
|
||||||
|
print("-" * 78)
|
||||||
|
print(f"With the 'skip assigned' rule: {would_download} would download, "
|
||||||
|
f"{would_skip} would skip (attachment-bearing tasks only)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""Only unassigned Not-Started tasks may be downloaded.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
SRC = ROOT / "src"
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskAssigneeIds:
|
||||||
|
def test_unassigned_task_has_no_assignees(self):
|
||||||
|
assert _task_assignee_ids({"title": "8.24.26"}) == []
|
||||||
|
assert _task_assignee_ids({"title": "8.21.26", "assignments": {}}) == []
|
||||||
|
assert _task_assignee_ids({"assignments": None}) == []
|
||||||
|
|
||||||
|
def test_assigned_task_returns_every_user_id(self):
|
||||||
|
task = {
|
||||||
|
"title": "Facesheets Needed (GG)",
|
||||||
|
"assignments": {
|
||||||
|
"aaaaaaaa-1111-2222-3333-444444444444": {"orderHint": "8"},
|
||||||
|
"bbbbbbbb-5555-6666-7777-888888888888": {"orderHint": "9"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert sorted(_task_assignee_ids(task)) == [
|
||||||
|
"aaaaaaaa-1111-2222-3333-444444444444",
|
||||||
|
"bbbbbbbb-5555-6666-7777-888888888888",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_blank_user_id_is_ignored(self):
|
||||||
|
assert _task_assignee_ids({"assignments": {"": {}}}) == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestSummaryWorkbook:
|
||||||
|
def _write(self, tmp_path: Path, payload: dict) -> Path:
|
||||||
|
out = tmp_path / "Download_Summary_HTTP_test.xlsx"
|
||||||
|
_write_summary({"Balboa Nursing and Rehab": payload}, [], out, "2026-08-24 18:00")
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _balboa_payload(self) -> dict:
|
||||||
|
"""The real Balboa plan: 2 unassigned with files, 2 assigned."""
|
||||||
|
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,
|
||||||
|
"status_changed": 2,
|
||||||
|
"task_rows": [
|
||||||
|
{"Facility Name": "Balboa Nursing and Rehab", "Task Name": "8.24.26",
|
||||||
|
"Assigned To": "", "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",
|
||||||
|
"Reason": "Downloaded 1/1 attachment(s)"},
|
||||||
|
{"Facility Name": "Balboa Nursing and Rehab",
|
||||||
|
"Task Name": "Facesheets Needed (GG)", "Assigned To": "A S",
|
||||||
|
"Attachments": "", "Downloaded": "No",
|
||||||
|
"Reason": "Skipped — assigned to A S"},
|
||||||
|
{"Facility Name": "Balboa Nursing and Rehab",
|
||||||
|
"Task Name": "Facesheets Needed (GG)", "Assigned To": "D M",
|
||||||
|
"Attachments": "", "Downloaded": "No",
|
||||||
|
"Reason": "Skipped — assigned to D M"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_download_sheet_reports_assigned_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, "Files Downloaded"] == 2
|
||||||
|
assert df.loc[0, "Status"] == "Success"
|
||||||
|
|
||||||
|
def test_tasks_sheet_has_a_yes_no_row_per_task(self, tmp_path):
|
||||||
|
out = self._write(tmp_path, self._balboa_payload())
|
||||||
|
df = pd.read_excel(out, sheet_name="Tasks")
|
||||||
|
assert list(df.columns) == [
|
||||||
|
"Facility Name", "Task Name", "Assigned To",
|
||||||
|
"Attachments", "Downloaded", "Reason",
|
||||||
|
]
|
||||||
|
assert list(df["Downloaded"]) == ["Yes", "Yes", "No", "No"]
|
||||||
|
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"])
|
||||||
|
|
||||||
|
def test_facility_whose_tasks_are_all_assigned(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,
|
||||||
|
"status_changed": 0,
|
||||||
|
"task_rows": [],
|
||||||
|
})
|
||||||
|
df = pd.read_excel(out, sheet_name="Download")
|
||||||
|
assert df.loc[0, "Status"] == "All assigned — 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": [],
|
||||||
|
})
|
||||||
|
df = pd.read_excel(out, sheet_name="Tasks")
|
||||||
|
assert df.empty
|
||||||
|
assert "Downloaded" in df.columns
|
||||||
Reference in new issue
Block a user