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:
2026-04-22 16:24:58 -07:00
parent ce52a62ff1
commit 7bffbfbb89
8 changed files with 1275 additions and 46 deletions

View 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, 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}")