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)
|