Spaces:
Paused
Paused
| 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 | |
| # ===================================================== | |
| 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("---") | |