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
This commit is contained in:
267
clients/cascades-tucson/scripts/build-open-questions-docx.py
Normal file
267
clients/cascades-tucson/scripts/build-open-questions-docx.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""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 <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}")
|
||||
Reference in New Issue
Block a user