Spaces:
Configuration error
Configuration error
| from typing import List, Optional | |
| from prisma.models import Department, AuditLog, Subject, Classroom, Designation | |
| from app.core.security import hash_password | |
| from app.db.client import db | |
| from app.repositories.user_repo import UserRepository | |
| from app.repositories.student_repo import StudentRepository | |
| from app.repositories.teacher_repo import TeacherRepository | |
| from app.repositories.class_repo import ClassRepository | |
| from app.repositories.enrollment_repo import EnrollmentRepository | |
| from app.schemas.student import StudentCreate, StudentResponse | |
| from app.schemas.teacher import TeacherCreate, TeacherResponse | |
| from app.schemas.admin import ClassCreate, ClassResponse | |
| class AdminService: | |
| def __init__(self) -> None: | |
| self.user_repo = UserRepository() | |
| self.student_repo = StudentRepository() | |
| self.teacher_repo = TeacherRepository() | |
| self.class_repo = ClassRepository() | |
| self.enrollment_repo = EnrollmentRepository() | |
| async def _log_action(event_type: str, severity: str, actor: str, target: str, description: str, ip: Optional[str] = None) -> None: | |
| await db.auditlog.create(data={ | |
| "eventType": event_type, | |
| "severity": severity, | |
| "actor": actor, | |
| "target": target, | |
| "description": description, | |
| "ip": ip, | |
| }) | |
| # --- Students --- | |
| async def create_student(self, data: StudentCreate, actor: str = "system", ip: Optional[str] = None) -> StudentResponse: | |
| if data.department_id: | |
| if not await db.department.find_unique(where={"id": data.department_id}): | |
| raise ValueError("Department not found.") | |
| user = await db.user.create(data={ | |
| "email": data.email, "hashedPassword": hash_password(data.password), "role": "STUDENT", | |
| }) | |
| student = await db.student.create( | |
| data={k: v for k, v in { | |
| "userId": user.id, "enrollmentNumber": data.enrollment_number, | |
| "firstName": data.first_name, "lastName": data.last_name, | |
| "phone": data.phone, "gender": data.gender, "dateOfBirth": data.date_of_birth, | |
| "semester": data.semester, "batch": data.batch, "departmentId": data.department_id, | |
| }.items() if v is not None}, | |
| include={"department": True}, | |
| ) | |
| await self._log_action("CREATE_STUDENT", "INFO", actor, student.id, f"Created student {data.email}", ip) | |
| return StudentResponse( | |
| id=student.id, user_id=user.id, enrollment_number=student.enrollmentNumber, email=user.email, | |
| first_name=student.firstName, last_name=student.lastName, phone=student.phone, | |
| gender=student.gender, date_of_birth=student.dateOfBirth, | |
| department_id=student.departmentId, | |
| department_name=student.department.name if student.department else None, | |
| semester=student.semester, batch=student.batch, | |
| ) | |
| async def get_all_students(self) -> List[StudentResponse]: | |
| students = await db.student.find_many(include={"user": True, "department": True}) | |
| return [ | |
| StudentResponse( | |
| id=s.id, user_id=s.userId, enrollment_number=s.enrollmentNumber, | |
| email=s.user.email if s.user else "", first_name=s.firstName, | |
| last_name=s.lastName, phone=s.phone, gender=s.gender, | |
| date_of_birth=s.dateOfBirth, department_id=s.departmentId, | |
| department_name=s.department.name if s.department else None, | |
| semester=s.semester, batch=s.batch, | |
| ) | |
| for s in students | |
| ] | |
| async def update_student(self, id: str, data: dict, actor: str = "system", ip: Optional[str] = None) -> StudentResponse: | |
| mapping = { | |
| "enrollment_number": "enrollmentNumber", "first_name": "firstName", "last_name": "lastName", | |
| "phone": "phone", "gender": "gender", "date_of_birth": "dateOfBirth", | |
| "semester": "semester", "batch": "batch", "department_id": "departmentId", | |
| } | |
| update_data = {mapping[k]: v for k, v in data.items() if k in mapping} | |
| student = await db.student.update(where={"id": id}, data=update_data, include={"user": True, "department": True}) | |
| await self._log_action("UPDATE_STUDENT", "INFO", actor, id, f"Updated student {student.user.email if student.user else id}", ip) | |
| return StudentResponse( | |
| id=student.id, user_id=student.userId, enrollment_number=student.enrollmentNumber, | |
| email=student.user.email if student.user else "", first_name=student.firstName, | |
| last_name=student.lastName, phone=student.phone, gender=student.gender, | |
| date_of_birth=student.dateOfBirth, department_id=student.departmentId, | |
| department_name=student.department.name if student.department else None, | |
| semester=student.semester, batch=student.batch, | |
| ) | |
| # --- Teachers --- | |
| async def create_teacher(self, data: TeacherCreate, actor: str = "system", ip: Optional[str] = None) -> TeacherResponse: | |
| if not await db.department.find_unique(where={"id": data.department_id}): | |
| raise ValueError(f"Department with id '{data.department_id}' not found.") | |
| if not await db.designation.find_unique(where={"id": data.designation_id}): | |
| raise ValueError(f"Designation with id '{data.designation_id}' not found.") | |
| user = await db.user.create(data={ | |
| "email": data.email, "hashedPassword": hash_password(data.password), "role": "TEACHER", | |
| }) | |
| teacher = await db.teacher.create( | |
| data={k: v for k, v in { | |
| "userId": user.id, "employeeId": data.employee_id, | |
| "firstName": data.first_name, "lastName": data.last_name, | |
| "departmentId": data.department_id, "designationId": data.designation_id, | |
| "phone": data.phone, "qualification": data.qualification, | |
| "specialization": data.specialization, "experienceYears": data.experience_years, | |
| "joiningDate": data.joining_date, | |
| }.items() if v is not None}, | |
| include={"department": True, "designation": True}, | |
| ) | |
| await self._log_action("CREATE_TEACHER", "INFO", actor, teacher.id, f"Created teacher {data.email}", ip) | |
| return TeacherResponse( | |
| id=teacher.id, user_id=user.id, email=user.email, employee_id=teacher.employeeId, | |
| first_name=teacher.firstName, last_name=teacher.lastName, | |
| department_id=teacher.departmentId, designation_id=teacher.designationId, | |
| department=teacher.department.name, designation=teacher.designation.name, | |
| phone=teacher.phone, qualification=teacher.qualification, | |
| specialization=teacher.specialization, experience_years=teacher.experienceYears, | |
| joining_date=teacher.joiningDate, | |
| ) | |
| async def get_all_teachers(self) -> List[TeacherResponse]: | |
| teachers = await db.teacher.find_many(include={"user": True, "department": True, "designation": True}) | |
| return [ | |
| TeacherResponse( | |
| id=t.id, user_id=t.userId, email=t.user.email if t.user else "", | |
| employee_id=t.employeeId, first_name=t.firstName, last_name=t.lastName, | |
| department_id=t.departmentId, designation_id=t.designationId, | |
| department=t.department.name if t.department else "", | |
| designation=t.designation.name if t.designation else "", | |
| phone=t.phone, qualification=t.qualification, specialization=t.specialization, | |
| experience_years=t.experienceYears, joining_date=t.joiningDate, | |
| ) | |
| for t in teachers | |
| ] | |
| async def update_teacher(self, id: str, data: dict, actor: str = "system", ip: Optional[str] = None) -> TeacherResponse: | |
| mapping = { | |
| "employee_id": "employeeId", "first_name": "firstName", "last_name": "lastName", | |
| "department_id": "departmentId", "designation_id": "designationId", "phone": "phone", | |
| "qualification": "qualification", "specialization": "specialization", | |
| "experience_years": "experienceYears", "joining_date": "joiningDate", | |
| } | |
| update_data = {mapping[k]: v for k, v in data.items() if k in mapping} | |
| teacher = await db.teacher.update(where={"id": id}, data=update_data, include={"user": True, "department": True, "designation": True}) | |
| await self._log_action("UPDATE_TEACHER", "INFO", actor, id, f"Updated teacher {teacher.user.email if teacher.user else id}", ip) | |
| return TeacherResponse( | |
| id=teacher.id, user_id=teacher.userId, email=teacher.user.email if teacher.user else "", | |
| employee_id=teacher.employeeId, first_name=teacher.firstName, last_name=teacher.lastName, | |
| department_id=teacher.departmentId, designation_id=teacher.designationId, | |
| department=teacher.department.name if teacher.department else "", | |
| designation=teacher.designation.name if teacher.designation else "", | |
| phone=teacher.phone, qualification=teacher.qualification, | |
| specialization=teacher.specialization, experience_years=teacher.experienceYears, | |
| joining_date=teacher.joiningDate, | |
| ) | |
| # --- Classes --- | |
| async def create_class(self, data: ClassCreate, actor: str = "system", ip: Optional[str] = None) -> ClassResponse: | |
| if not await self.teacher_repo.get_by_id(data.teacher_id): | |
| raise ValueError("Teacher profile not found.") | |
| if not await db.subject.find_unique(where={"id": data.subject_id}): | |
| raise ValueError(f"Subject with id '{data.subject_id}' not found.") | |
| if data.classroom_id and not await db.classroom.find_unique(where={"id": data.classroom_id}): | |
| raise ValueError(f"Classroom with id '{data.classroom_id}' not found.") | |
| cls = await db.academicclass.create( | |
| data={k: v for k, v in { | |
| "name": data.name, "teacherId": data.teacher_id, "subjectId": data.subject_id, | |
| "classroomId": data.classroom_id, "semester": data.semester, | |
| "batch": data.batch, "maxStudents": data.max_students, | |
| }.items() if v is not None}, | |
| include={"subject": True, "classroom": True, "enrollments": True}, | |
| ) | |
| await self._log_action("CREATE_CLASS", "INFO", actor, cls.id, f"Created class {data.name}", ip) | |
| return ClassResponse( | |
| id=cls.id, name=cls.name, subject_name=cls.subject.name, subject_code=cls.subject.code, | |
| teacherId=cls.teacherId, classroom_name=cls.classroom.name if cls.classroom else None, | |
| semester=cls.semester, batch=cls.batch, max_students=cls.maxStudents, | |
| enrolled_count=len(cls.enrollments) if cls.enrollments else 0, | |
| enrolled_student_ids=[e.studentId for e in cls.enrollments] if cls.enrollments else [], | |
| ) | |
| async def get_all_classes(self) -> List[ClassResponse]: | |
| classes = await db.academicclass.find_many(include={"subject": True, "classroom": True, "enrollments": True}) | |
| return [ | |
| ClassResponse( | |
| id=c.id, name=c.name, subject_name=c.subject.name if c.subject else "", | |
| subject_code=c.subject.code if c.subject else "", teacherId=c.teacherId, | |
| classroom_name=c.classroom.name if c.classroom else None, | |
| semester=c.semester, batch=c.batch, max_students=c.maxStudents, | |
| enrolled_count=len(c.enrollments) if c.enrollments else 0, | |
| enrolled_student_ids=[e.studentId for e in c.enrollments] if c.enrollments else [], | |
| ) | |
| for c in classes | |
| ] | |
| async def update_class(self, class_id: str, data: dict, actor: str = "system", ip: Optional[str] = None) -> ClassResponse: | |
| renames = {"subject_id": "subjectId", "classroom_id": "classroomId", "teacher_id": "teacherId"} | |
| for old, new in renames.items(): | |
| if old in data: | |
| data[new] = data.pop(old) | |
| cls = await db.academicclass.update(where={"id": class_id}, data=data, include={"subject": True, "classroom": True, "enrollments": True}) | |
| await self._log_action("UPDATE_CLASS", "INFO", actor, class_id, f"Updated class {cls.name}", ip) | |
| return ClassResponse( | |
| id=cls.id, name=cls.name, subject_name=cls.subject.name if cls.subject else "", | |
| subject_code=cls.subject.code if cls.subject else "", teacherId=cls.teacherId, | |
| classroom_name=cls.classroom.name if cls.classroom else None, | |
| semester=cls.semester, batch=cls.batch, max_students=cls.maxStudents, | |
| enrolled_count=len(cls.enrollments) if cls.enrollments else 0, | |
| enrolled_student_ids=[e.studentId for e in cls.enrollments] if cls.enrollments else [], | |
| ) | |
| async def assign_teacher(self, class_id: str, teacher_id: str, actor: str = "system", ip: Optional[str] = None) -> ClassResponse: | |
| if not await self.class_repo.get_by_id(class_id): | |
| raise ValueError("Academic class not found.") | |
| if not await self.teacher_repo.get_by_id(teacher_id): | |
| raise ValueError("Teacher profile not found.") | |
| cls = await db.academicclass.update(where={"id": class_id}, data={"teacherId": teacher_id}, include={"subject": True, "classroom": True, "enrollments": True}) | |
| await self._log_action("ASSIGN_TEACHER", "INFO", actor, class_id, f"Assigned teacher {teacher_id} to class {cls.name}", ip) | |
| return ClassResponse( | |
| id=cls.id, name=cls.name, subject_name=cls.subject.name if cls.subject else "", | |
| subject_code=cls.subject.code if cls.subject else "", teacherId=cls.teacherId, | |
| classroom_name=cls.classroom.name if cls.classroom else None, | |
| semester=cls.semester, batch=cls.batch, max_students=cls.maxStudents, | |
| enrolled_count=len(cls.enrollments) if cls.enrollments else 0, | |
| enrolled_student_ids=[e.studentId for e in cls.enrollments] if cls.enrollments else [], | |
| ) | |
| async def enroll_students(self, class_id: str, student_ids: List[str], actor: str = "system", ip: Optional[str] = None) -> int: | |
| if not await self.class_repo.get_by_id(class_id): | |
| raise ValueError("Academic class not found.") | |
| count = 0 | |
| for sid in student_ids: | |
| if await self.student_repo.get_by_id(sid): | |
| existing = await db.enrollment.find_first( | |
| where={"studentId": sid, "academicClassId": class_id} | |
| ) | |
| if not existing: | |
| await self.enrollment_repo.enroll_student(sid, class_id) | |
| count += 1 | |
| await self._log_action("ENROLL_STUDENTS", "INFO", actor, class_id, f"Enrolled {count} student(s) in class", ip) | |
| return count | |
| # --- Master Data Helpers --- | |
| async def _validate_delete(entity_name: str, record, ref_field: str, ref_table_name: str): | |
| if not record: | |
| raise ValueError(f"{entity_name} not found.") | |
| ref_count = await getattr(db, ref_table_name).count(where={ref_field: record.id}) | |
| if ref_count > 0: | |
| raise ValueError(f"Cannot delete {entity_name.lower()} because it is currently assigned.") | |
| # --- Departments --- | |
| async def get_all_departments(self) -> List[Department]: | |
| return await db.department.find_many() | |
| async def get_department_by_id(self, id: str) -> Optional[Department]: | |
| return await db.department.find_unique(where={"id": id}) | |
| async def create_department(self, name: str, code: str, head: Optional[str] = None, description: Optional[str] = None) -> Department: | |
| return await db.department.create(data={"name": name, "code": code, "head": head, "description": description}) | |
| async def update_department(self, id: str, data: dict) -> Department: | |
| return await db.department.update(where={"id": id}, data=data) | |
| async def delete_department(self, id: str, actor: str = "system", ip: Optional[str] = None) -> None: | |
| dept = await db.department.find_unique(where={"id": id}) | |
| await self._validate_delete("Department", dept, "departmentId", "teacher") | |
| await self._validate_delete("Department", dept, "departmentId", "student") | |
| await db.department.delete(where={"id": id}) | |
| await self._log_action("DELETE_DEPARTMENT", "WARNING", actor, id, f"Deleted department {dept.name if dept else id}", ip) | |
| # --- Subjects --- | |
| async def get_all_subjects(self) -> List[Subject]: | |
| return await db.subject.find_many() | |
| async def get_subject_by_id(self, id: str) -> Optional[Subject]: | |
| return await db.subject.find_unique(where={"id": id}) | |
| async def create_subject(self, name: str, code: str, description: Optional[str] = None) -> Subject: | |
| return await db.subject.create(data={"name": name, "code": code, "description": description}) | |
| async def update_subject(self, id: str, data: dict) -> Subject: | |
| return await db.subject.update(where={"id": id}, data=data) | |
| async def delete_subject(self, id: str, actor: str = "system", ip: Optional[str] = None) -> None: | |
| sub = await db.subject.find_unique(where={"id": id}) | |
| await self._validate_delete("Subject", sub, "subjectId", "academicclass") | |
| await db.subject.delete(where={"id": id}) | |
| await self._log_action("DELETE_SUBJECT", "WARNING", actor, id, f"Deleted subject {sub.name if sub else id}", ip) | |
| # --- Classrooms --- | |
| async def get_all_classrooms(self) -> List[Classroom]: | |
| return await db.classroom.find_many() | |
| async def get_classroom_by_id(self, id: str) -> Optional[Classroom]: | |
| return await db.classroom.find_unique(where={"id": id}) | |
| async def create_classroom(self, name: str, building: Optional[str] = None, capacity: Optional[int] = None) -> Classroom: | |
| return await db.classroom.create(data={"name": name, "building": building, "capacity": capacity}) | |
| async def update_classroom(self, id: str, data: dict) -> Classroom: | |
| return await db.classroom.update(where={"id": id}, data=data) | |
| async def delete_classroom(self, id: str, actor: str = "system", ip: Optional[str] = None) -> None: | |
| classroom = await db.classroom.find_unique(where={"id": id}) | |
| await self._validate_delete("Classroom", classroom, "classroomId", "academicclass") | |
| await db.classroom.delete(where={"id": id}) | |
| await self._log_action("DELETE_CLASSROOM", "WARNING", actor, id, f"Deleted classroom {classroom.name if classroom else id}", ip) | |
| # --- Designations --- | |
| async def get_all_designations(self) -> List[Designation]: | |
| return await db.designation.find_many() | |
| async def get_designation_by_id(self, id: str) -> Optional[Designation]: | |
| return await db.designation.find_unique(where={"id": id}) | |
| async def create_designation(self, name: str, code: str, description: Optional[str] = None) -> Designation: | |
| return await db.designation.create(data={"name": name, "code": code, "description": description}) | |
| async def update_designation(self, id: str, data: dict) -> Designation: | |
| return await db.designation.update(where={"id": id}, data=data) | |
| async def delete_designation(self, id: str, actor: str = "system", ip: Optional[str] = None) -> None: | |
| desig = await db.designation.find_unique(where={"id": id}) | |
| await self._validate_delete("Designation", desig, "designationId", "teacher") | |
| await db.designation.delete(where={"id": id}) | |
| await self._log_action("DELETE_DESIGNATION", "WARNING", actor, id, f"Deleted designation {desig.name if desig else id}", ip) | |
| # --- Misc --- | |
| async def get_audit_logs(self) -> List[AuditLog]: | |
| return await db.auditlog.find_many(order={"timestamp": "desc"}) | |
| async def get_stats(self) -> dict: | |
| return { | |
| "studentCount": await db.student.count(), | |
| "teacherCount": await db.teacher.count(), | |
| "classCount": await db.academicclass.count(), | |
| } | |
| async def reset_user_password(self, user_id: str, new_password: str, actor: str = "system", ip: Optional[str] = None) -> None: | |
| if not await db.user.find_unique(where={"id": user_id}): | |
| raise ValueError("User not found.") | |
| await db.user.update(where={"id": user_id}, data={"hashedPassword": hash_password(new_password)}) | |
| await self._log_action("RESET_PASSWORD", "WARNING", actor, user_id, "Reset user password", ip) | |