FInFront / dashboards /teacher_db.py
lanna_lalala;-
added folders
0aa6283
# dashboards/teacher_db.py
import os
import io
import csv
import datetime
import streamlit as st
import plotly.graph_objects as go
from utils import db as dbapi
import utils.api as api # backend Space client
# If DISABLE_DB=1 (default), don't call MySQL at all
USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1"
def load_css(file_name):
try:
with open(file_name, 'r', encoding="utf-8") as f:
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
except FileNotFoundError:
st.warning("โš ๏ธ Stylesheet not found. Please ensure 'assets/styles.css' exists.")
def tile(icon, label, value):
return f"""
<div class="metric-card">
<div class="metric-icon">{icon}</div>
<div class="metric-value">{value}</div>
<div class="metric-label">{label}</div>
</div>
"""
def _level_from_xp(xp: int) -> int:
"""
Prefer backend/db helper if available, else simple fallback (every 500 XP = +1 level).
"""
try:
if USE_LOCAL_DB and hasattr(dbapi, "level_from_xp"):
return int(dbapi.level_from_xp(xp))
if hasattr(api, "level_from_xp"): # if you add the endpoint
return int(api.level_from_xp(int(xp)))
except Exception:
pass
xp = int(xp or 0)
return 1 + (xp // 500)
def _safe_get_tiles(teacher_id: int) -> dict:
if USE_LOCAL_DB and hasattr(dbapi, "teacher_tiles"):
return dbapi.teacher_tiles(teacher_id)
try:
return api.teacher_tiles(teacher_id)
except Exception:
return {
"total_students": 0, "class_avg": 0.0,
"lessons_created": 0, "active_students": 0
}
def _safe_list_classes(teacher_id: int) -> list:
if USE_LOCAL_DB and hasattr(dbapi, "list_classes_by_teacher"):
return dbapi.list_classes_by_teacher(teacher_id)
try:
return api.list_classes_by_teacher(teacher_id)
except Exception:
return []
def _safe_create_class(teacher_id: int, name: str) -> dict:
if USE_LOCAL_DB and hasattr(dbapi, "create_class"):
return dbapi.create_class(teacher_id, name)
return api.create_class(teacher_id, name)
def _safe_class_student_metrics(class_id: int) -> list:
if USE_LOCAL_DB and hasattr(dbapi, "class_student_metrics"):
return dbapi.class_student_metrics(class_id)
try:
return api.class_student_metrics(class_id)
except Exception:
return []
def _safe_weekly_activity(class_id: int) -> list:
if USE_LOCAL_DB and hasattr(dbapi, "class_weekly_activity"):
return dbapi.class_weekly_activity(class_id)
try:
return api.class_weekly_activity(class_id)
except Exception:
return []
def _safe_progress_overview(class_id: int) -> dict:
if USE_LOCAL_DB and hasattr(dbapi, "class_progress_overview"):
return dbapi.class_progress_overview(class_id)
try:
return api.class_progress_overview(class_id)
except Exception:
return {
"overall_progress": 0.0, "quiz_performance": 0.0,
"lessons_completed": 0, "class_xp": 0
}
def _safe_recent_activity(class_id: int, limit=6, days=30) -> list:
if USE_LOCAL_DB and hasattr(dbapi, "class_recent_activity"):
return dbapi.class_recent_activity(class_id, limit=limit, days=days)
try:
return api.class_recent_activity(class_id, limit=limit, days=days)
except Exception:
return []
def _safe_list_students(class_id: int) -> list:
if USE_LOCAL_DB and hasattr(dbapi, "list_students_in_class"):
return dbapi.list_students_in_class(class_id)
try:
return api.list_students_in_class(class_id)
except Exception:
return []
def show_teacher_dashboard():
css_path = os.path.join("assets", "styles.css")
load_css(css_path)
user = st.session_state.user
teacher_id = user["user_id"]
name = user["name"]
# ========== HEADER / HERO ==========
colH1, colH2 = st.columns([5, 2])
with colH1:
st.markdown(f"""
<div class="header-container">
<div class="header-content">
<div class="header-left">
<div class="header-title">Welcome back, Teacher {name}!</div>
<div class="header-subtitle">Managing your classrooms</div>
</div>
</div>
</div>
""", unsafe_allow_html=True)
with colH2:
with st.popover("โž• Create Classroom"):
new_class_name = st.text_input("Classroom Name", key="new_class_name")
if st.button("Create Classroom", key="create_classroom_btn"):
if new_class_name.strip():
try:
out = _safe_create_class(teacher_id, new_class_name.strip())
code = out.get("code") or out.get("class_code") or "โ€”"
st.success(f"Classroom created. Code: **{code}**")
except Exception as e:
st.error(f"Create failed: {e}")
# ========== TILES ==========
tiles = _safe_get_tiles(teacher_id)
c1,c2,c3,c4 = st.columns(4)
c1.markdown(tile("๐Ÿ‘ฅ","Total Students", tiles.get("total_students", 0)), unsafe_allow_html=True)
c2.markdown(tile("๐Ÿ“Š","Class Average", f"{int((tiles.get('class_avg') or 0)*100)}%"), unsafe_allow_html=True)
c3.markdown(tile("๐Ÿ“š","Lessons Created", tiles.get("lessons_created", 0)), unsafe_allow_html=True)
c4.markdown(tile("๐Ÿ“ˆ","Active Students", tiles.get("active_students", 0)), unsafe_allow_html=True)
# ========== CLASS PICKER ==========
classes = _safe_list_classes(teacher_id)
if not classes:
st.info("No classes yet. Create one above, then share the code with students.")
return
idx = st.selectbox(
"Choose a class",
list(range(len(classes))),
index=0,
format_func=lambda i: f"{classes[i].get('name','Class')} (Code: {classes[i].get('code','')})"
)
selected = classes[idx]
class_id = selected.get("class_id") or selected.get("id")
class_code = selected.get("code","")
# secondary hero controls
cTop1, cTop2, cTop3 = st.columns([2,1,1])
with cTop1:
st.button(f"Class Code: {class_code}", disabled=True)
with cTop2:
if st.button("๐Ÿ“‹ Copy Code"):
st.toast("Code copied. Paste it anywhere your heart desires.")
with cTop3:
rows = _safe_class_student_metrics(class_id)
if rows:
headers = []
for r in rows:
for k in r.keys():
if k not in headers:
headers.append(k)
buf = io.StringIO()
writer = csv.DictWriter(buf, fieldnames=headers)
writer.writeheader()
for r in rows:
writer.writerow(r)
st.download_button(
"๐Ÿ“ค Export Class Report",
data=buf.getvalue(),
file_name=f"class_{class_id}_report.csv",
mime="text/csv"
)
else:
st.button("๐Ÿ“ค Export Class Report", disabled=True)
# ========== TOP ROW: WEEKLY ACTIVITY + CLASS PROGRESS ==========
left, right = st.columns([3,2])
with left:
st.subheader("Weekly Activity")
st.caption("Student engagement throughout the week")
activity = _safe_weekly_activity(class_id)
if activity:
days, lessons, quizzes, games = [], [], [], []
for row in activity:
date_str = row.get("date")
try:
# Support ISO date or datetime
day = datetime.datetime.fromisoformat(str(date_str)).strftime("%a")
except Exception:
day = str(date_str)
days.append(day)
lessons.append(row.get("lessons",0))
quizzes.append(row.get("quizzes",0))
games.append(row.get("games",0))
fig = go.Figure(data=[
go.Bar(name="Lessons", x=days, y=lessons),
go.Bar(name="Quizzes", x=days, y=quizzes),
go.Bar(name="Games", x=days, y=games),
])
fig.update_layout(barmode="group", xaxis_title="Day", yaxis_title="Count")
st.plotly_chart(fig, use_container_width=True)
else:
st.info("No activity in the last 7 days.")
with right:
st.subheader("Class Progress Overview")
st.caption("How your students are performing")
prog = _safe_progress_overview(class_id)
overall_pct = int(round((prog.get("overall_progress") or 0) * 100))
quiz_pct = int(round((prog.get("quiz_performance") or 0) * 100))
st.text("Overall Progress")
st.progress(min(1.0, overall_pct/100.0))
st.caption(f"{overall_pct}%")
st.text("Quiz Performance")
st.progress(min(1.0, quiz_pct/100.0))
st.caption(f"{quiz_pct}%")
k1, k2 = st.columns(2)
k1.metric("๐Ÿ“– Lessons Completed", prog.get("lessons_completed", 0))
k2.metric("๐Ÿช™ Total Class XP", prog.get("class_xp", 0))
# ========== BOTTOM ROW: RECENT ACTIVITY + QUICK ACTIONS ==========
b1, b2 = st.columns([3,2])
with b1:
st.subheader("Recent Student Activity")
st.caption("Latest activity from your students")
feed = _safe_recent_activity(class_id, limit=6, days=30)
if not feed:
st.caption("Nothing yet. Assign something, chief.")
else:
for r in feed:
kind = str(r.get("kind","")).lower()
icon = "๐Ÿ“˜" if kind == "lesson" else "๐Ÿ†" if kind == "quiz" else "๐ŸŽฎ"
lvl = r.get("level") or _level_from_xp(r.get("total_xp", 0))
tail = f" ยท {r['extra']}" if r.get("extra") else ""
st.write(f"{icon} **{r.get('student_name','(unknown)')}** โ€” {r.get('item_title','(untitled)')}{tail} \n"
f"*Level {lvl}*")
with b2:
st.subheader("Quick Actions")
st.caption("Manage your classroom")
if st.button("๐Ÿ“– Create New Lesson", use_container_width=True):
st.session_state.current_page = "Content Management"
st.rerun()
if st.button("๐Ÿ† Create New Quiz", use_container_width=True):
st.session_state.current_page = "Content Management"
st.rerun()
if st.button("๐Ÿ—“๏ธ Schedule Assignment", use_container_width=True):
st.session_state.current_page = "Class management"
st.rerun()
if st.button("๐Ÿ“„ Generate Reports", use_container_width=True):
st.session_state.current_page = "Students List"
st.rerun()
# optional: keep your per-class expanders below
for c in classes:
with st.expander(f"{c.get('name','Class')} ยท Code **{c.get('code','')}**"):
st.write(f"Students: {c.get('total_students', 0)}")
avg = c.get("class_avg", 0.0)
st.write(f"Average score: {round(float(avg)*100) if avg is not None else 0}%")
roster = _safe_list_students(c.get("class_id") or c.get("id"))
if roster:
for s in roster:
lvl_slug = (s.get("level_slug") or s.get("level") or "beginner")
st.write(f"- {s.get('name','(unknown)')} ยท {s.get('email','โ€”')} ยท Level {str(lvl_slug).capitalize()}")
else:
st.caption("No students yet. Share the code.")