diff --git a/backend/db/errors/database_errors.py b/backend/db/errors/database_errors.py index 05a400e7..4d0ced8e 100644 --- a/backend/db/errors/database_errors.py +++ b/backend/db/errors/database_errors.py @@ -1,13 +1,23 @@ class ItemNotFoundError(Exception): + """ + The specified item was not found in the database. + """ def __init__(self, message: str) -> None: super().__init__(message) class ActionAlreadyPerformedError(Exception): + """ + The specified action was already performed on the database once before + and may not be performed again as to keep consistency. + """ def __init__(self, message: str) -> None: super().__init__(message) class NoSuchRelationError(Exception): + """ + There is no relation between the two specified elements in the database. + """ def __init__(self, message: str) -> None: super().__init__(message) diff --git a/backend/db/extensions.py b/backend/db/extensions.py index 137d3c8a..c2714e9b 100644 --- a/backend/db/extensions.py +++ b/backend/db/extensions.py @@ -3,15 +3,32 @@ from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase +""" +Retrieve the variables needed for a connection to the database. +We use environment variables for the code to be more adaptable across different environments. +The second variable in os.getenv specifies the default value. +""" +# where the database is hosted db_host = os.getenv("DB_HOST", "localhost") +# port number on which the database server is listening db_port = os.getenv("DB_PORT", "5432") +# username for the database db_user = os.getenv("DB_USERNAME", "postgres") +# password for the user db_password = os.getenv("DB_PASSWORD", "postgres") +# name of the database db_database = os.getenv("DB_DATABASE", "delphi") +# dialect+driver://username:password@host:port/database DB_URI = f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_database}" + +# The engine manages database-operations. +# There is only one instance of the engine, specified here. engine = create_engine(DB_URI) class Base(DeclarativeBase): - pass + """ + This class is meant to be inherited from to define the database tables, see [db/models/models.py]. + For usage, please check https://docs.sqlalchemy.org/en/20/orm/declarative_styles.html#using-a-declarative-base-class. + """ diff --git a/backend/db/models/models.py b/backend/db/models/models.py index ba540dd1..09010ea0 100644 --- a/backend/db/models/models.py +++ b/backend/db/models/models.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass # automatically add special methods as __init__() and __repr__() from datetime import datetime from typing import Generic, TypeVar @@ -17,15 +17,25 @@ from domain.models.TeacherDataclass import TeacherDataclass from domain.models.UserDataclass import UserDataclass +# Create a generic type variable bound to subclasses of BaseModel. D = TypeVar("D", bound=BaseModel) @dataclass() class AbstractModel(Generic[D]): + """ + This class is meant to be inherited by the python classes for the database tables. + It makes sure that every child implements the to_domain_model function + and receives Pydantic data validation. + """ @abstractmethod def to_domain_model(self) -> D: - pass + """ + Change to an actual easy-to-use dataclass defined in [domain/models/*]. + This prevents working with instances of SQLAlchemy's Base class. + """ +# See the EER diagram for a more visual representation. @dataclass() class User(Base, AbstractModel): diff --git a/backend/db/sessions.py b/backend/db/sessions.py index da201a92..fdfda192 100644 --- a/backend/db/sessions.py +++ b/backend/db/sessions.py @@ -4,10 +4,15 @@ from db.extensions import engine +# Generator for db sessions. Check the [documentation.md] for the use of sessions. SessionLocal: sessionmaker[Session] = sessionmaker(autocommit=False, autoflush=False, bind=engine) def get_session() -> Generator[Session, None, None]: + """ + Returns a generator for session objects. + To be used as dependency injection. + """ db = SessionLocal() try: yield db diff --git a/backend/documentation.md b/backend/documentation.md index 8d2b6fc5..196a1158 100644 --- a/backend/documentation.md +++ b/backend/documentation.md @@ -4,9 +4,7 @@ #### 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. +Check the README.md for installation. 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. @@ -14,7 +12,7 @@ For test purposes, mockup data is available in [fill_database_mock.py]. A visual #### 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). +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/20/orm/declarative_styles.html#using-a-declarative-base-class). 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. @@ -50,6 +48,8 @@ We use **FastAPI** as framework. FastAPI follows the OpenAPI Specification. Its 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]. +For specific documentation about the API-endpoints, start the app and go to /api/docs. + ## 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. \ No newline at end of file +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 for the local version only. \ No newline at end of file diff --git a/backend/domain/logic/admin.py b/backend/domain/logic/admin.py index 8f5eb019..2a5d5418 100644 --- a/backend/domain/logic/admin.py +++ b/backend/domain/logic/admin.py @@ -6,6 +6,9 @@ def create_admin(session: Session, name: str, email: str) -> AdminDataclass: + """ + This function is meant to create a new user that is an admin. It does not change the role of an existing user. + """ new_user: User = User(name=name, email=email) session.add(new_user) session.commit() diff --git a/backend/domain/logic/basic_operations.py b/backend/domain/logic/basic_operations.py index 94fb15e3..1e74d534 100644 --- a/backend/domain/logic/basic_operations.py +++ b/backend/domain/logic/basic_operations.py @@ -6,10 +6,15 @@ from db.errors.database_errors import ItemNotFoundError from db.models.models import AbstractModel +# Create a generic type variable bound to subclasses of AbstractModel. T = TypeVar("T", bound=AbstractModel) def get(session: Session, object_type: type[T], ident: int) -> T: + """ + General function for retrieving a single object from the database. + The type of the object and its id as well a session object has to be provided. + """ generic_object: T | None = session.get(object_type, ident) if not generic_object: @@ -20,4 +25,7 @@ def get(session: Session, object_type: type[T], ident: int) -> T: def get_all(session: Session, object_type: type[T]) -> list[T]: + """ + General function for retrieving all objects of a certain type from the database. + """ return list(session.scalars(select(object_type)).all()) diff --git a/backend/domain/logic/group.py b/backend/domain/logic/group.py index eaab0039..fff26c2c 100644 --- a/backend/domain/logic/group.py +++ b/backend/domain/logic/group.py @@ -8,6 +8,9 @@ def create_group(session: Session, project_id: int) -> GroupDataclass: + """ + Create an empty group for a certain project. + """ project: Project = get(session, Project, project_id) new_group: Group = Group(project_id=project_id) project.groups.append(new_group) diff --git a/backend/domain/logic/project.py b/backend/domain/logic/project.py index 2e3b6967..91efcf89 100644 --- a/backend/domain/logic/project.py +++ b/backend/domain/logic/project.py @@ -18,6 +18,9 @@ def create_project( visible: bool, max_students: int, ) -> ProjectDataclass: + """ + Create a project for a certain subject. + """ subject: Subject = get(session, Subject, subject_id) new_project: Project = Project( diff --git a/backend/domain/logic/student.py b/backend/domain/logic/student.py index 4d161e10..a42e1933 100644 --- a/backend/domain/logic/student.py +++ b/backend/domain/logic/student.py @@ -6,6 +6,9 @@ def create_student(session: Session, name: str, email: str) -> StudentDataclass: + """ + This function is meant to create a new user that is a student. It does not change the role of an existing user. + """ new_user: User = User(name=name, email=email) session.add(new_user) session.commit() diff --git a/backend/domain/logic/submission.py b/backend/domain/logic/submission.py index e4bb8318..63e02d9f 100644 --- a/backend/domain/logic/submission.py +++ b/backend/domain/logic/submission.py @@ -15,6 +15,9 @@ def create_submission( state: SubmissionState, date_time: datetime, ) -> SubmissionDataclass: + """ + Create a submission for a certain project by a certain group. + """ student: Student = get(session, Student, ident=student_id) group: Group = get(session, Group, ident=group_id) diff --git a/backend/domain/logic/teacher.py b/backend/domain/logic/teacher.py index 4f665646..01e2d81b 100644 --- a/backend/domain/logic/teacher.py +++ b/backend/domain/logic/teacher.py @@ -6,6 +6,9 @@ def create_teacher(session: Session, name: str, email: str) -> TeacherDataclass: + """ + This function is meant to create a new user that is a teacher. It does not change the role of an existing user. + """ new_user: User = User(name=name, email=email) session.add(new_user) session.commit() diff --git a/backend/domain/logic/user.py b/backend/domain/logic/user.py index 1125191a..fd38f79d 100644 --- a/backend/domain/logic/user.py +++ b/backend/domain/logic/user.py @@ -12,6 +12,9 @@ def convert_user(session: Session, user: UserDataclass) -> APIUser: + """ + Given a UserDataclass, check what roles that user has and fill those in to convert it to an APIUser. + """ api_user = APIUser(id=user.id, name=user.name, email=user.email, roles=[]) if is_user_teacher(session, user.id): diff --git a/backend/domain/models/APIUser.py b/backend/domain/models/APIUser.py index c4ec1a67..b0b78bff 100644 --- a/backend/domain/models/APIUser.py +++ b/backend/domain/models/APIUser.py @@ -4,6 +4,9 @@ class APIUser(BaseModel): + """ + Same as UserDataclass, but with the roles specified in a list. + """ id: int name: str email: EmailStr diff --git a/backend/domain/models/UserDataclass.py b/backend/domain/models/UserDataclass.py index 33e94a5d..167a56f1 100644 --- a/backend/domain/models/UserDataclass.py +++ b/backend/domain/models/UserDataclass.py @@ -2,6 +2,10 @@ class UserDataclass(BaseModel): + """ + This user does not have any roles yet. + When the roles become specified, use the almost equivalent APIUser. + """ id: int name: str email: EmailStr