Pranesh64 commited on
Commit
30dacdf
·
verified ·
1 Parent(s): 5062a46

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1483 -0
app.py ADDED
@@ -0,0 +1,1483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ import gspread
4
+ from google.auth.transport.requests import Request
5
+ from google.oauth2.credentials import Credentials
6
+ from google_auth_oauthlib.flow import InstalledAppFlow
7
+ import json
8
+ import gradio as gr
9
+ import time
10
+ from datetime import datetime
11
+ from pytz import timezone
12
+ import threading
13
+ from dotenv import load_dotenv
14
+
15
+ # Load environment variables
16
+ load_dotenv()
17
+
18
+ # Time zone Conversion
19
+ ist = timezone("Asia/Kolkata")
20
+
21
+ # Scopes (read-only)
22
+ SCOPES = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
23
+
24
+ def authorize():
25
+ creds = None
26
+
27
+ # Get JSON content from environment variables
28
+ token_json_content = os.getenv('TOKEN_JSON')
29
+ credentials_json_content = os.getenv('CREDENTIALS_JSON')
30
+
31
+ # Load token from environment variable if exists
32
+ if token_json_content:
33
+ try:
34
+ token_info = json.loads(token_json_content)
35
+ creds = Credentials.from_authorized_user_info(token_info, SCOPES)
36
+ except json.JSONDecodeError:
37
+ print("⚠️ Invalid TOKEN_JSON format in environment variable")
38
+
39
+ # If no valid credentials, start OAuth flow
40
+ if not creds or not creds.valid:
41
+ if creds and creds.expired and creds.refresh_token:
42
+ creds.refresh(Request())
43
+ else:
44
+ if not credentials_json_content:
45
+ raise ValueError("CREDENTIALS_JSON environment variable is required for OAuth flow")
46
+
47
+ try:
48
+ credentials_info = json.loads(credentials_json_content)
49
+ flow = InstalledAppFlow.from_client_config(credentials_info, SCOPES)
50
+ creds = flow.run_local_server(port=0)
51
+ except json.JSONDecodeError:
52
+ raise ValueError("Invalid CREDENTIALS_JSON format in environment variable")
53
+
54
+ # Save token back to environment (for this session only)
55
+ # Note: You may want to update your .env file manually with the new token
56
+ print("🔄 New token generated. Consider updating TOKEN_JSON in your .env file with:")
57
+ print(f"TOKEN_JSON={creds.to_json()}")
58
+
59
+ return gspread.authorize(creds)
60
+
61
+ # NEW FUNCTION: Extract subjects and marks
62
+ def extract_subjects_and_marks_for_gradio(roll_no):
63
+ """
64
+ Extract subjects with their redeemed points and marks for Gradio interface
65
+ Uses cached studentwise_data with indexing for faster search
66
+ """
67
+ if not roll_no.strip():
68
+ return "❌ Please enter a roll number"
69
+
70
+ try:
71
+ # Get cached data instead of making API calls
72
+ combined_df, studentwise_data, details_info, reward_points_df = get_cached_data()
73
+
74
+ if not studentwise_data or len(studentwise_data) < 2:
75
+ return "❌ Studentwise data not available in cache"
76
+
77
+ headers = studentwise_data[0]
78
+ roll_no = roll_no.strip().upper()
79
+
80
+ # USE INDEX FOR FASTER SEARCH
81
+ student_row = None
82
+ if roll_no in data_cache.get("studentwise_index", {}):
83
+ row_idx = data_cache["studentwise_index"][roll_no]
84
+ student_row = studentwise_data[row_idx]
85
+ else:
86
+ # Fallback to original search method
87
+ for row in studentwise_data[1:]: # Skip header row
88
+ # Check multiple columns for roll number
89
+ for i, cell in enumerate(row[:5]): # Check first 5 columns
90
+ if cell.strip().upper() == roll_no:
91
+ student_row = row
92
+ break
93
+ if student_row is not None:
94
+ break
95
+
96
+ if student_row is None:
97
+ return f"❌ Student with Roll No '{roll_no}' not found."
98
+
99
+ # Rest of the function remains the same...
100
+ def get_value_if_not_empty(col_name):
101
+ """Helper function to get value only if it's not empty"""
102
+ if col_name in headers:
103
+ idx = headers.index(col_name)
104
+ value = student_row[idx] if idx < len(student_row) else ''
105
+ return value.strip() if value.strip() else None
106
+ return None
107
+
108
+ def get_numeric_value(col_name):
109
+ """Helper function to get numeric value, return 0 if empty or invalid"""
110
+ value = get_value_if_not_empty(col_name)
111
+ try:
112
+ return float(value) if value else 0.0
113
+ except (ValueError, TypeError):
114
+ return 0.0
115
+
116
+ # Get student basic info
117
+ student_name = get_value_if_not_empty("Student Name") or "Unknown"
118
+
119
+ # Collect theory subjects (optimized loops)
120
+ theory_subjects = []
121
+ for i in range(1, 10):
122
+ subject_code = get_value_if_not_empty(f"TS{i}")
123
+ if subject_code:
124
+ theory_subjects.append({
125
+ 'code': subject_code,
126
+ 'ip1_points': get_numeric_value(f"IP1TS{i}R"),
127
+ 'ip2_points': get_numeric_value(f"IP2TS{i}R"),
128
+ 'ip1_marks': get_numeric_value(f"IP1TS{i}M"),
129
+ 'ip2_marks': get_numeric_value(f"IP2TS{i}M")
130
+ })
131
+
132
+ # Collect lab subjects (optimized loops)
133
+ lab_subjects = []
134
+ for i in range(1, 3):
135
+ subject_code = get_value_if_not_empty(f"LS{i}")
136
+ if subject_code:
137
+ lab_subjects.append({
138
+ 'code': subject_code,
139
+ 'ip1_points': get_numeric_value(f"IP1LS{i}R"),
140
+ 'ip2_points': get_numeric_value(f"IP2LS{i}R"),
141
+ 'ip1_marks': get_numeric_value(f"IP1LS{i}M"),
142
+ 'ip2_marks': get_numeric_value(f"IP2LS{i}M")
143
+ })
144
+
145
+ # Calculate totals using list comprehensions (faster)
146
+ for subject in theory_subjects + lab_subjects:
147
+ subject['total_points'] = subject['ip1_points'] + subject['ip2_points']
148
+ subject['total_marks'] = subject['ip1_marks'] + subject['ip2_marks']
149
+
150
+ # Calculate grand totals
151
+ total_theory_ip1_points = sum(s['ip1_points'] for s in theory_subjects)
152
+ total_theory_ip2_points = sum(s['ip2_points'] for s in theory_subjects)
153
+ total_lab_ip1_points = sum(s['ip1_points'] for s in lab_subjects)
154
+ total_lab_ip2_points = sum(s['ip2_points'] for s in lab_subjects)
155
+
156
+ total_theory_ip1_marks = sum(s['ip1_marks'] for s in theory_subjects)
157
+ total_theory_ip2_marks = sum(s['ip2_marks'] for s in theory_subjects)
158
+ total_lab_ip1_marks = sum(s['ip1_marks'] for s in lab_subjects)
159
+ total_lab_ip2_marks = sum(s['ip2_marks'] for s in lab_subjects)
160
+
161
+ # Build output with clean card-style formatting
162
+ output = []
163
+ output.append("")
164
+ output.append("🏆 INNOVATIVE PRACTICE (IP) SUMMARY")
165
+ output.append("=" * 80)
166
+
167
+ # Theory subjects section
168
+ if theory_subjects:
169
+ output.append(f"\n📚 THEORY SUBJECTS ({len(theory_subjects)} subjects)")
170
+ output.append("-" * 50)
171
+
172
+ for subject in theory_subjects:
173
+ output.append(f"\n🔹 {subject['code']}")
174
+
175
+ # Points section
176
+ if subject['ip1_points'] > 0 or subject['ip2_points'] > 0:
177
+ points_line = " Reward Points: "
178
+ if subject['ip1_points'] > 0:
179
+ points_line += f"IP-1: {subject['ip1_points']:.2f}"
180
+ if subject['ip2_points'] > 0:
181
+ if subject['ip1_points'] > 0:
182
+ points_line += f" | IP-2: {subject['ip2_points']:.2f}"
183
+ else:
184
+ points_line += f"IP-2: {subject['ip2_points']:.2f}"
185
+ points_line += f" | Total: {subject['total_points']:.2f}"
186
+ output.append(points_line)
187
+
188
+ # Marks section
189
+ if subject['ip1_marks'] > 0 or subject['ip2_marks'] > 0:
190
+ marks_line = " Internal Marks: "
191
+ if subject['ip1_marks'] > 0:
192
+ marks_line += f"IP-1: {subject['ip1_marks']:.2f}"
193
+ if subject['ip2_marks'] > 0:
194
+ if subject['ip1_marks'] > 0:
195
+ marks_line += f" | IP-2: {subject['ip2_marks']:.2f}"
196
+ else:
197
+ marks_line += f"IP-2: {subject['ip2_marks']:.2f}"
198
+ marks_line += f" | Total: {subject['total_marks']:.2f}"
199
+ output.append(marks_line)
200
+
201
+ # Lab subjects section
202
+ if lab_subjects:
203
+ output.append(f"\n🧪 LAB SUBJECTS ({len(lab_subjects)} subjects)")
204
+ output.append("-" * 50)
205
+
206
+ for subject in lab_subjects:
207
+ output.append(f"\n🔹 {subject['code']}")
208
+
209
+ # Points section
210
+ if subject['ip1_points'] > 0 or subject['ip2_points'] > 0:
211
+ points_line = " Reward Points: "
212
+ if subject['ip1_points'] > 0:
213
+ points_line += f"IP-1: {subject['ip1_points']:.2f}"
214
+ if subject['ip2_points'] > 0:
215
+ if subject['ip1_points'] > 0:
216
+ points_line += f" | IP-2: {subject['ip2_points']:.2f}"
217
+ else:
218
+ points_line += f"IP-2: {subject['ip2_points']:.2f}"
219
+ points_line += f" | Total: {subject['total_points']:.2f}"
220
+ output.append(points_line)
221
+
222
+ # Marks section
223
+ if subject['ip1_marks'] > 0 or subject['ip2_marks'] > 0:
224
+ marks_line = " Internal Marks: "
225
+ if subject['ip1_marks'] > 0:
226
+ marks_line += f"IP-1: {subject['ip1_marks']:.2f}"
227
+ if subject['ip2_marks'] > 0:
228
+ if subject['ip1_marks'] > 0:
229
+ marks_line += f" | IP-2: {subject['ip2_marks']:.2f}"
230
+ else:
231
+ marks_line += f"IP-2: {subject['ip2_marks']:.2f}"
232
+ marks_line += f" | Total: {subject['total_marks']:.2f}"
233
+ output.append(marks_line)
234
+
235
+ # Summary section
236
+ output.append("\n" + "=" * 80)
237
+ output.append("📊 OVERALL SUMMARY")
238
+ output.append("=" * 80)
239
+
240
+ # Reward Points Summary
241
+ output.append("\n🏅 REWARD POINTS BREAKDOWN:")
242
+ if theory_subjects:
243
+ theory_total = total_theory_ip1_points + total_theory_ip2_points
244
+ output.append(f" Theory Subjects: {theory_total:.2f} points")
245
+ if total_theory_ip1_points > 0:
246
+ output.append(f" ➤ IP-1: {total_theory_ip1_points:.2f}")
247
+ if total_theory_ip2_points > 0:
248
+ output.append(f" ➤ IP-2: {total_theory_ip2_points:.2f}")
249
+
250
+ if lab_subjects:
251
+ lab_total = total_lab_ip1_points + total_lab_ip2_points
252
+ output.append(f" Lab Subjects: {lab_total:.2f} points")
253
+ if total_lab_ip1_points > 0:
254
+ output.append(f" ➤ IP-1: {total_lab_ip1_points:.2f}")
255
+ if total_lab_ip2_points > 0:
256
+ output.append(f" ➤ IP-2: {total_lab_ip2_points:.2f}")
257
+
258
+ grand_total_points = (total_theory_ip1_points + total_theory_ip2_points +
259
+ total_lab_ip1_points + total_lab_ip2_points)
260
+ output.append(f"\n🎯 TOTAL REWARD POINTS: {grand_total_points:.2f}")
261
+
262
+ # Internal Marks Summary
263
+ output.append("\n📝 INTERNAL MARKS BREAKDOWN:")
264
+ if theory_subjects:
265
+ theory_marks_total = total_theory_ip1_marks + total_theory_ip2_marks
266
+ output.append(f" Theory Subjects: {theory_marks_total:.2f} marks")
267
+ if total_theory_ip1_marks > 0:
268
+ output.append(f" ➤ IP-1: {total_theory_ip1_marks:.2f}")
269
+ if total_theory_ip2_marks > 0:
270
+ output.append(f" ➤ IP-2: {total_theory_ip2_marks:.2f}")
271
+
272
+ if lab_subjects:
273
+ lab_marks_total = total_lab_ip1_marks + total_lab_ip2_marks
274
+ output.append(f" Lab Subjects: {lab_marks_total:.2f} marks")
275
+ if total_lab_ip1_marks > 0:
276
+ output.append(f" ➤ IP-1: {total_lab_ip1_marks:.2f}")
277
+ if total_lab_ip2_marks > 0:
278
+ output.append(f" ➤ IP-2: {total_lab_ip2_marks:.2f}")
279
+
280
+ grand_total_marks = (total_theory_ip1_marks + total_theory_ip2_marks +
281
+ total_lab_ip1_marks + total_lab_ip2_marks)
282
+ output.append(f"\n📊 TOTAL INTERNAL MARKS: {grand_total_marks:.2f}")
283
+
284
+ total_subjects = len(theory_subjects) + len(lab_subjects)
285
+ output.append(f"\n📚 TOTAL SUBJECTS: {total_subjects}")
286
+
287
+ output.append("\n" + "=" * 80)
288
+
289
+ # Log the search
290
+ now_ist = datetime.now(ist).strftime("%Y-%m-%d %H:%M:%S")
291
+ print(f"IP Details Searched - Roll No: {roll_no} | Student: {student_name} | Time (IST): {now_ist}")
292
+
293
+ return "\n".join(output)
294
+
295
+ except Exception as e:
296
+ error_msg = f"❌ Error extracting subject details: {str(e)}"
297
+ print(error_msg)
298
+ return error_msg
299
+
300
+
301
+ # Function to get data from a specific sheet
302
+ def get_sheet_data(spreadsheet, gid, sheet_name):
303
+ try:
304
+ sheet = spreadsheet.get_worksheet_by_id(gid)
305
+ all_values = sheet.get_all_values()
306
+
307
+ if not all_values or len(all_values) <= 4:
308
+ print(f"⚠️ {sheet_name} sheet doesn't have enough data")
309
+ return pd.DataFrame()
310
+
311
+ # Use row 4 as headers (the actual column names)
312
+ headers = all_values[4]
313
+ clean_headers = []
314
+ seen_headers = {}
315
+
316
+ for i, header in enumerate(headers):
317
+ if header.strip(): # Non-empty header
318
+ base_header = header.strip()
319
+ # Handle duplicate headers by adding a counter
320
+ if base_header in seen_headers:
321
+ seen_headers[base_header] += 1
322
+ clean_header = f"{base_header}_{seen_headers[base_header]}"
323
+ else:
324
+ seen_headers[base_header] = 0
325
+ clean_header = base_header
326
+ clean_headers.append(clean_header)
327
+ else: # Empty header
328
+ clean_headers.append(f"Empty_Col_{i}")
329
+
330
+ # Create DataFrame starting from row 5 (after headers)
331
+ data_rows = all_values[5:]
332
+ if data_rows:
333
+ df = pd.DataFrame(data_rows, columns=clean_headers)
334
+ # Remove completely empty columns
335
+ df = df.loc[:, (df != '').any(axis=0)]
336
+ print(f"✅ Loaded {len(df)} rows from {sheet_name}")
337
+ return df
338
+ else:
339
+ print(f"⚠️ No data rows found in {sheet_name}")
340
+ return pd.DataFrame()
341
+
342
+ except Exception as e:
343
+ print(f"❌ Error loading {sheet_name}: {str(e)}")
344
+ return pd.DataFrame()
345
+
346
+ # Function to get studentwise reward points data
347
+ def get_studentwise_data(spreadsheet):
348
+ try:
349
+ worksheet = spreadsheet.worksheet("Studentwise Reward Points")
350
+ all_values = worksheet.get_all_values()
351
+
352
+ if len(all_values) < 3:
353
+ print("⚠️ Studentwise Reward Points sheet doesn't have enough data")
354
+ return None
355
+
356
+ print(f"✅ Loaded {len(all_values)} rows from Studentwise Reward Points")
357
+ return all_values
358
+
359
+ except Exception as e:
360
+ print(f"❌ Error loading Studentwise Reward Points: {str(e)}")
361
+ return None
362
+
363
+ # Function to load and cache reward points activity data
364
+ def load_reward_points_data():
365
+ """Load and cache reward points activity data"""
366
+ try:
367
+ # Get the reward points sheet ID from environment
368
+ REWARD_POINTS_SHEET_ID = os.getenv('REWARD_POINTS_SHEET_ID')
369
+
370
+ if not REWARD_POINTS_SHEET_ID:
371
+ print("⚠️ REWARD_POINTS_SHEET_ID not found in environment variables")
372
+ return None
373
+
374
+ client = authorize()
375
+ spreadsheet = client.open_by_key(REWARD_POINTS_SHEET_ID)
376
+ worksheet = spreadsheet.get_worksheet_by_id(1113414351) # Activity Sheet GID
377
+ all_values = worksheet.get_all_values()
378
+
379
+ if not all_values or len(all_values) < 2:
380
+ print("⚠️ Reward Points sheet doesn't have enough data")
381
+ return None
382
+
383
+ # First row is header
384
+ headers = all_values[0]
385
+ df = pd.DataFrame(all_values[1:], columns=headers)
386
+
387
+ if df.empty:
388
+ print("⚠️ Reward Points sheet is empty")
389
+ return None
390
+
391
+ print(f"✅ Loaded {len(df)} rows from Reward Points Entry sheet")
392
+ return df
393
+
394
+ except Exception as e:
395
+ print(f"❌ Error loading Reward Points data: {str(e)}")
396
+ return None
397
+
398
+ # Function to get activity details from cached data in breakdown format
399
+ def get_activity_details(roll_no, reward_points_df):
400
+ """Get activity details for a specific roll number from cached reward points data in breakdown format"""
401
+ try:
402
+ if reward_points_df is None or reward_points_df.empty:
403
+ return ""
404
+
405
+ # Normalize roll number for search
406
+ roll_no_search = roll_no.strip().upper()
407
+
408
+ # Try to find the roll number column
409
+ roll_col = None
410
+ for col in reward_points_df.columns:
411
+ if 'roll' in col.lower() and 'no' in col.lower():
412
+ roll_col = col
413
+ break
414
+
415
+ if not roll_col:
416
+ # Use first column as roll number column
417
+ roll_col = reward_points_df.columns[0]
418
+
419
+ # Create a copy to avoid modifying the original cached data
420
+ df = reward_points_df.copy()
421
+
422
+ # Normalize the roll number column
423
+ df[roll_col] = df[roll_col].astype(str).str.strip().str.upper()
424
+
425
+ # Filter rows by roll number
426
+ student_rows = df[df[roll_col] == roll_no_search]
427
+
428
+ if student_rows.empty:
429
+ # Try partial matching
430
+ partial_matches = df[df[roll_col].str.contains(roll_no_search, na=False)]
431
+ if not partial_matches.empty:
432
+ student_rows = partial_matches
433
+ else:
434
+ return ""
435
+
436
+ if student_rows.empty:
437
+ return ""
438
+
439
+ # Get student info from first record
440
+ first_record = student_rows.iloc[0]
441
+ student_name = first_record.get('NAME OF THE STUDENT', 'N/A')
442
+ student_year = first_record.get('YEAR OF STUDY', 'N/A')
443
+ student_dept = first_record.get('DEPARTMENT', 'N/A')
444
+
445
+ # Calculate activity summary by type
446
+ activity_summary = {}
447
+ activity_count = {}
448
+ total_points = 0
449
+
450
+ for _, row in student_rows.iterrows():
451
+ activity_type = str(row.get('Activity Type', 'N/A'))
452
+ reward_points = str(row.get('Reward Points', '0'))
453
+
454
+ # Convert points to float
455
+ try:
456
+ points_val = float(reward_points.replace(',', '')) if reward_points else 0
457
+ total_points += points_val
458
+ except:
459
+ points_val = 0
460
+
461
+ # Track activity summary
462
+ if activity_type in activity_summary:
463
+ activity_summary[activity_type] += points_val
464
+ activity_count[activity_type] += 1
465
+ else:
466
+ activity_summary[activity_type] = points_val
467
+ activity_count[activity_type] = 1
468
+
469
+ # Format output in breakdown style
470
+ output = []
471
+
472
+ # Define all possible activity categories in order
473
+ activity_categories = [
474
+ "INITIAL POINTS / CARRY-OVER",
475
+ "TECHNICAL EVENTS",
476
+ "SKILLS",
477
+ "ASSIGNMENTS",
478
+ "INTERVIEW",
479
+ "TECHNICAL SOCIETY ACTIVITIES",
480
+ "P SKILL",
481
+ "TAC",
482
+ "SPECIAL LAB INITIATIVES",
483
+ "EXTRA-CURRICULAR ACTIVITIES",
484
+ "STUDENT INITIATIVES",
485
+ "EXTERNAL EVENTS",
486
+ "EXTERNAL TECHNICAL EVENTS"
487
+ ]
488
+
489
+ # Map activity types to standard categories (case-insensitive matching)
490
+ category_mapping = {}
491
+ for activity_type in activity_summary.keys():
492
+ activity_upper = activity_type.upper()
493
+ matched_category = None
494
+
495
+ # Try exact matching first
496
+ for category in activity_categories:
497
+ if category.upper() in activity_upper or activity_upper in category.upper():
498
+ matched_category = category
499
+ break
500
+
501
+ # If no exact match, use the original activity type
502
+ if not matched_category:
503
+ matched_category = activity_type
504
+
505
+ category_mapping[activity_type] = matched_category
506
+
507
+ # Group activities by mapped categories
508
+ final_summary = {}
509
+ final_count = {}
510
+
511
+ for activity_type, points in activity_summary.items():
512
+ category = category_mapping[activity_type]
513
+ if category in final_summary:
514
+ final_summary[category] += points
515
+ final_count[category] += activity_count[activity_type]
516
+ else:
517
+ final_summary[category] = points
518
+ final_count[category] = activity_count[activity_type]
519
+
520
+ # Display all categories (including zeros)
521
+ for category in activity_categories:
522
+ count = final_count.get(category, 0)
523
+ points = final_summary.get(category, 0.0)
524
+
525
+
526
+ # Add any categories not in the standard list
527
+ for category, points in final_summary.items():
528
+ if category not in activity_categories:
529
+ count = final_count.get(category, 0)
530
+ output.append(f"📋 **{category}**")
531
+ output.append(f" Count: {count} | Points: {points:.2f}")
532
+
533
+ # Add summary totals
534
+ output.append("")
535
+
536
+ # Add detailed activity list if needed
537
+ if len(student_rows) <= 20: # Only show detailed list for reasonable number of activities
538
+ output.append("📋 DETAILED ACTIVITY LIST")
539
+ output.append("=" * 80)
540
+
541
+ for idx, (_, row) in enumerate(student_rows.iterrows(), 1):
542
+ activity_type = str(row.get('Activity Type', 'N/A'))
543
+ activity_name = str(row.get('Activity Name', 'N/A'))
544
+ reward_points = str(row.get('Reward Points', '0'))
545
+
546
+ try:
547
+ points_val = float(reward_points.replace(',', '')) if reward_points else 0
548
+ except:
549
+ points_val = 0
550
+
551
+ # Truncate long names for display
552
+ display_name = activity_name[:50] + "..." if len(activity_name) > 63 else activity_name
553
+ output.append(f"{idx:2d}. {activity_type}: {display_name} - {points_val:.2f} pts")
554
+
555
+ output.append("=" * 80)
556
+
557
+ return "\n".join(output)
558
+
559
+ except Exception as e:
560
+ print(f"❌ Error fetching activity details: {str(e)}")
561
+ return ""
562
+
563
+ # Function to get details sheet information
564
+ def get_details_info(spreadsheet):
565
+ try:
566
+ details_sheet = spreadsheet.get_worksheet_by_id(847680829)
567
+ all_values = details_sheet.get_all_values()
568
+
569
+ if not all_values:
570
+ return None
571
+
572
+ # Use row 4 as headers
573
+ headers = all_values[4]
574
+ clean_headers = []
575
+ for i, header in enumerate(headers):
576
+ if header.strip():
577
+ clean_headers.append(header.strip())
578
+ else:
579
+ clean_headers.append(f"Empty_Col_{i}")
580
+
581
+ # Get data rows after header
582
+ data_rows = all_values[5:]
583
+
584
+ if data_rows:
585
+ df = pd.DataFrame(data_rows, columns=clean_headers)
586
+ df = df.loc[:, (df != '').any(axis=0)]
587
+
588
+ details_info = {}
589
+
590
+ # Extract specific information
591
+ for idx in range(len(df)):
592
+ student_data = df.iloc[idx]
593
+ year_value = str(student_data.get('YEAR', '')).strip()
594
+
595
+ # Get Average Reward Points
596
+ if 'AVERAGE REWARD POINT' in year_value:
597
+ details_info['average_points'] = {
598
+ 'I': student_data.get('I', ''),
599
+ 'II': student_data.get('II', ''),
600
+ 'II L': student_data.get('II L', ''),
601
+ 'III': student_data.get('III', ''),
602
+ 'IV': student_data.get('IV', '')
603
+ }
604
+
605
+ # Get IP 2 Redemption Dates
606
+ elif 'Last Day for IP 2 Redemption Duration' in str(student_data.get('Redemption Dates', '')):
607
+ details_info['ip2_redemption'] = {
608
+ 'S1': student_data.get('S1', ''),
609
+ 'S2': student_data.get('S2', ''),
610
+ 'S3': student_data.get('S3', ''),
611
+ 'S4': student_data.get('S4', ''),
612
+ 'S5': student_data.get('S5', ''),
613
+ 'S6': student_data.get('S6', ''),
614
+ 'S7': student_data.get('S7', ''),
615
+ 'S8': student_data.get('S8', '')
616
+ }
617
+
618
+ # Get IP 1 Redemption Dates
619
+ elif 'Last Day for IP 1 Redemption Duration' in str(student_data.get('Redemption Dates', '')):
620
+ details_info['ip1_redemption'] = {
621
+ 'S1': student_data.get('S1', ''),
622
+ 'S2': student_data.get('S2', ''),
623
+ 'S3': student_data.get('S3', ''),
624
+ 'S4': student_data.get('S4', ''),
625
+ 'S5': student_data.get('S5', ''),
626
+ 'S6': student_data.get('S6', ''),
627
+ 'S7': student_data.get('S7', ''),
628
+ 'S8': student_data.get('S8', '')
629
+ }
630
+
631
+ # Get Last Updated Information
632
+ elif 'POINTS LAST UPDATED' in year_value:
633
+ details_info['last_updated'] = year_value
634
+
635
+ return details_info
636
+
637
+ except Exception as e:
638
+ print(f"❌ Error loading Details Sheet: {str(e)}")
639
+ return None
640
+
641
+ # Initialize global variables
642
+ print("🚀 Initializing application...")
643
+ client = authorize()
644
+
645
+ # Get spreadsheet IDs from environment variables
646
+ MAIN_SHEET_ID = os.getenv('GOOGLE_SHEET_ID') # Your main sheets (20 sheets)
647
+ STUDENTWISE_SHEET_ID = os.getenv('STUDENTWISE_SHEET_ID') # Studentwise Reward Points sheet
648
+
649
+ if not MAIN_SHEET_ID:
650
+ raise ValueError("GOOGLE_SHEET_ID environment variable is required")
651
+
652
+ # Open both spreadsheets
653
+ main_spreadsheet = client.open_by_key(MAIN_SHEET_ID)
654
+ studentwise_spreadsheet = client.open_by_key(STUDENTWISE_SHEET_ID)
655
+
656
+ # Load data from all sheets (Original 3 + New 17 = 20 sheets total)
657
+ sheet_configs = [
658
+ # Original sheets
659
+ {"gid": 688907204, "name": "AIML"},
660
+ {"gid": 451167295, "name": "AIDS"},
661
+ {"gid": 1955995189, "name": "Sheet_3"},
662
+ {"gid": 821473193, "name": "Sheet_4"},
663
+ {"gid": 1798819643, "name": "Sheet_5"},
664
+ {"gid": 1057532042, "name": "Sheet_6"},
665
+ {"gid": 1848020834, "name": "Sheet_7"},
666
+ {"gid": 48570283, "name": "Sheet_8"},
667
+ {"gid": 559332743, "name": "Sheet_9"},
668
+ {"gid": 1481375682, "name": "Sheet_10"},
669
+ {"gid": 1136877763, "name": "Sheet_11"},
670
+ {"gid": 510521423, "name": "Sheet_12"},
671
+ {"gid": 1936618, "name": "Sheet_13"},
672
+ {"gid": 91989289, "name": "Sheet_14"},
673
+ {"gid": 30073516, "name": "Sheet_15"},
674
+ {"gid": 857542309, "name": "Sheet_16"},
675
+ {"gid": 790318539, "name": "Sheet_17"},
676
+ {"gid": 587090068, "name": "Sheet_18"},
677
+ {"gid": 260192612, "name": "Sheet_19"},
678
+ {"gid": 400900059, "name": "Sheet_20"}
679
+ ]
680
+
681
+ # GLOBAL DATA CACHE WITH 12-HOUR AUTO-REFRESH
682
+ data_cache = {
683
+ "combined_df": None,
684
+ "studentwise_data": None,
685
+ "details_info": None,
686
+ "reward_points_df": None,
687
+ "last_update": None,
688
+ "cache_duration_hours": 12, # 12 hours cache
689
+ "is_loading": False,
690
+ "roll_index": {}, # Add this
691
+ "studentwise_index": {}, # Add this
692
+ "reward_points_index": {} # Add this
693
+ }
694
+
695
+ def load_all_data():
696
+ """Load and cache all data from Google Sheets (including reward points data)"""
697
+ global data_cache
698
+
699
+ if data_cache["is_loading"]:
700
+ print("⏳ Data loading already in progress...")
701
+ return (data_cache["combined_df"], data_cache["studentwise_data"],
702
+ data_cache["details_info"], data_cache["reward_points_df"])
703
+
704
+ data_cache["is_loading"] = True
705
+ print(f"🔄 Loading fresh data from {len(sheet_configs)} Google Sheets + Reward Points sheet...")
706
+ start_time = time.time()
707
+
708
+ try:
709
+ # Load all sheet data from main spreadsheet
710
+ all_dataframes = []
711
+ for config in sheet_configs:
712
+ df = get_sheet_data(main_spreadsheet, config["gid"], config["name"])
713
+ if not df.empty:
714
+ df['Source_Sheet'] = config["name"]
715
+ all_dataframes.append(df)
716
+ print(f" 📋 {config['name']}: {len(df)} rows")
717
+
718
+ # Combine dataframes
719
+ if all_dataframes:
720
+ try:
721
+ combined_df = pd.concat(all_dataframes, ignore_index=True, sort=False)
722
+ print(f"✅ Successfully combined {len(combined_df)} records from {len(all_dataframes)} sheets")
723
+ except Exception as e:
724
+ print(f"❌ Error combining dataframes: {str(e)}")
725
+ print("🔄 Trying alternative approach...")
726
+
727
+ # Alternative approach: standardize columns first
728
+ standard_columns = ['SL. NO.', 'YEAR', 'ROLL NO.', 'STUDENT NAME', 'COURSE CODE',
729
+ 'DEPARTMENT', 'MENTOR NAME', 'CUMULATIVE REWARD POINTS',
730
+ 'REEDEMED POINTS', 'BALANCE POINTS', 'Source_Sheet']
731
+
732
+ standardized_dfs = []
733
+ for df in all_dataframes:
734
+ new_df = pd.DataFrame()
735
+ for col in standard_columns:
736
+ if col == 'Source_Sheet':
737
+ new_df[col] = df.get('Source_Sheet', '')
738
+ else:
739
+ # Try to find matching column
740
+ found_col = None
741
+ for df_col in df.columns:
742
+ if col.upper() in df_col.upper() or df_col.upper() in col.upper():
743
+ found_col = df_col
744
+ break
745
+
746
+ if found_col:
747
+ new_df[col] = df[found_col]
748
+ else:
749
+ new_df[col] = ''
750
+
751
+ standardized_dfs.append(new_df)
752
+
753
+ combined_df = pd.concat(standardized_dfs, ignore_index=True, sort=False)
754
+ print(f"✅ Alternative approach successful: {len(combined_df)} records combined")
755
+ else:
756
+ combined_df = pd.DataFrame()
757
+ print("❌ No data found in any sheets")
758
+
759
+ # Load studentwise reward points data from separate spreadsheet
760
+ studentwise_data = get_studentwise_data(studentwise_spreadsheet)
761
+
762
+ # Load details info from main spreadsheet
763
+ details_info = get_details_info(main_spreadsheet)
764
+
765
+ # Load reward points activity data
766
+ reward_points_df = load_reward_points_data()
767
+
768
+ # Update cache
769
+ data_cache["combined_df"] = combined_df
770
+ data_cache["studentwise_data"] = studentwise_data
771
+ data_cache["details_info"] = details_info
772
+ data_cache["reward_points_df"] = reward_points_df
773
+ data_cache["last_update"] = datetime.now()
774
+
775
+ # CREATE INDEXES AFTER DATA LOADING
776
+ create_indexes()
777
+
778
+ load_time = time.time() - start_time
779
+ print(f"⏱️ Data loaded, indexed and cached in {load_time:.2f} seconds")
780
+ print(f"📊 Next auto-refresh in {data_cache['cache_duration_hours']} hours")
781
+
782
+ return combined_df, studentwise_data, details_info, reward_points_df
783
+
784
+ except Exception as e:
785
+ print(f"❌ Error loading data: {str(e)}")
786
+ return (data_cache.get("combined_df", pd.DataFrame()),
787
+ data_cache.get("studentwise_data", None),
788
+ data_cache.get("details_info", None),
789
+ data_cache.get("reward_points_df", None))
790
+
791
+ finally:
792
+ data_cache["is_loading"] = False
793
+
794
+ def create_indexes():
795
+ """Create indexes for faster search performance"""
796
+ global data_cache
797
+
798
+ if data_cache["combined_df"] is None or data_cache["combined_df"].empty:
799
+ print("⚠️ No data available for indexing")
800
+ return
801
+
802
+ try:
803
+ # Find roll number column
804
+ roll_column = None
805
+ for col in data_cache["combined_df"].columns:
806
+ if 'roll' in col.lower() and 'no' in col.lower():
807
+ roll_column = col
808
+ break
809
+
810
+ if roll_column:
811
+ # Create roll number index (normalize to uppercase)
812
+ data_cache["roll_index"] = {}
813
+ for idx, row in data_cache["combined_df"].iterrows():
814
+ roll_no = str(row[roll_column]).strip().upper()
815
+ if roll_no and len(roll_no) > 3: # Basic validation
816
+ data_cache["roll_index"][roll_no] = idx
817
+
818
+ # Create studentwise data index - IMPROVED VERSION
819
+ if data_cache["studentwise_data"] and len(data_cache["studentwise_data"]) > 1:
820
+ data_cache["studentwise_index"] = {}
821
+ headers = data_cache["studentwise_data"][0]
822
+
823
+ # Find roll number column in headers
824
+ roll_col_idx = None
825
+ for i, header in enumerate(headers):
826
+ if 'roll' in str(header).lower():
827
+ roll_col_idx = i
828
+ break
829
+
830
+ if roll_col_idx is None:
831
+ roll_col_idx = 1 # Fallback to second column
832
+
833
+ for i, row in enumerate(data_cache["studentwise_data"][1:], 1): # Skip header
834
+ if len(row) > roll_col_idx:
835
+ roll_no = str(row[roll_col_idx]).strip().upper()
836
+ if roll_no and len(roll_no) > 5: # Basic validation
837
+ data_cache["studentwise_index"][roll_no] = i
838
+
839
+ roll_count = len(data_cache.get('roll_index', {}))
840
+ studentwise_count = len(data_cache.get('studentwise_index', {}))
841
+ print(f"✅ Indexes created: {roll_count} main records, {studentwise_count} studentwise records indexed")
842
+
843
+ except Exception as e:
844
+ print(f"❌ Error creating indexes: {str(e)}")
845
+
846
+
847
+ def get_cached_data():
848
+ """Get data from cache or refresh if 12 hours have passed"""
849
+ now = datetime.now()
850
+
851
+ # Check if cache is empty or expired (12 hours)
852
+ if (data_cache["last_update"] is None or
853
+ data_cache["combined_df"] is None or
854
+ (now - data_cache["last_update"]).total_seconds() > (data_cache["cache_duration_hours"] * 3600)):
855
+
856
+ print("🔄 Cache expired or empty, loading fresh data...")
857
+ return load_all_data()
858
+ else:
859
+ cache_age_hours = (now - data_cache["last_update"]).total_seconds() / 3600
860
+ print(f"🚀 Using cached data (age: {cache_age_hours:.1f} hours)")
861
+ return (data_cache["combined_df"], data_cache["studentwise_data"],
862
+ data_cache["details_info"], data_cache["reward_points_df"])
863
+
864
+ def auto_refresh_worker():
865
+ """Background worker to auto-refresh data every 12 hours"""
866
+ while True:
867
+ try:
868
+ # Sleep for 12 hours (43200 seconds)
869
+ time.sleep(43200)
870
+ print("⏰ 12-hour auto-refresh triggered...")
871
+ load_all_data()
872
+ except Exception as e:
873
+ print(f"❌ Auto-refresh error: {str(e)}")
874
+ # If error, wait 1 hour before trying again
875
+ time.sleep(3600)
876
+
877
+ def details_sheet_watcher():
878
+ """Background watcher: checks every 30 seconds (drift-free, IST logs) if 'POINTS LAST UPDATED' changed"""
879
+ import time
880
+ from datetime import datetime
881
+
882
+ last_seen_update = None
883
+ consecutive_errors = 0
884
+ max_errors = 3
885
+
886
+ watcher_client = None
887
+ watcher_spreadsheet = None
888
+ watcher_sheet = None
889
+ last_connection_time = None
890
+ connection_duration = 2700 # 45 minutes
891
+
892
+ # Specific cell coordinates for "POINTS LAST UPDATED"
893
+ TARGET_ROW = 16
894
+ TARGET_COL = 2
895
+ CELL_RANGE = f"R{TARGET_ROW}C{TARGET_COL}" # Row 16, Column 2
896
+
897
+ check_interval = 30 # seconds
898
+ next_check = time.time()
899
+
900
+ print(f"👀 Starting optimized details sheet watcher (monitoring cell R{TARGET_ROW}C{TARGET_COL} every {check_interval} seconds)...")
901
+
902
+ while True:
903
+ start_time = time.time()
904
+ try:
905
+ if data_cache["is_loading"]:
906
+ print("⏳ Watcher: Skipping check – data loading in progress")
907
+ else:
908
+ current_time = datetime.now()
909
+ if (watcher_client is None or
910
+ watcher_spreadsheet is None or
911
+ watcher_sheet is None or
912
+ last_connection_time is None or
913
+ (current_time - last_connection_time).total_seconds() > connection_duration):
914
+
915
+ print("🔄 Watcher: Refreshing connection...")
916
+ watcher_client = authorize()
917
+ watcher_spreadsheet = watcher_client.open_by_key(os.getenv('GOOGLE_SHEET_ID'))
918
+ watcher_sheet = watcher_spreadsheet.get_worksheet_by_id(847680829) # Details sheet GID
919
+ last_connection_time = current_time
920
+ print(f"✅ Watcher: Connection refreshed (monitoring cell R{TARGET_ROW}C{TARGET_COL})")
921
+
922
+ # Get only the specific cell content
923
+ try:
924
+ # Use batch_get to get specific cell efficiently
925
+ cell_value = watcher_sheet.cell(TARGET_ROW, TARGET_COL).value
926
+ current_update = str(cell_value).strip() if cell_value else ""
927
+
928
+ now_ist = datetime.now(ist).strftime("%Y-%m-%d %H:%M:%S")
929
+
930
+ if last_seen_update is None:
931
+ last_seen_update = current_update
932
+ print(f"🕒 Watcher: Monitoring established ({now_ist})")
933
+ print(f" Target cell R{TARGET_ROW}C{TARGET_COL}: {current_update[:80]}...")
934
+ elif current_update != last_seen_update:
935
+ print(f"🔄 CHANGE DETECTED! ({now_ist})")
936
+ print(f" Old: {last_seen_update[:60]}...")
937
+ print(f" New: {current_update[:60]}...")
938
+ last_seen_update = current_update
939
+
940
+ if not data_cache["is_loading"]:
941
+ print("🔄 Reloading data directly...")
942
+ load_all_data()
943
+ print(f"✅ Data reload completed successfully ({now_ist})")
944
+ else:
945
+ print(f"⏳ Data already loading, skipping reload ({now_ist})")
946
+ else:
947
+ # Log heartbeat every 5 minutes in IST
948
+ current_ist = datetime.now(ist)
949
+ if current_ist.minute % 5 == 0 and current_ist.second < check_interval:
950
+ print(f"✅ Watcher: No changes detected in R{TARGET_ROW}C{TARGET_COL} ({current_ist.strftime('%H:%M:%S')})")
951
+
952
+ except Exception as cell_error:
953
+ print(f"⚠️ Error reading cell R{TARGET_ROW}C{TARGET_COL}: {str(cell_error)[:100]}")
954
+ consecutive_errors += 1
955
+
956
+ if consecutive_errors > 0 and not any([watcher_client is None, watcher_spreadsheet is None, watcher_sheet is None]):
957
+ print(f"✅ Watcher: Connection restored (cleared {consecutive_errors} errors)")
958
+ consecutive_errors = 0
959
+
960
+ except Exception as e:
961
+ consecutive_errors += 1
962
+ print(f"⚠️ Watcher error #{consecutive_errors}: {str(e)[:120]}")
963
+ watcher_client = None
964
+ watcher_spreadsheet = None
965
+ watcher_sheet = None
966
+ if consecutive_errors >= max_errors:
967
+ print("❌ Too many watcher errors, waiting 5 minutes before retry...")
968
+ time.sleep(300)
969
+ consecutive_errors = 0
970
+
971
+ # Drift-free timing — ensures consistent 30s intervals
972
+ next_check += check_interval
973
+ sleep_time = max(0, next_check - time.time())
974
+ time.sleep(sleep_time)
975
+
976
+
977
+ def get_detailed_student_points(roll_no, studentwise_data):
978
+ """Get detailed points breakdown from studentwise data with indexing"""
979
+ if not studentwise_data or len(studentwise_data) < 3:
980
+ return ""
981
+
982
+ # Get headers first - ALWAYS needed
983
+ headers = studentwise_data[0]
984
+
985
+ # USE INDEX FOR FASTER SEARCH
986
+ student_found = None
987
+ roll_no_upper = roll_no.strip().upper()
988
+
989
+ if roll_no_upper in data_cache.get("studentwise_index", {}):
990
+ row_idx = data_cache["studentwise_index"][roll_no_upper]
991
+ student_found = studentwise_data[row_idx]
992
+ else:
993
+ # Fallback to original search
994
+ for row in studentwise_data[2:]: # Skip header
995
+ if len(row) > 1 and row[1].strip().upper() == roll_no_upper:
996
+ student_found = row
997
+ break
998
+
999
+ if not student_found:
1000
+ return ""
1001
+
1002
+ # Create student_data dictionary
1003
+ student_data = {}
1004
+ for i, header in enumerate(headers):
1005
+ student_data[header] = student_found[i] if i < len(student_found) else ""
1006
+
1007
+ output = []
1008
+ output.append("")
1009
+ output.append("🏆 REWARD POINTS BREAKDOWN")
1010
+ output.append("=" * 80)
1011
+
1012
+ # Using a different approach - no column headers, just data with clear labels
1013
+ categories = [
1014
+ ("INITIAL POINTS / CARRY-OVER", "-", "Initial Points"),
1015
+ ("TECHNICAL EVENTS", "Technical Events Count", "Technical Events Points"),
1016
+ ("SKILLS", "Skill Count", "Skill Points"),
1017
+ ("ASSIGNMENTS", "Assignement Count", "Assignment Points"),
1018
+ ("INTERVIEW", "Interview Count", "Interview Points"),
1019
+ ("TECHNICAL SOCIETY ACTIVITIES", "TECHNICAL SOCIETY ACTIVITIES Count", "TECHNICAL SOCIETY ACTIVITIES Points"),
1020
+ ("P SKILL", "P Skill Count", "P Skill Points"),
1021
+ ("TAC", "TAC Count", "TAC Points"),
1022
+ ("SPECIAL LAB INITIATIVES", "Special Lab Initiatives Count", "Special Lab Initiatives Points"),
1023
+ ("EXTRA-CURRICULAR ACTIVITIES", "EXTRA-CURRICULAR ACTIVITIES COUNT", "EXTRA-CURRICULAR ACTIVITIES POINTS"),
1024
+ ("STUDENT INITIATIVES", "STUDENT INITIATIVES COUNT", "STUDENT INITIATIVES POINTS"),
1025
+ ("EXTERNAL EVENTS", "EXTERNAL EVENTS COUNT", "EXTERNAL EVENTS POINTS"),
1026
+ ("TOTAL (2023-2024 EVEN)", "Total Count", "Total Points"),
1027
+ ("PENALTIES", "Negative Count", "Negative Points"),
1028
+ ("CUMULATIVE POINTS", "-", "Cumulative Points"),
1029
+ ("INNOVATIVE PRACTICE - 1 (IP-1)", "-", "IP 1 R"),
1030
+ ("INNOVATIVE PRACTICE - 2 (IP-2)", "-", "IP 2 R"),
1031
+ ("REDEEMED POINTS", "-", "Redeemed Points"),
1032
+ ("BALANCE POINTS", "-", "Balance Points"),
1033
+ ("CARRY FORWARD TO NEXT SEMESTER", "-", "EL. CA. FR. POINTS")
1034
+ ]
1035
+
1036
+ total_earned = 0
1037
+ total_redeemed = 0
1038
+
1039
+ for idx, (category_name, count_key, points_key) in enumerate(categories):
1040
+ count_val = "-" if count_key == "-" else student_data.get(count_key, "0")
1041
+ points_val = student_data.get(points_key, "0.00")
1042
+
1043
+ try:
1044
+ if points_val and points_val != "-":
1045
+ points_float = float(str(points_val).replace(',', ''))
1046
+ points_val = f"{points_float:.2f}"
1047
+ if points_key == "Cumulative Points":
1048
+ total_earned = points_float
1049
+ elif points_key == "Redeemed Points":
1050
+ total_redeemed = points_float
1051
+ except:
1052
+ pass
1053
+
1054
+ # Alternative format - more readable
1055
+ output.append(f"📋 **{category_name}**")
1056
+ output.append(f" Count: {count_val} | Points: {points_val}")
1057
+
1058
+ output.append("=" * 80)
1059
+
1060
+ return "\n".join(output)
1061
+
1062
+
1063
+ def calculate_yearwise_average_points():
1064
+ """Calculate year-wise average points from the combined data"""
1065
+ combined_df, _, _, _ = get_cached_data()
1066
+ if combined_df.empty:
1067
+ return "❌ No data available to calculate averages"
1068
+
1069
+ # Try to find columns automatically
1070
+ year_col, points_col = None, None
1071
+ for col in combined_df.columns:
1072
+ if 'year' in col.lower():
1073
+ year_col = col
1074
+ if 'balance' in col.lower() and 'points' in col.lower():
1075
+ points_col = col
1076
+
1077
+ if not year_col or not points_col:
1078
+ return "⚠️ Required columns not found in the data"
1079
+
1080
+ # Create a copy for processing
1081
+ df = combined_df[[year_col, points_col]].copy()
1082
+
1083
+ # Clean and convert points data
1084
+ df[points_col] = pd.to_numeric(
1085
+ df[points_col].astype(str).str.replace(',', '').str.strip(),
1086
+ errors='coerce'
1087
+ )
1088
+ df.dropna(subset=[points_col], inplace=True)
1089
+
1090
+ # Remove rows with zero or negative points for more accurate averages
1091
+ df = df[df[points_col] > 0]
1092
+
1093
+ if df.empty:
1094
+ return "⚠️ No valid points data found for calculation"
1095
+
1096
+ # Group by year
1097
+ yearwise = df.groupby(year_col)[points_col].agg(['sum', 'count', 'mean', 'min', 'max']).reset_index()
1098
+ yearwise['average'] = yearwise['mean'] # Use pandas mean for consistency
1099
+
1100
+ # Format the output neatly
1101
+ output = []
1102
+ output.append("=" * 90)
1103
+ output.append(" ")
1104
+ output.append("📊 YEAR-WISE AVERAGE REWARD POINTS (CALCULATED)")
1105
+ output.append("-" * 90)
1106
+
1107
+ for _, row in yearwise.iterrows():
1108
+ year = str(row[year_col]).strip()
1109
+ total_points = f"{row['sum']:.0f}"
1110
+ count = int(row['count'])
1111
+ avg = f"{row['average']:.2f}"
1112
+ min_pts = f"{row['min']:.0f}"
1113
+ max_pts = f"{row['max']:.0f}"
1114
+
1115
+ output.append(f"Year {year:<10} {avg}")
1116
+
1117
+ output.append("=" * 90)
1118
+ return "\n".join(output)
1119
+
1120
+ # Load initial data
1121
+ print("📊 Loading initial data...")
1122
+ load_all_data()
1123
+
1124
+ # Start background auto-refresh thread
1125
+ refresh_thread = threading.Thread(target=auto_refresh_worker, daemon=True)
1126
+ refresh_thread.start()
1127
+ print("🕒 Auto-refresh thread started (updates every 12 hours)")
1128
+
1129
+ # Start details sheet watcher thread
1130
+ watcher_thread = threading.Thread(target=details_sheet_watcher, daemon=True)
1131
+ watcher_thread.start()
1132
+ print("👀 Details sheet watcher started (checks every 1 minute)")
1133
+
1134
+ # Function to search student with cached data
1135
+ def search_student(roll_no):
1136
+ if not roll_no.strip():
1137
+ return "❌ Please enter a roll number"
1138
+
1139
+ # Convert roll number to uppercase for consistent searching
1140
+ roll_no = roll_no.strip().upper()
1141
+
1142
+ # Get cached data (fast response, auto-refreshes every 12 hours)
1143
+ combined_df, studentwise_data, details_info, reward_points_df = get_cached_data()
1144
+
1145
+ if combined_df.empty:
1146
+ return "❌ No data available from Google Sheets"
1147
+
1148
+ # USE INDEX FOR FASTER SEARCH
1149
+ indexed_search = False
1150
+ if roll_no in data_cache.get("roll_index", {}):
1151
+ row_idx = data_cache["roll_index"][roll_no]
1152
+ record = combined_df.iloc[row_idx].to_dict()
1153
+ student_name = str(record.get('STUDENT NAME', 'Unknown')).strip()
1154
+ student_year = str(record.get('YEAR', '')).strip()
1155
+ indexed_search = True
1156
+ else:
1157
+ # Fallback to original search method
1158
+ indexed_search = False
1159
+ roll_column = None
1160
+ for col in combined_df.columns:
1161
+ if 'roll' in col.lower() and 'no' in col.lower():
1162
+ roll_column = col
1163
+ break
1164
+
1165
+
1166
+ if roll_column is None:
1167
+ return f"❌ Roll number column not found. Available columns: {list(combined_df.columns)}"
1168
+
1169
+ student = combined_df[combined_df[roll_column].astype(str).str.strip().str.upper() == roll_no]
1170
+ if student.empty:
1171
+ return f"❌ Roll No '{roll_no}' not found in any sheet"
1172
+
1173
+ record = student.iloc[0].to_dict()
1174
+ student_name = str(record.get('STUDENT NAME', 'Unknown')).strip()
1175
+ student_year = str(record.get('YEAR', '')).strip()
1176
+
1177
+ now_ist = datetime.now(ist).strftime("%Y-%m-%d %H:%M:%S")
1178
+ # Log to see which roll number and student name is searched by user
1179
+ search_method = "INDEXED" if indexed_search else "SEQUENTIAL"
1180
+ print(f"{search_method} Roll No Searched: {roll_no} | Student Name: {student_name} | Time (IST): {now_ist}")
1181
+
1182
+ # Format output - Simplified version
1183
+ output = []
1184
+ output.append(f"Hello {student_name} 👋")
1185
+ output.append("=" * 80)
1186
+ output.append("YOUR DETAILS")
1187
+ output.append("=" * 80)
1188
+
1189
+ # Main student details
1190
+ main_fields = ['ROLL NO.', 'STUDENT NAME', 'YEAR', 'DEPARTMENT', 'MENTOR NAME',
1191
+ 'CUMULATIVE REWARD POINTS', 'REEDEMED POINTS', 'BALANCE POINTS']
1192
+
1193
+ for field in main_fields:
1194
+ value = record.get(field, '')
1195
+ if str(value).strip():
1196
+ output.append(f"{field:<25}: {value}")
1197
+
1198
+ # Get student's current points (clean numeric value)
1199
+ try:
1200
+ student_points_str = str(record.get('BALANCE POINTS', '')).replace(',', '').strip()
1201
+ student_points = float(student_points_str) if student_points_str else 0
1202
+ except:
1203
+ student_points = 0
1204
+
1205
+ # Add year-specific average points and analysis
1206
+ if details_info and 'average_points' in details_info:
1207
+ output.append("\n" + "=" * 80)
1208
+ output.append(f"AVERAGE REWARD POINTS FOR YEAR {student_year}")
1209
+ output.append("=" * 80)
1210
+
1211
+ if student_year in details_info['average_points']:
1212
+ avg_points_str = details_info['average_points'][student_year]
1213
+ try:
1214
+ avg_points = float(avg_points_str) if avg_points_str else 0
1215
+ except:
1216
+ avg_points = 0
1217
+
1218
+ if avg_points > 0:
1219
+ output.append(f"Average Points for Year {student_year:<8}: {avg_points_str}")
1220
+
1221
+ # Calculate difference and provide guidance
1222
+ points_difference = avg_points - student_points
1223
+
1224
+ if points_difference > 0:
1225
+ # Student is below average
1226
+ output.append(f"\n🎯 POINTS NEEDED TO REACH AVERAGE: {points_difference:.0f} points")
1227
+ output.append("\n💡 WAYS TO EARN POINTS:")
1228
+ output.append(" • PS Activities")
1229
+ output.append(" • TAC")
1230
+ output.append(" • Hackathons / Technical Events")
1231
+ output.append(" • Project Competitions")
1232
+ output.append(" • Refer Reward points Breakdown for more details")
1233
+ else:
1234
+ # Student is at or above average
1235
+ output.append(f"\n🎉 EXCELLENT! You are {abs(points_difference):.0f} points ABOVE the average!")
1236
+ output.append(" Keep up the great work! 🌟")
1237
+ output.append(" Refer Reward points Breakdown for more details")
1238
+
1239
+ # Add individual activity details from cached reward points data
1240
+ activity_details = get_activity_details(roll_no, reward_points_df)
1241
+ if activity_details:
1242
+ output.append(activity_details)
1243
+
1244
+ # Add detailed points breakdown from studentwise data
1245
+ detailed_points = get_detailed_student_points(roll_no, studentwise_data)
1246
+ if detailed_points:
1247
+ output.append(detailed_points)
1248
+
1249
+ # Add last updated info
1250
+ if details_info and 'last_updated' in details_info:
1251
+ output.append("\n" + "-" * 60)
1252
+ output.append("LAST UPDATE INFO")
1253
+ output.append("-" * 60)
1254
+ output.append(details_info['last_updated'])
1255
+
1256
+ # Show cache info
1257
+ if data_cache["last_update"]:
1258
+ cache_age = datetime.now() - data_cache["last_update"]
1259
+ hours = cache_age.total_seconds() / 3600
1260
+ next_refresh_hours = 12 - hours
1261
+ output.append(f"\n📊 Data age: {hours:.1f} hours")
1262
+ if next_refresh_hours > 0:
1263
+ output.append(f"⏰ Next auto-refresh in: {next_refresh_hours:.1f} hours")
1264
+ else:
1265
+ output.append("⏰ Auto-refresh due now")
1266
+
1267
+ output.append("\n" + "=" * 80)
1268
+
1269
+ return "\n".join(output)
1270
+
1271
+ # Function to get system information
1272
+ def get_system_info():
1273
+ combined_df, studentwise_data, details_info, reward_points_df = get_cached_data()
1274
+
1275
+ if not details_info:
1276
+ return "❌ No system information available"
1277
+
1278
+ output = []
1279
+ output.append("=" * 80)
1280
+ output.append("SYSTEM INFORMATION")
1281
+ output.append("=" * 80)
1282
+
1283
+ # Average Points
1284
+ if 'average_points' in details_info:
1285
+ output.append("\nAVERAGE REWARD POINTS BY YEAR:")
1286
+ output.append("-" * 40)
1287
+ for year, points in details_info['average_points'].items():
1288
+ if points:
1289
+ output.append(f"Year {year:<10}: {points}")
1290
+
1291
+ # Calculated Year-wise Average Points
1292
+ calculated_averages = calculate_yearwise_average_points()
1293
+ if calculated_averages and not calculated_averages.startswith("❌") and not calculated_averages.startswith("⚠️"):
1294
+ output.append(calculated_averages)
1295
+
1296
+ # Redemption Dates
1297
+ if 'ip1_redemption' in details_info:
1298
+ output.append("\nIP 1 REDEMPTION DATES:")
1299
+ output.append("-" * 40)
1300
+ for semester, date in details_info['ip1_redemption'].items():
1301
+ if date and date != '-':
1302
+ output.append(f"{semester:<10}: {date}")
1303
+
1304
+ if 'ip2_redemption' in details_info:
1305
+ output.append("\nIP 2 REDEMPTION DATES:")
1306
+ output.append("-" * 40)
1307
+ for semester, date in details_info['ip2_redemption'].items():
1308
+ if date and date != '-':
1309
+ output.append(f"{semester:<10}: {date}")
1310
+
1311
+ if 'last_updated' in details_info:
1312
+ output.append(f"\nLAST UPDATED:")
1313
+ output.append("-" * 40)
1314
+ output.append(details_info['last_updated'])
1315
+
1316
+ # Cache info
1317
+ if data_cache["last_update"]:
1318
+ cache_age = datetime.now() - data_cache["last_update"]
1319
+ hours = cache_age.total_seconds() / 3600
1320
+ next_refresh_hours = 12 - hours
1321
+ output.append(f"\n📊 Data age: {hours:.1f} hours")
1322
+ if next_refresh_hours > 0:
1323
+ output.append(f"⏰ Next auto-refresh in: {next_refresh_hours:.1f} hours")
1324
+ else:
1325
+ output.append("⏰ Auto-refresh due now")
1326
+
1327
+ output.append("\n" + "=" * 80)
1328
+
1329
+ return "\n".join(output)
1330
+
1331
+ # Create Gradio interface
1332
+ with gr.Blocks(
1333
+ title="Student Reward Points Check",
1334
+ theme=gr.themes.Soft(),
1335
+ ) as app:
1336
+ gr.Markdown("## 🎓 Student Reward Points Checker")
1337
+ gr.Markdown("##### Search for Student Details such as Reward Points, Redemption Dates and Innovative Practice (IP) Details")
1338
+ gr.Markdown("##### எல்லா புகழும் இறைவனுக்கே ✝ 🕉 ☪")
1339
+ gr.Markdown("💻 **Mode**: Use Desktop Mode in browser for Good UI and UX")
1340
+ gr.Markdown("🕒 **Auto-Updates**: Data automatically refreshes when there is a change in Reward Points Sheet")
1341
+ gr.Markdown("📝 **Issue/Feedback Form** : [Issue/Feedback Form](https://docs.google.com/forms/d/e/1FAIpQLScnl0udcN2pUDENHl45HIj5HZbvDuwZ0g2eepBbp8tJYg-NvQ/viewform)")
1342
+
1343
+ with gr.Tabs():
1344
+ with gr.TabItem("🔍 Student Search"):
1345
+ with gr.Row():
1346
+ with gr.Column(scale=3):
1347
+ roll_input = gr.Textbox(
1348
+ label="Enter Roll Number",
1349
+ placeholder="e.g., 7376222AL181",
1350
+ value=""
1351
+ )
1352
+ with gr.Column(scale=1):
1353
+ search_btn = gr.Button("🔍 Search Student", variant="primary")
1354
+
1355
+ result_output = gr.Textbox(
1356
+ label="Student Details",
1357
+ lines=50,
1358
+ max_lines=60,
1359
+ show_copy_button=True,
1360
+ autoscroll=False
1361
+ )
1362
+
1363
+ with gr.TabItem("📚 Innovative Practice (IP) Details"):
1364
+ with gr.Row():
1365
+ with gr.Column(scale=3):
1366
+ subject_roll_input = gr.Textbox(
1367
+ label="Enter Roll Number for Innovative Practice (IP) Details",
1368
+ placeholder="e.g., 7376222AL181",
1369
+ value=""
1370
+ )
1371
+ with gr.Column(scale=1):
1372
+ subject_search_btn = gr.Button("📚 Get Innovative Practice (IP) Details", variant="primary")
1373
+
1374
+ subject_output = gr.Textbox(
1375
+ label="Innovative Practice (IP) Details",
1376
+ lines=50,
1377
+ max_lines=60,
1378
+ show_copy_button=True,
1379
+ autoscroll=False
1380
+ )
1381
+
1382
+ with gr.TabItem("ℹ️ System Information"):
1383
+ with gr.Row():
1384
+ with gr.Column():
1385
+ system_btn = gr.Button("📊 Get System Information", variant="secondary", size="lg")
1386
+
1387
+ system_output = gr.Textbox(
1388
+ label="System Information",
1389
+ lines=50,
1390
+ max_lines=60,
1391
+ show_copy_button=True,
1392
+ autoscroll=False,
1393
+ interactive=False,
1394
+ show_label=True
1395
+ )
1396
+
1397
+ # Event handlers - FIXED: Proper input/output mapping
1398
+
1399
+ # 1. Student search handlers
1400
+ search_btn.click(
1401
+ fn=search_student,
1402
+ inputs=roll_input, # Changed from [roll_input] to roll_input
1403
+ outputs=result_output
1404
+ )
1405
+
1406
+ roll_input.submit(
1407
+ fn=search_student,
1408
+ inputs=roll_input, # Changed from [roll_input] to roll_input
1409
+ outputs=result_output
1410
+ )
1411
+
1412
+ # 2. IP Details handlers - FIXED: Proper input parameter
1413
+ subject_search_btn.click(
1414
+ fn=extract_subjects_and_marks_for_gradio,
1415
+ inputs=subject_roll_input, # Changed from [subject_roll_input] to subject_roll_input
1416
+ outputs=subject_output
1417
+ )
1418
+
1419
+ subject_roll_input.submit(
1420
+ fn=extract_subjects_and_marks_for_gradio,
1421
+ inputs=subject_roll_input, # Changed from [subject_roll_input] to subject_roll_input
1422
+ outputs=subject_output
1423
+ )
1424
+
1425
+ # 3. System info handler - Correct (no inputs needed)
1426
+ system_btn.click(
1427
+ fn=get_system_info,
1428
+ inputs=[],
1429
+ outputs=system_output
1430
+ )
1431
+
1432
+ # Footer section
1433
+ gr.Markdown("---")
1434
+ with gr.Row():
1435
+ with gr.Column():
1436
+ gr.Markdown(
1437
+ """
1438
+ <div style="text-align: center; margin-top: 20px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white;">
1439
+ <h3 style="margin: 0; color: white;">💻 Developed with ❤️ by</h3>
1440
+ <a href="https://praneshjs.vercel.app" target="_blank" style="text-decoration: none;">
1441
+ <h2 style="margin: 5px 0; color: #ffd700; cursor: pointer; transition: color 0.3s ease;">PRANESH S</h2>
1442
+ </a>
1443
+ <div style="margin: 15px 0;">
1444
+ <a href="https://github.com/Pranesh-2005" target="_blank" style="color: #ffd700; text-decoration: none; margin: 0 10px; font-size: 16px;">
1445
+ 🐱 GitHub
1446
+ </a>
1447
+ <span style="color: #ffd700;">|</span>
1448
+ <a href="https://www.linkedin.com/in/pranesh5264/" target="_blank" style="color: #ffd700; text-decoration: none; margin: 0 10px; font-size: 16px;">
1449
+ 💼 LinkedIn
1450
+ </a>
1451
+ <span style="color: #ffd700;">|</span>
1452
+ <a href="https://mail.google.com/mail/?view=cm&fs=1&to=praneshmadhan646@gmail.com&su=Student%20Reward%20Points%20App%20-%20Feedback&body=Hi%20Pranesh,%0A%0AI%20am%20writing%20regarding%20the%20Student%20Reward%20Points%20application.%0A%0A" target="_blank" style="color: #ffd700; text-decoration: none; margin: 0 10px; font-size: 16px;">
1453
+ 📧 Contact Developer
1454
+ </a>
1455
+ </div>
1456
+ <p style="margin: 10px 0; font-style: italic; color: #e0e0e0;">Made with 💝 Love and Support</p>
1457
+ <p style="margin: 5px 0; font-size: 14px; color: #b0b0b0;">🚀 Empowering students with instant reward points tracking</p>
1458
+ </div>
1459
+ """,
1460
+ elem_id="footer"
1461
+ )
1462
+
1463
+ # System info initialization function - Fixed to handle errors gracefully
1464
+ def initialize_system_info():
1465
+ """Initialize system information display with error handling"""
1466
+ try:
1467
+ return get_system_info()
1468
+ except Exception as e:
1469
+ error_msg = f"⚠️ Error initializing system info: {str(e)}"
1470
+ print(error_msg)
1471
+ return "⚠️ System information will be available after data loads completely. Please click 'Get System Information' button to retry."
1472
+
1473
+ # Load system info on startup
1474
+ app.load(
1475
+ fn=initialize_system_info,
1476
+ inputs=[],
1477
+ outputs=system_output
1478
+ )
1479
+
1480
+ # Launch the app
1481
+ if __name__ == "__main__":
1482
+ print("🚀 Launching Gradio interface...")
1483
+ app.launch(share=False, debug=True)