Skip to content
This repository has been archived by the owner on Sep 27, 2024. It is now read-only.

Commit

Permalink
Merge pull request #127 from SELab-2/add-api-endpoints
Browse files Browse the repository at this point in the history
Add more API endpoints and switch to Poetry
  • Loading branch information
lbarraga authored Apr 2, 2024
2 parents 8eaabf5 + 7fb1592 commit 5f5c595
Show file tree
Hide file tree
Showing 28 changed files with 1,421 additions and 153 deletions.
14 changes: 9 additions & 5 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Poetry
run: pip install poetry
- name: Install dependencies
run: pip install -r backend/requirements.txt
run: cd backend && poetry install
- name: Lint code
run: ruff check backend
run: cd backend && poetry run ruff check
- name: Run Pyright
run: pyright
run: cd backend && poetry run pyright
test:
runs-on: self-hosted
steps:
Expand All @@ -23,9 +25,11 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Poetry
run: pip install poetry
- name: Install dependencies
run: pip install -r backend/requirements.txt
run: cd backend && poetry install
- name: Run tests
run: cd backend && python -m unittest discover tests
run: cd backend && poetry run python -m unittest discover tests
env:
TEST_DB_USER: githubactions
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
submissions
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -160,4 +161,4 @@ cython_debug/
.idea/

# Ruff linter
.ruff_cache
.ruff_cache
24 changes: 10 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# UGent-2
De mockup van ons project kan [hier](https://www.figma.com/file/py6Qk9lgFtzbCy9by2qsYU/SELab2?type=design&node-id=617%3A4348&mode=design&t=N4FQR50wAYEyG8qx-1)
gevonden worden.
gevonden worden.

## Project setup

Expand All @@ -10,23 +10,20 @@ gevonden worden.
```
## Backend

De backend gebruikt Python 3.12.
De backend gebruikt Python 3.12 en Poetry.
Volg deze stappen om de backend van het project op te zetten:


1. Navigeer naar de backend map:
```bash
cd UGent-2/backend
```

2. Start de Python virtual environment:
```bash
python3 -m venv venv
source venv/bin/activate
```
3. Installeer de benodigde Python packages met behulp van het `requirements.txt` bestand:
2. Installeer Poetry

3. Installeer de benodigde Python packages met behulp van Poetry en voer de rest van de stappen uit in die virtual environment:
```bash
pip install -r requirements.txt
poetry install
poetry shell
```
4. Installeer PostgreSQL:

Expand Down Expand Up @@ -108,13 +105,12 @@ Volg deze stappen om de backend van het project op te zetten:
npm run build
```
De gecompileerde html/css/js bevindt zich nu in de `dist` folder

5. Deploy:

Zet de inhoud van de `dist` folder op de juiste plaats, zodat het geserveerd kan worden.

6. De testen kunnen uitgevoerd worden met: (nog niet geïmplementeerd)
```bash
npm run tests
```

5 changes: 3 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM python:3.12-slim
EXPOSE 8000
RUN pip install poetry
COPY . /backend
WORKDIR /backend
RUN pip install -r requirements.txt
CMD uvicorn --host 0.0.0.0 app:app
RUN poetry install
CMD poetry run uvicorn --host 0.0.0.0 app:app
30 changes: 26 additions & 4 deletions backend/app.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import pathlib

import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette import status
from starlette.requests import Request
from starlette.responses import JSONResponse

from db.errors.database_errors import ActionAlreadyPerformedError, ItemNotFoundError, NoSuchRelationError
from routes.errors.authentication import InvalidAuthenticationError, InvalidRoleCredentialsError, NoAccessToSubjectError
from db.errors.database_errors import (
ActionAlreadyPerformedError,
ConflictingRelationError,
ItemNotFoundError,
NoSuchRelationError,
)
from routes.errors.authentication import (
InvalidAuthenticationError,
InvalidRoleCredentialsError,
NoAccessToDataError,
)
from routes.group import group_router
from routes.login import login_router
from routes.project import project_router
from routes.student import student_router
from routes.subject import subject_router
from routes.submission import submission_router
from routes.tags.swagger_tags import tags_metadata
from routes.teacher import teacher_router
from routes.user import users_router

pathlib.Path.mkdir(pathlib.Path("submissions"), exist_ok=True)
app = FastAPI(docs_url="/api/docs", openapi_tags=tags_metadata)

# Koppel routes uit andere modules.
Expand All @@ -26,6 +39,7 @@
app.include_router(project_router, prefix="/api")
app.include_router(subject_router, prefix="/api")
app.include_router(group_router, prefix="/api")
app.include_router(submission_router, prefix="/api")

DEBUG = False # Should always be false in repo

Expand Down Expand Up @@ -64,8 +78,8 @@ def item_not_found_error_handler(request: Request, exc: ItemNotFoundError) -> JS
)


@app.exception_handler(NoAccessToSubjectError)
def no_access_to_subject_error_handler(request: Request, exc: NoAccessToSubjectError) -> JSONResponse:
@app.exception_handler(NoAccessToDataError)
def no_access_to_data_error_handler(request: Request, exc: NoAccessToDataError) -> JSONResponse:
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"detail": str(exc)},
Expand Down Expand Up @@ -96,5 +110,13 @@ def invalid_authentication_error_handler(request: Request, exc: NoSuchRelationEr
)


@app.exception_handler(ConflictingRelationError)
def conflicting_relation_error_handler(request: Request, exc: ConflictingRelationError) -> JSONResponse:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": str(exc)},
)


if __name__ == "__main__":
uvicorn.run("app:app")
12 changes: 12 additions & 0 deletions backend/db/errors/database_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class ItemNotFoundError(Exception):
"""
The specified item was not found in the database.
"""

def __init__(self, message: str) -> None:
super().__init__(message)

Expand All @@ -11,6 +12,7 @@ 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)

Expand All @@ -19,5 +21,15 @@ class NoSuchRelationError(Exception):
"""
There is no relation between the two specified elements in the database.
"""

def __init__(self, message: str) -> None:
super().__init__(message)


class ConflictingRelationError(Exception):
"""
There is a conflicting relation
"""

def __init__(self, message: str) -> None:
super().__init__(message)
2 changes: 2 additions & 0 deletions backend/db/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class Submission(Base, AbstractModel):
date_time: Mapped[datetime]
state: Mapped[SubmissionState]
message: Mapped[str]
filename: Mapped[str]
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
group_id: Mapped[int] = mapped_column(ForeignKey(Group.id))
group: Mapped[Group] = relationship(back_populates="submissions")
Expand All @@ -181,4 +182,5 @@ def to_domain_model(self) -> SubmissionDataclass:
student_id=self.student_id,
state=self.state,
message=self.message,
filename=self.filename,
)
14 changes: 13 additions & 1 deletion backend/domain/logic/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ def add_student_to_group(session: Session, student_id: int, group_id: int) -> No
if student in group.students:
msg = f"Student with id {student_id} already in group with id {group_id}"
raise ActionAlreadyPerformedError(msg)

for i in group.project.groups:
if student in i.students:
msg = "Student is already in a group for this project"
raise ActionAlreadyPerformedError(msg)
group.students.append(student)
session.commit()

Expand All @@ -69,3 +72,12 @@ def get_students_of_group(session: Session, group_id: int) -> list[StudentDatacl
group: Group = get(session, Group, ident=group_id)
students: list[Student] = group.students
return [student.to_domain_model() for student in students]


def get_group_for_student_and_project(session: Session, student_id: int, project_id: int) -> GroupDataclass | None:
student: Student = get(session, Student, ident=student_id)
project: Project = get(session, Project, ident=project_id)
for group in project.groups:
if student in group.students:
return group.to_domain_model()
return None
14 changes: 13 additions & 1 deletion backend/domain/logic/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from db.models.models import Project, Student, Subject, Teacher
from domain.logic.basic_operations import get, get_all
from domain.models.ProjectDataclass import ProjectDataclass
from domain.models.ProjectDataclass import ProjectDataclass, ProjectInput


def create_project(
Expand Down Expand Up @@ -69,3 +69,15 @@ def get_projects_of_teacher(session: Session, user_id: int) -> list[ProjectDatac
for i in subjects:
projects += i.projects
return [project.to_domain_model() for project in projects]


def update_project(session: Session, project_id: int, project: ProjectInput) -> None:
project_db = get(session, Project, project_id)
project_db.archived = project.archived
project_db.deadline = project.deadline
project_db.description = project.description
project_db.max_students = project.max_students
project_db.name = project.name
project_db.requirements = project.requirements
project_db.visible = project.visible
session.commit()
14 changes: 14 additions & 0 deletions backend/domain/logic/subject.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
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.StudentDataclass import StudentDataclass
from domain.models.SubjectDataclass import SubjectDataclass
from domain.models.TeacherDataclass import TeacherDataclass


def create_subject(session: Session, name: str) -> SubjectDataclass:
Expand Down Expand Up @@ -68,3 +70,15 @@ def is_user_authorized_for_subject(subject_id: int, session: Session, uid: int)
if subject_id in [subject.id for subject in subjects]:
return True
return False


def get_teachers_of_subject(session: Session, subject_id: int) -> list[TeacherDataclass]:
subject: Subject = get(session, Subject, ident=subject_id)
teachers = subject.teachers
return [teacher.to_domain_model() for teacher in teachers]


def get_students_of_subject(session: Session, subject_id: int) -> list[StudentDataclass]:
subject: Subject = get(session, Subject, ident=subject_id)
students = subject.students
return [student.to_domain_model() for student in students]
19 changes: 13 additions & 6 deletions backend/domain/logic/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@


def create_submission(
session: Session,
student_id: int,
group_id: int,
message: str,
state: SubmissionState,
date_time: datetime,
session: Session,
student_id: int,
group_id: int,
message: str,
state: SubmissionState,
date_time: datetime,
filename: str,
) -> SubmissionDataclass:
"""
Create a submission for a certain project by a certain group.
Expand All @@ -27,6 +28,7 @@ def create_submission(
message=message,
state=state,
date_time=date_time,
filename=filename,
)
session.add(new_submission)
session.commit()
Expand All @@ -51,3 +53,8 @@ def get_submissions_of_group(session: Session, group_id: int) -> list[Submission
group: Group = get(session, Group, ident=group_id)
submissions: list[Submission] = group.submissions
return [submission.to_domain_model() for submission in submissions]


def get_last_submission(session: Session, group_id: int) -> SubmissionDataclass:
submissions = get_submissions_of_group(session, group_id)
return max(submissions, key=lambda submission: submission.date_time)
4 changes: 4 additions & 0 deletions backend/domain/models/APIUser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ class APIUser(BaseModel):

class LoginResponse(BaseModel):
token: str


class ValidateResponse(BaseModel):
valid: bool
1 change: 1 addition & 0 deletions backend/domain/models/SubmissionDataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ class SubmissionDataclass(BaseModel):
student_id: int
state: SubmissionState
message: str
filename: str
Loading

0 comments on commit 5f5c595

Please sign in to comment.