From e8c4bb119553337e260182c0558931e2373d41a6 Mon Sep 17 00:00:00 2001 From: Zaid Ahmad <109442753+zaidahmad16@users.noreply.github.com> Date: Tue, 26 May 2026 15:02:25 -0400 Subject: [PATCH] feat: add professor info and RMP ratings to course panel When a student clicks a course node, the panel now shows which professors are teaching it in Fall 2026 and Winter 2027, along with their RateMyProfessors rating, difficulty, and "would take again" percentage pulled live via ratemyprofessors-client. - backend/timetable_scraper.py: scrapes Carleton Central for both terms and populates a new course_instructors table in Neon - GET /courses/{code}/instructors: new endpoint returning instructors grouped by term - frontend/app/api/rmp/route.js: Next.js API route that queries RMP for a professor by name, scoped to Carleton (school ID 1420), with in-memory caching - CoursePanel.jsx: shows instructor cards with color-coded rating, difficulty, take-again %, and a link to their RMP profile --- backend/main.py | 29 +++ backend/timetable_scraper.py | 192 ++++++++++++++++++++ frontend/app/api/rmp/route.js | 46 +++++ frontend/app/map/components/CoursePanel.jsx | 180 +++++++++++++++++- frontend/package-lock.json | 22 +++ frontend/package.json | 1 + 6 files changed, 468 insertions(+), 2 deletions(-) create mode 100644 backend/timetable_scraper.py create mode 100644 frontend/app/api/rmp/route.js diff --git a/backend/main.py b/backend/main.py index f827449..b15c237 100644 --- a/backend/main.py +++ b/backend/main.py @@ -405,6 +405,35 @@ def get_courses_batch(request: Request, codes: List[str]): finally: conn.close() +@app.get("/courses/{course_code}/instructors") +@limiter.limit("60/minute") +def get_course_instructors(request: Request, course_code: str): + code = course_code.upper().replace("-", " ") + if not COURSE_CODE_RE.match(code): + raise HTTPException(status_code=400, detail="Invalid course code") + conn = get_connection() + try: + cur = conn.cursor() + cur.execute(""" + SELECT DISTINCT term, instructor_name + FROM course_instructors + WHERE course_code = %s + ORDER BY term, instructor_name + """, (code,)) + rows = cur.fetchall() + cur.close() + # Group by term + terms: dict = {} + for term, name in rows: + terms.setdefault(term, []).append(name) + return [{"term": t, "instructors": names} for t, names in terms.items()] + except Exception as e: + logger.error("GET /courses/%s/instructors failed: %s", course_code, e, exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") + finally: + conn.close() + + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/timetable_scraper.py b/backend/timetable_scraper.py new file mode 100644 index 0000000..83779c0 --- /dev/null +++ b/backend/timetable_scraper.py @@ -0,0 +1,192 @@ +""" +Scrapes Carleton Central timetable for instructor names per course per term. +Populates the course_instructors table. + +Usage: python3 timetable_scraper.py +""" + +import os, re, time, requests +from psycopg2.extras import execute_values +from db import get_connection +from dotenv import load_dotenv + +load_dotenv() + +BASE_URL = "https://central.carleton.ca/prod" +TERMS = [ + ("202630", "Fall 2026"), + ("202710", "Winter 2027"), +] + +# All undergrad subject codes we care about (pulled from the search page) +def get_subject_codes(session: requests.Session, term_code: str, session_id: str) -> list[str]: + resp = session.post(f"{BASE_URL}/bwysched.p_search_fields", data={ + "wsea_code": "EXT", + "term_code": term_code, + "session_id": session_id, + }) + return re.findall(r'