This repository has been archived by the owner on Sep 27, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into routeLoaderData
- Loading branch information
Showing
16 changed files
with
538 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Documentation Backend | ||
|
||
## 1) Database | ||
|
||
#### I) setup | ||
|
||
- step 1: Install and set up **PostgreSQL**. We refer to the [official documentation](https://www.postgresql.org/docs/16/admin.html). | ||
- step 2: Start a PostgreSQL server. | ||
- step 3: Create a database on this server. | ||
|
||
We use **SQLAlchemy** to access this database in our backend app. SQLAlchemy allows us to start defining tables, performing queries, etc.. The setup of SQLAlchemy happens in [db/extensions.py]. Here, an SQLAlchemy engine is created. This engine is the component that manages connections to the database. A [database URI](https://docs.sqlalchemy.org/en/20/core/engines.html) is needed to create such an engine. Because we host the backend app and the database in the same place, we use localhost in the URI as default. This is not mandatory; the database and backend app are two seperate things. | ||
|
||
For test purposes, mockup data is available in [fill_database_mock.py]. A visual representation of the database is also recommended (eg. [pgAdmin](https://www.pgadmin.org/)). | ||
|
||
#### II) tables | ||
|
||
Using our EER diagram, we now want to create the corresponding tables. We use SQLAlchemy's declarative base pattern. Start by defining a Base class. This class contains the necessary functionality to interact with the database. Next, we create a python class for each table we need, and make it inherit the Base class. We call this a model (see [db/models/models.py]). For specifics on how to define these models, see [this link](https://docs.sqlalchemy.org/en/13/orm/extensions/declarative/basic_use.html). | ||
|
||
An important thing to notice in this file is that other than the Base class, all models also inherit a class named AbstractModel. It makes sure that each model implements *to_domain_model*. We will come back to this function later on. | ||
|
||
|
||
#### III) sessions | ||
|
||
A SQLAlchemy session object provides an extra abstract layer to the engine. We will use these session objects in the logic part of our domain layer (see later). | ||
|
||
> Using SQLAlchemy session objects simplifies database interactions by encapsulating transactions, providing features such as identity management, automatic transaction management, and a unit of work pattern, promoting cleaner, more maintainable code. \ | ||
\- ChatGPT | ||
|
||
In [db/sessions.py], we define a generator for these session objects using our engine. | ||
|
||
## 2) Domain layer | ||
|
||
#### I) operations | ||
in [domain/logic/] we define the actual backend functionality. Examples are *get_subjects_of_teacher* or *create_submission*. Some things to notice is that every function needs a session object, and that we manually commit changes. In [domain/logic/basic_operations.py], we define an abstract *get* and *get_all*, as these type of operations happen a lot. This *get* is of course not the same as a get request to the API. Three main errors that we provide manual coverage for are ItemNotFoundError, ActionAlreadyPerformedError and NoSuchRelationError (see [db/errors/]). | ||
|
||
#### II) dataclasses | ||
|
||
Now is a good time to explain the function *to_domain_model* from earlier. When we call a logic function, we don't want to return an instance of the Base class from SQLAlchemy. Instead we want to return very universal objects with that correspond one-to-one with an entity + attribute from our EER diagram. That's what a dataclass is. They are defined in [domain/models/]. | ||
|
||
> In our code, we use the name dataclass for two seperate things. The first is the @dataclass tag from the standard python library [dataclasses](https://docs.python.org/3/library/dataclasses.html). The other is the domain layer object just explained like SubmissionDataclass and TeacherDataclass. | ||
These universal dataclasses inherit the **Pydantic** BaseModel. It allows for automatic data validation, JSON schema generation and more. More information on Pydantic can be found [here](https://docs.pydantic.dev/latest/why/). | ||
|
||
## 3) API | ||
|
||
We use **FastAPI** as framework. FastAPI follows the OpenAPI Specification. Its idea is to, in its turn, specify a REST API with a YAML document. This document can be used to generate documentation and methods for API endpoints. In every file in [/routes/], we can see a FastAPI router defined that represents some routes that are logically grouped together. | ||
|
||
> A route is a URL pattern that directs HTTP requests to specific handlers or controllers in a web application, defining how requests are processed and responses are generated. It plays a crucial role in organizing the flow of data within the application. \ | ||
\- ChatGPT | ||
|
||
Every route recieves a session object as a dependency injection, to forward to the corresponding logic operation. Dependencies are components that need to be executed before the route operation function is called. We let FastAPI handle this for us. Other than a database connection through the session object, we sometimes also inject some authentication/authorization logic (see [routes/dependencies/role_dependencies.py]) with corresponding errors in [routes/errors/authentication.py]. | ||
|
||
## 4) Running the app | ||
|
||
We start by defining app = FastAPI() in [app.py]. Next, we add our routers from the previous section. We also add some exception handlers using the corresponding tag. FastAPI calls these handlers for us if needed. this way, we only have to return a corresponding JSONResponse. Finally, we start the app using a **uvicorn** server. This is the standard for FastApi. "app:app" specifies the location of the FastApi object. The first "app" refers to the module (i.e., app.py), and the second "app" refers to the variable (i.e., the FastAPI application object). By default, Uvicorn will run on the localhost port 8000. Another thing to note in this file is that we provide [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) functionality in production. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# test_edge_cases.py | ||
import unittest | ||
from datetime import datetime | ||
|
||
from test_main import SessionLocal, test_engine | ||
|
||
from db.errors.database_errors import ItemNotFoundError | ||
from db.extensions import Base | ||
from domain.logic import group, student, submission | ||
from domain.models.SubmissionDataclass import SubmissionState | ||
|
||
|
||
class TestEdgeCases(unittest.TestCase): | ||
def setUp(self) -> None: | ||
Base.metadata.drop_all(test_engine) | ||
Base.metadata.create_all(test_engine) | ||
self.session = SessionLocal() | ||
|
||
def tearDown(self) -> None: | ||
self.session.rollback() | ||
self.session.close() | ||
|
||
def test_add_student_to_non_existent_group(self) -> None: | ||
stud = student.create_student(self.session, "Test Student", "[email protected]") | ||
with self.assertRaises(ItemNotFoundError): | ||
group.add_student_to_group(self.session, stud.id, 999) | ||
|
||
def test_create_submission_for_non_existent_project(self) -> None: | ||
stud = student.create_student(self.session, "Test Student", "[email protected]") | ||
with self.assertRaises(ItemNotFoundError): | ||
submission.create_submission(self.session, stud.id, 999, "Test Message", SubmissionState.Pending, | ||
datetime.now()) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from sqlalchemy import create_engine | ||
from sqlalchemy.orm import sessionmaker | ||
|
||
DB_TEST_URI = "postgresql://postgres:postgres@localhost:5432/delphi-test" | ||
test_engine = create_engine(DB_TEST_URI) | ||
SessionLocal = sessionmaker(autocommit=False, bind=test_engine) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# test_project.py | ||
import unittest | ||
from datetime import datetime | ||
|
||
from test_main import SessionLocal, test_engine | ||
|
||
from db.extensions import Base | ||
from domain.logic.project import ( | ||
create_project, | ||
get_all_projects, | ||
get_project, | ||
get_projects_of_student, | ||
get_projects_of_subject, | ||
get_projects_of_teacher, | ||
) | ||
from domain.logic.student import create_student | ||
from domain.logic.subject import add_student_to_subject, add_teacher_to_subject, create_subject | ||
from domain.logic.teacher import create_teacher | ||
|
||
|
||
class TestProject(unittest.TestCase): | ||
def setUp(self) -> None: | ||
Base.metadata.drop_all(test_engine) | ||
Base.metadata.create_all(test_engine) | ||
self.session = SessionLocal() | ||
|
||
def tearDown(self) -> None: | ||
self.session.rollback() | ||
self.session.close() | ||
|
||
def test_create_and_get_project(self) -> None: | ||
subject = create_subject(self.session, "Test Subject") | ||
project = create_project(self.session, subject.id, "Test Project", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
retrieved_project = get_project(self.session, project.id) | ||
self.assertEqual(project.id, retrieved_project.id) | ||
|
||
def test_get_all_projects(self) -> None: | ||
subject1 = create_subject(self.session, "Test Subject 1") | ||
subject2 = create_subject(self.session, "Test Subject 2") | ||
create_project(self.session, subject1.id, "Test Project 1", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
create_project(self.session, subject2.id, "Test Project 2", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
self.assertEqual(len(get_all_projects(self.session)), 2) | ||
|
||
def test_get_projects_of_subject(self) -> None: | ||
subject = create_subject(self.session, "Test Subject") | ||
create_project(self.session, subject.id, "Test Project 1", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
create_project(self.session, subject.id, "Test Project 2", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
projects_of_subject = get_projects_of_subject(self.session, subject.id) | ||
self.assertEqual(len(projects_of_subject), 2) | ||
|
||
def test_get_projects_of_student(self) -> None: | ||
student = create_student(self.session, "Test Student", "[email protected]") | ||
subject1 = create_subject(self.session, "Test Subject 1") | ||
subject2 = create_subject(self.session, "Test Subject 2") | ||
add_student_to_subject(self.session, student.id, subject1.id) | ||
add_student_to_subject(self.session, student.id, subject2.id) | ||
create_project(self.session, subject1.id, "Test Project 1", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
create_project(self.session, subject2.id, "Test Project 2", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
projects_of_student = get_projects_of_student(self.session, student.id) | ||
self.assertEqual(len(projects_of_student), 2) | ||
|
||
def test_get_projects_of_teacher(self) -> None: | ||
teacher = create_teacher(self.session, "Test Teacher", "[email protected]") | ||
subject1 = create_subject(self.session, "Test Subject 1") | ||
subject2 = create_subject(self.session, "Test Subject 2") | ||
add_teacher_to_subject(self.session, teacher.id, subject1.id) | ||
add_teacher_to_subject(self.session, teacher.id, subject2.id) | ||
create_project(self.session, subject1.id, "Test Project 1", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
create_project(self.session, subject2.id, "Test Project 2", datetime.now(), False, "Test Description", | ||
"Test Requirements", True, 2) | ||
projects_of_teacher = get_projects_of_teacher(self.session, teacher.id) | ||
self.assertEqual(len(projects_of_teacher), 2) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# test_stress.py | ||
import unittest | ||
from datetime import datetime | ||
|
||
from test_main import SessionLocal, test_engine | ||
|
||
from db.extensions import Base | ||
from domain.logic import admin, group, project, student, subject, submission, teacher | ||
from domain.models.SubmissionDataclass import SubmissionState | ||
|
||
|
||
class TestStress(unittest.TestCase): | ||
def setUp(self) -> None: | ||
Base.metadata.drop_all(test_engine) | ||
Base.metadata.create_all(test_engine) | ||
self.session = SessionLocal() | ||
|
||
def tearDown(self) -> None: | ||
self.session.rollback() | ||
self.session.close() | ||
|
||
def test_stress(self) -> None: | ||
# Create multiple instances of each entity | ||
for i in range(100): | ||
stud = student.create_student(self.session, f"Test Student {i}", f"teststudent{i}@gmail.com") | ||
subj = subject.create_subject(self.session, f"Test Subject {i}") | ||
proj = project.create_project(self.session, subj.id, f"Test Project {i}", datetime.now(), False, | ||
"Test Description", | ||
"Test Requirements", True, 2) | ||
grp = group.create_group(self.session, proj.id) | ||
subm = submission.create_submission(self.session, stud.id, grp.id, "Test Message", SubmissionState.Pending, | ||
datetime.now()) | ||
teach = teacher.create_teacher(self.session, f"Test Teacher {i}", f"testteacher{i}@gmail.com") | ||
adm = admin.create_admin(self.session, f"Test Admin {i}", f"testadmin{i}@gmail.com") | ||
|
||
# Perform operations on the entities | ||
subject.add_student_to_subject(self.session, stud.id, subj.id) | ||
group.add_student_to_group(self.session, stud.id, grp.id) | ||
subject.add_teacher_to_subject(self.session, teach.id, subj.id) | ||
|
||
# Assert the expected outcomes | ||
self.assertEqual(student.get_student(self.session, stud.id).id, stud.id) | ||
self.assertEqual(subject.get_subject(self.session, subj.id).id, subj.id) | ||
self.assertEqual(project.get_project(self.session, proj.id).id, proj.id) | ||
self.assertEqual(group.get_group(self.session, grp.id).id, grp.id) | ||
self.assertEqual(submission.get_submission(self.session, subm.id).id, subm.id) | ||
self.assertEqual(teacher.get_teacher(self.session, teach.id).id, teach.id) | ||
self.assertEqual(admin.get_admin(self.session, adm.id).id, adm.id) | ||
|
||
# Checks outside the loop | ||
self.assertEqual(len(student.get_all_students(self.session)), 100) | ||
self.assertEqual(len(subject.get_all_subjects(self.session)), 100) | ||
self.assertEqual(len(project.get_all_projects(self.session)), 100) | ||
self.assertEqual(len(group.get_all_groups(self.session)), 100) | ||
self.assertEqual(len(submission.get_all_submissions(self.session)), 100) | ||
self.assertEqual(len(teacher.get_all_teachers(self.session)), 100) | ||
self.assertEqual(len(admin.get_all_admins(self.session)), 100) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# test_student.py | ||
import unittest | ||
|
||
from test_main import SessionLocal, test_engine | ||
|
||
from db.extensions import Base | ||
from domain.logic.student import create_student, get_all_students, get_student | ||
from domain.logic.subject import add_student_to_subject, create_subject, get_subjects_of_student | ||
|
||
|
||
class TestStudent(unittest.TestCase): | ||
def setUp(self) -> None: | ||
Base.metadata.drop_all(test_engine) | ||
Base.metadata.create_all(test_engine) | ||
self.session = SessionLocal() | ||
|
||
def tearDown(self) -> None: | ||
self.session.rollback() | ||
self.session.close() | ||
|
||
def test_create_and_get_student(self) -> None: | ||
student = create_student(self.session, "Test Student", "[email protected]") | ||
retrieved_student = get_student(self.session, student.id) | ||
self.assertEqual(student.id, retrieved_student.id) | ||
|
||
def test_get_all_students(self) -> None: | ||
create_student(self.session, "Test Student 1", "[email protected]") | ||
create_student(self.session, "Test Student 2", "[email protected]") | ||
self.assertEqual(len(get_all_students(self.session)), 2) | ||
|
||
def test_add_student_to_subject(self) -> None: | ||
student = create_student(self.session, "Test Student", "[email protected]") | ||
subject = create_subject(self.session, "Test Subject") | ||
add_student_to_subject(self.session, student.id, subject.id) | ||
subjects_of_student = get_subjects_of_student(self.session, student.id) | ||
self.assertIn(subject.id, [subject.id for subject in subjects_of_student]) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# test_subject.py | ||
import unittest | ||
|
||
from test_main import SessionLocal, test_engine | ||
|
||
from db.extensions import Base | ||
from domain.logic.student import create_student | ||
from domain.logic.subject import ( | ||
add_student_to_subject, | ||
add_teacher_to_subject, | ||
create_subject, | ||
get_all_subjects, | ||
get_subject, | ||
get_subjects_of_student, | ||
get_subjects_of_teacher, | ||
) | ||
from domain.logic.teacher import create_teacher | ||
|
||
|
||
class TestSubject(unittest.TestCase): | ||
def setUp(self) -> None: | ||
Base.metadata.drop_all(test_engine) | ||
Base.metadata.create_all(test_engine) | ||
self.session = SessionLocal() | ||
|
||
def tearDown(self) -> None: | ||
self.session.rollback() | ||
self.session.close() | ||
|
||
def test_create_and_get_subject(self) -> None: | ||
subject = create_subject(self.session, "Test Subject") | ||
retrieved_subject = get_subject(self.session, subject.id) | ||
self.assertEqual(subject.id, retrieved_subject.id) | ||
|
||
def test_get_all_subjects(self) -> None: | ||
create_subject(self.session, "Test Subject 1") | ||
create_subject(self.session, "Test Subject 2") | ||
self.assertEqual(len(get_all_subjects(self.session)), 2) | ||
|
||
def test_add_student_to_subject(self) -> None: | ||
student = create_student(self.session, "Test Student", "[email protected]") | ||
subject = create_subject(self.session, "Test Subject") | ||
add_student_to_subject(self.session, student.id, subject.id) | ||
subjects_of_student = get_subjects_of_student(self.session, student.id) | ||
self.assertIn(subject.id, [subject.id for subject in subjects_of_student]) | ||
|
||
def test_add_teacher_to_subject(self) -> None: | ||
teacher = create_teacher(self.session, "Test Teacher", "[email protected]") | ||
subject = create_subject(self.session, "Test Subject") | ||
add_teacher_to_subject(self.session, teacher.id, subject.id) | ||
subjects_of_teacher = get_subjects_of_teacher(self.session, teacher.id) | ||
self.assertIn(subject.id, [subject.id for subject in subjects_of_teacher]) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
Oops, something went wrong.