Files
claudetools/clients/cascades-tucson/scripts/build-open-questions-docx.py
Howard Enos 6bd416657c sync: auto-sync from HOWARD-HOME at 2026-04-22 17:39:56
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-04-22 17:39:56
2026-04-22 17:39:57 -07:00

214 lines
8.4 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 — One Outstanding Item on Staff Access List"
SUBTITLE = "2026-04-22 · prepared by Howard Enos, Computer Guru · post John's reply"
INTRO = (
"Thank you for getting back to me on the staff list — almost everything is squared "
"away now. Britney and Polett have been removed from the roster (no longer employees), "
"Alma's title and access are set (Memory Care Life Enrichment, D+P, ALIS, offsite), "
"and drivers will stay on the roster for tracking but no longer get Cascades IT accounts. "
"On the two Reliable Agency caregivers: after a HIPAA compliance review, we cannot use "
"shared log-ins for PHI access (it's a federal rule, §164.312(a)(2)(i)). Reliable will "
"need to provide individual caregiver names before those caregivers can have their own "
"Cascades account — otherwise they will need to work under the direct supervision of a "
"Cascades-employed caregiver who is signed in. There is one small item still outstanding "
"from the staff list itself — see below."
)
QUESTIONS = [
{
"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?",
],
},
]
CLOSING = (
"Once that spelling is confirmed I will build every caregiver account in one pass. "
"The rest of the setup (Alma, Kyla, the two Reliable shared logins, and disabling "
"Britney's + the three driver accounts) is ready to start. 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}")