other changes
This commit is contained in:
parent
e9b10767d6
commit
20e2b0e3d7
298
content.py
298
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: <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'> </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]):
|
||||
|
|
|
|||
4
tasks.py
4
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"
|
||||
|
|
|
|||
8
users.py
8
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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue