other changes

This commit is contained in:
Peter Howell 2025-08-29 12:25:19 -07:00
parent e9b10767d6
commit 20e2b0e3d7
3 changed files with 303 additions and 7 deletions

View File

@ -1,4 +1,4 @@
from __future__ import annotations
#saved_titles = json.loads( codecs.open('cache/saved_youtube_titles.json','r','utf-8').read() ) #saved_titles = json.loads( codecs.open('cache/saved_youtube_titles.json','r','utf-8').read() )
@ -1235,6 +1235,299 @@ def download_web():
def flowgrid():
# a tiny DSL for lane/step "flow grid" diagrams rendered to HTML.
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Optional, Dict
import re
import html
# ---------------------- Data model ----------------------
@dataclass
class Step:
code: str
label: str
weeks: Optional[str] = None
hours: Optional[str] = None
tag: Optional[str] = None # 'req' | 'rec' | None
desc: Optional[str] = None # override/extra text for subline
klass: Optional[str] = None # additional css class
@dataclass
class Lane:
name: str
steps: List[Step] = field(default_factory=list)
@dataclass
class Doc:
title: str
lanes: List[Lane] = field(default_factory=list)
css_vars: Dict[str, str] = field(default_factory=dict)
# ---------------------- Parser ----------------------
def parse_spec(text: str) -> Doc:
"""
DSL syntax:
- Comments start with '#'
- KEY: value (supported keys: TITLE, VAR)
VAR: --step=260px; --arrow=34px; --done=110px (semicolon separated; optional)
- LANE: <name>
STEP: CODE | LABEL | weeks=2; hours=20; tag=req
STEP: CODE | LABEL | desc=1 hour on-site; tag=rec
- Empty lines are ignored.
- Indentation is optional and only for readability.
"""
title = "Untitled Diagram"
lanes: List[Lane] = []
current_lane: Optional[Lane] = None
css_vars: Dict[str, str] = {}
for raw in text.splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
# KEY: value
m = re.match(r'(?i)TITLE\s*:\s*(.+)$', line)
if m:
title = m.group(1).strip()
continue
# VAR line
m = re.match(r'(?i)VAR\s*:\s*(.+)$', line)
if m:
# semicolon separated k=v; allow CSS custom props like --step=300px
blob = m.group(1)
parts = [p.strip() for p in blob.split(";") if p.strip()]
for p in parts:
if "=" in p:
k, v = p.split("=", 1)
css_vars[k.strip()] = v.strip()
continue
# LANE
m = re.match(r'(?i)LANE\s*:\s*(.+)$', line)
if m:
current_lane = Lane(name=m.group(1).strip())
lanes.append(current_lane)
continue
# STEP
m = re.match(r'(?i)STEP\s*:\s*(.+)$', line)
if m:
if current_lane is None:
raise ValueError("STEP appears before any LANE is defined.")
body = m.group(1)
# Expect: CODE | LABEL | attrs
parts = [p.strip() for p in body.split("|")]
if len(parts) < 2:
raise ValueError(f"STEP needs 'CODE | LABEL | ...' got: {body}")
code = parts[0]
label = parts[1]
attrs_blob = parts[2] if len(parts) >=3 else ""
# Parse attrs: key=value; key=value
step_kwargs = {}
if attrs_blob:
for kv in [a.strip() for a in attrs_blob.split(";") if a.strip()]:
if "=" in kv:
k, v = kv.split("=", 1)
step_kwargs[k.strip().lower()] = v.strip()
else:
# allow bare tag 'req' or 'rec'
if kv.lower() in ("req", "rec"):
step_kwargs["tag"] = kv.lower()
step = Step(
code=code,
label=label,
weeks=step_kwargs.get("weeks") or step_kwargs.get("w"),
hours=step_kwargs.get("hours") or step_kwargs.get("hrs") or step_kwargs.get("h"),
tag=normalize_tag(step_kwargs.get("tag")),
desc=step_kwargs.get("desc"),
klass=step_kwargs.get("class") or step_kwargs.get("klass"),
)
current_lane.steps.append(step)
continue
raise ValueError(f"Unrecognized line: {line}")
return Doc(title=title, lanes=lanes, css_vars=css_vars)
def normalize_tag(tag: Optional[str]) -> Optional[str]:
if not tag:
return None
t = tag.lower().strip()
if t in ("req", "required"):
return "req"
if t in ("rec", "recommended"):
return "rec"
if t in ("none", "na", "n/a", "optional"):
return None
return t
# ---------------------- HTML rendering ----------------------
BASE_CSS = r"""
:root{
--ink:#0f172a;
--reqBorder:#2e7d32; --reqFill:#eef7ef;
--recBorder:#8a8a8a; --recFill:#ffffff;
--doneBorder:#9ca3af; --doneInk:#475569;
--modeCol:180px; --gap:12px;
--step:260px; --arrow:34px; --done:110px;
}
html,body{margin:0;background:#f6f7fb;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:var(--ink)}
.wrap{margin:24px auto 48px;padding:0 16px}
h1{font-size:22px;margin:0 0 8px}
.legend{display:flex;gap:18px;align-items:center;font-size:14px;margin:6px 0 24px}
.tag{display:inline-block;padding:2px 8px;border-radius:999px;border:1.5px solid var(--reqBorder);background:var(--reqFill);font-size:12px}
.tag.rec{border-color:var(--recBorder);background:var(--recFill);border-style:dashed}
.grid{display:flex;flex-direction:column;gap:18px}
.lane{display:grid;grid-template-columns:var(--modeCol) 1fr;gap:var(--gap);align-items:center;background:#ffffffcc;padding:12px;border-radius:12px}
.mode{font-weight:700;text-align:center;background:#fff;padding:16px 10px}
.flow{display:grid;align-items:center;gap:8px;padding:8px 0;}
.header {grid-column: span 4; }
.step{border-radius:10px;padding:10px 12px;border:2px solid var(--reqBorder);background:var(--reqFill);min-height:64px}
.step .title{font-weight:700}
.step .sub{font-size:12px;opacity:.8}
.step.rec{border-color:var(--recBorder);border-style:dashed;background:var(--recFill)}
.slot{}
.arrow{font-size:22px;line-height:1;text-align:center}
.arrow.blank{color:transparent}
.done{justify-self:start;border-radius:999px;border:2px dashed var(--doneBorder);padding:10px 14px;color:var(--doneInk);background:#fff;text-align:center}
@media (max-width:900px){
.lane{grid-template-columns:1fr}
.mode{order:-1}
.flow{grid-template-columns:1fr; background:none}
.arrow{display:none}
}
"""
def format_sub(step: Step) -> str:
if step.desc:
core = html.escape(step.desc)
else:
bits = [html.escape(step.label)]
wh = []
if step.weeks:
wh.append(f"{html.escape(str(step.weeks))} weeks")
if step.hours:
wh.append(f"~{html.escape(str(step.hours))} hrs")
if wh:
bits.append(" · " + " (".join([wh[0], " ".join(wh[1:])]) + ")" if len(wh)>1 else " · " + wh[0])
# Actually, the original used " · 2 weeks (~20 hrs)"
# Let's just do that directly:
if step.weeks and step.hours:
bits[-1] = f" · {html.escape(str(step.weeks))} weeks (~{html.escape(str(step.hours))} hrs)"
# Combine
core = "".join(bits)
# Tag
if step.tag == "req":
tag_html = '<span class="tag">Required</span>'
elif step.tag == "rec":
tag_html = '<span class="tag rec">Recommended</span>'
else:
tag_html = ""
if tag_html:
return f'{core} · {tag_html}'
return core
def render_html(doc: Doc) -> str:
max_steps = max((len(l.steps) for l in doc.lanes), default=1)
# grid-template-columns: repeat(max_steps, var(--step) var(--arrow)) var(--done)
pairs = " ".join(["var(--step) var(--arrow)"] * max_steps) + " var(--done)"
css_vars_block = ""
if doc.css_vars:
css_vars_block = ":root{\n" + "\n".join([f" {k}: {v};" for k,v in doc.css_vars.items()]) + "\n}\n"
html_parts = []
html_parts.append("<!DOCTYPE html><html><head><meta charset='utf-8'>")
html_parts.append("<meta name='viewport' content='width=device-width, initial-scale=1'>")
html_parts.append("<title>" + html.escape(doc.title) + "</title>")
html_parts.append("<style>")
html_parts.append(BASE_CSS)
if css_vars_block:
html_parts.append(css_vars_block)
html_parts.append(f".flow{{grid-template-columns:{pairs};}}")
html_parts.append("</style></head><body>")
html_parts.append("<div class='wrap'><div class='grid'>")
# Header/Title lane
html_parts.append("<div class='lane'><div class='mode'>&nbsp;</div><div class='flow'><div class='header'><h1>")
html_parts.append(html.escape(doc.title))
html_parts.append("</h1></div></div></div>")
for lane in doc.lanes:
html_parts.append("<div class='lane'>")
html_parts.append(f"<div class='mode'>{html.escape(lane.name)}</div>")
html_parts.append("<div class='flow'>")
for idx, step in enumerate(lane.steps):
cls = "step"
if step.tag == "rec":
cls += " rec"
if step.klass:
cls += " " + html.escape(step.klass)
html_parts.append(f"<div class='{cls}'>")
html_parts.append(f"<div class='title'>{html.escape(step.code)}</div>")
html_parts.append(f"<div class='sub'>{format_sub(step)}</div>")
html_parts.append("</div>") # step
# arrow after every step unless it's the last visible step
html_parts.append("<div class='arrow'>→</div>")
# Fill remaining slots (if any)
for _ in range(max_steps - len(lane.steps)):
html_parts.append("<div class='slot'></div>")
html_parts.append("<div class='arrow blank'>→</div>")
# Done bubble
html_parts.append("<div class='done'>Done</div>")
html_parts.append("</div></div>") # flow + lane
html_parts.append("</div></div></body></html>")
return "".join(html_parts)
spec_text = '''
TITLE: Online Teaching Requirements and Recommendations
# Optional CSS overrides
VAR: --step=180px; --modeCol=180px
LANE: In Person (with Canvas)
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=rec
LANE: Online
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=req
STEP: GOTT 2 | Introduction to Asynchronous Online Teaching and Learning | weeks=4; hours=40; tag=req
LANE: Hybrid
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=req
STEP: GOTT 2 | Introduction to Asynchronous Online Teaching and Learning | weeks=4; hours=40; tag=req
STEP: GOTT 5 | Essentials of Blended Learning | weeks=2; hours=20; tag=rec
LANE: Online Live
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=req
STEP: GOTT 2 | Introduction to Asynchronous Online Teaching and Learning | weeks=4; hours=40; tag=req
STEP: GOTT 6 | Introduction to Live Online Teaching and Learning | weeks=2; hours=20; tag=rec
LANE: HyFlex
STEP: GOTT 1 | Intro to Online Teaching with Canvas | weeks=2; hours=20; tag=req
STEP: GOTT 2 | Introduction to Asynchronous Online Teaching and Learning | weeks=4; hours=40; tag=req
STEP: GOTT 6 | Introduction to Live Online Teaching and Learning | weeks=2; hours=20; tag=rec
# You can override the subline using desc=
STEP: HyFlex Tech Training | ~1 hour on-site | desc=~1 hour on-site; tag=rec
'''
doc = parse_spec(spec_text)
out_html = render_html(doc)
Path('cache/flow.html').write_text(out_html, encoding="utf-8")
print(f"Wrote cache/flow.html")
@ -1256,7 +1549,8 @@ if __name__ == "__main__":
20: ['create support page', create_support_page], 20: ['create support page', create_support_page],
21: ['add support page to all shells in semester', add_support_page_full_semester], 21: ['add support page to all shells in semester', add_support_page_full_semester],
22: ['fetch all modules / items', check_modules_for_old_orientation], 22: ['fetch all modules / items', check_modules_for_old_orientation],
30: ['media fetch', media_testing] 30: ['media fetch', media_testing],
40: ['flow grid', flowgrid],
} }
if len(sys.argv) > 1 and re.search(r'^\d+',sys.argv[1]): if len(sys.argv) > 1 and re.search(r'^\d+',sys.argv[1]):

View File

@ -183,8 +183,8 @@ def convert_to_pdf(name1, name2):
# Build (docx/pdf) certificates for gott graduates # Build (docx/pdf) certificates for gott graduates
def certificates_gott_build(): def certificates_gott_build():
course = "gott_1_su25" course = "gott_1_fa25"
coursedate = "Summer 2025" coursedate = "Fall 2025"
certificate = "gott 1 template.docx" certificate = "gott 1 template.docx"
#course = "gott_4_su25" #course = "gott_4_su25"

View File

@ -2234,19 +2234,21 @@ def training_find_goos():
print() print()
def cross_ref_training(): def cross_ref_training():
from semesters import find_term
from openpyxl import Workbook, load_workbook from openpyxl import Workbook, load_workbook
from openpyxl.chart import BarChart, Series, Reference from openpyxl.chart import BarChart, Series, Reference
from openpyxl.styles import PatternFill, Border, Side, Alignment, Protection, Font, Fill from openpyxl.styles import PatternFill, Border, Side, Alignment, Protection, Font, Fill
wb = load_workbook("C:/Users/phowell/Downloads/GOTT_Completion_masterlist 2023 DEC.xlsx") wb = load_workbook("C:/Users/phowell/Downloads/GOTT_Completion_masterlist 2023 DEC.xlsx")
print(wb.sheetnames) print(wb.sheetnames)
term = "202570" term = find_term("fa25")
# Fetch from Canvas DB. Make sure its recently updated. # Fetch from Canvas DB. Make sure its recently updated.
# Also relies on schedule being in database. Run localcache2.courses_to_sched() # Also relies on schedule being in database. Run localcache2.courses_to_sched()
# OR localcache2.refresh_semester_schedule_db() # OR localcache2.refresh_semester_schedule_db()
#courses = all_2x_sem_courses_teachers('202550', '202570') # #courses = all_2x_sem_courses_teachers('202550', '202570') #
courses = all_sem_courses_teachers(term) courses = all_sem_courses_teachers(term['bannercode'])
# report for email # report for email
@ -2262,7 +2264,7 @@ def cross_ref_training():
if ask2.strip()=='y': RELOAD_SCHEDULE = 1 if ask2.strip()=='y': RELOAD_SCHEDULE = 1
if RELOAD_SCHEDULE: if RELOAD_SCHEDULE:
refresh_semester_schedule_db(term) refresh_semester_schedule_db(term['code'])
if RELOAD_TEACHERS: if RELOAD_TEACHERS:
teacherRolesUpdateCache() teacherRolesUpdateCache()