Files
claudetools/clients/cascades-tucson/scripts/build-open-questions-docx.py
Howard Enos 7bffbfbb89 sync: auto-sync from HOWARD-HOME at 2026-04-22 16:24:58
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-04-22 16:24:58
2026-04-22 16:24:58 -07:00

268 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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, TueSat)",
"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 (TueSat), 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, TueSat)",
"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 <w:p> with one run. Font size is in half-points."""
pPr = ""
if style:
pPr = f'<w:pPr><w:pStyle w:val="{style}"/></w:pPr>'
rPr_parts = []
if bold:
rPr_parts.append("<w:b/>")
if size is not None:
rPr_parts.append(f'<w:sz w:val="{size}"/>')
rPr = f"<w:rPr>{''.join(rPr_parts)}</w:rPr>" if rPr_parts else ""
return (
f'<w:p>{pPr}'
f'<w:r>{rPr}<w:t xml:space="preserve">{escape(text)}</w:t></w:r>'
f'</w:p>'
)
def bullet(text: str) -> str:
return (
'<w:p><w:pPr><w:pStyle w:val="ListParagraph"/>'
'<w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>'
f'<w:r><w:t xml:space="preserve">{escape(text)}</w:t></w:r></w:p>'
)
def blank_answer_line() -> str:
"""A hand-written answer placeholder: 'Answer: ________________'."""
return (
'<w:p>'
'<w:r><w:rPr><w:i/><w:color w:val="808080"/></w:rPr>'
'<w:t xml:space="preserve">Answer: </w:t></w:r>'
'<w:r><w:rPr><w:color w:val="C0C0C0"/></w:rPr>'
'<w:t xml:space="preserve">____________________________________________________________</w:t></w:r>'
'</w:p>'
)
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 (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
f'<w:document xmlns:w="{NSW}">'
f'<w:body>{body}'
'<w:sectPr>'
'<w:pgSz w:w="12240" w:h="15840"/>'
'<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>'
'</w:sectPr>'
'</w:body>'
'</w:document>'
)
STYLES_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:docDefaults>
<w:rPrDefault>
<w:rPr>
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
<w:sz w:val="22"/>
</w:rPr>
</w:rPrDefault>
<w:pPrDefault>
<w:pPr><w:spacing w:after="120"/></w:pPr>
</w:pPrDefault>
</w:docDefaults>
<w:style w:type="paragraph" w:styleId="Title">
<w:name w:val="Title"/>
<w:pPr><w:spacing w:before="240" w:after="120"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="44"/><w:color w:val="1A2030"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading2">
<w:name w:val="heading 2"/>
<w:pPr><w:spacing w:before="360" w:after="80"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="28"/><w:color w:val="2B6CB0"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="ListParagraph">
<w:name w:val="List Paragraph"/>
<w:pPr><w:ind w:left="720"/><w:contextualSpacing/></w:pPr>
</w:style>
</w:styles>
'''
NUMBERING_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0">
<w:start w:val="1"/>
<w:numFmt w:val="bullet"/>
<w:lvlText w:val="&#8226;"/>
<w:lvlJc w:val="left"/>
<w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr>
<w:rPr><w:rFonts w:ascii="Symbol" w:hAnsi="Symbol"/></w:rPr>
</w:lvl>
</w:abstractNum>
<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
</w:numbering>
'''
CONTENT_TYPES_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
<Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
</Types>
'''
ROOT_RELS_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>
'''
DOC_RELS_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
</Relationships>
'''
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}")