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>
89 lines
2.5 KiB
Python
89 lines
2.5 KiB
Python
#!/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())
|