| |
| import os |
| import streamlit as st |
| from utils import db as dbapi |
| import utils.api as api |
|
|
| USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" |
|
|
| def load_css(file_name: str): |
| try: |
| with open(file_name, "r", encoding="utf-8") as f: |
| st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True) |
| except FileNotFoundError: |
| pass |
|
|
| def _progress_0_1(v): |
| try: |
| f = float(v) |
| except Exception: |
| return 0.0 |
| |
| return max(0.0, min(1.0, f if f <= 1.0 else f / 100.0)) |
|
|
| |
|
|
| def _join_class_by_code(student_id: int, code: str): |
| if USE_LOCAL_DB and hasattr(dbapi, "join_class_by_code"): |
| return dbapi.join_class_by_code(student_id, code) |
| return api.join_class_by_code(student_id, code) |
|
|
| def _list_classes_for_student(student_id: int): |
| if USE_LOCAL_DB and hasattr(dbapi, "list_classes_for_student"): |
| return dbapi.list_classes_for_student(student_id) |
| try: |
| return api.list_classes_for_student(student_id) |
| except Exception: |
| return [] |
|
|
| def _class_content_counts(class_id: int): |
| if USE_LOCAL_DB and hasattr(dbapi, "class_content_counts"): |
| return dbapi.class_content_counts(class_id) |
| try: |
| return api.class_content_counts(class_id) |
| except Exception: |
| return {"lessons": 0, "quizzes": 0} |
|
|
| def _student_class_progress(student_id: int, class_id: int): |
| if USE_LOCAL_DB and hasattr(dbapi, "student_class_progress"): |
| return dbapi.student_class_progress(student_id, class_id) |
| try: |
| return api.student_class_progress(student_id, class_id) |
| except Exception: |
| return { |
| "overall_progress": 0, |
| "lessons_completed": 0, |
| "total_assigned_lessons": 0, |
| "avg_score": 0, |
| } |
|
|
| def _leave_class(student_id: int, class_id: int): |
| if USE_LOCAL_DB and hasattr(dbapi, "leave_class"): |
| return dbapi.leave_class(student_id, class_id) |
| return api.leave_class(student_id, class_id) |
|
|
| def _student_assignments_for_class(student_id: int, class_id: int): |
| if USE_LOCAL_DB and hasattr(dbapi, "student_assignments_for_class"): |
| return dbapi.student_assignments_for_class(student_id, class_id) |
| try: |
| return api.student_assignments_for_class(student_id, class_id) |
| except Exception: |
| return [] |
|
|
| def _mark_assignment_started(student_id: int, assignment_id: int): |
| if USE_LOCAL_DB and hasattr(dbapi, "mark_assignment_started"): |
| return dbapi.mark_assignment_started(student_id, assignment_id) |
| return api.mark_assignment_started(student_id, assignment_id) |
|
|
| def _set_assignment_progress(student_id: int, assignment_id: int, current_pos: int = 1, progress: float = 0.05): |
| |
| if USE_LOCAL_DB and hasattr(dbapi, "set_assignment_progress"): |
| return dbapi.set_assignment_progress(student_id, assignment_id, current_pos, progress) |
| return api.set_assignment_progress(student_id, assignment_id, current_pos, progress) |
|
|
|
|
| |
|
|
| def show_code(): |
| load_css(os.path.join("assets", "styles.css")) |
|
|
| if "user" not in st.session_state or not st.session_state.user: |
| st.error("Please log in as a student.") |
| return |
| if st.session_state.user["role"] != "Student": |
| st.error("This page is for students.") |
| return |
|
|
| student_id = st.session_state.user["user_id"] |
| st.markdown("## π₯ Join a Class") |
| st.caption("Enter class code from your teacher") |
|
|
| raw = st.text_input( |
| label="Class Code", |
| placeholder="e.g. FIN5A2024", |
| key="class_code_input", |
| label_visibility="collapsed" |
| ) |
|
|
| |
| st.markdown( |
| """ |
| <style> |
| .stButton>button#join_class_btn { |
| background-color: #28a745; /* Bootstrap green */ |
| color: white; |
| border-radius: 5px; |
| padding: 10px 16px; |
| font-weight: 600; |
| } |
| .stButton>button#join_class_btn:hover { |
| background-color: #218838; |
| color: white; |
| } |
| </style> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| if st.button("Join Class", key="join_class_btn"): |
| code = (raw or "").strip().upper() |
| if not code: |
| st.error("Enter a class code.") |
| else: |
| try: |
| _join_class_by_code(student_id, code) |
| st.success("π Joined the class!") |
| st.rerun() |
| except ValueError as e: |
| st.error(str(e)) |
| except Exception as e: |
| st.error(f"Could not join class: {e}") |
|
|
| st.markdown("---") |
| st.markdown("## Your Classes") |
|
|
| classes = _list_classes_for_student(student_id) |
| if not classes: |
| st.info("You havenβt joined any classes yet. Ask your teacher for a class code.") |
| return |
|
|
| |
| for c in classes: |
| class_id = c["class_id"] |
| counts = _class_content_counts(class_id) |
| prog = _student_class_progress(student_id, class_id) |
|
|
| st.markdown(f"### {c.get('name', 'Untitled Class')}") |
| st.caption( |
| f"Teacher: {c.get('teacher_name','β')} β’ " |
| f"Code: {c.get('code','β')} β’ " |
| f"Joined: {str(c.get('joined_at',''))[:10] if c.get('joined_at') else 'β'}" |
| ) |
|
|
| st.progress(_progress_0_1(prog.get("overall_progress", 0))) |
| avg_pct = int(round(100 * _progress_0_1(prog.get("avg_score", 0)))) |
| st.caption( |
| f"{prog.get('lessons_completed', 0)}/{prog.get('total_assigned_lessons', 0)} lessons completed β’ " |
| f"Avg quiz: {avg_pct}%" |
| ) |
|
|
| |
| m1, m2, m3, m4 = st.columns(4) |
| m1.metric("Lessons", counts.get("lessons", 0)) |
| m2.metric("Quizzes", counts.get("quizzes", 0)) |
| m3.metric("Overall", f"{int(round(100 * _progress_0_1(prog.get('overall_progress', 0))))}%") |
| m4.metric("Avg Quiz", f"{avg_pct}%") |
|
|
| |
| leave_col, _ = st.columns([1, 3]) |
| with leave_col: |
| if st.button("πͺ Leave Class", key=f"leave_{class_id}"): |
| try: |
| _leave_class(student_id, class_id) |
| st.toast("Left class.", icon="π") |
| st.rerun() |
| except Exception as e: |
| st.error(f"Could not leave class: {e}") |
|
|
| |
| st.markdown("#### Teacher Lessons & Quizzes") |
| rows = _student_assignments_for_class(student_id, class_id) |
| if not rows: |
| st.info("No assignments yet.") |
| else: |
| lessons_tab, quizzes_tab = st.tabs(["π Lessons", "π Quizzes"]) |
|
|
| with lessons_tab: |
| for r in rows: |
| if r.get("lesson_id") is None: |
| continue |
|
|
| status = r.get("status") or "not_started" |
| pos = r.get("current_pos") or 0 |
| |
| pct = r.get("progress") |
| if pct is None: |
| |
| pct = 1.0 if status == "completed" else min(0.95, float(pos or 0) * 0.1) |
|
|
| st.subheader(r.get("title", "Untitled")) |
| due = r.get("due_at") |
| due_txt = f"Due: {str(due)[:10]}" if due else "β" |
| st.caption(f"{r.get('subject','General')} β’ {r.get('level','Beginner')} β’ {due_txt}") |
| st.progress(_progress_0_1(pct)) |
|
|
| c1, c2 = st.columns(2) |
| with c1: |
| if st.button("βΆοΈ Start Lesson", key=f"start_lesson_{r.get('assignment_id')}"): |
| lesson_id = int(r.get("lesson_id")) |
| assignment_id = int(r.get("assignment_id")) |
|
|
| try: |
| _mark_assignment_started(student_id, assignment_id) |
| _set_assignment_progress(student_id, assignment_id, current_pos=1, progress=0.05) |
| except Exception as e: |
| st.warning(f"Could not mark as started yet. Continuing anyway. {e}") |
|
|
| st.session_state.selected_lesson = lesson_id |
| st.session_state.selected_assignment = assignment_id |
| st.session_state.lesson_route = { |
| "source": "teacher", |
| "lesson_id": lesson_id, |
| "assignment_id": assignment_id, |
| } |
| st.session_state.current_page = "Lessons" |
| st.rerun() |
| with c2: |
| st.write(f"Status: **{status}**") |
|
|
| with quizzes_tab: |
| any_quiz = False |
| for r in rows: |
| quiz_id = r.get("quiz_id") |
| if not quiz_id: |
| continue |
| any_quiz = True |
|
|
| st.subheader(r.get("title", "Untitled")) |
| score, total = r.get("score"), r.get("total") |
| if score is not None and total: |
| try: |
| pct = int(round(100 * float(score) / float(total))) |
| st.caption(f"Last score: {pct}%") |
| except Exception: |
| st.caption("Last score: β") |
| else: |
| st.caption("No submission yet") |
|
|
| |
| if st.button("π Start Quiz", key=f"start_quiz_{class_id}_{quiz_id}"): |
| st.session_state.selected_quiz = quiz_id |
| st.session_state.current_assignment = r.get("assignment_id") |
| st.session_state.current_page = "Quiz" |
| st.rerun() |
|
|
| if not any_quiz: |
| st.info("No quizzes yet for this class.") |
|
|
| st.markdown("---") |
|
|