File size: 17,125 Bytes
2fe573b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2a1cd96
2fe573b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import json
import os
from pathlib import Path
from dotenv import load_dotenv

# Load environment variables from .env file (for local development)
load_dotenv()

app = FastAPI()

# CORS configuration - allow all origins for flexibility
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Data file path
DATA_FILE = Path(__file__).parent / "data" / "students.json"
DATA_FILE.parent.mkdir(exist_ok=True)

# Pydantic models
class Student(BaseModel):
    studentNumber: str
    name: str
    nationalCode: str = ""
    mbti: Optional[str] = None
    learningStyle: Optional[str] = None
    ams: Optional[str] = None
    cooperative: Optional[str] = None
    grade: float
    preferredStudents: List[str] = []
    group: Optional[int] = None

class StudentUpdate(BaseModel):
    mbti: Optional[str] = None
    learningStyle: Optional[str] = None
    ams: Optional[str] = None
    cooperative: Optional[str] = None
    preferredStudents: Optional[List[str]] = None

class GroupingRequest(BaseModel):
    courseName: str

class TeacherAuthRequest(BaseModel):
    password: str

class StudentAuthRequest(BaseModel):
    studentNumber: str
    nationalCode: str

class SystemData(BaseModel):
    students: List[Student]
    courseName: str = ""
    groupingComplete: bool = False
    groupingResults: Optional[Dict[str, Any]] = None
    resultsVisible: bool = False
    teacherPassword: str = "teacher123"

# Initialize data
def load_data() -> SystemData:
    if DATA_FILE.exists():
        with open(DATA_FILE, 'r', encoding='utf-8') as f:
            data = json.load(f)
            return SystemData(**data)
    else:
        # Initialize with real 30 students from class data
        initial_students = [
            Student(studentNumber='S001', name='یاسمن آدینه پور', nationalCode='929986644', grade=18.77),
            Student(studentNumber='S002', name='پریا احمدزاده', nationalCode='980085330', grade=17.28),
            Student(studentNumber='S003', name='فاطمه اکبرزاده', nationalCode='970154550', grade=16.71),
            Student(studentNumber='S004', name='آناهیتا الهی مهر', nationalCode='26425955', grade=19.05),
            Student(studentNumber='S005', name='مریم امیری', nationalCode='980093341', grade=18.87),
            Student(studentNumber='S006', name='باران برادران رحیمی', nationalCode='960043985', grade=19.07),
            Student(studentNumber='S007', name='مایسا بصیری امین', nationalCode='960089446', grade=19.33),
            Student(studentNumber='S008', name='دلارام ثابت عهد', nationalCode='960125620', grade=19.55),
            Student(studentNumber='S009', name='شاینا جان محمدی', nationalCode='960068041', grade=19.47),
            Student(studentNumber='S010', name='آیدا جوان', nationalCode='95112313', grade=16.77),
            Student(studentNumber='S011', name='سارینا حاجی آبادی', nationalCode='999216751', grade=16.08),
            Student(studentNumber='S012', name='هستی حسن پور جوان', nationalCode='960074198', grade=19.55),
            Student(studentNumber='S013', name='فاطمه حسینی', nationalCode='2400410259', grade=19.07),
            Student(studentNumber='S014', name='غزل خسروی', nationalCode='929995767', grade=15.05),
            Student(studentNumber='S015', name='غزل ذباح', nationalCode='960110186', grade=19.25),
            Student(studentNumber='S016', name='نازنین زهرا راشکی', nationalCode='3661516087', grade=17.02),
            Student(studentNumber='S017', name='ویونا روح نواز', nationalCode='314458344', grade=18.70),
            Student(studentNumber='S018', name='روژینا سعادتی', nationalCode='960051023', grade=18.20),
            Student(studentNumber='S019', name='ترنم شعبانی', nationalCode='950083100', grade=19.37),
            Student(studentNumber='S020', name='ستایش شفابخش', nationalCode='960126899', grade=18.36),
            Student(studentNumber='S021', name='فاطمه شیرزادخان', nationalCode='980120756', grade=19.33),
            Student(studentNumber='S022', name='آرزو علی جوی', nationalCode='960054316', grade=17.98),
            Student(studentNumber='S023', name='آناهیتا قنادزاده', nationalCode='960089836', grade=18.84),
            Student(studentNumber='S024', name='نیایش کارگر', nationalCode='929956052', grade=17.74),
            Student(studentNumber='S025', name='باران کبریایی نسب', nationalCode='980119588', grade=18.82),
            Student(studentNumber='S026', name='زینب کیانوش', nationalCode='970072678', grade=18.58),
            Student(studentNumber='S027', name='ستایش محمودی', nationalCode='929904656', grade=19.33),
            Student(studentNumber='S028', name='ستایش مشتاقی', nationalCode='361282217', grade=17.67),
            Student(studentNumber='S029', name='مهتاب معلمی', nationalCode='960070265', grade=18.56),
            Student(studentNumber='S030', name='باران وحدتی', nationalCode='929916913', grade=15.02),
        ]
        
        data = SystemData(students=initial_students)
        save_data(data)
        return data

def save_data(data: SystemData):
    with open(DATA_FILE, 'w', encoding='utf-8') as f:
        json.dump(data.dict(), f, ensure_ascii=False, indent=2)

# API Endpoints
@app.get("/")
def read_root():
    return {"message": "TalimBot API is running"}

@app.get("/api/students")
def get_all_students():
    data = load_data()
    return {"students": data.students}

@app.get("/api/student/{student_number}")
def get_student(student_number: str):
    # Return demo account if requested
    if student_number == "DEMO":
        return Student(
            studentNumber="DEMO",
            name="پریناز عاکف",
            nationalCode="0921111111",
            grade=0.0,
            mbti=None,
            learningStyle=None,
            ams=None,
            cooperative=None,
            preferredStudents=[],
            group=None
        )
    
    data = load_data()
    student = next((s for s in data.students if s.studentNumber == student_number), None)
    if not student:
        raise HTTPException(status_code=404, detail="Student not found")
    return student

@app.put("/api/student/{student_number}")
def update_student(student_number: str, updates: StudentUpdate):
    # Silently ignore updates to demo account (pretend it worked)
    if student_number == "DEMO":
        demo_student = Student(
            studentNumber="DEMO",
            name="پریناز عاکف",
            nationalCode="0921111111",
            grade=0.0,
            mbti=updates.mbti,
            learningStyle=updates.learningStyle,
            ams=updates.ams,
            cooperative=updates.cooperative,
            preferredStudents=updates.preferredStudents or [],
            group=None
        )
        return {"success": True, "student": demo_student}
    
    data = load_data()
    student = next((s for s in data.students if s.studentNumber == student_number), None)
    if not student:
        raise HTTPException(status_code=404, detail="Student not found")
    
    # Update only provided fields
    update_dict = updates.dict(exclude_unset=True)
    for key, value in update_dict.items():
        setattr(student, key, value)
    
    save_data(data)
    return {"success": True, "student": student}

class GroupingRequest(BaseModel):
    courseName: str

@app.post("/api/grouping/perform")
async def perform_grouping(request: GroupingRequest):
    data = load_data()
    
    # Get API key from environment variable
    api_key = os.getenv("OPENROUTER_API_KEY")
    
    if not api_key:
        raise HTTPException(
            status_code=500, 
            detail="OpenRouter API key not configured. Please set OPENROUTER_API_KEY in your environment variables or hosting platform settings"
        )
    
    # Get students with complete info (mbti and learningStyle required)
    students_with_info = [s for s in data.students if s.mbti and s.learningStyle]
    
    if len(students_with_info) == 0:
        raise HTTPException(status_code=400, detail="No students have completed their profiles yet")
    
    try:
        # Import grouping logic
        from grouping_logic import group_students_with_ai
        
        # group_students_with_ai is now synchronous (uses requests library)
        # Run it in a thread pool to avoid blocking
        import asyncio
        from concurrent.futures import ThreadPoolExecutor
        
        loop = asyncio.get_event_loop()
        with ThreadPoolExecutor() as executor:
            grouping_result = await loop.run_in_executor(
                executor,
                lambda: group_students_with_ai(students_with_info, request.courseName, api_key)
            )
        
        # Apply grouping to students
        for student in data.students:
            student.group = None
        
        for group in grouping_result["groups"]:
            for student_number in group["students"]:
                student = next((s for s in data.students if s.studentNumber == student_number), None)
                if student:
                    student.group = group["groupNumber"]
        
        data.courseName = request.courseName
        data.groupingComplete = True
        data.groupingResults = grouping_result
        data.resultsVisible = False  # Teacher must manually show results
        
        save_data(data)
        return {"success": True, "results": grouping_result}
    
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/api/grouping/status")
def get_grouping_status():
    data = load_data()
    students_with_info = [s for s in data.students if s.mbti and s.learningStyle]
    students_grouped = [s for s in data.students if s.group is not None]
    
    return {
        "totalStudents": len(data.students),
        "studentsWithCompleteInfo": len(students_with_info),
        "studentsGrouped": len(students_grouped),
        "groupingComplete": data.groupingComplete,
        "resultsVisible": data.resultsVisible,
        "courseName": data.courseName,
        "groups": data.groupingResults.get("groups", []) if data.groupingResults else []
    }

class PasswordRequest(BaseModel):
    password: str

@app.post("/api/grouping/toggle-visibility")
def toggle_results_visibility(request: PasswordRequest):
    data = load_data()
    if request.password != data.teacherPassword:
        raise HTTPException(status_code=403, detail="Invalid password")
    
    data.resultsVisible = not data.resultsVisible
    save_data(data)
    return {"success": True, "resultsVisible": data.resultsVisible}

@app.post("/api/grouping/reset")
def reset_grouping(request: PasswordRequest):
    data = load_data()
    if request.password != data.teacherPassword:
        raise HTTPException(status_code=403, detail="Invalid password")
    
    # Clear ONLY grouping-related data, keep student profiles intact
    for student in data.students:
        student.group = None
    
    data.groupingComplete = False
    data.groupingResults = None
    data.resultsVisible = False
    data.courseName = ""
    
    save_data(data)
    return {"success": True}

@app.post("/api/data/reset-all")
def reset_all_data(request: PasswordRequest):
    data = load_data()
    if request.password != data.teacherPassword:
        raise HTTPException(status_code=403, detail="Invalid password")
    
    # Clear ALL student data fields AND grouping
    for student in data.students:
        student.group = None
        student.mbti = None
        student.learningStyle = None
        student.ams = None
        student.cooperative = None
        student.preferredStudents = []
    
    data.groupingComplete = False
    data.groupingResults = None
    data.resultsVisible = False
    data.courseName = ""
    
    save_data(data)
    return {"success": True}

@app.post("/api/auth/teacher")
def check_teacher_password(request: TeacherAuthRequest):
    data = load_data()
    return {"valid": request.password == data.teacherPassword}

@app.post("/api/auth/student")
def authenticate_student(request: StudentAuthRequest):
    # Special demo account - not stored in database
    # Check for national code without leading zero (frontend strips it)
    if request.nationalCode == "921111111":
        demo_student = Student(
            studentNumber="DEMO",
            name="پریناز عاکف",
            nationalCode="921111111",
            grade=0.0,
            mbti=None,
            learningStyle=None,
            ams=None,
            cooperative=None,
            preferredStudents=[],
            group=None
        )
        return {"valid": True, "student": demo_student}
    
    data = load_data()
    student = next((s for s in data.students if s.studentNumber == request.studentNumber), None)
    if not student:
        raise HTTPException(status_code=404, detail="Student not found")
    
    if student.nationalCode != request.nationalCode:
        raise HTTPException(status_code=401, detail="Invalid national code")
    
    return {"valid": True, "student": student}

class NationalCodeAuthRequest(BaseModel):
    nationalCode: str

@app.post("/api/auth/student-by-nationalcode")
def authenticate_student_by_nationalcode(request: NationalCodeAuthRequest):
    # Special demo account - not stored in database
    # Check for national code without leading zero (frontend strips it)
    if request.nationalCode == "921111111":
        demo_student = Student(
            studentNumber="DEMO",
            name="پریناز عاکف",
            nationalCode="921111111",
            grade=0.0,
            mbti=None,
            learningStyle=None,
            ams=None,
            cooperative=None,
            preferredStudents=[],
            group=None
        )
        return {"valid": True, "student": demo_student}
    
    data = load_data()
    # Find student by national code (without leading zero)
    student = next((s for s in data.students if s.nationalCode == request.nationalCode), None)
    if not student:
        raise HTTPException(status_code=404, detail="کد ملی در سیستم یافت نشد")
    
    return {"valid": True, "student": student}

@app.get("/api/student/{student_number}/group")
def get_student_group(student_number: str):
    # Demo account has no group
    if student_number == "DEMO":
        raise HTTPException(status_code=404, detail="Demo account is not part of any group")
    
    data = load_data()
    
    if not data.resultsVisible:
        raise HTTPException(status_code=403, detail="Results are not yet visible")
    
    student = next((s for s in data.students if s.studentNumber == student_number), None)
    if not student:
        raise HTTPException(status_code=404, detail="Student not found")
    
    if student.group is None:
        raise HTTPException(status_code=404, detail="Student not assigned to a group yet")
    
    # Find all students in the same group
    group_members = [s for s in data.students if s.group == student.group]
    
    # Find the group details from results
    group_info = None
    if data.groupingResults:
        for g in data.groupingResults.get("groups", []):
            if g["groupNumber"] == student.group:
                group_info = g
                break
    
    return {
        "groupNumber": student.group,
        "members": group_members,
        "reasoning": group_info.get("reasoning", "") if group_info else "",
        "courseName": data.courseName
    }

@app.get("/api/data/backup")
def get_data_backup():
    """Download complete student data as JSON backup for safekeeping"""
    data = load_data()
    return data.dict()

# ============================================
# STATIC FILE SERVING (Frontend HTML/CSS/JS)
# ============================================

# Define static directory path
STATIC_DIR = Path(__file__).parent / "static"

# Mount static files FIRST - this handles all non-API routes
app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)