214 lines
8.4 KiB
Python
214 lines
8.4 KiB
Python
"""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, 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?",
|
||
],
|
||
},
|
||
]
|
||
|
||
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="•"/>
|
||
<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}")
|