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() )
@ -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],
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]):

View File

@ -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"

View File

@ -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()