"""Build a minimal Word document listing the six people with open questions. No external deps — a .docx is a zip of OOXML files, so we construct it directly with the stdlib. Output: cascades-staff-open-questions-2026-04-22.docx """ from __future__ import annotations import os import zipfile from xml.sax.saxutils import escape OUT = "clients/cascades-tucson/docs/cloud/questionnaires/cascades-staff-open-questions-2026-04-22.docx" TITLE = "Cascades — Open Items on Staff Access List" SUBTITLE = "2026-04-22 · prepared by Howard Enos, Computer Guru · matches the 2026-04-22 email" INTRO = ( "Thank you for sending back the staff list. Almost everything is squared away. " "Below are the few items I still need from you. One of them (Polett) is NOT a " "question — it is the setup I am planning to use; stop me if it's wrong. The rest " "are questions. Short answers are fine. I will send a full list for you all to " "look over separately." ) QUESTIONS = [ { "name": "Britney Thompson", "dept": "Assisted Living Nursing / Clinical", "context": ( "Britney has an active Active Directory account today as Memory Care Nurse. " "She was not on the staff list you returned; Howard has confirmed she is still " "an employee, so the account stays active. I just need the two flags below." ), "questions": [ "Phone — Y or N? (Does she need a Cascades-issued phone / business cell, in addition to a desktop?)", "Outside sign-in — Y or N? (Default for everyone is N / building-only. Mark Y only if she legitimately works off-site.)", ], }, { "name": "Alma R Montt", "dept": "Life Enrichment", "context": ( "Alma was on the returned list but the Title / Role column was blank. " "I see she is in Life Enrichment — is she an admin, manager, or something else?" ), "questions": [ "What is Alma's title or role? (It will go on her account and email signature.)", ], }, { "name": "Polett Pinazavala — NOT a question, just a heads-up", "dept": "Caregivers (Memory Care, MedTech, Tue–Sat)", "context": ( "Polett was on an earlier caregiver roster (MedTech, Memory Care, AM shift) but she was " "not on the list you sent back. Howard has confirmed she is still an employee. Unless you " "tell me otherwise, this is the setup she will get:" ), "questions": [ "MedTech — Memory Care (Tue–Sat), Desktop + phone, ALIS access, NO outside sign-in. " "Stop me below if any of that is wrong — otherwise no action needed.", ], }, { "name": "Ederick Yuzon", "dept": "Caregivers (Tower, Tue–Sat)", "context": ( "Just want to match the spelling on his payroll / ID so his account name is correct." ), "questions": [ "Is his first name spelled \"Ederick\", \"Edrick\", or something else?", ], }, { "name": "Reliable Agency caregiver #1 (shared login)", "dept": "Caregivers — Agency", "context": ( "John added this agency row without a specific person's name, so I am treating it as a " "shared login — whichever Reliable Agency caregiver is on shift signs in with this account. " "That works, but I want to keep the username short." ), "questions": [ "What short username would you like for this shared account? " "`reliable.agency.caregiver1` is long — I can use `reliable1` instead. OK, or prefer something else?", ], }, { "name": "Reliable Agency caregiver #2 (shared login)", "dept": "Caregivers — Agency", "context": ( "Same situation as #1." ), "questions": [ "Short username for the second shared agency login? Proposed: `reliable2`.", ], }, ] CLOSING = ( "Once I have these answers back, I will set up every account in one pass and let you know " "when they are ready for the users to sign in. Thank you!" ) # ----------------------------------------------------------------------------- # OOXML building # ----------------------------------------------------------------------------- NSW = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" def para(text: str, *, style: str | None = None, bold: bool = False, size: int | None = None) -> str: """A single with one run. Font size is in half-points.""" pPr = "" if style: pPr = f'' rPr_parts = [] if bold: rPr_parts.append("") if size is not None: rPr_parts.append(f'') rPr = f"{''.join(rPr_parts)}" if rPr_parts else "" return ( f'{pPr}' f'{rPr}{escape(text)}' f'' ) def bullet(text: str) -> str: return ( '' '' f'{escape(text)}' ) def blank_answer_line() -> str: """A hand-written answer placeholder: 'Answer: ________________'.""" return ( '' '' 'Answer: ' '' '____________________________________________________________' '' ) def section_for(q: dict) -> str: parts = [] parts.append(para(q["name"], style="Heading2")) parts.append(para(q["dept"], bold=True, size=20)) parts.append(para(q["context"])) for ques in q["questions"]: parts.append(bullet(ques)) parts.append(blank_answer_line()) parts.append(para("")) # spacer return "".join(parts) def build_document_xml() -> str: body_parts = [] body_parts.append(para(TITLE, style="Title")) body_parts.append(para(SUBTITLE, bold=False, size=20)) body_parts.append(para("")) body_parts.append(para(INTRO)) body_parts.append(para("")) for q in QUESTIONS: body_parts.append(section_for(q)) body_parts.append(para("")) body_parts.append(para(CLOSING)) body = "".join(body_parts) return ( '' f'' f'{body}' '' '' '' '' '' '' ) STYLES_XML = ''' ''' NUMBERING_XML = ''' ''' CONTENT_TYPES_XML = ''' ''' ROOT_RELS_XML = ''' ''' DOC_RELS_XML = ''' ''' def build_docx(path: str) -> None: os.makedirs(os.path.dirname(path), exist_ok=True) with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z: z.writestr("[Content_Types].xml", CONTENT_TYPES_XML) z.writestr("_rels/.rels", ROOT_RELS_XML) z.writestr("word/_rels/document.xml.rels", DOC_RELS_XML) z.writestr("word/document.xml", build_document_xml()) z.writestr("word/styles.xml", STYLES_XML) z.writestr("word/numbering.xml", NUMBERING_XML) if __name__ == "__main__": build_docx(OUT) print(f"Wrote {OUT}")