FInFront / phase /Teacher_view /studentlist.py
lanna_lalala;-
added folders
0aa6283
# 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 = """
<style>
.sm-chip{display:inline-block;padding:4px 10px;border-radius:999px;background:#eef7f1;color:#0b8f5d;font-weight:600;font-size:.80rem;margin-left:8px}
.sm-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid #e6e6e6;border-radius:8px;background:#fff}
.sm-row{border:1px solid #eee;border-radius:12px;padding:16px 16px;margin:10px 0;background:#fff}
.sm-row:hover{box-shadow:0 2px 10px rgba(0,0,0,.04)}
.sm-right{display:flex;gap:16px;align-items:center;justify-content:flex-end}
.sm-metric{min-width:90px;text-align:right}
.sm-metric .label{color:#777;font-size:.75rem}
.sm-metric .value{font-weight:700;font-size:1.1rem}
.sm-name{font-size:1.05rem;font-weight:700}
.sm-sub{color:#6c6c6c;font-size:.85rem}
</style>
"""
# ---------- 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'<div class="sm-chip">Code: {code_row.get("code","")}</div>',
unsafe_allow_html=True
)
with chip2:
st.markdown(
f'<div class="sm-chip">πŸ‘₯ {len(rows)} Students</div>',
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('<div class="sm-row">', 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'<div class="sm-name">{name}</div>', unsafe_allow_html=True)
st.markdown(f'<div class="sm-sub">{email} Β· Joined {joined}</div>', unsafe_allow_html=True)
with c:
st.markdown(
'<div class="sm-right">'
f'<div class="sm-metric"><div class="value">{level}</div><div class="label">Level</div></div>'
f'<div class="sm-metric"><div class="value">{avg_pct}%</div><div class="label">Avg Score</div></div>'
f'<div class="sm-metric"><div class="value">{streak}</div><div class="label">Streak</div></div>'
"</div>",
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('</div>', unsafe_allow_html=True)