mikaelJ46 commited on
Commit
f06cbd7
·
verified ·
1 Parent(s): 9c89cd0

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +425 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,427 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
1
+ # app.py - Empower Reports: Cambridge School Report System
2
+ # Features: Admin + Teacher roles, CSV/Excel bulk upload, subject switching, averages, PDF reports, audit log, export, teacher self-update
3
+
4
  import streamlit as st
5
+ import pandas as pd
6
+ from sqlalchemy import create_engine, Column, Integer, String, Float, Text, ForeignKey, DateTime
7
+ from sqlalchemy.ext.declarative import declarative_base
8
+ from sqlalchemy.orm import sessionmaker
9
+ import hashlib
10
+ from datetime import datetime
11
+ from reportlab.lib.pagesizes import letter
12
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
13
+ from reportlab.lib.styles import getSampleStyleSheet
14
+ from reportlab.lib import colors
15
+ import io
16
+ from st_aggrid import AgGrid
17
+ import yaml
18
+ import os
19
+
20
+ # -------------------------------
21
+ # 1. DATABASE & MODELS
22
+ # -------------------------------
23
+ ENGINE = create_engine('sqlite:///empower.db')
24
+ Base = declarative_base()
25
+ Session = sessionmaker(bind=ENGINE)
26
+
27
+ class User(Base):
28
+ __tablename__ = 'users'
29
+ id = Column(Integer, primary_key=True)
30
+ name = Column(String)
31
+ email = Column(String, unique=True) # Used as username
32
+ role = Column(String) # 'admin', 'teacher', 'staff'
33
+ password_hash = Column(String)
34
+ subjects_taught = Column(String)
35
+ class_teacher_for = Column(String)
36
+
37
+ class Student(Base):
38
+ __tablename__ = 'students'
39
+ id = Column(Integer, primary_key=True)
40
+ name = Column(String)
41
+ year = Column(Integer)
42
+ subjects = Column(String) # e.g., "{'Math': 'Active', 'Science': 'Dropped'}"
43
+ subject_history = Column(Text)
44
+
45
+ class Mark(Base):
46
+ __tablename__ = 'marks'
47
+ id = Column(Integer, primary_key=True)
48
+ student_id = Column(Integer, ForeignKey('students.id'))
49
+ subject = Column(String)
50
+ term = Column(Integer)
51
+ coursework = Column(Float)
52
+ midterm = Column(Float)
53
+ endterm = Column(Float)
54
+ average = Column(Float)
55
+ comment = Column(Text)
56
+ submitted_by = Column(Integer, ForeignKey('users.id'))
57
+ submitted_at = Column(String, default=lambda: datetime.now().isoformat())
58
+
59
+ class AuditLog(Base):
60
+ __tablename__ = 'audit_logs'
61
+ id = Column(Integer, primary_key=True)
62
+ user_id = Column(Integer, ForeignKey('users.id'))
63
+ action = Column(String)
64
+ details = Column(Text)
65
+ timestamp = Column(DateTime, default=datetime.utcnow)
66
+
67
+ Base.metadata.create_all(ENGINE)
68
+
69
+ # -------------------------------
70
+ # 2. AUTHENTICATION (Manual Login)
71
+ # -------------------------------
72
+ auth_config = {
73
+ 'credentials': {
74
+ 'usernames': {
75
+ 'admin': {
76
+ 'email': 'admin',
77
+ 'name': 'Admin',
78
+ 'password': hashlib.sha256('admin123'.encode()).hexdigest()
79
+ }
80
+ }
81
+ },
82
+ 'cookie': {'name': 'empower_auth', 'key': 'empower_key', 'expiry_days': 30}
83
+ }
84
+
85
+ if 'authenticator' not in st.session_state:
86
+ with open('config.yaml', 'w') as f:
87
+ yaml.dump(auth_config, f)
88
+ import streamlit_authenticator as stauth
89
+ st.session_state.authenticator = stauth.Authenticate(
90
+ 'config.yaml', 'empower_auth', 'localhost', 30
91
+ )
92
+
93
+ # -------------------------------
94
+ # 3. HELPER FUNCTIONS
95
+ # -------------------------------
96
+ def compute_average(cw, mt, et):
97
+ return round(cw * 0.3 + mt * 0.35 + et * 0.35, 2)
98
+
99
+ def log_audit(session, user_id, action, details=""):
100
+ log = AuditLog(user_id=user_id, action=action, details=details)
101
+ session.add(log)
102
+ session.commit()
103
+
104
+ # -------------------------------
105
+ # 4. STREAMLIT APP
106
+ # -------------------------------
107
+ st.set_page_config(page_title="Empower Reports", layout="wide")
108
+ st.markdown("<h1 style='text-align: center; color: #1e3a8a;'>Empower International Academy</h1>", unsafe_allow_html=True)
109
+
110
+ # Login
111
+ if 'logged_in' not in st.session_state:
112
+ st.session_state.logged_in = False
113
+ st.session_state.user_role = None
114
+ st.session_state.user_id = None
115
+
116
+ name, authentication_status, username = st.session_state.authenticator.login('Login to Empower Reports', 'main')
117
+
118
+ if authentication_status:
119
+ st.session_state.logged_in = True
120
+ session = Session()
121
+ user = session.query(User).filter_by(email=username).first()
122
+ st.session_state.user_role = user.role
123
+ st.session_state.user_id = user.id
124
+ session.close()
125
+ st.session_state.authenticator.logout('Logout', 'main')
126
+ elif authentication_status is False:
127
+ st.error('Username/password is incorrect')
128
+ st.stop()
129
+ elif authentication_status is None:
130
+ st.stop()
131
+
132
+ # Sidebar
133
+ st.sidebar.title(f"Welcome, {name}")
134
+ if st.session_state.user_role == 'admin':
135
+ page = st.sidebar.selectbox("Menu", [
136
+ "Dashboard", "Manage Users", "Manage Students", "Data Export",
137
+ "Report Designer", "Generate Reports"
138
+ ])
139
+ else:
140
+ page = st.sidebar.selectbox("Menu", [
141
+ "Dashboard", "Enter Marks", "My Classes", "Change Login Details"
142
+ ])
143
+
144
+ # -------------------------------
145
+ # 5. PAGES
146
+ # -------------------------------
147
+
148
+ # DASHBOARD
149
+ if page == "Dashboard":
150
+ st.header("Dashboard")
151
+ session = Session()
152
+ st.metric("Total Students", session.query(Student).count())
153
+ st.metric("Total Teachers", session.query(User).filter_by(role='teacher').count())
154
+
155
+ st.subheader("Recent Audit Logs")
156
+ logs = pd.read_sql("SELECT * FROM audit_logs ORDER BY timestamp DESC LIMIT 10", ENGINE)
157
+ if not logs.empty:
158
+ AgGrid(logs, height=250)
159
+ session.close()
160
+
161
+ # ADMIN: MANAGE USERS
162
+ elif page == "Manage Users" and st.session_state.user_role == 'admin':
163
+ st.header("Manage Teachers & Staff")
164
+ session = Session()
165
+
166
+ # Add Single User
167
+ with st.expander("Add New User", expanded=False):
168
+ with st.form("add_user_form"):
169
+ col1, col2 = st.columns(2)
170
+ with col1:
171
+ name = st.text_input("Full Name")
172
+ username = st.text_input("Username (for login)")
173
+ role = st.selectbox("Role", ["teacher", "staff"])
174
+ with col2:
175
+ subjects = st.text_input("Subjects Taught (comma-separated)")
176
+ class_for = st.text_input("Class Teacher For")
177
+ password = st.text_input("Password", type="password")
178
+ confirm = st.text_input("Confirm Password", type="password")
179
+
180
+ if st.form_submit_button("Create Account"):
181
+ if password != confirm:
182
+ st.error("Passwords do not match")
183
+ elif session.query(User).filter_by(email=username).first():
184
+ st.error("Username already taken")
185
+ else:
186
+ hash_pass = hashlib.sha256(password.encode()).hexdigest()
187
+ new_user = User(
188
+ name=name, email=username, role=role,
189
+ password_hash=hash_pass,
190
+ subjects_taught=subjects, class_teacher_for=class_for
191
+ )
192
+ session.add(new_user)
193
+ session.commit()
194
+ log_audit(session, st.session_state.user_id, "create_user", f"{name} (@{username})")
195
+ st.success(f"Account created!\n\n**Username:** {username}\n**Password:** {password}\n\nGive this to the teacher.")
196
+
197
+ # Bulk Upload
198
+ with st.expander("Bulk Upload Teachers (CSV/Excel)", expanded=False):
199
+ st.markdown("**Columns:** `name,username,role,subjects_taught,class_teacher_for,password`")
200
+ file = st.file_uploader("Upload file", type=["csv", "xlsx"], key="bulk_teachers")
201
+ if file:
202
+ try:
203
+ df = pd.read_excel(file) if file.name.endswith('.xlsx') else pd.read_csv(file)
204
+ required = {"name", "username", "role", "subjects_taught", "class_teacher_for", "password"}
205
+ if not required.issubset(df.columns):
206
+ st.error(f"Missing columns: {', '.join(required - set(df.columns))}")
207
+ else:
208
+ added = 0
209
+ details = []
210
+ for _, row in df.iterrows():
211
+ u = str(row["username"]).strip()
212
+ if session.query(User).filter_by(email=u).first():
213
+ details.append(f"Skipped {u} (exists)")
214
+ continue
215
+ hash_pass = hashlib.sha256(str(row["password"]).encode()).hexdigest()
216
+ new = User(
217
+ name=row["name"], email=u, role=row["role"],
218
+ password_hash=hash_pass,
219
+ subjects_taught=row["subjects_taught"],
220
+ class_teacher_for=row["class_teacher_for"]
221
+ )
222
+ session.add(new)
223
+ added += 1
224
+ details.append(f"Added {u}")
225
+ session.commit()
226
+ log_audit(session, st.session_state.user_id, "bulk_upload_teachers", "; ".join(details))
227
+ st.success(f"{added} accounts created. Give login details manually.")
228
+ except Exception as e:
229
+ st.error(f"Error: {e}")
230
+
231
+ # List Users
232
+ users_df = pd.read_sql("SELECT id, name, email, role, subjects_taught, class_teacher_for FROM users WHERE role != 'admin'", ENGINE)
233
+ AgGrid(users_df, height=400, editable=False)
234
+ session.close()
235
+
236
+ # ADMIN: MANAGE STUDENTS
237
+ elif page == "Manage Students" and st.session_state.user_role == 'admin':
238
+ st.header("Manage Students")
239
+ session = Session()
240
+
241
+ # Add Single
242
+ with st.expander("Add Student"):
243
+ with st.form("add_student"):
244
+ name = st.text_input("Name")
245
+ year = st.selectbox("Year", range(8, 14))
246
+ subjects = st.text_area("Subjects", "{'Math': 'Active', 'English': 'Active'}")
247
+ if st.form_submit_button("Add"):
248
+ new = Student(name=name, year=year, subjects=subjects, subject_history=f"Initial: {subjects}")
249
+ session.add(new)
250
+ session.commit()
251
+ log_audit(session, st.session_state.user_id, "add_student", name)
252
+ st.success("Student added")
253
+
254
+ # Bulk Upload
255
+ with st.expander("Bulk Upload Students"):
256
+ file = st.file_uploader("CSV/Excel: name,year,subjects", type=["csv", "xlsx"], key="bulk_students")
257
+ if file:
258
+ df = pd.read_excel(file) if file.name.endswith('.xlsx') else pd.read_csv(file)
259
+ added = updated = 0
260
+ for _, row in df.iterrows():
261
+ s = session.query(Student).filter_by(name=row["name"]).first()
262
+ if s:
263
+ s.year = row["year"]
264
+ s.subjects = row["subjects"]
265
+ s.subject_history += f"; {datetime.now()}: {row['subjects']}"
266
+ updated += 1
267
+ else:
268
+ new = Student(name=row["name"], year=row["year"], subjects=row["subjects"],
269
+ subject_history=f"Initial: {row['subjects']}")
270
+ session.add(new)
271
+ added += 1
272
+ session.commit()
273
+ log_audit(session, st.session_state.user_id, "bulk_upload_students", f"{added} added, {updated} updated")
274
+ st.success(f"{added} added, {updated} updated")
275
+
276
+ # Edit Subjects
277
+ students = pd.read_sql("SELECT * FROM students", ENGINE)
278
+ if not students.empty:
279
+ sel = st.selectbox("Edit Student", students['name'])
280
+ student = session.query(Student).filter_by(name=sel).first()
281
+ new_subj = st.text_area("Subjects", student.subjects)
282
+ if st.button("Update Subjects"):
283
+ student.subjects = new_subj
284
+ student.subject_history += f"; {datetime.now()}: {new_subj}"
285
+ session.commit()
286
+ st.success("Updated")
287
+
288
+ AgGrid(students, height=400)
289
+ session.close()
290
+
291
+ # ADMIN: DATA EXPORT
292
+ elif page == "Data Export" and st.session_state.user_role == 'admin':
293
+ st.header("Export Data")
294
+ if st.button("Download All Marks (CSV)"):
295
+ df = pd.read_sql("""
296
+ SELECT s.name as student, m.subject, m.term, m.coursework, m.midterm, m.endterm,
297
+ m.average, m.comment, u.name as teacher
298
+ FROM marks m
299
+ JOIN students s ON m.student_id = s.id
300
+ JOIN users u ON m.submitted_by = u.id
301
+ """, ENGINE)
302
+ csv = df.to_csv(index=False)
303
+ st.download_button("Download Marks", csv, "all_marks.csv", "text/csv")
304
+ log_audit(Session(), st.session_state.user_id, "export_marks", "Downloaded all marks")
305
+
306
+ # ADMIN: REPORT DESIGNER
307
+ elif page == "Report Designer" and st.session_state.user_role == 'admin':
308
+ st.header("Report Template")
309
+ template = st.text_area("JSON Template", '{"header": "Term Report", "grading": "A*:90+"}', height=200)
310
+ if st.button("Save Template"):
311
+ with open("report_template.json", "w") as f:
312
+ f.write(template)
313
+ st.success("Template saved")
314
+
315
+ # ADMIN: GENERATE REPORTS
316
+ elif page == "Generate Reports" and st.session_state.user_role == 'admin':
317
+ st.header("Generate Student Report")
318
+ session = Session()
319
+ students = pd.read_sql("SELECT name FROM students", ENGINE)
320
+ sel = st.selectbox("Student", students['name'])
321
+ if sel:
322
+ marks = pd.read_sql(f"SELECT * FROM marks WHERE student_id = (SELECT id FROM students WHERE name='{sel}')", ENGINE)
323
+ if not marks.empty:
324
+ AgGrid(marks.groupby(['term', 'subject'])['average'].mean().reset_index())
325
+
326
+ if st.button("Generate PDF"):
327
+ buffer = io.BytesIO()
328
+ doc = SimpleDocTemplate(buffer, pagesize=letter)
329
+ story = []
330
+ styles = getSampleStyleSheet()
331
+ story.append(Paragraph(f"<b>{sel} - Term Report</b>", styles['Title']))
332
+ data = [['Term', 'Subject', 'Avg', 'Comment']] + marks[['term', 'subject', 'average', 'comment']].values.tolist()
333
+ table = Table(data)
334
+ table.setStyle(TableStyle([('BACKGROUND', (0,0), (-1,0), colors.grey), ('GRID', (0,0), (-1,-1), 1, colors.black)]))
335
+ story.append(table)
336
+ doc.build(story)
337
+ st.download_button("Download PDF", buffer.getvalue(), f"{sel}_report.pdf", "application/pdf")
338
+ session.close()
339
+
340
+ # TEACHER: ENTER MARKS
341
+ elif page == "Enter Marks" and st.session_state.user_role == 'teacher':
342
+ st.header("Enter Marks")
343
+ session = Session()
344
+ user = session.query(User).get(st.session_state.user_id)
345
+ my_subjects = [s.strip() for s in user.subjects_taught.split(',')] if user.subjects_taught else []
346
+
347
+ # Single Entry
348
+ students = pd.read_sql("SELECT name FROM students", ENGINE)
349
+ sel_student = st.selectbox("Student", students['name'])
350
+ sel_subject = st.selectbox("Subject", my_subjects)
351
+ term = st.selectbox("Term", [1,2,3])
352
+
353
+ with st.form("single_mark"):
354
+ cw = st.number_input("Coursework", 0.0, 100.0)
355
+ mt = st.number_input("Mid-term", 0.0, 100.0)
356
+ et = st.number_input("End-term", 0.0, 100.0)
357
+ comment = st.text_area("Comment")
358
+ if st.form_submit_button("Submit"):
359
+ student = session.query(Student).filter_by(name=sel_student).first()
360
+ if sel_subject not in str(student.subjects):
361
+ st.error("Subject not active")
362
+ else:
363
+ avg = compute_average(cw, mt, et)
364
+ new = Mark(student_id=student.id, subject=sel_subject, term=term,
365
+ coursework=cw, midterm=mt, endterm=et, average=avg,
366
+ comment=comment, submitted_by=st.session_state.user_id)
367
+ session.add(new)
368
+ session.commit()
369
+ st.success("Submitted")
370
+
371
+ # Bulk Upload
372
+ with st.expander("Bulk Upload Marks"):
373
+ file = st.file_uploader("CSV/Excel", type=["csv", "xlsx"], key="bulk_marks")
374
+ if file:
375
+ df = pd.read_excel(file) if file.name.endswith('.xlsx') else pd.read_csv(file)
376
+ saved = 0
377
+ for _, row in df.iterrows():
378
+ student = session.query(Student).filter_by(name=row["student_name"]).first()
379
+ if student and row["subject"] in my_subjects:
380
+ avg = compute_average(row["coursework"], row["midterm"], row["endterm"])
381
+ new = Mark(student_id=student.id, subject=row["subject"], term=row["term"],
382
+ coursework=row["coursework"], midterm=row["midterm"], endterm=row["endterm"],
383
+ average=avg, comment=row.get("comment", ""), submitted_by=st.session_state.user_id)
384
+ session.add(new)
385
+ saved += 1
386
+ session.commit()
387
+ st.success(f"{saved} marks saved")
388
+ session.close()
389
+
390
+ # TEACHER: MY CLASSES
391
+ elif page == "My Classes":
392
+ st.header("My Classes")
393
+ session = Session()
394
+ user = session.query(User).get(st.session_state.user_id)
395
+ st.write(f"Class Teacher: {user.class_teacher_for or 'None'}")
396
+ students = pd.read_sql("SELECT name, year FROM students", ENGINE)
397
+ AgGrid(students)
398
+ session.close()
399
 
400
+ # TEACHER: CHANGE LOGIN
401
+ elif page == "Change Login Details":
402
+ st.header("Change Username & Password")
403
+ session = Session()
404
+ user = session.query(User).get(st.session_state.user_id)
405
+
406
+ with st.form("change_form"):
407
+ new_user = st.text_input("New Username", value=user.email)
408
+ curr_pass = st.text_input("Current Password", type="password")
409
+ new_pass = st.text_input("New Password", type="password")
410
+ confirm = st.text_input("Confirm New Password", type="password")
411
+
412
+ if st.form_submit_button("Update"):
413
+ if hashlib.sha256(curr_pass.encode()).hexdigest() != user.password_hash:
414
+ st.error("Incorrect current password")
415
+ elif new_pass != confirm:
416
+ st.error("Passwords don't match")
417
+ elif session.query(User).filter(User.email == new_user, User.id != user.id).first():
418
+ st.error("Username taken")
419
+ else:
420
+ user.email = new_user
421
+ if new_pass:
422
+ user.password_hash = hashlib.sha256(new_pass.encode()).hexdigest()
423
+ session.commit()
424
+ log_audit(session, st.session_state.user_id, "change_login", f"to {new_user}")
425
+ st.success("Updated! Logging out...")
426
+ st.experimental_rerun()
427
+ session.close()