Course-Grades / src /streamlit_app.py
iurbinah's picture
Update src/streamlit_app.py
df1cc64 verified
import os
import tempfile
from pathlib import Path
from typing import Union
# -----------------------------------------------------
# Ensure Streamlit can write its internal files in Spaces
# -----------------------------------------------------
TMP_HOME = tempfile.gettempdir()
# Force HOME to a writable tmp directory (override any existing)
os.environ["HOME"] = TMP_HOME
# Disable anonymous usage stats to avoid metrics file write
os.environ["STREAMLIT_BROWSER_GATHERUSAGESTATS"] = "false"
os.makedirs(os.path.join(TMP_HOME, ".streamlit"), exist_ok=True)
# -----------------------------------------------------
TMP_HOME = tempfile.gettempdir()
os.environ.setdefault("HOME", TMP_HOME)
os.makedirs(os.path.join(TMP_HOME, ".streamlit"), exist_ok=True)
import pandas as pd
import streamlit as st
import pandas as pd
import streamlit as st
# =====================================================
# Configuration & Authentication
# =====================================================
COURSE_PASSWORD = os.getenv("PASSWORD")
if not COURSE_PASSWORD:
st.error("Course password not configured. Please set the PASSWORD environment variable (key: PASSWORD).")
st.stop()
def authenticate() -> None:
"""Simple session‑based password gate."""
if st.session_state.get("authenticated", False):
return # already logged in
pwd = st.text_input("Enter course password:", type="password")
if st.button("Submit", key="pwd_submit"):
if pwd == COURSE_PASSWORD:
st.session_state["authenticated"] = True
# Rerun so the app shows the rest of the UI
if hasattr(st, "experimental_rerun"):
st.experimental_rerun()
elif hasattr(st, "rerun"):
st.rerun()
else:
st.info("Login successful — please refresh the page.")
st.stop()
else:
st.error("Incorrect password. Please try again.")
st.stop()
st.stop()
authenticate()
# =====================================================
# Grading helpers
# =====================================================
def assign_letter(score: float) -> str:
"""Instructor‑specified letter‑grade curve."""
if score <= 59.99:
return "F"
elif score <= 66.99:
return "D"
elif score <= 69.99:
return "D+"
elif score <= 76.99:
return "C-"
elif score <= 79.99:
return "C"
elif score <= 82.99:
return "C+"
elif score <= 85.99:
return "B-"
elif score <= 86.99:
return "B"
elif score <= 88.99:
return "B+"
elif score <= 90.99:
return "A-"
else:
return "A"
GRADE_ORDER = ["F", "D", "D+", "C-", "C", "C+", "B-", "B", "B+", "A-", "A"]
# =====================================================
# Data loading & preprocessing
# =====================================================
@st.cache_data
def load_data(csv_name: Union[str, Path] = "course_grades_csv.csv"):
"""Load the gradebook, compute quiz averages, course scores, and letters.
Only students whose final exam is posted are retained.
Returns: df_final (only students w/ final), quiz_cols list
"""
csv_name = Path(csv_name)
search_paths = [
Path.cwd() / csv_name,
Path(__file__).resolve().parent / csv_name,
Path(__file__).resolve().parent.parent / csv_name,
]
file_path = next((p for p in search_paths if p.is_file()), None)
if file_path is None:
st.error("CSV file not found. Searched: " + ", ".join(str(p) for p in search_paths))
st.stop()
df = pd.read_csv(file_path)
# Locate quiz columns
quiz_cols = [c for c in df.columns if c.lower().startswith("quiz")]
if len(quiz_cols) != 8:
st.error(f"Expected 8 quiz columns, found {len(quiz_cols)}: {quiz_cols}")
st.stop()
# Coerce quizzes to numeric
df[quiz_cols] = df[quiz_cols].apply(pd.to_numeric, errors="coerce")
# Quiz average: sort, drop two lowest, then average
def drop_two_and_avg(row):
scores = sorted([float(x) for x in row if pd.notna(x)])
# Remove the first two (lowest) scores
trimmed = scores[2:] if len(scores) >= 2 else scores
return sum(trimmed) / len(trimmed) if trimmed else None
df["Quiz_Avg"] = df[quiz_cols].apply(drop_two_and_avg, axis=1)
# Keep students whose final is available
df_final = df[df.get("Is Final Missing?", "") != "Yes, Final Is Pending"].copy()
# Compute course score
df_final["Course_Score"] = (
0.5 * df_final["Quiz_Avg"]
+ 0.2 * df_final["Midterm Score"].astype(float)
+ 0.3 * df_final["Final Score"].astype(float)
+ df_final.get("ExtraCredit", 0).astype(float)
)
# Letter grades
df_final["Letter_Grade"] = df_final["Course_Score"].apply(assign_letter)
return df_final, quiz_cols
# Load (cached)
df_final, quiz_cols = load_data()
# =====================================================
# User Interface
# =====================================================
st.title("Course Grade Report")
sbuid_input = st.number_input("Enter your student ID (SBUID):", min_value=0, step=1, format="%d")
sbuid = int(sbuid_input) if sbuid_input else None
if sbuid:
student = df_final[df_final["SBUID"].astype(int) == sbuid]
if student.empty:
st.warning("ID not found, or your final exam grade has not been posted yet.")
st.stop()
row = student.iloc[0]
# ---------------- Quiz breakdown ----------------
st.subheader("Quiz Scores (lowest two dropped)")
quiz_scores = pd.to_numeric(row[quiz_cols], errors="coerce")
lowest_two = set(quiz_scores.nsmallest(2).index)
quiz_df = pd.DataFrame({
"Score": quiz_scores,
"Counted in Avg": [col not in lowest_two for col in quiz_cols],
})
quiz_df.loc["Average"] = [row["Quiz_Avg"], ""]
st.table(quiz_df)
# -------------- Exams & Extra Credit -------------
st.subheader("Exams & Extra Credit")
exams_df = pd.DataFrame({
"Component": ["Midterm Score", "Final Score", "ExtraCredit"],
"Score": [row["Midterm Score"], row["Final Score"], row.get("ExtraCredit", 0)],
}).set_index("Component")
st.table(exams_df)
# -------------- Course result -------------------
st.subheader("Your Course Result")
col_score, col_grade = st.columns(2)
col_score.metric("Course Score", f"{row['Course_Score']:.1f}")
col_grade.metric("Letter Grade", row["Letter_Grade"])
# -------------- Distribution --------------------
st.subheader("Class‑wide Grade Distribution (students with posted finals)")
distribution = (
df_final["Letter_Grade"].value_counts().reindex(GRADE_ORDER).fillna(0).astype(int)
)
st.bar_chart(distribution)
# Footer
st.markdown("---")