sync: auto-sync from ACG-TECH03L at 2026-04-19 12:50:13
Author: Howard Enos Machine: ACG-TECH03L Timestamp: 2026-04-19 12:50:13
This commit is contained in:
693
clients/cascades-tucson/scripts/generate-user-questionnaire.py
Normal file
693
clients/cascades-tucson/scripts/generate-user-questionnaire.py
Normal file
@@ -0,0 +1,693 @@
|
||||
"""
|
||||
Generate one Cascades Tucson user-confirmation questionnaire per recipient
|
||||
(Meredith, Ashley, John). Output: three .docx files plus a shared .md source.
|
||||
|
||||
Format: Section 1 is the roster grouped by department with a [Keep] checkbox,
|
||||
editable name/title, and a Notes line for role description. Sections 2-7 are
|
||||
per-attribute checklists ("Who is a driver?", "Who uses ALIS?", etc.),
|
||||
organized by department so scanning is fast. Section 8 is free text.
|
||||
|
||||
Usage:
|
||||
python generate-user-questionnaire.py
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from docx import Document
|
||||
from docx.shared import Pt, Inches, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from lxml import etree
|
||||
|
||||
OUT_DIR = Path(__file__).parent.parent / "docs" / "cloud" / "questionnaires"
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Word XML namespaces for content control checkboxes (clickable in Word)
|
||||
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml"
|
||||
|
||||
# Global counter for unique SDT IDs
|
||||
_sdt_id_counter = [100000]
|
||||
|
||||
|
||||
def _next_sdt_id():
|
||||
_sdt_id_counter[0] += 1
|
||||
return _sdt_id_counter[0]
|
||||
|
||||
|
||||
def add_clickable_checkbox(paragraph):
|
||||
"""
|
||||
Append a Word content-control checkbox to the given paragraph. Renders as
|
||||
an empty box that toggles to a checked box on click in Word 2010+, Word
|
||||
for the web, and Word mobile. Adds a trailing space run for readability.
|
||||
"""
|
||||
sdt_id = _next_sdt_id()
|
||||
xml = f'''<w:sdt xmlns:w="{W_NS}" xmlns:w14="{W14_NS}">
|
||||
<w:sdtPr>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="MS Gothic" w:eastAsia="MS Gothic" w:hAnsi="MS Gothic" w:hint="eastAsia"/>
|
||||
</w:rPr>
|
||||
<w:id w:val="{sdt_id}"/>
|
||||
<w14:checkbox>
|
||||
<w14:checked w14:val="0"/>
|
||||
<w14:checkedState w14:val="2612" w14:font="MS Gothic"/>
|
||||
<w14:uncheckedState w14:val="2610" w14:font="MS Gothic"/>
|
||||
</w14:checkbox>
|
||||
</w:sdtPr>
|
||||
<w:sdtContent>
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:rFonts w:ascii="MS Gothic" w:eastAsia="MS Gothic" w:hAnsi="MS Gothic" w:hint="eastAsia"/>
|
||||
</w:rPr>
|
||||
<w:t>\u2610</w:t>
|
||||
</w:r>
|
||||
</w:sdtContent>
|
||||
</w:sdt>'''
|
||||
sdt = etree.fromstring(xml)
|
||||
paragraph._p.append(sdt)
|
||||
# Trailing space after the checkbox so the label has breathing room
|
||||
space_run = paragraph.add_run(" ")
|
||||
return space_run
|
||||
|
||||
RECIPIENTS = [
|
||||
("Meredith Kuhn", "Executive Director"),
|
||||
("Ashley Jensen", "Assistant Executive Director"),
|
||||
("John Trozzi", "Maintenance Director"),
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Staff roster by department
|
||||
# Each entry: (display_name, title, notes_flag)
|
||||
# notes_flag text is prefilled into the Notes line when helpful
|
||||
# -----------------------------------------------------------------------------
|
||||
STAFF = {
|
||||
"Administrative": [
|
||||
("Meredith Kuhn", "Executive Director", ""),
|
||||
("Ashley Jensen", "Assistant Executive Director", ""),
|
||||
("Lauren Hasselman", "Business Office Director", ""),
|
||||
("Allison Reibschied", "Accounting Assistant", ""),
|
||||
],
|
||||
"Marketing / Sales": [
|
||||
("Megan Hiatt", "Sales Director", "Handles resident intake forms (PHI)"),
|
||||
("Crystal Rodriguez", "Sales Associate", "Handles resident intake forms (PHI)"),
|
||||
("Tamra Matthews", "Move-In Coordinator", "Leaving June 2026 — please confirm"),
|
||||
],
|
||||
"Care, Assisted Living (Nursing / Clinical)": [
|
||||
("Lois Lane", "Health Services Director", ""),
|
||||
("Karen Rossini", "Health Services Manager", ""),
|
||||
("Britney Thompson", "Memory Care Nurse", "Title says Memory Care but department currently Assisted Living — which is correct?"),
|
||||
("Veronica Feller", "Care, Assisted Living Aide", ""),
|
||||
],
|
||||
"Care, Memory Care": [
|
||||
("Shelby Trozzi", "Memory Care Director", ""),
|
||||
("Christine Nyanzunda", "Memory Care Admin Assistant", "Also appears on caregiver list (Sun/Mon AM) — same person, right?"),
|
||||
],
|
||||
"Resident Services": [
|
||||
("Christina DuPras", "Resident Services Director", ""),
|
||||
("Cathy Kingston", "Receptionist", "Front desk shared PC"),
|
||||
("Shontiel Nunn", "Receptionist", "Front desk shared PC"),
|
||||
("Kyla Quick Tiffany", "Receptionist", "NEW — not yet in AD. Spelling correct?"),
|
||||
("Michelle Shestko", "MC Receptionist", "MC front desk shared PC"),
|
||||
("Sebastian Leon", "Courtesy Patrol", ""),
|
||||
("Sheldon Gardfrey", "Courtesy Patrol", ""),
|
||||
("Ray Rai", "Courtesy Patrol", ""),
|
||||
],
|
||||
"Life Enrichment": [
|
||||
("Susan Hicks", "Life Enrichment Director", ""),
|
||||
("Sharon Edwards", "Life Enrichment Assistant", ""),
|
||||
],
|
||||
"Culinary": [
|
||||
("JD Martin", "Culinary Director", ""),
|
||||
("Ramon Castaneda", "Kitchen Manager", ""),
|
||||
("Alyssa Brooks", "Dining Manager", ""),
|
||||
],
|
||||
"Maintenance": [
|
||||
("John Trozzi", "Maintenance Director", ""),
|
||||
("Matt Brooks", "Memory Care Receptionist", "HR record shows department = Maintenance. Which is correct — Maintenance or Memory Care?"),
|
||||
],
|
||||
"Housekeeping": [
|
||||
("Lupe Sanchez", "Housekeeping Director", "AKA Guadalupe Sanchez"),
|
||||
],
|
||||
"Transportation": [
|
||||
("Richard Adams", "Driver", ""),
|
||||
("Julian Crim", "Driver", ""),
|
||||
("Christopher Holick", "Driver", ""),
|
||||
],
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Caregivers by shift pattern (39 total, none currently in AD/M365)
|
||||
# Each entry: (display_name, role, location, phone_number, flag_note)
|
||||
# -----------------------------------------------------------------------------
|
||||
CAREGIVERS = {
|
||||
"Tuesday-Saturday (15)": [
|
||||
("Thelma Abainza", "Caregiver", "Tower", "520-867-2579", ""),
|
||||
("Niel Castro", "MedTech / CCG", "Tower", "520-697-4644", ""),
|
||||
("Espe Esperance", "MedTech", "Tower", "520-788-9558", ""),
|
||||
("Barbara Johnson", "Caregiver", "Tower", "520-204-3449", ""),
|
||||
("Kasey Flores", "Caregiver", "Memory Care", "520-250-1451", ""),
|
||||
("Richard Flores", "Caregiver", "Memory Care", "520-873-7727", ""),
|
||||
("Marie Kastner", "Caregiver", "Memory Care", "714-576-9858", ""),
|
||||
("Bella Mendoza", "Caregiver", "Memory Care", "520-358-2000", ""),
|
||||
("Rosa Morales", "MedTech", "Memory Care", "312-213-8780", ""),
|
||||
("Sandra Padilla", "MedTech / CCG", "Tower", "520-585-3317", ""),
|
||||
("Polett Pinazavala", "MedTech", "Memory Care", "520-449-5533", "Please confirm spelling"),
|
||||
("Whisper Reed", "MedTech", "Tower (overnight)", "520-312-7575", ""),
|
||||
("Patricia Sandoval-Beck", "MedTech", "Tower", "520-343-8093", "Hyphenated last name correct?"),
|
||||
("Charity Sika", "Caregiver", "Memory Care", "623-251-8032", ""),
|
||||
("Ederick Yuzon", "Caregiver", "Tower", "520-603-8816", "Please confirm spelling"),
|
||||
],
|
||||
"Sunday-Thursday (10)": [
|
||||
("Juan Andrade", "Caregiver", "Memory Care", "520-528-4078", ""),
|
||||
("Jahmeka Clarke", "MedTech", "Memory Care", "520-649-7034", ""),
|
||||
("Karina Aziakpo", "MedTech / CCG", "Memory Care (overnight)", "520-392-6859", ""),
|
||||
("Jinnelle Dittbenner", "Caregiver", "Tower", "520-499-9996", ""),
|
||||
("Christine Nyanzunda (AM Sun/Mon only)", "MedTech", "Memory Care", "520-304-4251", "SAME person as the Memory Care Admin Assistant? (One account, not two)"),
|
||||
("Agnes McFerren", "Caregiver", "Tower", "520-406-3063", ""),
|
||||
("Samuel Ramirez", "Caregiver", "Tower", "520-488-5798", ""),
|
||||
("Erica Sanchez", "Caregiver", "Memory Care", "520-528-3387", ""),
|
||||
("Katrina Wyzykowski", "MedTech", "Memory Care", "520-347-1448", ""),
|
||||
("Corey Tate", "Caregiver (no MedTech)", "Tower (NOC)", "520-535-7821", ""),
|
||||
],
|
||||
"Friday-Monday / weekend (5)": [
|
||||
("Ashli Atwood", "MedTech / CCG", "Memory Care (overnight)", "715-200-1295", ""),
|
||||
("Cole Johnson", "MedTech", "Tower", "818-970-0890", ""),
|
||||
("Roseline Cooper", "Caregiver", "Memory Care (overnight)", "520-278-6817", ""),
|
||||
("Monique Lopez", "Caregiver (Fri+Sat doubles)", "Tower", "520-596-0969", ""),
|
||||
("Gloria Williford", "MedTech (Fri+Sat doubles)", "Memory Care", "928-551-1682", ""),
|
||||
],
|
||||
"Thursday-Monday (3)": [
|
||||
("Sarah Carroll", "Caregiver", "Tower", "520-409-2341", ""),
|
||||
("Luke Hogan", "Caregiver", "Tower", "520-312-0141", ""),
|
||||
("Gina Williams", "Caregiver", "Tower", "520-612-5075", ""),
|
||||
],
|
||||
"Split / partial-week (3)": [
|
||||
("Jen Higdon", "Caregiver", "Tower (M/W/F AM)", "520-730-3548", ""),
|
||||
("Mary Kariuki", "Caregiver", "Tower (Sat-Mon + Wed PM)", "520-309-1247", ""),
|
||||
("CeCe Lassey", "Caregiver", "Tower (Sun/Mon doubles + Tue PM)", "520-248-5982", ""),
|
||||
],
|
||||
"Sunday & Monday only (1)": [
|
||||
("Paty Doran", "MedTech / CCG", "Tower", "520-591-7368", "Paty, Patti, or Patricia? Please confirm"),
|
||||
],
|
||||
"PRN / float (2)": [
|
||||
("Ezekiel Huerta", "Caregiver (PRN)", "Tower", "520-591-6113", ""),
|
||||
("Maia Baker", "MedTech (PRN)", "Memory Care", "TBD", "On secondary sheet only — still employed?"),
|
||||
],
|
||||
}
|
||||
|
||||
# Flat list of ALL people, grouped by section, for the attribute checklists
|
||||
def all_people_by_group():
|
||||
groups = []
|
||||
for dept, people in STAFF.items():
|
||||
names = [p[0] for p in people]
|
||||
groups.append((dept, names))
|
||||
cg_all = []
|
||||
for _, people in CAREGIVERS.items():
|
||||
cg_all.extend(p[0] for p in people)
|
||||
groups.append(("Caregivers (all 39)", cg_all))
|
||||
return groups
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Document helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
def add_heading(doc, text, level=1):
|
||||
h = doc.add_heading(text, level=level)
|
||||
return h
|
||||
|
||||
|
||||
def add_para(doc, text, bold=False, size=11, italic=False, color=None):
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run(text)
|
||||
run.font.size = Pt(size)
|
||||
run.bold = bold
|
||||
run.italic = italic
|
||||
if color:
|
||||
run.font.color.rgb = color
|
||||
return p
|
||||
|
||||
|
||||
def add_checkbox_line(doc, text, indent=0.25):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Inches(indent)
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
run = p.add_run(f"{CHECKBOX} {text}")
|
||||
run.font.size = Pt(11)
|
||||
return p
|
||||
|
||||
|
||||
def add_notes_line(doc, label="Notes: ", indent=0.5):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Inches(indent)
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
run = p.add_run(label + "_" * 80)
|
||||
run.font.size = Pt(10)
|
||||
run.font.color.rgb = RGBColor(0x60, 0x60, 0x60)
|
||||
return p
|
||||
|
||||
|
||||
def add_blank_line(doc):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
return p
|
||||
|
||||
|
||||
def add_separator(doc):
|
||||
p = doc.add_paragraph()
|
||||
run = p.add_run("_" * 90)
|
||||
run.font.color.rgb = RGBColor(0xCC, 0xCC, 0xCC)
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
|
||||
def add_attribute_question(doc, question_num, question_text, helper_text, include_caregivers=True):
|
||||
"""A question that lists names by department with a checkbox to mark yes."""
|
||||
add_heading(doc, f"{question_num}. {question_text}", level=2)
|
||||
if helper_text:
|
||||
add_para(doc, helper_text, italic=True, size=10, color=RGBColor(0x50, 0x50, 0x50))
|
||||
add_blank_line(doc)
|
||||
|
||||
for dept, people in STAFF.items():
|
||||
add_para(doc, dept, bold=True, size=11)
|
||||
for name, title, _ in people:
|
||||
add_checkbox_line(doc, f"{name} — {title}", indent=0.35)
|
||||
add_blank_line(doc)
|
||||
|
||||
if include_caregivers:
|
||||
add_para(doc, "Caregivers (none in M365 yet)", bold=True, size=11)
|
||||
for group_name, people in CAREGIVERS.items():
|
||||
add_para(doc, f" {group_name}", italic=True, size=10)
|
||||
for name, role, location, phone, _ in people:
|
||||
add_checkbox_line(doc, f"{name} — {role} ({location})", indent=0.5)
|
||||
add_blank_line(doc)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Document builder
|
||||
# -----------------------------------------------------------------------------
|
||||
def build_doc(recipient_name, recipient_title):
|
||||
doc = Document()
|
||||
|
||||
style = doc.styles["Normal"]
|
||||
style.font.name = "Calibri"
|
||||
style.font.size = Pt(11)
|
||||
|
||||
# ---- Title ----
|
||||
title = doc.add_heading("Cascades of Tucson — User + Access Confirmation", level=0)
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run(f"Prepared for: {recipient_name}, {recipient_title}")
|
||||
r.bold = True
|
||||
r.font.size = Pt(12)
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("Prepared by: Howard Enos, Computer Guru \u2022 Date: 2026-04-18")
|
||||
r.font.size = Pt(10)
|
||||
r.italic = True
|
||||
|
||||
add_separator(doc)
|
||||
|
||||
# ---- Intro ----
|
||||
add_heading(doc, "Why we need this", level=1)
|
||||
add_para(
|
||||
doc,
|
||||
"Before we purchase the Microsoft 365 Business Premium licenses, create the 39 new "
|
||||
"caregiver accounts, and configure the security policies that lock down the shared "
|
||||
"phones and PHI access, I need to know four things about every person on your staff:",
|
||||
)
|
||||
add_checkbox_line(doc, "Is their name and department correct?", indent=0.3)
|
||||
add_checkbox_line(doc, "How do they use technology day-to-day (company phone, PC, both)?", indent=0.3)
|
||||
add_checkbox_line(doc, "Should they be allowed to sign in from outside the building?", indent=0.3)
|
||||
add_checkbox_line(doc, "Which systems / mailboxes / apps do they actually need?", indent=0.3)
|
||||
|
||||
add_para(
|
||||
doc,
|
||||
"Please answer the sections below at your own pace. Mark a box by double-clicking the "
|
||||
"\u2610 and replacing it with an X, or simply type an X in front of the name. Cross out "
|
||||
"anyone who should be removed. Write additions where it says \"Add anyone missing.\" "
|
||||
"Leave Comments / Notes where helpful \u2014 the more context, the better the lockdown.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
add_para(
|
||||
doc,
|
||||
"A quick note about ALIS: ALIS is the resident records website. When we tie it to "
|
||||
"Microsoft, users who log into their Microsoft account will automatically be signed "
|
||||
"into ALIS \u2014 no second username/password or second MFA prompt. That only works if "
|
||||
"we know who needs it (Section 5).",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
doc.add_page_break()
|
||||
|
||||
# ---- Section 1: Roster ----
|
||||
add_heading(doc, "Section 1 \u2014 The Staff Roster", level=1)
|
||||
add_para(
|
||||
doc,
|
||||
"Below is every person we currently have on file, organized by department. For "
|
||||
"each person: leave the Keep box checked if they are a current employee at that title, "
|
||||
"or uncheck and strike through anyone who should be removed. Correct spellings "
|
||||
"inline. Add a short note on the Notes line describing what they actually do day-to-day, "
|
||||
"especially anything involving resident information (PHI), medication, or outside-network "
|
||||
"access. Blank rows are at the bottom of each department for additions.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
add_blank_line(doc)
|
||||
|
||||
for dept, people in STAFF.items():
|
||||
add_heading(doc, dept, level=2)
|
||||
for name, title, flag in people:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
r = p.add_run(f"{CHECKBOX} Keep ")
|
||||
r.bold = True
|
||||
r = p.add_run(f"{name} \u2014 {title}")
|
||||
if flag:
|
||||
p2 = doc.add_paragraph()
|
||||
p2.paragraph_format.left_indent = Inches(0.5)
|
||||
p2.paragraph_format.space_after = Pt(2)
|
||||
r = p2.add_run(f"\u203A {flag}")
|
||||
r.italic = True
|
||||
r.font.size = Pt(10)
|
||||
r.font.color.rgb = RGBColor(0xB0, 0x40, 0x00)
|
||||
add_notes_line(doc, " Notes: ")
|
||||
|
||||
# Three blank add-row lines
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
r = p.add_run("Add anyone missing from this department:")
|
||||
r.italic = True
|
||||
r.font.size = Pt(10)
|
||||
r.font.color.rgb = RGBColor(0x40, 0x40, 0x80)
|
||||
for _ in range(3):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
r = p.add_run(f"{CHECKBOX} Add Name: ________________________ Title: ________________________")
|
||||
r.font.size = Pt(10)
|
||||
add_notes_line(doc, " Notes: ")
|
||||
add_blank_line(doc)
|
||||
|
||||
# ---- Caregivers section ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Section 1b \u2014 Caregivers (NEW \u2014 no current accounts)", level=1)
|
||||
add_para(
|
||||
doc,
|
||||
"These are the 39 staff currently using shared workstation logins. None has an email "
|
||||
"account or a Microsoft identity yet. They are the reason we are deploying the 25 "
|
||||
"shared Android phones. Please confirm each name + role, correct any spelling, and "
|
||||
"add anyone missing. Default assumption for every caregiver: phone-only, no outside "
|
||||
"access (locked to Cascades network + managed shared phone only). Flag exceptions "
|
||||
"in the Notes line.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
add_blank_line(doc)
|
||||
|
||||
for group_name, people in CAREGIVERS.items():
|
||||
add_heading(doc, group_name, level=2)
|
||||
for name, role, location, phone, flag in people:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
r = p.add_run(f"{CHECKBOX} Keep ")
|
||||
r.bold = True
|
||||
r = p.add_run(f"{name} \u2014 {role} ({location}) [{phone}]")
|
||||
if flag:
|
||||
p2 = doc.add_paragraph()
|
||||
p2.paragraph_format.left_indent = Inches(0.5)
|
||||
p2.paragraph_format.space_after = Pt(2)
|
||||
r = p2.add_run(f"\u203A {flag}")
|
||||
r.italic = True
|
||||
r.font.size = Pt(10)
|
||||
r.font.color.rgb = RGBColor(0xB0, 0x40, 0x00)
|
||||
add_notes_line(doc, " Notes: ")
|
||||
add_blank_line(doc)
|
||||
|
||||
# Add-new-caregiver block
|
||||
p = doc.add_paragraph()
|
||||
r = p.add_run("Add any caregivers missing from this list:")
|
||||
r.italic = True
|
||||
r.font.size = Pt(10)
|
||||
r.font.color.rgb = RGBColor(0x40, 0x40, 0x80)
|
||||
for _ in range(5):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.left_indent = Inches(0.25)
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
r = p.add_run(f"{CHECKBOX} Add Name: _______________________ Role: _______________________ Phone: ______________")
|
||||
r.font.size = Pt(10)
|
||||
|
||||
# ---- Attribute-based questions ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Section 2 \u2014 Roles (check everyone who fits)", level=1)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"2a",
|
||||
"Who is a Driver? (Transportation / residents to appointments)",
|
||||
"Drivers typically use a company phone for dispatch and communication \u2014 usually no personal PC.",
|
||||
include_caregivers=False,
|
||||
)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"2b",
|
||||
"Who is a Nurse, MedTech, or Clinical Staff?",
|
||||
"People in this group see resident medical information (PHI) and are the main reason we care about HIPAA audit logs and ALIS single sign-on.",
|
||||
include_caregivers=True,
|
||||
)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"2c",
|
||||
"Who handles resident intake paperwork, leases, or medical forms by email?",
|
||||
"These people receive PHI via email from doctors, families, or facilities. They need the strongest anti-phishing and DLP (data loss prevention) policies.",
|
||||
include_caregivers=False,
|
||||
)
|
||||
|
||||
# ---- Section 3: Access questions ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Section 3 \u2014 How They Use Technology", level=1)
|
||||
add_para(
|
||||
doc,
|
||||
"For each person, please tell us whether they sit at a company PC, use a company "
|
||||
"phone on shift, or both.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"3a",
|
||||
"Who uses a COMPANY PC / DESKTOP?",
|
||||
"Anyone who signs into a Windows computer at Cascades. Includes shared front-desk PCs.",
|
||||
include_caregivers=True,
|
||||
)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"3b",
|
||||
"Who uses a COMPANY PHONE (the 25 shared Android phones)?",
|
||||
"Anyone who will pick up a shared phone at start of shift and use it for work (medication passes, resident records, email, rounds).",
|
||||
include_caregivers=True,
|
||||
)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"3c",
|
||||
"Who uses a COMPANY TABLET / iPad?",
|
||||
"Specifically the 9 kitchen iPads + any others. Likely Culinary staff.",
|
||||
include_caregivers=True,
|
||||
)
|
||||
|
||||
# ---- Section 4: Outside access ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Section 4 \u2014 Sign-In From Outside the Building", level=1)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"4a",
|
||||
"Who should be allowed to sign in from OUTSIDE the building?",
|
||||
"Home office, travel, personal cell, hotel, etc. Anyone NOT checked will be locked to "
|
||||
"the Cascades network and managed devices only \u2014 which is what HIPAA and "
|
||||
"Conditional Access prefer by default.",
|
||||
include_caregivers=True,
|
||||
)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"4b",
|
||||
"Who should be allowed to check email on their PERSONAL cell phone?",
|
||||
"Separate question: some people may need outside access on a company laptop but NOT on a personal phone (HIPAA risk). Check only the people who genuinely need email on a personal device.",
|
||||
include_caregivers=True,
|
||||
)
|
||||
|
||||
# ---- Section 5: ALIS ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Section 5 \u2014 ALIS (Resident Records)", level=1)
|
||||
add_para(
|
||||
doc,
|
||||
"ALIS is the resident records website. If we connect ALIS to Microsoft, users who "
|
||||
"sign into their Microsoft account will be automatically signed into ALIS \u2014 no "
|
||||
"second username / password, no second MFA prompt, no separate login. This is also "
|
||||
"where the per-person audit log lives (who opened what chart, when, from which device) "
|
||||
"which is the HIPAA story.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
add_para(
|
||||
doc,
|
||||
"We only want to connect ALIS for people who actually use it, so please check "
|
||||
"carefully.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"5a",
|
||||
"Who LOGS INTO ALIS today (resident charts, care plans, medication)?",
|
||||
"",
|
||||
include_caregivers=True,
|
||||
)
|
||||
|
||||
add_attribute_question(
|
||||
doc,
|
||||
"5b",
|
||||
"Who should be able to access ALIS from OUTSIDE the building?",
|
||||
"Only matters for the people checked in 5a. Example: an on-call nurse charting from home.",
|
||||
include_caregivers=True,
|
||||
)
|
||||
|
||||
# ---- Section 6: Shared mailboxes ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Section 6 \u2014 Shared Mailboxes (Accounting@, Frontdesk@, etc.)", level=1)
|
||||
add_para(
|
||||
doc,
|
||||
"These are department mailboxes multiple people can access from their own sign-in. "
|
||||
"For each mailbox below, check everyone who should receive and send email through it. "
|
||||
"Leaving a mailbox blank means nobody needs it and we will decommission it.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
add_blank_line(doc)
|
||||
|
||||
shared_mailboxes = [
|
||||
("accounting@cascadestucson.com", "Accounting / AR / AP"),
|
||||
("accountingassistant@cascadestucson.com", "Accounting assistant desk"),
|
||||
("boadmin@cascadestucson.com", "Business Office admin"),
|
||||
("frontdesk@cascadestucson.com", "Main front desk (Tower)"),
|
||||
("memcarereceptionist@cascadestucson.com", "Memory Care front desk"),
|
||||
("hr@cascadestucson.com", "HR / employee matters"),
|
||||
("nurse@cascadestucson.com", "Clinical nursing desk"),
|
||||
("medtech@cascadestucson.com", "MedTech medication pass"),
|
||||
("transportation@cascadestucson.com", "Drivers / resident transport"),
|
||||
("security@cascadestucson.com", "Security / courtesy patrol"),
|
||||
("courtesypatrol@cascadestucson.com", "Courtesy Patrol (new \u2014 please confirm it\u2019s needed)"),
|
||||
("training@cascadestucson.com", "Training"),
|
||||
("sales@cascadestucson.com", "Sales inquiries"),
|
||||
("fax@cascadestucson.com", "Fax-to-email"),
|
||||
]
|
||||
for addr, desc in shared_mailboxes:
|
||||
add_heading(doc, f"{addr} \u2014 {desc}", level=3)
|
||||
for dept, people in STAFF.items():
|
||||
add_para(doc, dept, bold=True, size=10)
|
||||
for name, title, _ in people:
|
||||
add_checkbox_line(doc, f"{name} \u2014 {title}", indent=0.35)
|
||||
add_blank_line(doc)
|
||||
|
||||
# ---- Section 7: Odds and ends ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Section 7 \u2014 Specific Questions", level=1)
|
||||
|
||||
add_heading(doc, "7a. Departments that may need a re-label", level=2)
|
||||
add_para(doc, "Please mark the correct department for each:")
|
||||
add_checkbox_line(doc, "Britney Thompson \u2014 currently \"Care, Assisted Living,\" title says Memory Care Nurse. Correct department is:", indent=0.35)
|
||||
add_notes_line(doc, " ")
|
||||
add_checkbox_line(doc, "Matt Brooks \u2014 HR record says Maintenance, title says Memory Care Receptionist. Correct department is:", indent=0.35)
|
||||
add_notes_line(doc, " ")
|
||||
add_checkbox_line(doc, "Christine Nyanzunda \u2014 Memory Care Admin Assistant AND appears on caregiver shift list (Sun/Mon AM). One account or two?", indent=0.35)
|
||||
add_notes_line(doc, " ")
|
||||
|
||||
add_blank_line(doc)
|
||||
add_heading(doc, "7b. Spelling confirmations (caregivers)", level=2)
|
||||
for name, reason in [
|
||||
("Polett Pinazavala", "unusual spelling"),
|
||||
("Patricia Sandoval-Beck", "hyphenated last name"),
|
||||
("Ederick Yuzon", "unusual spelling"),
|
||||
("Paty Doran", "Paty / Patti / Patricia"),
|
||||
("Maia Baker", "only on secondary sheet \u2014 still employed?"),
|
||||
("Kyla Quick Tiffany", "three names or hyphenated?"),
|
||||
]:
|
||||
add_checkbox_line(doc, f"{name} \u2014 {reason} Correct spelling: ________________________", indent=0.35)
|
||||
|
||||
add_blank_line(doc)
|
||||
add_heading(doc, "7c. Departures / upcoming changes", level=2)
|
||||
add_para(doc, "Anyone leaving, transferring, or changing role in the next 90 days?", italic=True, size=10)
|
||||
for _ in range(5):
|
||||
add_notes_line(doc, "")
|
||||
|
||||
# ---- Section 8: Free text ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Section 8 \u2014 Anything Else", level=1)
|
||||
add_para(
|
||||
doc,
|
||||
"Anything we should know about a specific person or situation? Unusual access needs, "
|
||||
"contractors, people who share a phone / PC, dual-role employees, anyone who handles "
|
||||
"legal or compliance records, etc. Write freely below.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
add_blank_line(doc)
|
||||
for _ in range(20):
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.space_after = Pt(2)
|
||||
r = p.add_run("_" * 100)
|
||||
r.font.color.rgb = RGBColor(0xCC, 0xCC, 0xCC)
|
||||
|
||||
# ---- Closing ----
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "Thank you", level=1)
|
||||
add_para(
|
||||
doc,
|
||||
f"{recipient_name.split()[0]} \u2014 thank you for taking the time on this. Once I have "
|
||||
"your answers (and Meredith\u2019s and Ashley\u2019s / John\u2019s), I will merge the "
|
||||
"three responses into one final list and come back with the exact license count, the "
|
||||
"exact accounts we are creating, and the exact Conditional Access rules before I change "
|
||||
"anything in your tenant. Nothing gets purchased or changed until you\u2019ve seen the "
|
||||
"final plan.",
|
||||
)
|
||||
add_para(
|
||||
doc,
|
||||
"Easiest way to return this: save and email back to howard@azcomputerguru.com. "
|
||||
"You can also drop a copy on our shared OneDrive or Teams folder and I\u2019ll pick it up.",
|
||||
italic=True,
|
||||
size=10,
|
||||
color=RGBColor(0x50, 0x50, 0x50),
|
||||
)
|
||||
add_para(
|
||||
doc,
|
||||
"\u2014 Howard",
|
||||
bold=True,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Generate files
|
||||
# -----------------------------------------------------------------------------
|
||||
if __name__ == "__main__":
|
||||
for name, title in RECIPIENTS:
|
||||
doc = build_doc(name, title)
|
||||
first = name.split()[0]
|
||||
filename = OUT_DIR / f"cascades-user-access-questionnaire-{first}.docx"
|
||||
doc.save(str(filename))
|
||||
print(f"[OK] {filename}")
|
||||
print(f"\nAll files written to: {OUT_DIR}")
|
||||
152
clients/cascades-tucson/scripts/prep-profile-for-redirection.ps1
Normal file
152
clients/cascades-tucson/scripts/prep-profile-for-redirection.ps1
Normal file
@@ -0,0 +1,152 @@
|
||||
# Offline NTUSER.DAT prep for a ProfWiz-migrated Cascades user who is about
|
||||
# to log in for the first time since the Folder Redirection GPO was fixed.
|
||||
#
|
||||
# Combines two fixes that otherwise would require separate post-logon cleanup:
|
||||
#
|
||||
# 1. UNPOISON — any User Shell Folders values pointing at
|
||||
# C:\Windows\system32\config\systemprofile\... (a ProfWiz side effect)
|
||||
# get reset to %USERPROFILE% defaults so the Folder Redirection CSE
|
||||
# doesn't hang trying to access SYSTEM's profile.
|
||||
#
|
||||
# 2. PRE-SEED UNC TARGETS — the legacy name (Personal) AND the modern
|
||||
# KnownFolder GUID forms for Documents and Downloads get set to the
|
||||
# UNC redirect path in advance, so the Explorer sidebar resolves
|
||||
# correctly on first login (no more "this file has no associated app"
|
||||
# post-login patch like we needed for Sharon Edwards).
|
||||
#
|
||||
# WHEN TO USE
|
||||
# Run BEFORE the user's first login after the Folder Redirection GPO has
|
||||
# been linked to their OU. Requires the user to be logged OFF so
|
||||
# NTUSER.DAT can be loaded as a hive.
|
||||
#
|
||||
# HOW TO RUN
|
||||
# ScreenConnect Backstage PowerShell (runs as SYSTEM) is the reliable path.
|
||||
# Example for Susan Hicks on DESKTOP-ROK7VNM:
|
||||
#
|
||||
# .\prep-profile-for-redirection.ps1 `
|
||||
# -ProfilePath 'C:\Users\Susan.Hicks' `
|
||||
# -RedirectionRoot '\\CS-SERVER\homes' `
|
||||
# -SamName 'Susan.Hicks'
|
||||
#
|
||||
# SAFETY
|
||||
# - Idempotent: running twice is a no-op. Re-runs only log [KEEP].
|
||||
# - Never touches Desktop, Music, Pictures, Video, Favorites UNC paths
|
||||
# (only resets them to %USERPROFILE% defaults IF they're poisoned).
|
||||
# - Timestamped NTUSER.DAT backup at C:\ProfileBackups\ before any write.
|
||||
#
|
||||
# ROLLBACK
|
||||
# Copy-Item <backup> <ntuser-path> -Force (while user logged off)
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProfilePath, # e.g. 'C:\Users\Susan.Hicks'
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RedirectionRoot, # e.g. '\\CS-SERVER\homes'
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SamName, # e.g. 'Susan.Hicks'
|
||||
|
||||
[string]$BackupDir = 'C:\ProfileBackups',
|
||||
[string]$TempHiveName = 'ProfileFix'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$ntuser = Join-Path $ProfilePath 'NTUSER.DAT'
|
||||
if (-not (Test-Path $ntuser)) { throw "NTUSER.DAT not found at $ntuser" }
|
||||
|
||||
# Target UNC paths
|
||||
$uncBase = "$RedirectionRoot\$SamName"
|
||||
$docsUNC = "$uncBase\Documents"
|
||||
$dlUNC = "$uncBase\Downloads"
|
||||
|
||||
# Backup
|
||||
New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null
|
||||
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$leaf = Split-Path $ProfilePath -Leaf
|
||||
$backup = Join-Path $BackupDir "$leaf-NTUSER.DAT.$stamp.bak"
|
||||
Copy-Item $ntuser $backup -Force
|
||||
Write-Host "[OK] Backup -> $backup"
|
||||
|
||||
# Unload stale mount if any, then load hive
|
||||
if (Test-Path "Registry::HKEY_USERS\$TempHiveName") {
|
||||
reg unload "HKU\$TempHiveName" 2>&1 | Out-Null
|
||||
Start-Sleep 1
|
||||
}
|
||||
$loadResult = reg load "HKU\$TempHiveName" $ntuser 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "reg load failed: $loadResult" }
|
||||
Write-Host "[OK] Hive loaded at HKU\$TempHiveName"
|
||||
|
||||
try {
|
||||
$USF = "Registry::HKEY_USERS\$TempHiveName\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders"
|
||||
$SF = "Registry::HKEY_USERS\$TempHiveName\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
|
||||
|
||||
# --- Part 1: pre-seed UNC targets (legacy + GUID form, both hives) ---
|
||||
$targets = [ordered]@{
|
||||
'Personal' = $docsUNC # legacy name for Documents
|
||||
'{FDD39AD0-238F-46AF-ADB4-6C85480369C7}' = $docsUNC # modern Documents KF GUID
|
||||
'{374DE290-123F-4565-9164-39C4925E467B}' = $dlUNC # modern Downloads KF GUID
|
||||
}
|
||||
|
||||
Write-Host "`n--- Pre-seeding redirect targets ---"
|
||||
foreach ($name in $targets.Keys) {
|
||||
$new = $targets[$name]
|
||||
$current = (Get-ItemProperty -Path $USF -Name $name -ErrorAction SilentlyContinue).$name
|
||||
|
||||
if ($current -eq $new) {
|
||||
Write-Host " [KEEP] $name = '$current' (already correct)"
|
||||
} elseif ($null -eq $current) {
|
||||
New-ItemProperty -Path $USF -Name $name -Value $new -PropertyType ExpandString -Force | Out-Null
|
||||
Write-Host " [ADDED] $name = $new"
|
||||
} else {
|
||||
Set-ItemProperty -Path $USF -Name $name -Value $new -Type ExpandString
|
||||
Write-Host " [CHANGED] $name : '$current' -> '$new'"
|
||||
}
|
||||
|
||||
# Mirror into Shell Folders (REG_SZ, resolved path)
|
||||
New-ItemProperty -Path $SF -Name $name -Value $new -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
|
||||
# Legacy "My Documents" entry some older apps read
|
||||
New-ItemProperty -Path $SF -Name 'My Documents' -Value $docsUNC -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
# --- Part 2: unpoison anything still pointing at systemprofile ---
|
||||
$unpoison = [ordered]@{
|
||||
'My Music' = '%USERPROFILE%\Music'
|
||||
'My Pictures' = '%USERPROFILE%\Pictures'
|
||||
'My Video' = '%USERPROFILE%\Videos'
|
||||
'Favorites' = '%USERPROFILE%\Favorites'
|
||||
}
|
||||
$poisonPrefix = 'C:\Windows\system32\config\systemprofile'
|
||||
|
||||
Write-Host "`n--- Unpoisoning non-redirected shell folders ---"
|
||||
foreach ($name in $unpoison.Keys) {
|
||||
$new = $unpoison[$name]
|
||||
$current = (Get-ItemProperty -Path $USF -Name $name -ErrorAction SilentlyContinue).$name
|
||||
|
||||
if ($null -eq $current) {
|
||||
New-ItemProperty -Path $USF -Name $name -Value $new -PropertyType ExpandString -Force | Out-Null
|
||||
Write-Host " [ADDED] $name = $new"
|
||||
} elseif ($current -like "$poisonPrefix*") {
|
||||
Set-ItemProperty -Path $USF -Name $name -Value $new -Type ExpandString
|
||||
Write-Host " [CHANGED] $name : '$current' -> '$new' (was poisoned)"
|
||||
} else {
|
||||
Write-Host " [KEEP] $name = '$current' (not poisoned)"
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
# Release any handles so the hive can unload cleanly
|
||||
[gc]::Collect()
|
||||
Start-Sleep 2
|
||||
reg unload "HKU\$TempHiveName" 2>&1 | Out-Null
|
||||
Write-Host "[OK] Hive unloaded"
|
||||
}
|
||||
|
||||
Write-Host "`nBackup: $backup"
|
||||
Write-Host "Rollback: Copy-Item '$backup' '$ntuser' -Force (while user logged off)"
|
||||
Write-Host ""
|
||||
Write-Host "Next step: have the user sign in. The Folder Redirection CSE will apply the"
|
||||
Write-Host "GPO (no hang because hive is clean) and the sidebar will resolve correctly"
|
||||
Write-Host "because the GUID-form shell folders already point at the UNC target."
|
||||
Reference in New Issue
Block a user