-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
355 lines (295 loc) · 15.1 KB
/
Copy pathapp.py
File metadata and controls
355 lines (295 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
"""
UOM Results Ranking Tool - Streamlit Web Application
A user-friendly tool for non-CS students to process exam results and generate rankings.
Security measures implemented:
- File size limitation (10MB)
- File type validation
- No shell command execution
- Sandboxed file processing
- Input sanitization
- LFI prevention (UUID-based filenames, fixed export directory)
"""
import streamlit as st
import pandas as pd
import warnings
# Suppress warnings
warnings.filterwarnings("ignore")
# Import from modules package
from modules.config import (
ALLOWED_DEPT_EXTENSIONS,
ALLOWED_PDF_EXTENSION,
EXPORT_CLEANUP_HOURS
)
from modules.security import (
validate_file_size,
validate_file_extension,
cleanup_old_exports,
save_export_file
)
from modules.pdf_processor import process_pdf
from modules.data_processor import (
load_department_data,
merge_grades_with_department,
calculate_sgpa_and_rank,
get_existing_modules
)
from modules.ui_components import (
render_header,
render_how_it_works,
render_existing_modules_banner,
render_department_file_guide,
render_footer
)
def main():
"""Main application entry point."""
render_header()
render_how_it_works()
# Initialize session state
if 'department_df' not in st.session_state:
st.session_state.department_df = None
if 'weights' not in st.session_state:
st.session_state.weights = {}
if 'processed_modules' not in st.session_state:
st.session_state.processed_modules = []
# =========================================================================
# STEP 1: Department File Upload
# =========================================================================
st.markdown('<div class="step-header"><h3>📁 Step 1: Upload Department File</h3></div>', unsafe_allow_html=True)
# Show guide about what the department file should contain
render_department_file_guide()
dept_file = st.file_uploader(
"Choose Department File (CSV or Excel)",
type=['csv', 'xlsx', 'xls'],
key="dept_uploader",
help="Maximum file size: 10MB"
)
if dept_file:
# Validate file
is_valid_size, size_msg = validate_file_size(dept_file)
is_valid_ext, ext_msg = validate_file_extension(dept_file.name, ALLOWED_DEPT_EXTENSIONS)
if not is_valid_size:
st.error(f"❌ {size_msg}")
elif not is_valid_ext:
st.error(f"❌ {ext_msg}")
else:
# Only reload department file if it's a NEW file (different name)
current_dept_file = st.session_state.get('loaded_dept_filename')
if current_dept_file != dept_file.name:
# New file uploaded - load it
dept_file.seek(0)
dept_bytes = dept_file.read()
df = load_department_data(dept_bytes, dept_file.name)
if df is not None:
st.session_state.department_df = df
st.session_state.loaded_dept_filename = dept_file.name
# Check for existing modules in the NEW file
existing_modules = get_existing_modules(df)
st.session_state.processed_modules = existing_modules
st.session_state.weights = {}
# Display info for loaded department file
if st.session_state.department_df is not None:
df = st.session_state.department_df
st.markdown(f"""
<div class="success-box">
<h4>✅ Department File Loaded Successfully!</h4>
<p><strong>{len(df)}</strong> students found |
<strong>{len(df.columns)}</strong> columns |
<strong>{len(st.session_state.processed_modules)}</strong> module grades</p>
</div>
""", unsafe_allow_html=True)
# Show existing modules and weight inputs
if st.session_state.processed_modules:
st.session_state.weights = render_existing_modules_banner(
st.session_state.processed_modules,
st.session_state.weights
)
# Preview data
with st.expander("👀 Preview Department Data", expanded=False):
st.dataframe(st.session_state.department_df.head(10), use_container_width=True)
# =========================================================================
# STEP 2: Add Result PDFs
# =========================================================================
if st.session_state.department_df is not None:
st.markdown("---")
st.markdown('<div class="step-header"><h3>📄 Step 2: Add Result PDFs</h3></div>', unsafe_allow_html=True)
st.markdown("""
Upload result PDF files one at a time. Each PDF will be processed and the grades will be
**appended as a new column** to your data.
🔄 **Remember**: This ADDS to your existing data - it doesn't replace it!
""")
pdf_file = st.file_uploader(
"Choose Result PDF",
type=['pdf'],
key="pdf_uploader",
help="Maximum file size: 10MB"
)
if pdf_file:
# Validate file
is_valid_size, size_msg = validate_file_size(pdf_file)
is_valid_ext, ext_msg = validate_file_extension(pdf_file.name, [ALLOWED_PDF_EXTENSION])
if not is_valid_size:
st.error(f"❌ {size_msg}")
elif not is_valid_ext:
st.error(f"❌ {ext_msg}")
else:
# Cache PDF bytes
pdf_file.seek(0)
pdf_bytes = pdf_file.read()
st.session_state['pending_pdf_bytes'] = pdf_bytes
st.session_state['pending_pdf_name'] = pdf_file.name
col1, col2 = st.columns([2, 1])
with col1:
new_weight = st.number_input(
"Enter credit weight for this module:",
min_value=0.5,
max_value=10.0,
value=3.0,
step=0.5,
key="new_module_weight"
)
with col2:
st.markdown("<br>", unsafe_allow_html=True)
process_btn = st.button("➕ Add This Result", type="primary", use_container_width=True)
if process_btn:
with st.spinner("Processing PDF..."):
cached_bytes = st.session_state.get('pending_pdf_bytes')
cached_name = st.session_state.get('pending_pdf_name', pdf_file.name)
if cached_bytes is None or len(cached_bytes) == 0:
st.error("❌ Could not read PDF file. Please try uploading again.")
st.stop()
grades_df, module_code, module_name = process_pdf(cached_bytes, cached_name)
if grades_df is None:
st.warning(f"Debug: PDF bytes length = {len(cached_bytes)}, Module detected = {module_code}")
if grades_df is None or grades_df.empty:
st.error("❌ Could not extract grades from PDF. Please check the file format.")
else:
# Check if module already exists
if module_code in st.session_state.processed_modules:
st.warning(f"⚠️ Module **{module_code}** already exists. Updating grades.")
old_col = f"{module_code}_Grade"
if old_col in st.session_state.department_df.columns:
st.session_state.department_df = st.session_state.department_df.drop(columns=[old_col])
# Merge grades
st.session_state.department_df = merge_grades_with_department(
st.session_state.department_df,
grades_df,
module_code
)
# Update weights and modules list
st.session_state.weights[module_code] = new_weight
if module_code not in st.session_state.processed_modules:
st.session_state.processed_modules.append(module_code)
st.success(f"""
✅ **{module_code} - {module_name}** added successfully!
- {len(grades_df)} grades extracted from PDF
- Weight set to **{new_weight}** credits
""")
st.rerun()
# Show current modules summary
if st.session_state.processed_modules:
st.markdown("---")
st.markdown("### 📋 Current Modules in Your Data")
module_info = []
for module in st.session_state.processed_modules:
weight = st.session_state.weights.get(module, "Not set")
grade_col = f"{module}_Grade"
if grade_col in st.session_state.department_df.columns:
grades_found = (st.session_state.department_df[grade_col] != 'N/A').sum()
else:
grades_found = 0
module_info.append({
"Module Code": module,
"Weight (Credits)": weight,
"Grades Found": grades_found
})
st.dataframe(pd.DataFrame(module_info), use_container_width=True, hide_index=True)
# =========================================================================
# STEP 4: Generate Rankings
# =========================================================================
if st.session_state.department_df is not None and st.session_state.processed_modules:
st.markdown("---")
st.markdown('<div class="step-header"><h3>🏆 Step 4: Generate Rankings</h3></div>', unsafe_allow_html=True)
# Check if all weights are set
all_weights_set = all(
module in st.session_state.weights
for module in st.session_state.processed_modules
)
if not all_weights_set:
st.warning("⚠️ Please set weights for all modules before generating rankings.")
else:
if st.button("🚀 Generate SGPA & Rankings", type="primary", use_container_width=True):
with st.spinner("Calculating SGPA and generating rankings..."):
result_df = calculate_sgpa_and_rank(
st.session_state.department_df.copy(),
st.session_state.weights
)
st.session_state.result_df = result_df
st.session_state.ranking_generated = True
st.info(f"📊 Generated rankings for {len(result_df)} students with {len(result_df.columns)} columns")
# Display results if they exist
if st.session_state.get('ranking_generated') and st.session_state.get('result_df') is not None:
result_df = st.session_state.result_df
st.markdown("""
<div class="success-box">
<h4>🎉 Rankings Generated Successfully!</h4>
</div>
""", unsafe_allow_html=True)
# Statistics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Students", len(result_df))
with col2:
st.metric("Highest SGPA", f"{result_df['SGPA'].max():.3f}")
with col3:
st.metric("Average SGPA", f"{result_df['SGPA'].mean():.3f}")
with col4:
st.metric("Modules Used", len(st.session_state.processed_modules))
# Display results
st.markdown("### 📊 Top 20 Students")
display_cols = ['Rank', 'Index']
if 'Name' in result_df.columns:
display_cols.append('Name')
if 'Firstname' in result_df.columns and 'Lastname' in result_df.columns:
display_cols.extend(['Firstname', 'Lastname'])
display_cols.append('SGPA')
display_cols.extend([f"{m}_Grade" for m in st.session_state.processed_modules])
display_cols = [c for c in display_cols if c in result_df.columns]
st.dataframe(result_df[display_cols].head(20), use_container_width=True, hide_index=True)
# Download buttons
st.markdown("### 📥 Download Results")
cleanup_old_exports(max_age_hours=EXPORT_CLEANUP_HOURS)
col1, col2 = st.columns(2)
with col1:
try:
csv_filename, csv_filepath = save_export_file(result_df, 'csv')
with open(csv_filepath, 'rb') as f:
csv_data = f.read()
st.download_button(
label="📄 Download as CSV",
data=csv_data,
file_name="ranked_results.csv",
mime="text/csv",
use_container_width=True,
key="download_csv"
)
except Exception as e:
st.error(f"Error creating CSV: {e}")
with col2:
try:
xlsx_filename, xlsx_filepath = save_export_file(result_df, 'xlsx')
with open(xlsx_filepath, 'rb') as f:
xlsx_data = f.read()
st.download_button(
label="📊 Download as Excel",
data=xlsx_data,
file_name="ranked_results.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
use_container_width=True,
key="download_excel"
)
except Exception as e:
st.error(f"Error creating Excel: {e}")
# Footer
render_footer()
if __name__ == "__main__":
main()