dcata004 commited on
Commit
ce0cada
Β·
verified Β·
1 Parent(s): 029d333

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -0
app.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import plotly.express as px
5
+ import datetime
6
+ import time
7
+
8
+ # ==========================================
9
+ # 1. CONFIGURATION & MOCK DATA GENERATION
10
+ # ==========================================
11
+ st.set_page_config(
12
+ page_title="WDE Accountability Dashboard",
13
+ page_icon="🀠",
14
+ layout="wide"
15
+ )
16
+
17
+ # Simulated Wyoming Districts
18
+ WY_DISTRICTS = [
19
+ "Laramie County SD #1",
20
+ "Natrona County SD #1",
21
+ "Sheridan County SD #2",
22
+ "Teton County SD #1",
23
+ "Albany County SD #1"
24
+ ]
25
+
26
+ # Simulated "Nightly Update" Data Generator
27
+ @st.cache_data(ttl=3600) # Cache mimics the static nature of nightly builds
28
+ def load_data(simulation_date):
29
+ """
30
+ Simulates fetching data from a Data Warehouse updated nightly.
31
+ """
32
+ data = []
33
+
34
+ # Generate mock data
35
+ for dist in WY_DISTRICTS:
36
+ # Create 3 schools per district
37
+ for school_level in ['Elementary', 'Middle', 'High']:
38
+ school_name = f"{dist.split(' ')[0]} {school_level}"
39
+
40
+ # Generate 50 students per school for the prototype
41
+ for i in range(50):
42
+ student_id = f"WY-{np.random.randint(100000, 999999)}"
43
+
44
+ # Simulate WY-TOPP Scores (Scale 200-800)
45
+ math_score = np.random.normal(500, 50) + (20 if "Teton" in dist else 0)
46
+ ela_score = np.random.normal(510, 45)
47
+ attendance_rate = np.clip(np.random.normal(92, 5), 50, 100)
48
+
49
+ data.append({
50
+ "District": dist,
51
+ "School": school_name,
52
+ "Student_ID": student_id,
53
+ "Grade_Level": np.random.choice([3, 4, 5, 6, 7, 8, 9, 10, 11]),
54
+ "WY_TOPP_Math": int(math_score),
55
+ "WY_TOPP_ELA": int(ela_score),
56
+ "Attendance_Pct": round(attendance_rate, 1),
57
+ "At_Risk": "Yes" if attendance_rate < 85 or math_score < 450 else "No",
58
+ "Last_Updated": simulation_date
59
+ })
60
+
61
+ return pd.DataFrame(data)
62
+
63
+ # ==========================================
64
+ # 2. AUTHENTICATION & SECURITY (RBAC)
65
+ # ==========================================
66
+ # In production, connect this to SSO / LDAP / Active Directory
67
+ USERS = {
68
+ "admin": {"password": "password123", "role": "State_Admin", "access": "All"},
69
+ "laramie_supt": {"password": "wyo", "role": "District_Admin", "access": "Laramie County SD #1"},
70
+ "teton_principal": {"password": "mountains", "role": "School_Admin", "access": "Teton Elementary"},
71
+ }
72
+
73
+ def login():
74
+ st.markdown("## πŸ”’ WDE Secure Login")
75
+
76
+ if "authenticated" not in st.session_state:
77
+ st.session_state["authenticated"] = False
78
+
79
+ if not st.session_state["authenticated"]:
80
+ username = st.text_input("Username")
81
+ password = st.text_input("Password", type="password")
82
+
83
+ if st.button("Login"):
84
+ if username in USERS and USERS[username]["password"] == password:
85
+ st.session_state["authenticated"] = True
86
+ st.session_state["user"] = username
87
+ st.session_state["role"] = USERS[username]["role"]
88
+ st.session_state["access"] = USERS[username]["access"]
89
+ st.rerun()
90
+ else:
91
+ st.error("Invalid credentials")
92
+ else:
93
+ return True
94
+ return False
95
+
96
+ def logout():
97
+ st.session_state["authenticated"] = False
98
+ st.rerun()
99
+
100
+ # ==========================================
101
+ # 3. DASHBOARD LOGIC
102
+ # ==========================================
103
+ def main_dashboard():
104
+ # --- Sidebar ---
105
+ st.sidebar.title("Navigation")
106
+ st.sidebar.write(f"Logged in as: **{st.session_state['user']}**")
107
+ st.sidebar.write(f"Role: *{st.session_state['role']}*")
108
+
109
+ if st.sidebar.button("Logout"):
110
+ logout()
111
+
112
+ st.sidebar.markdown("---")
113
+
114
+ # Simulate Nightly Update Info
115
+ today = datetime.date.today()
116
+ st.sidebar.info(f"πŸ“… Data Current As Of: \n{today} 03:00 AM MST")
117
+
118
+ # --- Data Loading ---
119
+ df = load_data(today)
120
+
121
+ # --- Security Filter (Row Level Security) ---
122
+ # Filter data based on user role before it hits the UI
123
+ if st.session_state["access"] != "All":
124
+ if st.session_state["role"] == "District_Admin":
125
+ df = df[df["District"] == st.session_state["access"]]
126
+ elif st.session_state["role"] == "School_Admin":
127
+ df = df[df["School"] == st.session_state["access"]]
128
+
129
+ # --- Page Content ---
130
+ st.title("πŸ”οΈ Wyoming Accountability & Analytics")
131
+
132
+ # Top Level Metrics
133
+ col1, col2, col3, col4 = st.columns(4)
134
+ col1.metric("Total Students", f"{len(df)}")
135
+ col2.metric("Avg Math Score", f"{int(df['WY_TOPP_Math'].mean())}")
136
+ col3.metric("Avg Attendance", f"{df['Attendance_Pct'].mean():.1f}%")
137
+
138
+ at_risk_count = len(df[df["At_Risk"] == "Yes"])
139
+ col4.metric("At-Risk Students", f"{at_risk_count}", delta="-Action Required", delta_color="inverse")
140
+
141
+ st.markdown("---")
142
+
143
+ # --- Visualizations ---
144
+
145
+ # 1. Drill Down Controls
146
+ st.subheader("πŸ“Š Performance Analytics")
147
+
148
+ view_type = st.radio("Analyze by:", ["District", "School", "Grade_Level"], horizontal=True)
149
+
150
+ # Group data based on selection
151
+ agg_df = df.groupby(view_type)[["WY_TOPP_Math", "WY_TOPP_ELA", "Attendance_Pct"]].mean().reset_index()
152
+
153
+ # Chart
154
+ fig = px.bar(
155
+ agg_df,
156
+ x=view_type,
157
+ y=["WY_TOPP_Math", "WY_TOPP_ELA"],
158
+ barmode='group',
159
+ title=f"Average WY-TOPP Scores by {view_type}",
160
+ color_discrete_sequence=["#F2A900", "#53565A"] # Wyoming Colors (approx)
161
+ )
162
+ st.plotly_chart(fig, use_container_width=True)
163
+
164
+ # 2. Risk Analysis
165
+ c1, c2 = st.columns(2)
166
+
167
+ with c1:
168
+ st.subheader("Attendance vs. Performance")
169
+ fig2 = px.scatter(
170
+ df,
171
+ x="Attendance_Pct",
172
+ y="WY_TOPP_Math",
173
+ color="At_Risk",
174
+ hover_data=["Student_ID", "School"],
175
+ title="Correlation: Attendance vs Math Scores"
176
+ )
177
+ st.plotly_chart(fig2, use_container_width=True)
178
+
179
+ with c2:
180
+ st.subheader("At-Risk Distribution")
181
+ risk_dist = df['At_Risk'].value_counts()
182
+ fig3 = px.pie(values=risk_dist, names=risk_dist.index, hole=0.4, color_discrete_sequence=['#2ecc71', '#e74c3c'])
183
+ st.plotly_chart(fig3, use_container_width=True)
184
+
185
+ # --- Data Grid (Secure View) ---
186
+ st.subheader("πŸ“‚ Student Data Details")
187
+
188
+ # Search / Filter
189
+ text_search = st.text_input("Search Student ID", "")
190
+ grade_filter = st.multiselect("Filter by Grade", sorted(df["Grade_Level"].unique()))
191
+
192
+ filtered_df = df.copy()
193
+ if text_search:
194
+ filtered_df = filtered_df[filtered_df["Student_ID"].str.contains(text_search)]
195
+ if grade_filter:
196
+ filtered_df = filtered_df[filtered_df["Grade_Level"].isin(grade_filter)]
197
+
198
+ # Styling the dataframe (Highlighting low attendance)
199
+ st.dataframe(
200
+ filtered_df.style.map(lambda x: 'color: red; font-weight: bold' if isinstance(x, (int, float)) and x < 85 else '', subset=['Attendance_Pct']),
201
+ use_container_width=True,
202
+ hide_index=True
203
+ )
204
+
205
+ # Export Button (Audit Log Placeholder)
206
+ if st.button("πŸ“₯ Export Report to CSV"):
207
+ st.toast("Export started... Logged in Audit Trail.", icon="βœ…")
208
+ # In production, this would trigger a download and write to an SQL audit log
209
+
210
+ # ==========================================
211
+ # 4. APP ENTRY POINT
212
+ # ==========================================
213
+ if __name__ == "__main__":
214
+ if login():
215
+ main_dashboard()