# phase/Student_view/teacherlink.py
import os
import streamlit as st
from utils import db as dbapi
import utils.api as api # <-- backend Space client
USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" # DB only when DISABLE_DB=0
def load_css(file_name: str):
try:
with open(file_name, "r", encoding="utf-8") as f:
st.markdown(f"", unsafe_allow_html=True)
except FileNotFoundError:
pass
def _progress_0_1(v):
try:
f = float(v)
except Exception:
return 0.0
# accept 0..1 or 0..100
return max(0.0, min(1.0, f if f <= 1.0 else f / 100.0))
# --- Small wrappers to switch between DB and Backend ---
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):
# optional nudge so the bar is not 0
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)
# --- UI ---
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"
)
# custom button style
st.markdown(
"""
""",
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
# one card per class
for c in classes:
class_id = c["class_id"]
counts = _class_content_counts(class_id) # lessons/quizzes count
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}%"
)
# top metrics
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 class
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}")
# Assignments for THIS class with THIS student's progress
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
# if backend returns explicit progress % or 0..1, keep it sane:
pct = r.get("progress")
if pct is None:
# fallback: estimate from position
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")
# pass quiz & assignment to the Quiz page
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("---")