# phase/Teacher_view/studentlist.py import os import streamlit as st from utils import db as dbapi import utils.api as api # backend Space client # Use local DB only when DISABLE_DB != "1" USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" # ---------- tiny helpers ---------- def _avatar(name: str) -> str: return "๐Ÿง‘โ€๐ŸŽ“" if hash(name) % 2 else "๐Ÿ‘ฉโ€๐ŸŽ“" def _avg_pct_from_row(r) -> int: """ Accepts either: - r['avg_pct'] in [0, 100] - r['avg_score'] in [0, 1] or [0, 100] Returns an int 0..100. """ v = r.get("avg_pct", r.get("avg_score", 0)) or 0 try: f = float(v) if f <= 1.0: # treat as 0..1 f *= 100.0 return max(0, min(100, int(round(f)))) except Exception: return 0 def _level_from_xp(total_xp: int) -> int: try: xp = int(total_xp or 0) except Exception: xp = 0 return 1 + xp // 500 def _report_text(r, level, avg_pct): return ( "STUDENT PROGRESS REPORT\n" "======================\n" f"Student: {r.get('name','')}\n" f"Email: {r.get('email','')}\n" f"Joined: {str(r.get('joined_at',''))[:10]}\n\n" "PROGRESS OVERVIEW\n" "-----------------\n" f"Lessons Completed: {int(r.get('lessons_completed') or 0)}/{int(r.get('total_assigned_lessons') or 0)}\n" f"Average Quiz Score: {avg_pct}%\n" f"Total XP: {int(r.get('total_xp') or 0)}\n" f"Current Level: {level}\n" f"Study Streak: {int(r.get('streak_days') or 0)} days\n" ) ROW_CSS = """ """ # ---------- data access (DB or Backend) ---------- @st.cache_data(show_spinner=False, ttl=30) def _list_classes_by_teacher(teacher_id: int): if USE_LOCAL_DB and hasattr(dbapi, "list_classes_by_teacher"): return dbapi.list_classes_by_teacher(teacher_id) or [] try: return api.list_classes_by_teacher(teacher_id) or [] except Exception: return [] @st.cache_data(show_spinner=False, ttl=30) def _get_class(class_id: int): if USE_LOCAL_DB and hasattr(dbapi, "get_class"): return dbapi.get_class(class_id) or {} try: return api.get_class(class_id) or {} except Exception: return {} @st.cache_data(show_spinner=False, ttl=30) def _class_student_metrics(class_id: int): if USE_LOCAL_DB and hasattr(dbapi, "class_student_metrics"): return dbapi.class_student_metrics(class_id) or [] try: return api.class_student_metrics(class_id) or [] except Exception: return [] @st.cache_data(show_spinner=False, ttl=30) def _list_assignments_for_student(student_id: int): if USE_LOCAL_DB and hasattr(dbapi, "list_assignments_for_student"): return dbapi.list_assignments_for_student(student_id) or [] try: return api.list_assignments_for_student(student_id) or [] except Exception: return [] # ---------- page ---------- def show_page(): st.title("๐ŸŽ“ Student Management") st.caption("Monitor and manage your students' progress") st.markdown(ROW_CSS, unsafe_allow_html=True) teacher = st.session_state.get("user") if not teacher: st.error("Please log in.") return teacher_id = teacher["user_id"] classes = _list_classes_by_teacher(teacher_id) if not classes: st.info("No classes yet. Create one in Classroom Management.") return # class selector idx = st.selectbox( "Choose a class", list(range(len(classes))), index=0, format_func=lambda i: f"{classes[i].get('name','(unnamed)')}" ) selected = classes[idx] class_id = selected.get("class_id") or selected.get("id") # be tolerant to backend naming if class_id is None: st.error("Selected class is missing an ID.") return code_row = _get_class(class_id) # get students before drawing chips rows = _class_student_metrics(class_id) # code + student chip row chip1, chip2 = st.columns([1, 1]) with chip1: st.markdown( f'
Code: {code_row.get("code","")}
', unsafe_allow_html=True ) with chip2: st.markdown( f'
๐Ÿ‘ฅ {len(rows)} Students
', unsafe_allow_html=True ) st.markdown("---") # search line query = st.text_input( "Search students by name or email", placeholder="Type a name or email..." ).strip().lower() if query: rows = [ r for r in rows if query in (r.get("name","").lower()) or query in (r.get("email","").lower()) ] # student rows for r in rows: name = r.get("name", "Unknown") email = r.get("email", "") joined = str(r.get("joined_at", ""))[:10] total_xp = int(r.get("total_xp") or 0) level = _level_from_xp(total_xp) lessons_completed = int(r.get("lessons_completed") or 0) total_assigned = int(r.get("total_assigned_lessons") or 0) avg_pct = _avg_pct_from_row(r) streak = int(r.get("streak_days") or 0) student_id = r.get("student_id") or r.get("id") with st.container(): st.markdown('
', unsafe_allow_html=True) # top bar: avatar + name/email + right metrics a, b, c = st.columns([0.7, 4, 3]) with a: st.markdown(f"### {_avatar(name)}") with b: st.markdown(f'
{name}
', unsafe_allow_html=True) st.markdown(f'
{email} ยท Joined {joined}
', unsafe_allow_html=True) with c: st.markdown( '
' f'
{level}
Level
' f'
{avg_pct}%
Avg Score
' f'
{streak}
Streak
' "
", unsafe_allow_html=True ) # progress bar st.caption("Overall Progress") frac = (lessons_completed / total_assigned) if total_assigned > 0 else 0.0 st.progress(min(1.0, frac)) st.caption(f"{lessons_completed}/{total_assigned} lessons") # actions row d1, d2, spacer = st.columns([2, 1.3, 5]) with d1: with st.popover("๐Ÿ‘๏ธ View Details"): if student_id is not None: items = _list_assignments_for_student(int(student_id)) else: items = [] if items: for it in items[:25]: tag = " + Quiz" if it.get("quiz_id") else "" st.markdown( f"- **{it.get('title','Untitled')}** ยท {it.get('subject','General')} ยท " f"{it.get('level','')} {tag} ยท Status: {it.get('status','unknown')}" ) else: st.info("No assignments yet.") with d2: rep = _report_text(r, level, avg_pct) st.download_button( "โฌ‡๏ธ Export", data=rep, file_name=f"{str(name).replace(' ','_')}_report.txt", mime="text/plain", key=f"dl_{student_id or name}" ) st.markdown('
', unsafe_allow_html=True)