Spaces:
Sleeping
Sleeping
| import os | |
| import sqlite3 | |
| import pathlib | |
| from functools import wraps | |
| from flask import Flask, render_template, request, jsonify, redirect, url_for, session | |
| APP = Flask(__name__) | |
| # Use environment variable for secret key (required for Spaces) | |
| APP.secret_key = os.environ.get("SECRET_KEY", "dev-minerva-lite") | |
| ROOT = pathlib.Path(__file__).parent | |
| # Use /tmp for DB on Hugging Face Spaces | |
| if "SPACE_ID" in os.environ: | |
| DB_PATH = pathlib.Path("/tmp/minerva.sqlite3") | |
| APP.config["SESSION_COOKIE_SAMESITE"] = "None" | |
| APP.config["SESSION_COOKIE_SECURE"] = True | |
| else: | |
| DB_PATH = ROOT / "db" / "minerva.sqlite3" | |
| STEP_DESTINATIONS = { | |
| "home": {"endpoint": "registration_menu", "label": "Registration Menu"}, | |
| "search": {"endpoint": "search", "label": "Search Class Schedule"}, | |
| "add_drop": {"endpoint": "add_drop", "label": "Add or Drop Course Sections"}, | |
| "schedule": {"endpoint": "schedule", "label": "Student Schedule"}, | |
| "schedule_week": {"endpoint": "schedule_week", "label": "Weekly Schedule View"}, | |
| "accounts": {"endpoint": "accounts", "label": "Tuition Fee and Legal Status"}, | |
| } | |
| STEP_KEYS_BY_ENDPOINT = { | |
| "registration_menu": "home", | |
| "search": "search", | |
| "add_drop": "add_drop", | |
| "schedule": "schedule", | |
| "schedule_week": "schedule_week", | |
| "accounts": "accounts", | |
| } | |
| def require_term(step_key): | |
| def decorator(func): | |
| def wrapper(*args, **kwargs): | |
| if not session.get("term_confirmed"): | |
| return redirect(url_for("select_term", next=step_key)) | |
| return func(*args, **kwargs) | |
| return wrapper | |
| return decorator | |
| # Always ensure DB is seeded: if DB file is missing or 'courses' table is missing, run seed | |
| def ensure_db_seeded(): | |
| need_seed = False | |
| if not DB_PATH.exists(): | |
| need_seed = True | |
| else: | |
| try: | |
| con = sqlite3.connect(DB_PATH) | |
| cur = con.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='courses'") | |
| if not cur.fetchone(): | |
| need_seed = True | |
| con.close() | |
| except Exception: | |
| need_seed = True | |
| if need_seed: | |
| import seed # side effect: creates DB | |
| ensure_db_seeded() | |
| def q(sql, *params): | |
| con = sqlite3.connect(DB_PATH) | |
| con.row_factory = sqlite3.Row | |
| cur = con.execute(sql, params) | |
| rows = [dict(r) for r in cur.fetchall()] | |
| con.close() | |
| return rows | |
| def execute(sql, *params): | |
| with sqlite3.connect(DB_PATH) as con: | |
| con.execute(sql, params) | |
| con.commit() | |
| def status(): | |
| return jsonify({"service": "minerva-lite", "ok": True}) | |
| def registration_menu(): | |
| msg = request.args.get("msg", "") | |
| return render_template("registration_menu.html", msg=msg) | |
| def search(): | |
| subject = request.args.get("subject", "").strip().upper() | |
| number = request.args.get("number", "").strip() | |
| title = request.args.get("title", "").strip() | |
| campus = request.args.get("campus", "").strip() | |
| term = session.get("term", "Fall 2025") | |
| sql = "SELECT * FROM courses WHERE 1=1" | |
| args = [] | |
| if term: | |
| sql += " AND term = ?" | |
| args.append(term) | |
| if subject: | |
| sql += " AND subject = ?" | |
| args.append(subject) | |
| if number: | |
| sql += " AND number LIKE ?" | |
| args.append(f"%{number}%") | |
| if title: | |
| sql += " AND lower(title) LIKE ?" | |
| args.append(f"%{title.lower()}%") | |
| if campus: | |
| sql += " AND campus = ?" | |
| args.append(campus) | |
| sql += " ORDER BY subject, number" | |
| results = q(sql, *args) | |
| return render_template("search.html", results=results) | |
| def add_course(): | |
| crn = request.form.get("crn", "").strip() | |
| if not crn: | |
| return redirect(url_for("add_drop", msg="Enter a CRN.")) | |
| # Check if exists and open | |
| rows = q("SELECT * FROM courses WHERE crn=?", crn) | |
| if not rows: | |
| return redirect(url_for("add_drop", msg="Invalid CRN.")) | |
| course = rows[0] | |
| # Ensure CRN belongs to the currently selected term | |
| current_term = session.get("term", "Fall 2025") | |
| if course.get("term") != current_term: | |
| return redirect(url_for("add_drop", msg=f"CRN not in {current_term} term.")) | |
| reg = q("SELECT * FROM registrations WHERE student_id=1 AND crn=?", crn) | |
| if reg: | |
| return redirect(url_for("add_drop", msg="Already registered.")) | |
| if course["seats_taken"] >= course["seats_total"]: | |
| return redirect(url_for("add_drop", msg="Section full.")) | |
| execute("INSERT INTO registrations(student_id,crn) VALUES(1,?)", crn) | |
| execute( | |
| "UPDATE courses SET seats_taken = seats_taken + 1 WHERE crn=?", | |
| crn, | |
| ) | |
| return redirect(url_for("schedule", msg="Added CRN %s" % crn)) | |
| def drop_course(): | |
| crn = request.form.get("crn", "").strip() | |
| reg = q("SELECT * FROM registrations WHERE student_id=1 AND crn=?", crn) | |
| if not reg: | |
| return redirect(url_for("schedule", msg="Not registered.")) | |
| execute("DELETE FROM registrations WHERE student_id=1 AND crn=?", crn) | |
| execute( | |
| "UPDATE courses SET seats_taken = CASE WHEN seats_taken>0 THEN seats_taken-1 ELSE 0 END WHERE crn=?", | |
| crn, | |
| ) | |
| return redirect(url_for("schedule", msg="Dropped CRN %s" % crn)) | |
| def add_drop(): | |
| msg = request.args.get("msg", "") | |
| current_term = session.get("term", "Fall 2025") | |
| regs = q( | |
| "SELECT c.* FROM registrations r JOIN courses c ON r.crn=c.crn WHERE r.student_id=1 AND c.term=? ORDER BY c.subject, c.number", | |
| current_term, | |
| ) | |
| return render_template("add_drop.html", regs=regs, msg=msg) | |
| def schedule(): | |
| msg = request.args.get("msg", "") | |
| current_term = session.get("term", "Fall 2025") | |
| regs = q( | |
| "SELECT c.* FROM registrations r JOIN courses c ON r.crn=c.crn WHERE r.student_id=1 AND c.term=? ORDER BY c.subject, c.number", | |
| current_term, | |
| ) | |
| return render_template("schedule.html", regs=regs, msg=msg) | |
| def _parse_time_range(range_str): | |
| try: | |
| s, e = [t.strip() for t in range_str.split("-")] | |
| sh, sm = [int(x) for x in s.split(":")] | |
| eh, em = [int(x) for x in e.split(":")] | |
| return sh * 60 + sm, eh * 60 + em | |
| except Exception: | |
| return None, None | |
| def schedule_week(): | |
| # Build weekly event blocks for registered sections | |
| current_term = session.get("term", "Fall") | |
| regs = q( | |
| "SELECT c.* FROM registrations r JOIN courses c ON r.crn=c.crn WHERE r.student_id=1 AND c.term=?", | |
| current_term, | |
| ) | |
| start_day_min = 8 * 60 # 08:00 grid start | |
| end_day_min = 18 * 60 # 18:00 grid end | |
| span = end_day_min - start_day_min | |
| grid_px = 800 # matches CSS .grid-body height | |
| day_map = {"M": 1, "T": 2, "W": 3, "R": 4, "F": 5} | |
| events = [] | |
| for c in regs: | |
| smin, emin = _parse_time_range(c.get("time") or "") | |
| if smin is None: | |
| continue | |
| duration = max(emin - smin, 30) | |
| # pixel-perfect placement using the same 800px grid height | |
| top_px = max(int(round((smin - start_day_min) / span * grid_px)), 0) | |
| height_px = max(int(round(duration / span * grid_px)), 2) | |
| for ch in (c.get("days") or ""): # e.g., "MWF" or "TR" | |
| if ch in day_map: | |
| events.append({ | |
| "day_col": day_map[ch], | |
| "title": f"{c['subject']} {c['number']}", | |
| "subtitle": c["title"], | |
| "time": c["time"], | |
| "top_px": top_px, | |
| "height_px": height_px, | |
| "crn": c["crn"], | |
| }) | |
| # Time labels every hour for the left rail | |
| time_labels = [] | |
| for h in range(8, 18): | |
| time_labels.append(f"{h:02d}:00") | |
| return render_template( | |
| "schedule_week.html", | |
| events=events, | |
| time_labels=time_labels, | |
| ) | |
| def accounts(): | |
| return render_template("accounts.html") | |
| def set_term(): | |
| term = request.form.get("term", "").strip().title() | |
| if term not in ("Fall 2025", "Winter 2026", "Summer 2026"): | |
| return redirect(url_for("registration_menu", msg="Invalid term.")) | |
| session["term"] = term | |
| session["term_confirmed"] = True | |
| next_key = request.form.get("next", "").strip() | |
| if next_key in STEP_DESTINATIONS: | |
| endpoint = STEP_DESTINATIONS[next_key]["endpoint"] | |
| return redirect(url_for(endpoint)) | |
| # Redirect back to the referrer if possible | |
| ref = request.headers.get("Referer") | |
| return redirect(ref or url_for("registration_menu", msg=f"Term set to {term}")) | |
| def select_term(): | |
| next_key = request.args.get("next", "search").strip() | |
| destination = STEP_DESTINATIONS.get(next_key, STEP_DESTINATIONS["search"]) | |
| if request.args.get("reset"): | |
| session["term_confirmed"] = False | |
| return render_template( | |
| "select_term.html", | |
| next_key=next_key, | |
| destination_label=destination["label"], | |
| ) | |
| def inject_term(): | |
| current_step_key = STEP_KEYS_BY_ENDPOINT.get(request.endpoint, "") | |
| if request.endpoint == "select_term": | |
| current_step_key = request.args.get("next", current_step_key or "search") | |
| return { | |
| "current_term": session.get("term", "Fall 2025"), | |
| "terms": ["Fall 2025", "Winter 2026", "Summer 2026"], | |
| "term_confirmed": session.get("term_confirmed", False), | |
| "current_step_key": current_step_key, | |
| } | |
| def reset(): | |
| if DB_PATH.exists(): | |
| DB_PATH.unlink() | |
| import seed # re-seed | |
| return jsonify({"reset": True}) | |
| import os | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 8000)) | |
| APP.run(host="0.0.0.0", port=port) | |