diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 00000000..33b50ed7 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,15 @@ +name: Backend +on: [push] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: pip install -r backend/requirements.txt + - name: Lint code + run: ruff check backend diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d5a428ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,163 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version routes. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version routes. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version routes. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version routes. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Ruff linter +.ruff_cache \ No newline at end of file diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 00000000..efaf32bc --- /dev/null +++ b/backend/app.py @@ -0,0 +1,24 @@ +import os + +from flask import Flask + +from db.extensions import db +from routes.teachers import teachers_blueprint + +app = Flask(__name__) + +# Koppel routes uit andere modules. +app.register_blueprint(teachers_blueprint) + +db_host = os.getenv("DB_HOST", "localhost") +db_port = os.getenv("DB_PORT", "5432") +db_user = os.getenv("DB_USERNAME", "postgres") +db_password = os.getenv("DB_PASSWORD", "postgres") +db_database = os.getenv("DB_DATABASE", "delphi") + +# Koppel postgres uri en db aan app instantie +app.config["SQLALCHEMY_DATABASE_URI"] = f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_database}" +db.init_app(app) + +if __name__ == "__main__": + app.run(debug=True) diff --git a/backend/db/errors/database_errors.py b/backend/db/errors/database_errors.py new file mode 100644 index 00000000..1b338da7 --- /dev/null +++ b/backend/db/errors/database_errors.py @@ -0,0 +1,4 @@ +class ItemNotFoundError(Exception): + + def __init__(self, message: str): + super().__init__(message) diff --git a/backend/db/extensions.py b/backend/db/extensions.py new file mode 100644 index 00000000..86ff1dad --- /dev/null +++ b/backend/db/extensions.py @@ -0,0 +1,9 @@ +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass + + +db = SQLAlchemy(model_class=Base) diff --git a/backend/db/implementation/SqlLesgeverDAO.py b/backend/db/implementation/SqlLesgeverDAO.py new file mode 100644 index 00000000..52581633 --- /dev/null +++ b/backend/db/implementation/SqlLesgeverDAO.py @@ -0,0 +1,27 @@ +from db.errors.database_errors import ItemNotFoundError +from db.extensions import db +from db.interface.TeacherDAO import TeacherDAO +from db.models.models import Teacher +from domain.models.TeacherDataclass import TeacherDataclass + + +class SqlTeacherDAO(TeacherDAO): + def get_teacher(self, ident: int): + teacher: Teacher = Teacher.query.get(ident=ident) + + if not teacher: + raise ItemNotFoundError("TeacherDataclass with given id not found.") + + return teacher.to_domain_model() + + def get_all_teachers(self) -> list[TeacherDataclass]: + teachers: list[Teacher] = Teacher.query.all() + return [lesgever.to_domain_model() for lesgever in teachers] + + def create_teacher(self, teacher: TeacherDataclass): + new_teacher = Teacher(name=teacher.name) + + db.session.add(new_teacher) + db.session.commit() + + teacher.id = new_teacher.id diff --git a/backend/db/implementation/SqlVakDAO.py b/backend/db/implementation/SqlVakDAO.py new file mode 100644 index 00000000..ea059734 --- /dev/null +++ b/backend/db/implementation/SqlVakDAO.py @@ -0,0 +1,36 @@ +from db.errors.database_errors import ItemNotFoundError +from db.extensions import db +from db.interface.SubjectDAO import SubjectDAO +from db.models.models import Subject, Teacher +from domain.models.SubjectDataclass import SubjectDataclass + + +class SqlSubjectDAO(SubjectDAO): + def create_subject(self, subject: SubjectDataclass, teacher_id: int): + teacher = Teacher.query.get(teacher_id) + + if not teacher: + raise ItemNotFoundError(f"De teacher met id {teacher_id} kon niet in de databank gevonden worden") + + new_subject = Subject(name=subject.name, teacher=teacher) + + db.session.add(new_subject) + db.session.commit() + + subject.id = new_subject.id + + def get_subject(self, teacher_id: int): + subject = Subject.query.get(teacher_id) + if not subject: + raise ItemNotFoundError(f"De lesgever met id {teacher_id} kon niet in de databank gevonden worden") + + return subject.to_domain_model() + + def get_subjects(self, teacher_id: int) -> list[SubjectDataclass]: + teacher: Teacher = Teacher.query.get(ident=teacher_id) + + if not teacher: + raise ItemNotFoundError(f"De teacher met id {teacher_id} kon niet in de databank gevonden worden") + + subjects: list[Subject] = teacher.subjects + return [vak.to_domain_model() for vak in subjects] diff --git a/backend/db/interface/SubjectDAO.py b/backend/db/interface/SubjectDAO.py new file mode 100644 index 00000000..9c5c5015 --- /dev/null +++ b/backend/db/interface/SubjectDAO.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod + +from domain.models.SubjectDataclass import SubjectDataclass + + +class SubjectDAO(ABC): + @abstractmethod + def create_subject(self, subject: SubjectDataclass, teacher_id: int): + """ + Creƫert een nieuw SubjectDataclass in de database en associeert het met een TeacherDataclass. + + :param subject: De SubjectDataclass domeinmodel-instantie die aan de database moet worden toegevoegd. + :param teacher_id: De identificatie van de TeacherDataclass waarmee het SubjectDataclass geassocieerd wordt. + :raises: ItemNotFoundException: Als er geen TeacherDataclass met de opgegeven `teacher_id` in de database is. + """ + raise NotImplementedError() + + @abstractmethod + def get_subject(self, teacher_id: int): + """ + Haalt een SubjectDataclass op aan de hand van zijn identificatie. + + :param teacher_id: De identificatie van het op te halen SubjectDataclass. + :raises ItemNotFoundException: Als er geen SubjectDataclass met de opgegeven `ident` in de database bestaat. + :returns: De domeinmodel-instantie van het opgehaalde SubjectDataclass. + """ + raise NotImplementedError() + + @abstractmethod + def get_subjects(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() diff --git a/backend/db/interface/TeacherDAO.py b/backend/db/interface/TeacherDAO.py new file mode 100644 index 00000000..25bf91da --- /dev/null +++ b/backend/db/interface/TeacherDAO.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod + +from domain.models.TeacherDataclass import TeacherDataclass + + +class TeacherDAO(ABC): + @abstractmethod + def get_teacher(self, ident: int) -> TeacherDataclass: + """ + Haalt een teacher op aan de hand van zijn identificatie. + + :param ident: Het id van de te zoeken teacher. + :return: De teacher die overeenkomt met de gegeven id. + :raises ItemNotFoundException: Als geen teacher met het gegeven id gevonden werd. + """ + raise NotImplementedError() + + @abstractmethod + def get_all_teachers(self) -> list[TeacherDataclass]: + """ + Haalt alle lesgevers op. + + :return: Een lijst van alle lesgevers. + """ + raise NotImplementedError() + + @abstractmethod + def create_teacher(self, teacher: TeacherDataclass): + """ + Maakt een nieuwe teacher aan. + + :param teacher: De teacher die aangemaakt moet worden. + """ + raise NotImplementedError() diff --git a/backend/db/models/models.py b/backend/db/models/models.py new file mode 100644 index 00000000..85174c52 --- /dev/null +++ b/backend/db/models/models.py @@ -0,0 +1,136 @@ +from datetime import datetime + +from sqlalchemy import Column, ForeignKey, Table +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from db.extensions import db +from domain.models.AdminDataclass import AdminDataclass +from domain.models.GroupDataclass import GroupDataclass +from domain.models.ProjectDataclass import ProjectDataclass +from domain.models.StudentDataclass import StudentDataclass +from domain.models.SubjectDataclass import SubjectDataclass +from domain.models.SubmissionDataclass import SubmissionDataclass, SubmissionState +from domain.models.TeacherDataclass import TeacherDataclass +from domain.models.UserDataclass import UserDataclass + + +class User(db.Model): + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] + email: Mapped[str] + + def to_domain_model(self) -> UserDataclass: + return UserDataclass(id=self.id, name=self.name, email=self.email) + + +class Admin(User): + id: Mapped[int] = mapped_column(ForeignKey(User.id), primary_key=True) + + def to_domain_model(self) -> AdminDataclass: + return AdminDataclass(id=self.id, name=self.name, email=self.email) + + +teachers_subjects = Table( + "teachers_subjects", + db.Model.metadata, + Column("teacher_id", ForeignKey("teacher.id"), primary_key=True), + Column("subject_id", ForeignKey("subject.id"), primary_key=True), +) +students_subjects = Table( + "students_subjects", + db.Model.metadata, + Column("student_id", ForeignKey("student.id"), primary_key=True), + Column("subject_id", ForeignKey("subject.id"), primary_key=True), +) +students_groups = Table( + "students_groups", + db.Model.metadata, + Column("student_id", ForeignKey("student.id"), primary_key=True), + Column("group_id", ForeignKey("group.id"), primary_key=True), +) + + +class Teacher(User): + id: Mapped[int] = mapped_column(ForeignKey(User.id), primary_key=True) + subjects: Mapped[list["Subject"]] = relationship(secondary=teachers_subjects, back_populates="teachers") + + def to_domain_model(self) -> TeacherDataclass: + return TeacherDataclass(id=self.id, name=self.name, email=self.email) + + +class Student(User): + id: Mapped[int] = mapped_column(ForeignKey(User.id), primary_key=True) + subjects: Mapped[list["Subject"]] = relationship(secondary=students_subjects, back_populates="students") + groups: Mapped[list["Group"]] = relationship(secondary=students_groups, back_populates="students") + submissions: Mapped[list["Submission"]] = relationship(back_populates="student") + + def to_domain_model(self) -> StudentDataclass: + return StudentDataclass(id=self.id, name=self.name, email=self.email) + + +class Subject(db.Model): + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] + teachers: Mapped[list[Teacher]] = relationship(secondary=teachers_subjects, back_populates="subjects") + students: Mapped[list[Student]] = relationship(secondary=students_subjects, back_populates="subjects") + projects: Mapped[list["Project"]] = relationship(back_populates="subject") + + def to_domain_model(self) -> SubjectDataclass: + return SubjectDataclass(id=self.id, name=self.name) + + +class Project(db.Model): + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + name: Mapped[str] + deadline: Mapped[datetime] + archived: Mapped[bool] + requirements: Mapped[str] + visible: Mapped[bool] + max_students: Mapped[int] + subject_id: Mapped[int] = mapped_column(ForeignKey(Subject.id)) + subject: Mapped[Subject] = relationship(back_populates="projects") + groups: Mapped[list["Group"]] = relationship(back_populates="project") + + def to_domain_model(self) -> ProjectDataclass: + return ProjectDataclass( + id=self.id, + name=self.name, + deadline=self.deadline, + archived=self.archived, + requirements=self.requirements, + visible=self.visible, + max_students=self.max_students, + subject_id=self.subject_id, + ) + + +class Group(db.Model): + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + project_id: Mapped[int] = mapped_column(ForeignKey(Project.id)) + project: Mapped[Project] = relationship(back_populates="groups") + students: Mapped[list[Student]] = relationship(secondary=students_groups, back_populates="groups") + submissions: Mapped[list["Submission"]] = relationship(back_populates="group") + + def to_domain_model(self) -> GroupDataclass: + return GroupDataclass(id=self.id, project_id=self.project_id) + + +class Submission(db.Model): + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + date_time: Mapped[datetime] + group_id: Mapped[int] = mapped_column(ForeignKey(Group.id)) + group: Mapped[Group] = relationship(back_populates="submissions") + student_id: Mapped[int] = mapped_column(ForeignKey(Student.id)) + student: Mapped[Student] = relationship(back_populates="submissions") + state: Mapped[SubmissionState] + message: Mapped[str] + + def to_domain_model(self) -> SubmissionDataclass: + return SubmissionDataclass( + id=self.id, + date_time=self.date_time, + group_id=self.group_id, + student_id=self.student_id, + state=self.state, + message=self.message, + ) diff --git a/backend/domain/models/AdminDataclass.py b/backend/domain/models/AdminDataclass.py new file mode 100644 index 00000000..d1b28923 --- /dev/null +++ b/backend/domain/models/AdminDataclass.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from domain.models.UserDataclass import UserDataclass + + +@dataclass() +class AdminDataclass(UserDataclass): + pass diff --git a/backend/domain/models/GroupDataclass.py b/backend/domain/models/GroupDataclass.py new file mode 100644 index 00000000..a67a9ffc --- /dev/null +++ b/backend/domain/models/GroupDataclass.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + +from domain.models.base_model import JsonRepresentable + + +@dataclass() +class GroupDataclass(JsonRepresentable): + id: int + project_id: int diff --git a/backend/domain/models/ProjectDataclass.py b/backend/domain/models/ProjectDataclass.py new file mode 100644 index 00000000..37dd6975 --- /dev/null +++ b/backend/domain/models/ProjectDataclass.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from datetime import datetime + +from domain.models.base_model import JsonRepresentable + + +@dataclass() +class ProjectDataclass(JsonRepresentable): + id: int + name: str + deadline: datetime + archived: bool + requirements: str + visible: bool + max_students: int + subject_id: int diff --git a/backend/domain/models/StudentDataclass.py b/backend/domain/models/StudentDataclass.py new file mode 100644 index 00000000..3f46b7d5 --- /dev/null +++ b/backend/domain/models/StudentDataclass.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from domain.models.UserDataclass import UserDataclass + + +@dataclass() +class StudentDataclass(UserDataclass): + pass diff --git a/backend/domain/models/SubjectDataclass.py b/backend/domain/models/SubjectDataclass.py new file mode 100644 index 00000000..92f0a92b --- /dev/null +++ b/backend/domain/models/SubjectDataclass.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + +from domain.models.base_model import JsonRepresentable + + +@dataclass() +class SubjectDataclass(JsonRepresentable): + id: int + name: str diff --git a/backend/domain/models/SubmissionDataclass.py b/backend/domain/models/SubmissionDataclass.py new file mode 100644 index 00000000..5d2641ea --- /dev/null +++ b/backend/domain/models/SubmissionDataclass.py @@ -0,0 +1,21 @@ +import enum +from dataclasses import dataclass +from datetime import datetime + +from domain.models.base_model import JsonRepresentable + + +class SubmissionState(enum.Enum): + Pending = 1 + Approved = 2 + Rejected = 3 + + +@dataclass() +class SubmissionDataclass(JsonRepresentable): + id: int + date_time: datetime + group_id: int + student_id: int + state: SubmissionState + message: str diff --git a/backend/domain/models/TeacherDataclass.py b/backend/domain/models/TeacherDataclass.py new file mode 100644 index 00000000..d36d1215 --- /dev/null +++ b/backend/domain/models/TeacherDataclass.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from domain.models.UserDataclass import UserDataclass + + +@dataclass() +class TeacherDataclass(UserDataclass): + pass diff --git a/backend/domain/models/UserDataclass.py b/backend/domain/models/UserDataclass.py new file mode 100644 index 00000000..1a0696a7 --- /dev/null +++ b/backend/domain/models/UserDataclass.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from domain.models.base_model import JsonRepresentable + + +@dataclass() +class UserDataclass(JsonRepresentable): + id: int + name: str + email: str diff --git a/backend/domain/models/base_model.py b/backend/domain/models/base_model.py new file mode 100644 index 00000000..5f8dc6cc --- /dev/null +++ b/backend/domain/models/base_model.py @@ -0,0 +1,9 @@ +import dataclasses +from abc import ABC +from dataclasses import dataclass + + +@dataclass() +class JsonRepresentable(ABC): + def to_dict(self) -> dict: + return dataclasses.asdict(self) diff --git a/backend/domain/validation/SubjectValidator.py b/backend/domain/validation/SubjectValidator.py new file mode 100644 index 00000000..636417dd --- /dev/null +++ b/backend/domain/validation/SubjectValidator.py @@ -0,0 +1,18 @@ +from domain.validation.ValidationResult import ValidationResult + + +class SubjectValidator: + @staticmethod + def validate(json_data: dict): + result = ValidationResult() + + name = json_data.get("name") + teacher_id = json_data.get("teacher_id") + + if not name: + result.add_error("Veld 'name' ontbreekt.") + + if not teacher_id: + result.add_error("Veld 'teacher_id' ontbreekt.") + + return result diff --git a/backend/domain/validation/TeacherValidator.py b/backend/domain/validation/TeacherValidator.py new file mode 100644 index 00000000..a90afa6d --- /dev/null +++ b/backend/domain/validation/TeacherValidator.py @@ -0,0 +1,14 @@ +from domain.validation.ValidationResult import ValidationResult + + +class TeacherValidator: + @staticmethod + def validate(json_data: dict): + result = ValidationResult() + + name = json_data.get("name") + + if not name: + result.add_error("Veld 'name' ontbreekt.") + + return result diff --git a/backend/domain/validation/ValidationResult.py b/backend/domain/validation/ValidationResult.py new file mode 100644 index 00000000..ed238332 --- /dev/null +++ b/backend/domain/validation/ValidationResult.py @@ -0,0 +1,11 @@ +class ValidationResult: + def __init__(self, is_ok: bool = True, errors: list[str] | None = None): + self.is_ok = is_ok + self.errors = errors if errors is not None else [] + + def add_error(self, error: str): + self.is_ok = False + self.errors.append(error) + + def __bool__(self): + return self.is_ok diff --git a/backend/fill_database_mock.py b/backend/fill_database_mock.py new file mode 100644 index 00000000..9ba844d8 --- /dev/null +++ b/backend/fill_database_mock.py @@ -0,0 +1,45 @@ +import sys + +from app import app +from db.extensions import db +from db.implementation.SqlLesgeverDAO import SqlTeacherDAO +from db.implementation.SqlVakDAO import SqlSubjectDAO +from db.interface.SubjectDAO import SubjectDAO +from db.interface.TeacherDAO import TeacherDAO +from domain.models.SubjectDataclass import SubjectDataclass +from domain.models.TeacherDataclass import TeacherDataclass + +if __name__ == "__main__": + with app.app_context(): + db.create_all() + sys.exit() # De DAO's moeten nog aangemaakt worden + teacher_dao: TeacherDAO = SqlTeacherDAO() + subject_dao: SubjectDAO = SqlSubjectDAO() + + # Maak nieuwe lesgevers aan. + Gunnar = TeacherDataclass(name="Gunnar Brinkmann") + Peter = TeacherDataclass(name="Peter Dawyndt") + Eric = TeacherDataclass(name="Eric Laermans") + + # Voeg lesgevers toe aan de databank via de teacher DAO + teacher_dao.create_teacher(Gunnar) + teacher_dao.create_teacher(Peter) + teacher_dao.create_teacher(Eric) + + # Maak nieuwe subjects aan + AD2 = SubjectDataclass(name="Algoritmen en Datastructuren II") + AD3 = SubjectDataclass(name="Algoritmen en Datastructuren III") + Computergebruik = SubjectDataclass(name="Computergebruik") + ComputationeleBiologie = SubjectDataclass(name="Computationele Biologie") + RAF = SubjectDataclass(name="Redeneren, Abstraheren en Formuleren") + InformationSecurity = SubjectDataclass(name="Information Security") + + # Steek de subjects in de databank + subject_dao.create_subject(AD2, Gunnar.id) + subject_dao.create_subject(AD3, Gunnar.id) + + subject_dao.create_subject(Computergebruik, Peter.id) + subject_dao.create_subject(ComputationeleBiologie, Peter.id) + + subject_dao.create_subject(RAF, Eric.id) + subject_dao.create_subject(InformationSecurity, Eric.id) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 00000000..0e9c38d8 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,13 @@ +blinker==1.7.0 +click==8.1.7 +Flask==3.0.2 +Flask-SQLAlchemy==3.1.1 +greenlet==3.0.3 +itsdangerous==2.1.2 +Jinja2==3.1.3 +MarkupSafe==2.1.5 +psycopg2==2.9.9 +ruff==0.2.2 +SQLAlchemy==2.0.27 +typing_extensions==4.9.0 +Werkzeug==3.0.1 diff --git a/backend/routes/teachers.py b/backend/routes/teachers.py new file mode 100644 index 00000000..d35d4737 --- /dev/null +++ b/backend/routes/teachers.py @@ -0,0 +1,51 @@ +import json +from http import HTTPStatus + +from flask import Blueprint, Response, request + +from db.implementation.SqlLesgeverDAO import SqlTeacherDAO +from db.interface.TeacherDAO import TeacherDAO +from domain.models.TeacherDataclass import TeacherDataclass +from domain.validation.TeacherValidator import TeacherValidator +from domain.validation.ValidationResult import ValidationResult + +teachers_blueprint = Blueprint("teachers", __name__) + + +@teachers_blueprint.route("/teachers") +def get_teachers(): + dao: TeacherDAO = SqlTeacherDAO() + + teachers: list[TeacherDataclass] = dao.get_all_teachers() + teachers_json = [teacher.to_dict() for teacher in teachers] + + return Response(json.dumps(teachers_json, indent=4), content_type="application/json") + + +@teachers_blueprint.route("/teachers/") +def get_teacher(teacher_id): + dao: TeacherDAO = SqlTeacherDAO() + + teacher: TeacherDataclass = dao.get_teacher(teacher_id) + teacher_json = teacher.to_dict() + + return Response(json.dumps(teacher_json, indent=4), content_type="application/json") + + +@teachers_blueprint.route("/teachers", methods=["POST"]) +def create_teacher(): + teacher_data: dict = request.get_json() + + if not teacher_data: + return json.dumps({"error": "Foute JSON of Content-Type"}), HTTPStatus.BAD_REQUEST + + validation_result: ValidationResult = TeacherValidator.validate(teacher_data) + + if not validation_result.is_ok: + return json.dumps({"error": validation_result.errors}), HTTPStatus.BAD_REQUEST + + dao: TeacherDAO = SqlTeacherDAO() + lesgever = TeacherDataclass(**teacher_data) # Vul alle velden van het dataobject in met de json + dao.create_teacher(lesgever) + + return json.dumps(lesgever.to_dict()), HTTPStatus.CREATED diff --git a/backend/ruff.toml b/backend/ruff.toml new file mode 100644 index 00000000..61f66dd2 --- /dev/null +++ b/backend/ruff.toml @@ -0,0 +1,92 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 120 +indent-width = 4 + +# Assume Python 3.8 +target-version = "py312" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = [ + "F", # Pyflakes + "E", # Pycodestyle + "W", # Warnings + "I", # Isort (sorted imports) + "N", # PEP8 + "A", # Flake8-builtins + "C4", # Comprehensions + "PIE", # flake8 pie + "Q", # Quotes + "RET", # returns + "SIM", # simplify + "ARG", # unused arguments + "ERA", # no commented out code + "PL" # all pylint +] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" diff --git a/backend/tests/teachers_test.py b/backend/tests/teachers_test.py new file mode 100644 index 00000000..2b4a14bb --- /dev/null +++ b/backend/tests/teachers_test.py @@ -0,0 +1,25 @@ +import json +import unittest +from http import HTTPStatus + +from app import app + + +class LesgeverTestCase(unittest.TestCase): + def setUp(self): + self.app = app.test_client() + self.app.testing = True + + def test_create_teacher_bad_request(self): + response = self.app.post("/teachers", data=json.dumps({}), content_type="application/json") + self.assertEqual(HTTPStatus.BAD_REQUEST, response.status_code) + self.assertIn("error", json.loads(response.data)) + + def test_create_teacher_success(self): + teacher_data = {"name": "Bart De Bruyn"} + response = self.app.post("/teachers", data=json.dumps(teacher_data), content_type="application/json") + self.assertEqual(HTTPStatus.CREATED, response.status_code) + + +if __name__ == "__main__": + unittest.main()