| 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_dotenv()
|
|
|
| app = FastAPI()
|
|
|
|
|
| app.add_middleware(
|
| CORSMiddleware,
|
| allow_origins=["*"],
|
| allow_credentials=True,
|
| allow_methods=["*"],
|
| allow_headers=["*"],
|
| )
|
|
|
|
|
| DATA_FILE = Path(__file__).parent / "data" / "students.json"
|
| DATA_FILE.parent.mkdir(exist_ok=True)
|
|
|
|
|
| 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"
|
|
|
|
|
| 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:
|
|
|
| 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)
|
|
|
|
|
| @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):
|
|
|
| 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):
|
|
|
| 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_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()
|
|
|
|
|
| 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"
|
| )
|
|
|
|
|
| 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:
|
|
|
| from grouping_logic import group_students_with_ai
|
|
|
|
|
|
|
| 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)
|
| )
|
|
|
|
|
| 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
|
|
|
| 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")
|
|
|
|
|
| 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")
|
|
|
|
|
| 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):
|
|
|
|
|
| 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):
|
|
|
|
|
| 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.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):
|
|
|
| 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")
|
|
|
|
|
| group_members = [s for s in data.students if s.group == student.group]
|
|
|
|
|
| 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_DIR = Path(__file__).parent / "static"
|
|
|
|
|
| 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)
|
|
|