diff --git a/backend/app.py b/backend/app.py index d488b4c7..d53e552f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,16 +1,55 @@ import uvicorn from fastapi import FastAPI +from starlette import status +from starlette.requests import Request +from starlette.responses import JSONResponse -from routes.projects import projects_router -from routes.subjects import subjects_router -from routes.users import users_router +from db.errors.database_errors import ActionAlreadyPerformedError, ItemNotFoundError +from routes.errors.authentication import InvalidRoleCredentialsError, StudentNotEnrolledError +from routes.student import student_router +from routes.teacher import teacher_router +from routes.user import users_router app = FastAPI() # Koppel routes uit andere modules. -app.include_router(subjects_router, prefix="/api") +app.include_router(student_router, prefix="/api") +app.include_router(teacher_router, prefix="/api") app.include_router(users_router, prefix="/api") -app.include_router(projects_router, prefix="/api") + + +# Koppel de exception handlers +@app.exception_handler(InvalidRoleCredentialsError) +def invalid_admin_credentials_error_handler(request: Request, exc: InvalidRoleCredentialsError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content={"message": exc.ERROR_MESSAGE}, + ) + + +@app.exception_handler(ItemNotFoundError) +def item_not_found_error_handler(request: Request, exc: ItemNotFoundError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={"detail": str(exc)}, + ) + + +@app.exception_handler(StudentNotEnrolledError) +def student_already_enrolled_error_handler(request: Request, exc: StudentNotEnrolledError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(exc)}, + ) + + +@app.exception_handler(ActionAlreadyPerformedError) +def action_already_performed_error_handler(request: Request, exc: ActionAlreadyPerformedError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(exc)}, + ) + if __name__ == "__main__": uvicorn.run("app:app") diff --git a/backend/db/errors/database_errors.py b/backend/db/errors/database_errors.py index eb45dc51..e2805cc3 100644 --- a/backend/db/errors/database_errors.py +++ b/backend/db/errors/database_errors.py @@ -4,6 +4,6 @@ def __init__(self, message: str) -> None: super().__init__(message) -class UniqueConstraintError(Exception): +class ActionAlreadyPerformedError(Exception): def __init__(self, message: str) -> None: super().__init__(message) diff --git a/backend/db/implementation/SqlAbstractDAO.py b/backend/db/implementation/SqlAbstractDAO.py deleted file mode 100644 index 540fbc2b..00000000 --- a/backend/db/implementation/SqlAbstractDAO.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Generic, TypeVar - -from pydantic import BaseModel -from sqlalchemy import select -from sqlalchemy.orm import Session - -from db.errors.database_errors import ItemNotFoundError -from db.extensions import engine -from db.models.models import AbstractModel - -T = TypeVar("T", bound=AbstractModel) -D = TypeVar("D", bound=BaseModel) - - -class SqlAbstractDAO(Generic[T, D]): - def __init__(self) -> None: - self.model_class: type[T] - - def get(self, ident: int) -> D: - with Session(engine) as session: - generic_object: T | None = session.get(self.model_class, ident) - - if not generic_object: - msg = f"object with id {ident} not found" - raise ItemNotFoundError(msg) - - return generic_object.to_domain_model() - - def get_all(self) -> list[D]: - with Session(engine) as session: - generic_objects: list[T] = list(session.scalars(select(self.model_class)).all()) - return [generic_object.to_domain_model() for generic_object in generic_objects] diff --git a/backend/db/implementation/SqlAdminDAO.py b/backend/db/implementation/SqlAdminDAO.py deleted file mode 100644 index e9a213f7..00000000 --- a/backend/db/implementation/SqlAdminDAO.py +++ /dev/null @@ -1,24 +0,0 @@ -from sqlalchemy.orm import Session - -from db.extensions import engine -from db.implementation.SqlAbstractDAO import SqlAbstractDAO -from db.interface.AdminDAO import AdminDAO -from db.models.models import Admin -from domain.models.AdminDataclass import AdminDataclass - - -class SqlAdminDAO(SqlAbstractDAO[Admin, AdminDataclass], AdminDAO): - def __init__(self) -> None: - self.model_class = Admin - - def create_admin(self, name: str, email: str) -> AdminDataclass: - with Session(engine) as session: - new_admin: Admin = Admin(name=name, email=email) - session.add(new_admin) - session.commit() - return new_admin.to_domain_model() - - def is_user_admin(self, user_id: int) -> bool: - with Session(engine) as session: - admin = session.get(Admin, user_id) - return admin is not None diff --git a/backend/db/implementation/SqlDAOProvider.py b/backend/db/implementation/SqlDAOProvider.py deleted file mode 100644 index e8b2cb9f..00000000 --- a/backend/db/implementation/SqlDAOProvider.py +++ /dev/null @@ -1,43 +0,0 @@ -from db.implementation.SqlAdminDAO import SqlAdminDAO -from db.implementation.SqlGroupDAO import SqlGroupDAO -from db.implementation.SqlProjectDAO import SqlProjectDAO -from db.implementation.SqlStudentDAO import SqlStudentDAO -from db.implementation.SqlSubjectDAO import SqlSubjectDAO -from db.implementation.SqlSubmissionDAO import SqlSubmissionDAO -from db.implementation.SqlTeacherDAO import SqlTeacherDAO -from db.implementation.SqlUserDAO import SqlUserDAO -from db.interface.AdminDAO import AdminDAO -from db.interface.DAOProvider import DAOProvider -from db.interface.GroupDAO import GroupDAO -from db.interface.ProjectDAO import ProjectDAO -from db.interface.StudentDAO import StudentDAO -from db.interface.SubjectDAO import SubjectDAO -from db.interface.SubmissionDAO import SubmissionDAO -from db.interface.TeacherDAO import TeacherDAO -from db.interface.UserDAO import UserDAO - - -class SqlDAOProvider(DAOProvider): - def get_admin_dao(self) -> AdminDAO: - return SqlAdminDAO() - - def get_group_dao(self) -> GroupDAO: - return SqlGroupDAO() - - def get_project_dao(self) -> ProjectDAO: - return SqlProjectDAO() - - def get_student_dao(self) -> StudentDAO: - return SqlStudentDAO() - - def get_subject_dao(self) -> SubjectDAO: - return SqlSubjectDAO() - - def get_submission_dao(self) -> SubmissionDAO: - return SqlSubmissionDAO() - - def get_teacher_dao(self) -> TeacherDAO: - return SqlTeacherDAO() - - def get_user_dao(self) -> UserDAO: - return SqlUserDAO() diff --git a/backend/db/implementation/SqlGroupDAO.py b/backend/db/implementation/SqlGroupDAO.py deleted file mode 100644 index 7f7fae72..00000000 --- a/backend/db/implementation/SqlGroupDAO.py +++ /dev/null @@ -1,69 +0,0 @@ -from sqlalchemy.orm import Session - -from db.errors.database_errors import ItemNotFoundError, UniqueConstraintError -from db.extensions import engine -from db.implementation.SqlAbstractDAO import SqlAbstractDAO -from db.interface.GroupDAO import GroupDAO -from db.models.models import Group, Project, Student -from domain.models.GroupDataclass import GroupDataclass -from domain.models.StudentDataclass import StudentDataclass - - -class SqlGroupDAO(SqlAbstractDAO[Group, GroupDataclass], GroupDAO): - def __init__(self) -> None: - self.model_class = Group - - def create_group(self, project_id: int) -> GroupDataclass: - with Session(engine) as session: - project: Project | None = session.get(Project, ident=project_id) - if not project: - msg = f"Project with id {project} not found" - raise ItemNotFoundError(msg) - new_group: Group = Group(project_id=project_id) - session.add(new_group) - session.commit() - return new_group.to_domain_model() - - def get_groups_of_project(self, project_id: int) -> list[GroupDataclass]: - with Session(engine) as session: - project: Project | None = session.get(Project, ident=project_id) - if not project: - msg = f"Project with id {project} not found" - raise ItemNotFoundError(msg) - groups: list[Group] = project.groups - return [group.to_domain_model() for group in groups] - - def get_groups_of_student(self, student_id: int) -> list[GroupDataclass]: - with Session(engine) as session: - student: Student | None = session.get(Student, ident=student_id) - if not student: - msg = f"Student with id {student_id} not found" - raise ItemNotFoundError(msg) - groups: list[Group] = student.groups - return [group.to_domain_model() for group in groups] - - def add_student_to_group(self, student_id: int, group_id: int) -> None: - with Session(engine) as session: - student: Student | None = session.get(Student, ident=student_id) - group: Group | None = session.get(Group, ident=group_id) - if not student: - msg = f"Student with id {student_id} not found" - raise ItemNotFoundError(msg) - if not group: - msg = f"Group with id {group_id} not found" - raise ItemNotFoundError(msg) - if student in group.students: - msg = f"Student with id {student_id} already in group with id {group_id}" - raise UniqueConstraintError(msg) - - group.students.append(student) - session.commit() - - def get_students_of_group(self, group_id: int) -> list[StudentDataclass]: - with Session(engine) as session: - group: Group | None = session.get(Group, ident=group_id) - if not group: - msg = f"Group with id {group_id} not found" - raise ItemNotFoundError(msg) - students: list[Student] = group.students - return [student.to_domain_model() for student in students] diff --git a/backend/db/implementation/SqlProjectDAO.py b/backend/db/implementation/SqlProjectDAO.py deleted file mode 100644 index c3ae7674..00000000 --- a/backend/db/implementation/SqlProjectDAO.py +++ /dev/null @@ -1,56 +0,0 @@ -from datetime import datetime - -from sqlalchemy.orm import Session - -from db.errors.database_errors import ItemNotFoundError -from db.extensions import engine -from db.implementation.SqlAbstractDAO import SqlAbstractDAO -from db.interface.ProjectDAO import ProjectDAO -from db.models.models import Project, Subject -from domain.models.ProjectDataclass import ProjectDataclass - - -class SqlProjectDAO(SqlAbstractDAO[Project, ProjectDataclass], ProjectDAO): - def __init__(self) -> None: - self.model_class = Project - - def create_project( - self, - subject_id: int, - name: str, - deadline: datetime, - archived: bool, - description: str, - requirements: str, - visible: bool, - max_students: int, - ) -> ProjectDataclass: - with Session(engine) as session: - subject: Subject | None = session.get(Subject, subject_id) - if not subject: - msg = f"Subject with id {subject_id} not found" - raise ItemNotFoundError(msg) - - new_project: Project = Project( - subject_id=subject_id, - name=name, - deadline=deadline, - archived=archived, - description=description, - requirements=requirements, - visible=visible, - max_students=max_students, - ) - - session.add(new_project) - session.commit() - return new_project.to_domain_model() - - def get_projects_of_subject(self, subject_id: int) -> list[ProjectDataclass]: - with Session(engine) as session: - subject: Subject | None = session.get(Subject, ident=subject_id) - if not subject: - msg = f"Subject with id {subject_id} not found" - raise ItemNotFoundError(msg) - projects: list[Project] = subject.projects - return [project.to_domain_model() for project in projects] diff --git a/backend/db/implementation/SqlStudentDAO.py b/backend/db/implementation/SqlStudentDAO.py deleted file mode 100644 index 2dfd6fe0..00000000 --- a/backend/db/implementation/SqlStudentDAO.py +++ /dev/null @@ -1,24 +0,0 @@ -from sqlalchemy.orm import Session - -from db.extensions import engine -from db.implementation.SqlAbstractDAO import SqlAbstractDAO -from db.interface.StudentDAO import StudentDAO -from db.models.models import Student -from domain.models.StudentDataclass import StudentDataclass - - -class SqlStudentDAO(SqlAbstractDAO[Student, StudentDataclass], StudentDAO): - def __init__(self) -> None: - self.model_class = Student - - def create_student(self, name: str, email: str) -> StudentDataclass: - with Session(engine) as session: - new_student: Student = Student(name=name, email=email) - session.add(new_student) - session.commit() - return new_student.to_domain_model() - - def is_user_student(self, user_id: int) -> bool: - with Session(engine) as session: - student = session.get(Student, user_id) - return student is not None diff --git a/backend/db/implementation/SqlSubjectDAO.py b/backend/db/implementation/SqlSubjectDAO.py deleted file mode 100644 index 5d368901..00000000 --- a/backend/db/implementation/SqlSubjectDAO.py +++ /dev/null @@ -1,74 +0,0 @@ -from sqlalchemy.orm import Session - -from db.errors.database_errors import ItemNotFoundError, UniqueConstraintError -from db.extensions import engine -from db.implementation.SqlAbstractDAO import SqlAbstractDAO -from db.interface.SubjectDAO import SubjectDAO -from db.models.models import Student, Subject, Teacher -from domain.models.SubjectDataclass import SubjectDataclass - - -class SqlSubjectDAO(SqlAbstractDAO[Subject, SubjectDataclass], SubjectDAO): - def __init__(self) -> None: - self.model_class = Subject - - def create_subject(self, name: str) -> SubjectDataclass: - with Session(engine) as session: - new_subject = Subject(name=name) - session.add(new_subject) - session.commit() - return new_subject.to_domain_model() - - def get_subjects_of_teacher(self, teacher_id: int) -> list[SubjectDataclass]: - with Session(engine) as session: - teacher: Teacher | None = session.get(Teacher, ident=teacher_id) - if not teacher: - msg = f"Teacher with id {teacher_id} not found" - raise ItemNotFoundError(msg) - subjects: list[Subject] = teacher.subjects - return [vak.to_domain_model() for vak in subjects] - - def add_student_to_subject(self, student_id: int, subject_id: int) -> None: - with Session(engine) as session: - student: Student | None = session.get(Student, ident=student_id) - subject: Subject | None = session.get(Subject, ident=subject_id) - - if not student: - msg = f"Student with id {student_id} not found" - raise ItemNotFoundError(msg) - if not subject: - msg = f"Subject with id {subject_id} not found" - raise ItemNotFoundError(msg) - if subject in student.subjects: - msg = f"Student with id {student_id} already has subject with id {subject_id}" - raise UniqueConstraintError(msg) - - student.subjects.append(subject) - session.commit() - - def add_teacher_to_subject(self, teacher_id: int, subject_id: int) -> None: - with Session(engine) as session: - teacher: Teacher | None = session.get(Teacher, ident=teacher_id) - subject: Subject | None = session.get(Subject, ident=subject_id) - - if not teacher: - msg = f"Teacher with id {teacher_id} not found" - raise ItemNotFoundError(msg) - if not subject: - msg = f"Subject with id {subject_id} not found" - raise ItemNotFoundError(msg) - if subject in teacher.subjects: - msg = f"Teacher with id {teacher_id} already has subject with id {subject_id}" - raise UniqueConstraintError(msg) - - teacher.subjects.append(subject) - session.commit() - - def get_subjects_of_student(self, student_id: int) -> list[SubjectDataclass]: - with Session(engine) as session: - student: Student | None = session.get(Student, ident=student_id) - if not student: - msg = f"Student with id {student_id} not found" - raise ItemNotFoundError(msg) - subjects: list[Subject] = student.subjects - return [vak.to_domain_model() for vak in subjects] diff --git a/backend/db/implementation/SqlSubmissionDAO.py b/backend/db/implementation/SqlSubmissionDAO.py deleted file mode 100644 index 432296dd..00000000 --- a/backend/db/implementation/SqlSubmissionDAO.py +++ /dev/null @@ -1,51 +0,0 @@ -from datetime import datetime - -from sqlalchemy.orm import Session - -from db.errors.database_errors import ItemNotFoundError -from db.extensions import engine -from db.implementation.SqlAbstractDAO import SqlAbstractDAO -from db.interface.SubmissionDAO import SubmissionDAO -from db.models.models import Group, Student, Submission -from domain.models.SubmissionDataclass import SubmissionDataclass, SubmissionState - - -class SqlSubmissionDAO(SqlAbstractDAO[Submission, SubmissionDataclass], SubmissionDAO): - def __init__(self) -> None: - self.model_class = Submission - - def create_submission(self, student_id: int, group_id: int, message: str, state: SubmissionState, - date_time: datetime) -> SubmissionDataclass: - with Session(engine) as session: - student: Student | None = session.get(Student, ident=student_id) - group: Group | None = session.get(Group, ident=group_id) - if not student: - msg = f"Student with id {student_id} not found" - raise ItemNotFoundError(msg) - if not group: - msg = f"Group with id {group_id} not found" - raise ItemNotFoundError(msg) - new_submission: Submission = Submission(student_id=student_id, - group_id=group_id, message=message, state=state, - date_time=date_time) - session.add(new_submission) - session.commit() - return new_submission.to_domain_model() - - def get_submissions_of_student(self, student_id: int) -> list[SubmissionDataclass]: - with Session(engine) as session: - student: Student | None = session.get(Student, ident=student_id) - if not student: - msg = f"Student with id {student_id} not found" - raise ItemNotFoundError(msg) - submissions: list[Submission] = student.submissions - return [submission.to_domain_model() for submission in submissions] - - def get_submissions_of_group(self, group_id: int) -> list[SubmissionDataclass]: - with Session(engine) as session: - group: Group | None = session.get(Group, ident=group_id) - if not group: - msg = f"Group with id {group_id} not found" - raise ItemNotFoundError(msg) - submissions: list[Submission] = group.submissions - return [submission.to_domain_model() for submission in submissions] diff --git a/backend/db/implementation/SqlTeacherDAO.py b/backend/db/implementation/SqlTeacherDAO.py deleted file mode 100644 index 08fd49d4..00000000 --- a/backend/db/implementation/SqlTeacherDAO.py +++ /dev/null @@ -1,24 +0,0 @@ -from sqlalchemy.orm import Session - -from db.extensions import engine -from db.implementation.SqlAbstractDAO import SqlAbstractDAO -from db.interface.TeacherDAO import TeacherDAO -from db.models.models import Teacher -from domain.models.TeacherDataclass import TeacherDataclass - - -class SqlTeacherDAO(SqlAbstractDAO[Teacher, TeacherDataclass], TeacherDAO): - def __init__(self) -> None: - self.model_class = Teacher - - def create_teacher(self, name: str, email: str) -> TeacherDataclass: - with Session(engine) as session: - new_teacher = Teacher(name=name, email=email) - session.add(new_teacher) - session.commit() - return new_teacher.to_domain_model() - - def is_user_teacher(self, user_id: int) -> bool: - with Session(engine) as session: - teacher = session.get(Teacher, user_id) - return teacher is not None diff --git a/backend/db/implementation/SqlUserDAO.py b/backend/db/implementation/SqlUserDAO.py deleted file mode 100644 index 85e3dd18..00000000 --- a/backend/db/implementation/SqlUserDAO.py +++ /dev/null @@ -1,9 +0,0 @@ -from db.implementation.SqlAbstractDAO import SqlAbstractDAO -from db.interface.UserDAO import UserDAO -from db.models.models import User -from domain.models.UserDataclass import UserDataclass - - -class SqlUserDAO(SqlAbstractDAO[User, UserDataclass], UserDAO): - def __init__(self) -> None: - self.model_class = User diff --git a/backend/db/interface/AbstractDAO.py b/backend/db/interface/AbstractDAO.py deleted file mode 100644 index 2c4ade30..00000000 --- a/backend/db/interface/AbstractDAO.py +++ /dev/null @@ -1,16 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Generic, TypeVar - -T = TypeVar("T") -D = TypeVar("D") - - -class AbstractDAO(Generic[T, D], ABC): - - @abstractmethod - def get(self, ident: int) -> D: - raise NotImplementedError - - @abstractmethod - def get_all(self) -> list[D]: - raise NotImplementedError diff --git a/backend/db/interface/AdminDAO.py b/backend/db/interface/AdminDAO.py deleted file mode 100644 index 7b78c652..00000000 --- a/backend/db/interface/AdminDAO.py +++ /dev/null @@ -1,22 +0,0 @@ -from abc import abstractmethod - -from db.interface.AbstractDAO import AbstractDAO -from db.models.models import Admin -from domain.models.AdminDataclass import AdminDataclass - - -class AdminDAO(AbstractDAO[Admin, AdminDataclass]): - @abstractmethod - def create_admin(self, name: str, email: str) -> AdminDataclass: - """ - Maakt een nieuwe admin aan. - - :param name: De naam van de nieuwe admin. - :param email: De email van de nieuwe admin. - :return: De nieuwe admin - """ - raise NotImplementedError - - @abstractmethod - def is_user_admin(self, user_id: int) -> bool: - raise NotImplementedError diff --git a/backend/db/interface/DAOProvider.py b/backend/db/interface/DAOProvider.py deleted file mode 100644 index 6ebe43c5..00000000 --- a/backend/db/interface/DAOProvider.py +++ /dev/null @@ -1,44 +0,0 @@ -from abc import ABC, abstractmethod - -from db.interface.AdminDAO import AdminDAO -from db.interface.GroupDAO import GroupDAO -from db.interface.ProjectDAO import ProjectDAO -from db.interface.StudentDAO import StudentDAO -from db.interface.SubjectDAO import SubjectDAO -from db.interface.SubmissionDAO import SubmissionDAO -from db.interface.TeacherDAO import TeacherDAO -from db.interface.UserDAO import UserDAO - - -class DAOProvider(ABC): - @abstractmethod - def get_admin_dao(self) -> AdminDAO: - raise NotImplementedError - - @abstractmethod - def get_group_dao(self) -> GroupDAO: - raise NotImplementedError - - @abstractmethod - def get_project_dao(self) -> ProjectDAO: - raise NotImplementedError - - @abstractmethod - def get_student_dao(self) -> StudentDAO: - raise NotImplementedError - - @abstractmethod - def get_subject_dao(self) -> SubjectDAO: - raise NotImplementedError - - @abstractmethod - def get_submission_dao(self) -> SubmissionDAO: - raise NotImplementedError - - @abstractmethod - def get_teacher_dao(self) -> TeacherDAO: - raise NotImplementedError - - @abstractmethod - def get_user_dao(self) -> UserDAO: - raise NotImplementedError diff --git a/backend/db/interface/GroupDAO.py b/backend/db/interface/GroupDAO.py deleted file mode 100644 index b1b9e9d3..00000000 --- a/backend/db/interface/GroupDAO.py +++ /dev/null @@ -1,60 +0,0 @@ -from abc import abstractmethod - -from db.interface.AbstractDAO import AbstractDAO -from db.models.models import Group -from domain.models.GroupDataclass import GroupDataclass -from domain.models.StudentDataclass import StudentDataclass - - -class GroupDAO(AbstractDAO[Group, GroupDataclass]): - @abstractmethod - def create_group(self, project_id: int) -> GroupDataclass: - """ - Creëert een nieuw GroupDataClass in de database en associeert het met een ProjectDataClass. - - :param project_id: Id van het project dat gelinkt is aan de groep - :raises: ItemNotFoundException: Als er geen ProjectDataClass met de opgegeven `project_id` in de database is. - :returns: De nieuw aangemaakte groep - """ - raise NotImplementedError - - @abstractmethod - def get_groups_of_project(self, project_id: int) -> list[GroupDataclass]: - """ - Haalt alle groepen op die bij een bepaald project horen. - - :param project_id: Het subject waarvan de projecten opgehaald moeten worden. - :return: Een lijst van projecten die bij een bepaald project horen. - """ - raise NotImplementedError - - @abstractmethod - def get_groups_of_student(self, student_id: int) -> list[GroupDataclass]: - """ - Haalt alle groepen op die bij een bepaalde student horen. - - :param student_id: De student waarvan de groepen opgehaald moeten worden. - :return: Een lijst van groepen die bij een bepaald student horen. - """ - raise NotImplementedError - - @abstractmethod - def add_student_to_group(self, student_id: int, group_id: int) -> None: - """ - Gaat een student toevoegen aan een groep - - :param student_id: De student die aan de groep moet toegevoegd worden. - :param group_id: De groep waaraan de student moet toegevoegd worden. - :raises ItemNotFoundException: Als er geen group/student met gegeven id in de databank zit. - """ - raise NotImplementedError - - @abstractmethod - def get_students_of_group(self, group_id: int) -> list[StudentDataclass]: - """ - Gaat alle studenten geven die in een bepaalde groep zitten - - :param group_id: De groep waarvan de studenten worden opgeroepen - :raises ItemNotFoundException: Als er geen group met gegeven id in de databank zit. - """ - raise NotImplementedError diff --git a/backend/db/interface/ProjectDAO.py b/backend/db/interface/ProjectDAO.py deleted file mode 100644 index ed2d1e6a..00000000 --- a/backend/db/interface/ProjectDAO.py +++ /dev/null @@ -1,46 +0,0 @@ -from abc import abstractmethod -from datetime import datetime - -from db.interface.AbstractDAO import AbstractDAO -from db.models.models import Project -from domain.models.ProjectDataclass import ProjectDataclass - - -class ProjectDAO(AbstractDAO[Project, ProjectDataclass]): - @abstractmethod - def create_project( - self, - subject_id: int, - name: str, - deadline: datetime, - archived: bool, - description: str, - requirements: str, - visible: bool, - max_students: int, - ) -> ProjectDataclass: - """ - Creëert een nieuw ProjectDataClass in de database en associeert het met een SubjectDataClass. - - :param max_students: maximaal aantal studenten per groep per project - :param visible: of het project zichtbaar is voor de studentent - :param description: Beschrijving van het project - :param requirements: Vereisten van het project - :param archived: Of het project gearchiveerd is - :param name: De naam van het project - :param deadline: De deadline van het project - :param subject_id: De identificatie van de SubjectDataClass waarmee het ProjectDataClass geassocieerd wordt. - :raises: ItemNotFoundException: Als er geen SubjectDataClass met de opgegeven `teacher_id` in de database is. - :returns: Het nieuw aangemaakte project - """ - raise NotImplementedError - - @abstractmethod - def get_projects_of_subject(self, subject_id: int) -> list[ProjectDataclass]: - """ - Haalt alle projecten op die bij een bepaald subject horen. - - :param subject_id: Het subject waarvan de projecten opgehaald moeten worden. - :return: Een lijst van projecten die bij een bepaald subject horen. - """ - raise NotImplementedError diff --git a/backend/db/interface/StudentDAO.py b/backend/db/interface/StudentDAO.py deleted file mode 100644 index e7f9e8da..00000000 --- a/backend/db/interface/StudentDAO.py +++ /dev/null @@ -1,22 +0,0 @@ -from abc import abstractmethod - -from db.interface.AbstractDAO import AbstractDAO -from db.models.models import Student -from domain.models.StudentDataclass import StudentDataclass - - -class StudentDAO(AbstractDAO[Student, StudentDataclass]): - @abstractmethod - def create_student(self, name: str, email: str) -> StudentDataclass: - """ - Maakt een nieuwe student aan. - - :param name: De naam van de nieuwe student - :param email: De email van de nieuwe student - :returns: De nieuw aangemaakte student - """ - raise NotImplementedError - - @abstractmethod - def is_user_student(self, user_id: int) -> bool: - raise NotImplementedError diff --git a/backend/db/interface/SubjectDAO.py b/backend/db/interface/SubjectDAO.py deleted file mode 100644 index b759ab12..00000000 --- a/backend/db/interface/SubjectDAO.py +++ /dev/null @@ -1,59 +0,0 @@ -from abc import abstractmethod - -from db.interface.AbstractDAO import AbstractDAO -from db.models.models import Subject -from domain.models.SubjectDataclass import SubjectDataclass - - -class SubjectDAO(AbstractDAO[Subject, SubjectDataclass]): - @abstractmethod - def create_subject(self, name: str) -> SubjectDataclass: - """ - Creëert een nieuw SubjectDataclass in de database. - - :param name: De naam van het nieuwe vak. - :returns: Het nieuw aangemaakte subject. - """ - raise NotImplementedError - - @abstractmethod - def get_subjects_of_teacher(self, teacher_id: int) -> list[SubjectDataclass]: - """ - Haalt de subjects op die door een bepaalde teacher worden gegeven. - - :param teacher_id: De teacher waarvan de subjects opgehaald moeten worden. - :return: Een lijst van subjects die door de gegeven teacher worden gegeven. - """ - raise NotImplementedError - - @abstractmethod - def get_subjects_of_student(self, student_id: int) -> list[SubjectDataclass]: - """ - Haalt de subjects op die door een bepaalde student worden gevolgd. - - :param student_id: De student waarvan de subjects opgehaald moeten worden. - :return: Een lijst van subjects die door de gegeven student worden gegeven. - """ - raise NotImplementedError - - @abstractmethod - def add_student_to_subject(self, student_id: int, subject_id: int) -> None: - """ - Voegt een student toe aan een vak. - - :param subject_id: De id van subject die door de student wordt gevolgd. - :param student_id: De student die subject volgt. - :raises: ItemNotFoundException: Als er geen student/subject met de opgegeven id in de database is. - """ - raise NotImplementedError - - @abstractmethod - def add_teacher_to_subject(self, teacher_id: int, subject_id: int) -> None: - """ - Voegt een teacher toe aan een vak. - - :param subject_id: De id van subject die door de teacher gegeven wordt. - :param teacher_id: De teacher die dit subject geeft. - :raises: ItemNotFoundException: Als er geen teacher/subject met de opgegeven id in de database is. - """ - raise NotImplementedError diff --git a/backend/db/interface/SubmissionDAO.py b/backend/db/interface/SubmissionDAO.py deleted file mode 100644 index c5fd33c2..00000000 --- a/backend/db/interface/SubmissionDAO.py +++ /dev/null @@ -1,49 +0,0 @@ -from abc import abstractmethod -from datetime import datetime - -from db.interface.AbstractDAO import AbstractDAO -from db.models.models import Submission -from domain.models.SubmissionDataclass import SubmissionDataclass, SubmissionState - - -class SubmissionDAO(AbstractDAO[Submission, SubmissionDataclass]): - @abstractmethod - def create_submission( - self, - student_id: int, - group_id: int, - message: str, - state: SubmissionState, - date_time: datetime, - ) -> SubmissionDataclass: - """ - Creëert een nieuw SubmissionDataClass in de database en associeert het met een StudentDataclass en een - GroupDataClass. - - :param student_id: De identificatie van de StudentDataclass waarmee het SubmissionDataClass geassocieerd wordt. - :param group_id: De identificatie van de GroupDataClass waarmee het SubmissionDataClass geassocieerd wordt - :raises: ItemNotFoundException: Als er geen StudentDataclass of GroupDataClass met de opgegeven `student_id` of - `group_id` in de database is. - :return: De nieuw aangemaakte submission - """ - raise NotImplementedError - - @abstractmethod - def get_submissions_of_student(self, student_id: int) -> list[SubmissionDataclass]: - """ - Haalt alle projecten op die bij een bepaalde student horen. - - :param student_id: De student waarvan de submissions opgehaald moeten worden. - :return: Een lijst van submissions die bij een bepaalde student horen. - """ - raise NotImplementedError - - @abstractmethod - def get_submissions_of_group(self, group_id: int) -> list[SubmissionDataclass]: - """ - Haalt alle projecten op die bij een bepaalde groep horen. - - :param group_id: De groep waarvan de submissions opgehaald moeten worden. - :return: Een lijst van submissions die bij een bepaalde groep horen. - """ - raise NotImplementedError diff --git a/backend/db/interface/TeacherDAO.py b/backend/db/interface/TeacherDAO.py deleted file mode 100644 index 99bea93c..00000000 --- a/backend/db/interface/TeacherDAO.py +++ /dev/null @@ -1,22 +0,0 @@ -from abc import abstractmethod - -from db.interface.AbstractDAO import AbstractDAO -from db.models.models import Teacher -from domain.models.TeacherDataclass import TeacherDataclass - - -class TeacherDAO(AbstractDAO[Teacher, TeacherDataclass]): - @abstractmethod - def create_teacher(self, name: str, email: str) -> TeacherDataclass: - """ - Maakt een nieuwe teacher aan. - - :param name: De naam van de nieuwe teacher. - :param email: De email van de nieuwe teacher. - :returns: De nieuw aangemaakte teacher. - """ - raise NotImplementedError - - @abstractmethod - def is_user_teacher(self, user_id: int) -> bool: - raise NotImplementedError diff --git a/backend/db/interface/UserDAO.py b/backend/db/interface/UserDAO.py deleted file mode 100644 index 6769baba..00000000 --- a/backend/db/interface/UserDAO.py +++ /dev/null @@ -1,7 +0,0 @@ -from db.interface.AbstractDAO import AbstractDAO -from db.models.models import User -from domain.models.UserDataclass import UserDataclass - - -class UserDAO(AbstractDAO[User, UserDataclass]): - pass diff --git a/backend/db/sessions.py b/backend/db/sessions.py new file mode 100644 index 00000000..da201a92 --- /dev/null +++ b/backend/db/sessions.py @@ -0,0 +1,15 @@ +from collections.abc import Generator + +from sqlalchemy.orm import Session, sessionmaker + +from db.extensions import engine + +SessionLocal: sessionmaker[Session] = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_session() -> Generator[Session, None, None]: + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/domain/logic/SubjectLogic.py b/backend/domain/logic/SubjectLogic.py deleted file mode 100644 index 7475ee63..00000000 --- a/backend/domain/logic/SubjectLogic.py +++ /dev/null @@ -1,17 +0,0 @@ -from db.interface.DAOProvider import DAOProvider -from domain.models.SubjectDataclass import SubjectDataclass -from domain.models.UserDataclass import UserDataclass - - -def is_user_authorized_for_subject(subject: SubjectDataclass, user: UserDataclass, dao_provider: DAOProvider) -> bool: - teacher_dao = dao_provider.get_teacher_dao() - student_dao = dao_provider.get_student_dao() - subject_dao = dao_provider.get_subject_dao() - - if teacher_dao.is_user_teacher(user.id) and subject in subject_dao.get_subjects_of_teacher(user.id): - return True - - if student_dao.is_user_student(user.id) and subject in subject_dao.get_subjects_of_student(user.id): - return True - - return False diff --git a/backend/domain/logic/UserLogic.py b/backend/domain/logic/UserLogic.py deleted file mode 100644 index 428465f6..00000000 --- a/backend/domain/logic/UserLogic.py +++ /dev/null @@ -1,18 +0,0 @@ -from db.interface.DAOProvider import DAOProvider -from domain.models.APIUser import APIUser -from domain.models.UserDataclass import UserDataclass - - -def convert_user(user: UserDataclass, dao_provider: DAOProvider) -> APIUser: - api_user = APIUser(id=user.id, name=user.name, email=user.email, roles=[]) - - if dao_provider.get_teacher_dao().is_user_teacher(user.id): - api_user.roles.append("teacher") - - if dao_provider.get_admin_dao().is_user_admin(user.id): - api_user.roles.append("admin") - - if dao_provider.get_student_dao().is_user_student(user.id): - api_user.roles.append("student") - - return api_user diff --git a/backend/domain/logic/admin.py b/backend/domain/logic/admin.py index be239d11..aaa83f61 100644 --- a/backend/domain/logic/admin.py +++ b/backend/domain/logic/admin.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session from db.models.models import Admin +from domain.logic.basic_operations import get, get_all from domain.models.AdminDataclass import AdminDataclass @@ -11,6 +12,15 @@ def create_admin(session: Session, name: str, email: str) -> AdminDataclass: return new_admin.to_domain_model() +def get_admin(session: Session, admin_id: int) -> AdminDataclass: + return get(session, Admin, admin_id).to_domain_model() + + +def get_all_admins(session: Session) -> list[AdminDataclass]: + return [admin.to_domain_model() for admin in get_all(session, Admin)] + + + def is_user_admin(session: Session, user_id: int) -> bool: admin = session.get(Admin, user_id) return admin is not None diff --git a/backend/domain/logic/basic_operations.py b/backend/domain/logic/basic_operations.py index fda2a77d..94fb15e3 100644 --- a/backend/domain/logic/basic_operations.py +++ b/backend/domain/logic/basic_operations.py @@ -1,4 +1,4 @@ -from typing import TypeVar, Type +from typing import TypeVar from sqlalchemy import select from sqlalchemy.orm import Session @@ -9,15 +9,15 @@ T = TypeVar("T", bound=AbstractModel) -def get(session: Session, object_type: Type, ident: int) -> T: +def get(session: Session, object_type: type[T], ident: int) -> T: generic_object: T | None = session.get(object_type, ident) if not generic_object: msg = f"object with id {ident} not found" raise ItemNotFoundError(msg) - return generic_object.to_domain_model() + return generic_object -def get_all(session: Session) -> list[T]: - return list(session.scalars(select(T)).all()) +def get_all(session: Session, object_type: type[T]) -> list[T]: + return list(session.scalars(select(object_type)).all()) diff --git a/backend/domain/logic/group.py b/backend/domain/logic/group.py index 4f280946..76c2582c 100644 --- a/backend/domain/logic/group.py +++ b/backend/domain/logic/group.py @@ -1,16 +1,15 @@ -from typing import Type from sqlalchemy.orm import Session -from db.errors.database_errors import UniqueConstraintError -from db.models.models import Project, Group, Student -from domain.logic.basic_operations import get +from db.errors.database_errors import ActionAlreadyPerformedError +from db.models.models import Group, Project, Student +from domain.logic.basic_operations import get, get_all from domain.models.GroupDataclass import GroupDataclass from domain.models.StudentDataclass import StudentDataclass def create_group(session: Session, project_id: int) -> GroupDataclass: - project: Project = get(session, Type[Project], project_id) + project: Project = get(session, Project, project_id) new_group: Group = Group(project_id=project_id) project.groups.append(new_group) @@ -20,31 +19,39 @@ def create_group(session: Session, project_id: int) -> GroupDataclass: return new_group.to_domain_model() +def get_group(session: Session, group_id: int) -> GroupDataclass: + return get(session, Group, group_id).to_domain_model() + + +def get_all_groups(session: Session) -> list[GroupDataclass]: + return [group.to_domain_model() for group in get_all(session, Group)] + + def get_groups_of_project(session: Session, project_id: int) -> list[GroupDataclass]: - project: Project = get(session, Type[Project], project_id) + project: Project = get(session, Project, project_id) groups: list[Group] = project.groups return [group.to_domain_model() for group in groups] def get_groups_of_student(session: Session, student_id: int) -> list[GroupDataclass]: - student: Student = get(session, Type[Student], ident=student_id) + student: Student = get(session, Student, ident=student_id) groups: list[Group] = student.groups return [group.to_domain_model() for group in groups] def add_student_to_group(session: Session, student_id: int, group_id: int) -> None: - student: Student = get(session, Type[Student], ident=student_id) - group: Group = get(session, Type[Group], ident=group_id) + student: Student = get(session, Student, ident=student_id) + group: Group = get(session, Group, ident=group_id) if student in group.students: msg = f"Student with id {student_id} already in group with id {group_id}" - raise UniqueConstraintError(msg) + raise ActionAlreadyPerformedError(msg) group.students.append(student) session.commit() def get_students_of_group(session: Session, group_id: int) -> list[StudentDataclass]: - group: Group = get(session, Type[Group], ident=group_id) + group: Group = get(session, Group, ident=group_id) students: list[Student] = group.students return [student.to_domain_model() for student in students] diff --git a/backend/domain/logic/project.py b/backend/domain/logic/project.py index 61ec971f..835d7d6b 100644 --- a/backend/domain/logic/project.py +++ b/backend/domain/logic/project.py @@ -1,10 +1,9 @@ from datetime import datetime -from typing import Type from sqlalchemy.orm import Session -from db.models.models import Subject, Project -from domain.logic.basic_operations import get +from db.models.models import Project, Subject +from domain.logic.basic_operations import get, get_all from domain.models.ProjectDataclass import ProjectDataclass @@ -19,7 +18,7 @@ def create_project( visible: bool, max_students: int, ) -> ProjectDataclass: - subject: Subject = get(session, Type[Subject], subject_id) + subject: Subject = get(session, Subject, subject_id) new_project: Project = Project( name=name, @@ -37,8 +36,16 @@ def create_project( return new_project.to_domain_model() +def get_project(session: Session, project_id: int) -> ProjectDataclass: + return get(session, Project, project_id).to_domain_model() + + +def get_all_projects(session: Session) -> list[ProjectDataclass]: + return [project.to_domain_model() for project in get_all(session, Project)] + + def get_projects_of_subject(session: Session, subject_id: int) -> list[ProjectDataclass]: - subject: Subject = get(session, Type[Subject], ident=subject_id) + subject: Subject = get(session, Subject, ident=subject_id) projects: list[Project] = subject.projects return [project.to_domain_model() for project in projects] diff --git a/backend/domain/logic/role_enum.py b/backend/domain/logic/role_enum.py new file mode 100644 index 00000000..94479eb4 --- /dev/null +++ b/backend/domain/logic/role_enum.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class Role(Enum): + ADMIN = "ADMIN" + TEACHER = "TEACHER" + STUDENT = "STUDENT" diff --git a/backend/domain/logic/student.py b/backend/domain/logic/student.py index a6b33583..59c4049e 100644 --- a/backend/domain/logic/student.py +++ b/backend/domain/logic/student.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session from db.models.models import Student +from domain.logic.basic_operations import get, get_all from domain.models.StudentDataclass import StudentDataclass @@ -11,6 +12,14 @@ def create_student(session: Session, name: str, email: str) -> StudentDataclass: return new_student.to_domain_model() +def get_student(session: Session, student_id: int) -> StudentDataclass: + return get(session, Student, student_id).to_domain_model() + + +def get_all_students(session: Session) -> list[StudentDataclass]: + return [student.to_domain_model() for student in get_all(session, Student)] + + def is_user_student(session: Session, user_id: int) -> bool: student = session.get(Student, user_id) return student is not None diff --git a/backend/domain/logic/subject.py b/backend/domain/logic/subject.py index 6a3420c3..38e470e9 100644 --- a/backend/domain/logic/subject.py +++ b/backend/domain/logic/subject.py @@ -1,24 +1,22 @@ -from typing import Type - from sqlalchemy.orm import Session -from db.errors.database_errors import UniqueConstraintError -from db.models.models import Subject, Teacher, Student -from domain.logic.basic_operations import get +from db.errors.database_errors import ActionAlreadyPerformedError +from db.models.models import Student, Subject, Teacher +from domain.logic.basic_operations import get, get_all +from domain.logic.student import is_user_student +from domain.logic.teacher import is_user_teacher from domain.models.SubjectDataclass import SubjectDataclass from domain.models.UserDataclass import UserDataclass -def is_user_authorized_for_subject(subject: SubjectDataclass, user: UserDataclass) -> bool: - teacher_dao = dao_provider.get_teacher_dao() - student_dao = dao_provider.get_student_dao() - subject_dao = dao_provider.get_subject_dao() # TODO - - if teacher_dao.is_user_teacher(user.id) and subject in subject_dao.get_subjects_of_teacher(user.id): - return True +def is_user_authorized_for_subject(session: Session, subject: SubjectDataclass, user: UserDataclass) -> bool: + if is_user_teacher(session, user.id): + subjects_of_teacher: list[SubjectDataclass] = get_subjects_of_teacher(session, subject.id) + return subject in subjects_of_teacher - if student_dao.is_user_student(user.id) and subject in subject_dao.get_subjects_of_student(user.id): - return True + if is_user_student(session, user.id): + subjects_of_student: list[SubjectDataclass] = get_subjects_of_student(session, subject.id) + return subject in subjects_of_student return False @@ -30,37 +28,45 @@ def create_subject(session: Session, name: str) -> SubjectDataclass: return new_subject.to_domain_model() +def get_subject(session: Session, subject_id: int) -> SubjectDataclass: + return get(session, Subject, subject_id).to_domain_model() + + +def get_all_subjects(session: Session) -> list[SubjectDataclass]: + return [subject.to_domain_model() for subject in get_all(session, Subject)] + + def get_subjects_of_teacher(session: Session, teacher_id: int) -> list[SubjectDataclass]: - teacher: Teacher = get(session, Type[Teacher], ident=teacher_id) + teacher: Teacher = get(session, Teacher, ident=teacher_id) subjects: list[Subject] = teacher.subjects return [vak.to_domain_model() for vak in subjects] def add_student_to_subject(session: Session, student_id: int, subject_id: int) -> None: - student: Student = get(session, Type[Student], ident=student_id) - subject: Subject = get(session, Type[Subject], ident=subject_id) + student: Student = get(session, Student, ident=student_id) + subject: Subject = get(session, Subject, ident=subject_id) if subject in student.subjects: msg = f"Student with id {student_id} already has subject with id {subject_id}" - raise UniqueConstraintError(msg) + raise ActionAlreadyPerformedError(msg) student.subjects.append(subject) session.commit() def add_teacher_to_subject(session: Session, teacher_id: int, subject_id: int) -> None: - teacher: Teacher | None = get(session, Type[Teacher], ident=teacher_id) - subject: Subject | None = get(session, Type[Subject], ident=subject_id) + teacher: Teacher | None = get(session, Teacher, ident=teacher_id) + subject: Subject | None = get(session, Subject, ident=subject_id) if subject in teacher.subjects: msg = f"Teacher with id {teacher_id} already has subject with id {subject_id}" - raise UniqueConstraintError(msg) + raise ActionAlreadyPerformedError(msg) teacher.subjects.append(subject) session.commit() def get_subjects_of_student(session: Session, student_id: int) -> list[SubjectDataclass]: - student: Student = get(session, Type[Student], ident=student_id) + student: Student = get(session, Student, ident=student_id) subjects: list[Subject] = student.subjects return [vak.to_domain_model() for vak in subjects] diff --git a/backend/domain/logic/submission.py b/backend/domain/logic/submission.py index c4229ebe..e4bb8318 100644 --- a/backend/domain/logic/submission.py +++ b/backend/domain/logic/submission.py @@ -1,10 +1,9 @@ from datetime import datetime -from typing import Type from sqlalchemy.orm import Session -from db.models.models import Student, Group, Submission -from domain.logic.basic_operations import get +from db.models.models import Group, Student, Submission +from domain.logic.basic_operations import get, get_all from domain.models.SubmissionDataclass import SubmissionDataclass, SubmissionState @@ -14,30 +13,38 @@ def create_submission( group_id: int, message: str, state: SubmissionState, - date_time: datetime + date_time: datetime, ) -> SubmissionDataclass: - student: Student = get(session, Type[Student], ident=student_id) - group: Group = get(session, Type[Group], ident=group_id) + student: Student = get(session, Student, ident=student_id) + group: Group = get(session, Group, ident=group_id) new_submission: Submission = Submission( student_id=student.id, group_id=group.id, message=message, state=state, - date_time=date_time + date_time=date_time, ) session.add(new_submission) session.commit() return new_submission.to_domain_model() +def get_submission(session: Session, submission_id: int) -> SubmissionDataclass: + return get(session, Submission, submission_id).to_domain_model() + + +def get_all_submissions(session: Session) -> list[SubmissionDataclass]: + return [submission.to_domain_model() for submission in get_all(session, Submission)] + + def get_submissions_of_student(session: Session, student_id: int) -> list[SubmissionDataclass]: - student: Student = get(session, Type[Student], ident=student_id) + student: Student = get(session, Student, ident=student_id) submissions: list[Submission] = student.submissions return [submission.to_domain_model() for submission in submissions] def get_submissions_of_group(session: Session, group_id: int) -> list[SubmissionDataclass]: - group: Group = get(session, Type[Group], ident=group_id) + group: Group = get(session, Group, ident=group_id) submissions: list[Submission] = group.submissions return [submission.to_domain_model() for submission in submissions] diff --git a/backend/domain/logic/teacher.py b/backend/domain/logic/teacher.py index 229320ee..1bb93b08 100644 --- a/backend/domain/logic/teacher.py +++ b/backend/domain/logic/teacher.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session from db.models.models import Teacher +from domain.logic.basic_operations import get, get_all from domain.models.TeacherDataclass import TeacherDataclass @@ -11,6 +12,14 @@ def create_teacher(session: Session, name: str, email: str) -> TeacherDataclass: return new_teacher.to_domain_model() +def get_teacher(session: Session, teacher_id: int) -> TeacherDataclass: + return get(session, Teacher, teacher_id).to_domain_model() + + +def get_all_teachers(session: Session) -> list[TeacherDataclass]: + return [teacher.to_domain_model() for teacher in get_all(session, Teacher)] + + def is_user_teacher(session: Session, user_id: int) -> bool: teacher = session.get(Teacher, user_id) return teacher is not None diff --git a/backend/domain/logic/user.py b/backend/domain/logic/user.py new file mode 100644 index 00000000..4e902e57 --- /dev/null +++ b/backend/domain/logic/user.py @@ -0,0 +1,33 @@ +from sqlalchemy.orm import Session + +from db.models.models import User +from domain.logic.admin import is_user_admin +from domain.logic.basic_operations import get, get_all +from domain.logic.role_enum import Role +from domain.logic.student import is_user_student +from domain.logic.teacher import is_user_teacher +from domain.models.APIUser import APIUser +from domain.models.UserDataclass import UserDataclass + + +def convert_user(session: Session, user: UserDataclass) -> APIUser: + api_user = APIUser(id=user.id, name=user.name, email=user.email, roles=[]) + + if is_user_teacher(session, user.id): + api_user.roles.append(Role.TEACHER) + + if is_user_admin(session, user.id): + api_user.roles.append(Role.ADMIN) + + if is_user_student(session, user.id): + api_user.roles.append(Role.STUDENT) + + return api_user + + +def get_user(session: Session, user_id: int) -> UserDataclass: + return get(session, User, user_id).to_domain_model() + + +def get_all_users(session: Session) -> list[UserDataclass]: + return [user.to_domain_model() for user in get_all(session, User)] diff --git a/backend/domain/models/APIUser.py b/backend/domain/models/APIUser.py index f1397bfd..c37fb477 100644 --- a/backend/domain/models/APIUser.py +++ b/backend/domain/models/APIUser.py @@ -1,8 +1,10 @@ from pydantic import BaseModel, EmailStr +from domain.logic.role_enum import Role + class APIUser(BaseModel): id: int name: str email: EmailStr - roles: list[str] + roles: list[Role] diff --git a/backend/fill_database_mock.py b/backend/fill_database_mock.py index da04f028..880048fa 100644 --- a/backend/fill_database_mock.py +++ b/backend/fill_database_mock.py @@ -1,55 +1,98 @@ from datetime import datetime from psycopg2 import tz +from sqlalchemy.orm import sessionmaker from db.extensions import Base, engine -from db.implementation.SqlAdminDAO import SqlAdminDAO -from db.implementation.SqlGroupDAO import SqlGroupDAO -from db.implementation.SqlProjectDAO import SqlProjectDAO -from db.implementation.SqlStudentDAO import SqlStudentDAO -from db.implementation.SqlSubjectDAO import SqlSubjectDAO -from db.implementation.SqlTeacherDAO import SqlTeacherDAO +from domain.logic.admin import create_admin +from domain.logic.group import add_student_to_group, create_group +from domain.logic.project import create_project +from domain.logic.student import create_student +from domain.logic.subject import add_teacher_to_subject, create_subject +from domain.logic.teacher import create_teacher if __name__ == "__main__": Base.metadata.create_all(engine) + SessionLocal = sessionmaker(autocommit=False, bind=engine) + session = SessionLocal() - admin_dao = SqlAdminDAO() - student_dao = SqlStudentDAO() - teacher_dao = SqlTeacherDAO() - subject_dao = SqlSubjectDAO() - group_dao = SqlGroupDAO() - project_dao = SqlProjectDAO() + # Create subjects + objeprog = create_subject(session, name="Objectgericht Programmeren") + algoritmen = create_subject(session, name="Algoritmen en Datastructuren") + webtech = create_subject(session, name="Webtechnologie") - # maak een vak - objeprog = subject_dao.create_subject(name="OBJECTGERICHTPROGRAMMEREN") - - # maak een project voor dat vak - objprog_project = project_dao.create_project( + # Create projects for subjects + objprog_project = create_project( + session=session, subject_id=objeprog.id, - name="PROJECT", + name="Flash Cards", archived=False, visible=True, - description="Maak iets in javafx", - requirements="Een zip bestand met java-code", + description="Maak iets in JavaFX", + requirements="Een zip bestand met Java-code", max_students=2, - deadline=datetime(2000, 1, 1, 0, 0, 0, tzinfo=tz.LOCAL), + deadline=datetime(2024, 12, 31, 23, 59, 59, tzinfo=tz.LOCAL), ) - # maak een groepje voor het project van objeprog - groep1 = group_dao.create_group(objprog_project.id) + algo_project = create_project( + session=session, + subject_id=algoritmen.id, + name="Sorteer Algoritmen Implementatie", + archived=False, + visible=True, + description="Implementeer verschillende sorteeralgoritmen", + requirements="Code in Python", + max_students=3, + deadline=datetime(2024, 11, 15, 23, 59, 59, tzinfo=tz.LOCAL), + ) + + web_project = create_project( + session=session, + subject_id=webtech.id, + name="Webshop", + archived=False, + visible=True, + description="Bouw een eenvoudige webshop", + requirements="Gebruik van HTML, CSS, en JavaScript", + max_students=4, + deadline=datetime(2024, 10, 30, 23, 59, 59, tzinfo=tz.LOCAL), + ) + + # Create groups for projects + groep1_objprog = create_group(session, objprog_project.id) + groep2_algo = create_group(session, algo_project.id) + groep3_web = create_group(session, web_project.id) + + # Create students + student1 = create_student(session, "Lukas", "lukas@gmail.com") + student2 = create_student(session, "Alberic", "alberic@gmail.com") + student3 = create_student(session, "Matthias", "matthias@gmail.com") + student4 = create_student(session, "Eva", "eva@gmail.com") + student5 = create_student(session, "Emma", "emma@gmail.com") + + # Create teachers + teacher1 = create_teacher(session, "Kris Coolsaet", "kris.coolsaet@ugent.be") + teacher2 = create_teacher(session, "Sophie Devolder", "sophie.devolder@ugent.be") + teacher3 = create_teacher(session, "Pieter-Jan De Smet", "pj.desmet@ugent.be") + + # Create admin + admin = create_admin(session, "Admin", "admin@gmail.com") + + # Add teachers to subjects + add_teacher_to_subject(session, teacher1.id, objeprog.id) + add_teacher_to_subject(session, teacher2.id, algoritmen.id) + add_teacher_to_subject(session, teacher3.id, webtech.id) - # maak studenten - student1 = student_dao.create_student("Student1", "Student1@gmail.com") - student2 = student_dao.create_student("Student2", "Student2@gmail.com") - student3 = student_dao.create_student("Student3", "Student3@gmail.com") + # Add students to groups + add_student_to_group(session, student1.id, groep1_objprog.id) + add_student_to_group(session, student2.id, groep1_objprog.id) + add_student_to_group(session, student3.id, groep1_objprog.id) - # maak teacher - teacher1 = teacher_dao.create_teacher("Teacher1", "Teacher1@gmail.com") + add_student_to_group(session, student4.id, groep2_algo.id) + add_student_to_group(session, student5.id, groep2_algo.id) - # voeg teacher toe aan objeprog - subject_dao.add_teacher_to_subject(teacher1.id, objeprog.id) + add_student_to_group(session, student1.id, groep3_web.id) + add_student_to_group(session, student3.id, groep3_web.id) + add_student_to_group(session, student5.id, groep3_web.id) - # voeg studenten toe aan de groep - group_dao.add_student_to_group(student1.id, groep1.id) - group_dao.add_student_to_group(student2.id, groep1.id) - group_dao.add_student_to_group(student3.id, groep1.id) + session.commit() diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1539153d..eaabfa47 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -82,6 +82,7 @@ select = [ ignore = [ "D", # Docstrings "ANN101", # type annotation for self + "ARG001", "S201", # Use of `debug=True` in Flask app detected "INP001", # File `...` is part of an implicit namespace package "S101", # is nodig voor de testen diff --git a/backend/routes/db.py b/backend/routes/db.py deleted file mode 100644 index 584aedad..00000000 --- a/backend/routes/db.py +++ /dev/null @@ -1,6 +0,0 @@ -from db.implementation.SqlDAOProvider import SqlDAOProvider -from db.interface.DAOProvider import DAOProvider - - -def get_dao_provider() -> DAOProvider: - return SqlDAOProvider() diff --git a/backend/routes/dependencies/role_dependencies.py b/backend/routes/dependencies/role_dependencies.py new file mode 100644 index 00000000..a8e6e994 --- /dev/null +++ b/backend/routes/dependencies/role_dependencies.py @@ -0,0 +1,54 @@ +from fastapi import Depends +from sqlalchemy.orm import Session + +from db.sessions import get_session +from domain.logic.admin import get_admin, is_user_admin +from domain.logic.student import get_student, is_user_student +from domain.logic.subject import get_subjects_of_student +from domain.logic.teacher import get_teacher, is_user_teacher +from domain.models.AdminDataclass import AdminDataclass +from domain.models.StudentDataclass import StudentDataclass +from domain.models.TeacherDataclass import TeacherDataclass +from routes.errors.authentication import ( + InvalidAdminCredentialsError, + InvalidStudentCredentialsError, + InvalidTeacherCredentialsError, + StudentNotEnrolledError, +) + + +def get_authenticated_user() -> int: + return 1 # Checken of een user bestaat en/of hij de juiste credentials heeft. + + +def get_authenticated_admin(session: Session = Depends(get_session)) -> AdminDataclass: + user_id = get_authenticated_user() + if not is_user_admin(session, user_id): + raise InvalidAdminCredentialsError + return get_admin(session, user_id) + + +def get_authenticated_teacher(session: Session = Depends(get_session)) -> TeacherDataclass: + user_id = get_authenticated_user() + if not is_user_teacher(session, user_id): + raise InvalidTeacherCredentialsError + return get_teacher(session, user_id) + + +def get_authenticated_student(session: Session = Depends(get_session)) -> StudentDataclass: + user_id = get_authenticated_user() + if not is_user_student(session, user_id): + raise InvalidStudentCredentialsError + return get_student(session, user_id) + + +def get_authenticated_student_for_subject( + subject_id: int, + session: Session = Depends(get_session), + student: StudentDataclass = Depends(get_authenticated_student), +) -> StudentDataclass: + subjects_of_student = get_subjects_of_student(session, student.id) + if subject_id not in [subject.id for subject in subjects_of_student]: + raise StudentNotEnrolledError + return student + diff --git a/backend/routes/errors/authentication.py b/backend/routes/errors/authentication.py new file mode 100644 index 00000000..f1437edf --- /dev/null +++ b/backend/routes/errors/authentication.py @@ -0,0 +1,18 @@ +class InvalidRoleCredentialsError(Exception): + ERROR_MESSAGE = "User does not have the required role" + + +class InvalidAdminCredentialsError(InvalidRoleCredentialsError): + ERROR_MESSAGE = "User does not have the required admin role" + + +class InvalidTeacherCredentialsError(InvalidRoleCredentialsError): + ERROR_MESSAGE = "User does not have the required teacher role" + + +class InvalidStudentCredentialsError(InvalidRoleCredentialsError): + ERROR_MESSAGE = "User does not have the required student role" + + +class StudentNotEnrolledError(Exception): + ERROR_MESSAGE = "Student is not enrolled in the subject" diff --git a/backend/routes/exception_handlers.py b/backend/routes/exception_handlers.py new file mode 100644 index 00000000..a0076c22 --- /dev/null +++ b/backend/routes/exception_handlers.py @@ -0,0 +1,38 @@ +from fastapi import Request, status +from fastapi.responses import JSONResponse + +from app import app +from db.errors.database_errors import ActionAlreadyPerformedError, ItemNotFoundError +from routes.errors.authentication import InvalidRoleCredentialsError, StudentNotEnrolledError + + +@app.exception_handler(InvalidRoleCredentialsError) +def invalid_admin_credentials_error_handler(request: Request, exc: InvalidRoleCredentialsError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content={"message": exc.ERROR_MESSAGE}, + ) + + +@app.exception_handler(ItemNotFoundError) +def item_not_found_error_handler(request: Request, exc: ItemNotFoundError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={"detail": str(exc)}, + ) + + +@app.exception_handler(StudentNotEnrolledError) +def student_already_enrolled_error_handler(request: Request, exc: StudentNotEnrolledError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(exc)}, + ) + + +@app.exception_handler(ActionAlreadyPerformedError) +def action_already_performed_error_handler(request: Request, exc: ActionAlreadyPerformedError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(exc)}, + ) diff --git a/backend/routes/group.py b/backend/routes/group.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/routes/login.py b/backend/routes/login.py index d2e32ced..e69de29b 100644 --- a/backend/routes/login.py +++ b/backend/routes/login.py @@ -1,12 +0,0 @@ -from domain.models.UserDataclass import UserDataclass -from routes.db import get_dao_provider - - -def get_authenticated_user() -> UserDataclass: - return get_dao_provider().get_user_dao().get(1) # Actually authenticate user - - -def is_user_admin() -> bool: - user = get_authenticated_user() - admin_dao = get_dao_provider().get_admin_dao() - return admin_dao.is_user_admin(user.id) diff --git a/backend/routes/projects.py b/backend/routes/projects.py deleted file mode 100644 index e73b6574..00000000 --- a/backend/routes/projects.py +++ /dev/null @@ -1,42 +0,0 @@ -from fastapi import APIRouter, HTTPException, status - -from db.errors.database_errors import ItemNotFoundError -from domain.logic.SubjectLogic import is_user_authorized_for_subject -from domain.models.ProjectDataclass import ProjectDataclass -from routes.db import get_dao_provider -from routes.login import get_authenticated_user - -projects_router = APIRouter() - - -@projects_router.get("/projects") -def get_subjects(teacher: bool = False) -> list[ProjectDataclass]: - user = get_authenticated_user() - project_dao = get_dao_provider().get_project_dao() - subject_dao = get_dao_provider().get_subject_dao() - try: - if teacher: - subjects = subject_dao.get_subjects_of_teacher(user.id) - else: - subjects = subject_dao.get_subjects_of_student(user.id) - projects = [] - for i in subjects: - projects += project_dao.get_projects_of_subject(i.id) - except ItemNotFoundError as err: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err - return projects - - -@projects_router.get("/projects/{project_id}") -def get_project(project_id: int) -> ProjectDataclass: - project_dao = get_dao_provider().get_project_dao() - subject_dao = get_dao_provider().get_subject_dao() - user = get_authenticated_user() - try: - project = project_dao.get(project_id) - except ItemNotFoundError as err: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err - subject = subject_dao.get(project.subject_id) - if not is_user_authorized_for_subject(subject, user, get_dao_provider()): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) - return project diff --git a/backend/routes/student.py b/backend/routes/student.py new file mode 100644 index 00000000..006c3d2d --- /dev/null +++ b/backend/routes/student.py @@ -0,0 +1,36 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from db.sessions import get_session +from domain.logic.project import get_project, get_projects_of_subject +from domain.logic.subject import get_subject, get_subjects_of_student +from domain.models.ProjectDataclass import ProjectDataclass +from domain.models.StudentDataclass import StudentDataclass +from domain.models.SubjectDataclass import SubjectDataclass +from routes.dependencies.role_dependencies import get_authenticated_student, get_authenticated_student_for_subject + +student_router = APIRouter() + + +@student_router.get("/student/subjects") +def subjects_of_student_get( + session: Session = Depends(get_session), + student: StudentDataclass = Depends(get_authenticated_student), +) -> list[SubjectDataclass]: + return get_subjects_of_student(session, student.id) + + +@student_router.get("/student/subjects/{subject_id}", dependencies=[Depends(get_authenticated_student)]) +def subject_get(subject_id: int, session: Session = Depends(get_session)) -> SubjectDataclass: + return get_subject(session, subject_id) + + +@student_router.get("/student/subjects/{subject_id}/projects", dependencies=[Depends(get_authenticated_student)]) +def get_subject_projects(subject_id: int, session: Session = Depends(get_session)) -> list[ProjectDataclass]: + return get_projects_of_subject(session, subject_id) + + +@student_router.get("/student/subjects/{subject_id}/projects/{project_id}") +def get_subject_project(subject_id: int, project_id: int, session: Session = Depends(get_session)) -> ProjectDataclass: + get_authenticated_student_for_subject(subject_id) + return get_project(session, project_id) diff --git a/backend/routes/subjects.py b/backend/routes/subjects.py deleted file mode 100644 index db602a81..00000000 --- a/backend/routes/subjects.py +++ /dev/null @@ -1,44 +0,0 @@ -from fastapi import APIRouter, HTTPException, status - -from db.errors.database_errors import ItemNotFoundError -from domain.logic.SubjectLogic import is_user_authorized_for_subject -from domain.models.ProjectDataclass import ProjectDataclass -from domain.models.SubjectDataclass import SubjectDataclass -from routes.db import get_dao_provider -from routes.login import get_authenticated_user - -subjects_router = APIRouter() - - -@subjects_router.get("/subjects") -def get_subjects(teacher: bool = False) -> list[SubjectDataclass]: - user = get_authenticated_user() - subject_dao = get_dao_provider().get_subject_dao() - try: - if teacher: - return subject_dao.get_subjects_of_teacher(user.id) - return subject_dao.get_subjects_of_student(user.id) - except ItemNotFoundError as err: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err - - -@subjects_router.get("/subjects/{subject_id}") -def get_subject(subject_id: int) -> SubjectDataclass: - subject_dao = get_dao_provider().get_subject_dao() - try: - return subject_dao.get(subject_id) - except ItemNotFoundError as err: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err - - -@subjects_router.get("/subjects/{subject_id}/projects") -def get_subject_projects(subject_id: int) -> list[ProjectDataclass]: - subject_dao = get_dao_provider().get_subject_dao() - project_dao = get_dao_provider().get_project_dao() - try: - subject = subject_dao.get(subject_id) - if not is_user_authorized_for_subject(subject, get_authenticated_user(), get_dao_provider()): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) - return project_dao.get_projects_of_subject(subject_id) - except ItemNotFoundError as err: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err diff --git a/backend/routes/teacher.py b/backend/routes/teacher.py new file mode 100644 index 00000000..604f5774 --- /dev/null +++ b/backend/routes/teacher.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends, status +from sqlalchemy.orm import Session +from starlette.responses import Response + +from db.sessions import get_session +from domain.logic.subject import create_subject, get_subjects_of_teacher +from domain.models.SubjectDataclass import SubjectDataclass +from domain.models.TeacherDataclass import TeacherDataclass +from routes.dependencies.role_dependencies import get_authenticated_teacher + +teacher_router = APIRouter() + + +@teacher_router.get("/teacher/subjects") +def subjects_of_teacher_get( + session: Session = Depends(get_session), + teacher: TeacherDataclass = Depends(get_authenticated_teacher), +) -> list[SubjectDataclass]: + return get_subjects_of_teacher(session, teacher.id) + + +@teacher_router.post("teacher/subjects") +def create_subject_post( + subject: SubjectDataclass, + session: Session = Depends(get_session), +) -> Response: + create_subject(session, name=subject.name) + return Response(status_code=status.HTTP_201_CREATED) + + + diff --git a/backend/routes/user.py b/backend/routes/user.py new file mode 100644 index 00000000..c14c52d4 --- /dev/null +++ b/backend/routes/user.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from db.models.models import User +from db.sessions import get_session +from domain.logic.basic_operations import get, get_all +from domain.logic.user import convert_user +from domain.models.APIUser import APIUser +from domain.models.UserDataclass import UserDataclass +from routes.dependencies.role_dependencies import get_authenticated_admin, get_authenticated_user + +users_router = APIRouter() + + +@users_router.get("/user") +def get_current_user( + session: Session = Depends(get_session), + user_id: int = Depends(get_authenticated_user), +) -> APIUser: + user: UserDataclass = get(session, User, user_id).to_domain_model() + return convert_user(session, user) + + +@users_router.get("/users", dependencies=[Depends(get_authenticated_admin)]) +def get_users(session: Session = Depends(get_session)) -> list[APIUser]: + users: list[UserDataclass] = [user.to_domain_model() for user in get_all(session, User)] + return [convert_user(session, user) for user in users] + + +@users_router.get("/users/{uid}", dependencies=[Depends(get_authenticated_admin)]) +def get_user(uid: int, session: Session = Depends(get_session)) -> APIUser: + user: UserDataclass = get(session, User, uid).to_domain_model() + return convert_user(session, user) diff --git a/backend/routes/users.py b/backend/routes/users.py deleted file mode 100644 index 0c08846e..00000000 --- a/backend/routes/users.py +++ /dev/null @@ -1,36 +0,0 @@ -from fastapi import APIRouter, HTTPException, status - -from db.errors.database_errors import ItemNotFoundError -from domain.logic.UserLogic import convert_user -from domain.models.APIUser import APIUser -from routes.db import get_dao_provider -from routes.login import get_authenticated_user, is_user_admin - -users_router = APIRouter() - - -@users_router.get("/user") -def get_current_user() -> APIUser: - user = get_authenticated_user() - return convert_user(user, get_dao_provider()) - - -@users_router.get("/users") -def get_users() -> list[APIUser]: - if not is_user_admin(): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) - user_dao = get_dao_provider().get_user_dao() - users = user_dao.get_all() - return [convert_user(user, get_dao_provider()) for user in users] - - -@users_router.get("/users/{uid}") -def get_user(uid: int) -> APIUser: - if not is_user_admin(): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) - user_dao = get_dao_provider().get_user_dao() - try: - user = user_dao.get(uid) - except ItemNotFoundError as err: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) from err - return convert_user(user, get_dao_provider())