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("---")