smart-attendance-api / backend /app /services /admin_service.py
Pratham200Rajbhar
first commit
78013c4
Raw
History Blame Contribute Delete
20.7 kB
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()
@staticmethod
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 ---
@staticmethod
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)