From 20e2b0e3d7648175a9eb708fcc19c7d8e97adf56 Mon Sep 17 00:00:00 2001 From: Peter Howell Date: Fri, 29 Aug 2025 12:25:19 -0700 Subject: [PATCH] other changes --- content.py | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++++- tasks.py | 4 +- users.py | 8 +- 3 files changed, 303 insertions(+), 7 deletions(-) diff --git a/content.py b/content.py index 436b454..9332113 100644 --- a/content.py +++ b/content.py @@ -1,4 +1,4 @@ - +from __future__ import annotations #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: + 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 = 'Required' + elif step.tag == "rec": + tag_html = 'Recommended' + 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("") + html_parts.append("") + html_parts.append("" + html.escape(doc.title) + "") + html_parts.append("") + html_parts.append("
") + + # Header/Title lane + html_parts.append("
 

") + html_parts.append(html.escape(doc.title)) + html_parts.append("

") + + for lane in doc.lanes: + html_parts.append("
") + html_parts.append(f"
{html.escape(lane.name)}
") + html_parts.append("
") + 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"
") + html_parts.append(f"
{html.escape(step.code)}
") + html_parts.append(f"
{format_sub(step)}
") + html_parts.append("
") # step + # arrow after every step unless it's the last visible step + html_parts.append("
") + # Fill remaining slots (if any) + for _ in range(max_steps - len(lane.steps)): + html_parts.append("
") + html_parts.append("
") + + # Done bubble + html_parts.append("
Done
") + html_parts.append("
") # flow + lane + + html_parts.append("
") + 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], 21: ['add support page to all shells in semester', add_support_page_full_semester], 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]): diff --git a/tasks.py b/tasks.py index 41d5fb3..de222fb 100644 --- a/tasks.py +++ b/tasks.py @@ -183,8 +183,8 @@ def convert_to_pdf(name1, name2): # Build (docx/pdf) certificates for gott graduates def certificates_gott_build(): - course = "gott_1_su25" - coursedate = "Summer 2025" + course = "gott_1_fa25" + coursedate = "Fall 2025" certificate = "gott 1 template.docx" #course = "gott_4_su25" diff --git a/users.py b/users.py index b634ddc..bba2e03 100644 --- a/users.py +++ b/users.py @@ -2234,19 +2234,21 @@ def training_find_goos(): print() def cross_ref_training(): + from semesters import find_term from openpyxl import Workbook, load_workbook from openpyxl.chart import BarChart, Series, Reference from openpyxl.styles import PatternFill, Border, Side, Alignment, Protection, Font, Fill wb = load_workbook("C:/Users/phowell/Downloads/GOTT_Completion_masterlist 2023 DEC.xlsx") print(wb.sheetnames) - term = "202570" + term = find_term("fa25") + # Fetch from Canvas DB. Make sure its recently updated. # Also relies on schedule being in database. Run localcache2.courses_to_sched() # OR localcache2.refresh_semester_schedule_db() #courses = all_2x_sem_courses_teachers('202550', '202570') # - courses = all_sem_courses_teachers(term) + courses = all_sem_courses_teachers(term['bannercode']) # report for email @@ -2262,7 +2264,7 @@ def cross_ref_training(): if ask2.strip()=='y': RELOAD_SCHEDULE = 1 if RELOAD_SCHEDULE: - refresh_semester_schedule_db(term) + refresh_semester_schedule_db(term['code']) if RELOAD_TEACHERS: teacherRolesUpdateCache()