From d4507f835f34025d199104be47b901940dcef814 Mon Sep 17 00:00:00 2001 From: driesaster Date: Mon, 4 Mar 2024 15:45:53 +0100 Subject: [PATCH 01/29] adds project schemes,routes,services into backend (not complete) --- backend/src/project/dependencies.py | 34 +++++++++++++++++++++++++++++ backend/src/project/exceptions.py | 0 backend/src/project/models.py | 17 +++++++++++++++ backend/src/project/router.py | 33 ++++++++++++++++++++++++++++ backend/src/project/schemas.py | 26 ++++++++++++++++++++++ backend/src/project/service.py | 14 ++++++++++++ backend/src/subject/service.py | 7 ++++++ 7 files changed, 131 insertions(+) create mode 100644 backend/src/project/dependencies.py create mode 100644 backend/src/project/exceptions.py create mode 100644 backend/src/project/models.py create mode 100644 backend/src/project/router.py create mode 100644 backend/src/project/schemas.py create mode 100644 backend/src/project/service.py diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py new file mode 100644 index 00000000..18df27db --- /dev/null +++ b/backend/src/project/dependencies.py @@ -0,0 +1,34 @@ +from fastapi import Depends +from sqlalchemy.orm import Session +from src.dependencies import get_db +from src.user.dependencies import get_authenticated_user +from src.user.exceptions import NotAuthorized +from src.user.schemas import User +from src.subject.schemas import Subject +from src.subject.schemas import SubjectList + + +from . import service +# from .exceptions import SubjectNotFound +# from .schemas import Subject, SubjectList + + + + + +async def retrieve_subjects( + user: User = Depends(get_authenticated_user), db: Session = Depends(get_db) +) -> SubjectList: + teacher_subjects, student_subjects = await service.get_subjects(db, user.uid) + return SubjectList(as_teacher=teacher_subjects, as_student=student_subjects) + + +async def user_permission_validation( + subject_id: int, + user: User = Depends(get_authenticated_user), + db: Session = Depends(get_db), +): + if not user.is_admin: + teachers = await service.get_teachers(db, subject_id) + if not list(filter(lambda teacher: teacher.id == user.uid, teachers)): + raise NotAuthorized() diff --git a/backend/src/project/exceptions.py b/backend/src/project/exceptions.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/src/project/models.py b/backend/src/project/models.py new file mode 100644 index 00000000..1db2ace3 --- /dev/null +++ b/backend/src/project/models.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, BigInteger, String, Date, ForeignKey +from sqlalchemy.orm import relationship +from src.database import Base + + +class Project(Base): + __tablename__ = 'Project' + + id = Column(BigInteger, primary_key=True, autoincrement=True, index=True) + deadline = Column(Date, nullable=False, check_constraint='deadline >= CURRENT_DATE') + name = Column(String, nullable=False) + subjectId = Column(String, ForeignKey('Subject.subjectId', ondelete="SET NULL"), nullable=True) + description = Column(String, nullable=True) + + # Relationships + teams = relationship("Team", back_populates="project") + diff --git a/backend/src/project/router.py b/backend/src/project/router.py new file mode 100644 index 00000000..87d5e942 --- /dev/null +++ b/backend/src/project/router.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from src.user.schemas import User +from src.user.dependencies import get_authenticated_user, get_db +from .schemas import ProjectCreate, ProjectResponse +from .service import create_project +from ..subject.service import is_teacher_of_subject + +router = APIRouter( + prefix="/subject/{subject_id}", + tags=["subject_overview"], + responses={404: {"description": "Not found"}}, +) + +@router.post("/subjects/{subject_id}/projects/create") +async def create_project( + subject_id: int, + project_in: ProjectCreate, + user: User = Depends(get_authenticated_user), + db: Session = Depends(get_db) +): + # Check if the user is a teacher of the subject + if not await is_teacher_of_subject(db, user.uid, subject_id): + raise HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") + + project = create_project(db=db, project_in=project_in, user_id=user.id) + return project + +@router.get("/projects/{project_id}", response_model=ProjectResponse) +async def get_project(project_id: int, db: Session = Depends(get_db)): + project = db.get(models.Project, project_id) + return project + diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py new file mode 100644 index 00000000..cc8c9b7d --- /dev/null +++ b/backend/src/project/schemas.py @@ -0,0 +1,26 @@ +from datetime import date +from pydantic import BaseModel, Field, validator + +class ProjectCreate(BaseModel): + name: str = Field(..., min_length=1) + deadline: date + subject_id: int + description: str + + # Check if deadline is not in the past + @validator('deadline', pre=True, always=True) + def validate_deadline(cls, value): + if value < date.today(): + raise ValueError('The deadline cannot be in the past') + return value + +class ProjectResponse(BaseModel): + id: int + name: str + deadline: date + subject_id: int + description: str + + class Config: + orm_mode = True + diff --git a/backend/src/project/service.py b/backend/src/project/service.py new file mode 100644 index 00000000..ca888768 --- /dev/null +++ b/backend/src/project/service.py @@ -0,0 +1,14 @@ +from sqlalchemy.orm import Session +from . import models, schemas + +async def create_project(db: Session, project_in: ProjectCreate, user_id: str) -> Project: + new_project = Project( + name=project_in.name, + deadline=project_in.deadline, + subject_id=project_in.subject_id + description=project_in.description + ) + db.add(new_project) + db.commit() + db.refresh(new_project) + return new_project diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index e446452f..0119d4b0 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -42,6 +42,13 @@ async def create_subject(db: Session, subject: schemas.SubjectCreate) -> models. return db_subject +async def is_teacher_of_subject(db: Session, user_id: str, subject_id: int) -> bool: + """Check if a user is a teacher of the subject.""" + teachers = await get_subject_teachers(db, subject_id) + return any(teacher.uid == user_id for teacher in teachers) + + + async def remove_subject(db: Session, subject: schemas.Subject): """Remove a subject""" db.delete(subject) From a0181e215f7f759fe72afca471be869df6a9b2df Mon Sep 17 00:00:00 2001 From: driesaster Date: Mon, 4 Mar 2024 16:34:58 +0100 Subject: [PATCH 02/29] router + services, added create delete, get not yet update --- backend/src/project/router.py | 56 +++++++++++++++++++++++++--------- backend/src/project/service.py | 26 +++++++++++++++- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 87d5e942..b0883a3f 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -1,33 +1,59 @@ from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from src.user.schemas import User -from src.user.dependencies import get_authenticated_user, get_db +from sqlalchemy.ext.asyncio import AsyncSession +from src.user.models import User +from src.dependencies import get_db +from src.user.dependencies import get_authenticated_user from .schemas import ProjectCreate, ProjectResponse -from .service import create_project +from .service import create_project, get_project, delete_project, update_project from ..subject.service import is_teacher_of_subject router = APIRouter( - prefix="/subject/{subject_id}", - tags=["subject_overview"], + prefix="/subjects/{subject_id}/projects", + tags=["projects"], responses={404: {"description": "Not found"}}, ) -@router.post("/subjects/{subject_id}/projects/create") -async def create_project( + +@router.post("/", response_model=ProjectResponse) +async def create_project_for_subject( subject_id: int, project_in: ProjectCreate, user: User = Depends(get_authenticated_user), - db: Session = Depends(get_db) + db: AsyncSession = Depends(get_db) ): - # Check if the user is a teacher of the subject - if not await is_teacher_of_subject(db, user.uid, subject_id): + if not await is_teacher_of_subject(db, user.id, subject_id): raise HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") - project = create_project(db=db, project_in=project_in, user_id=user.id) + project = await create_project(db=db, project_in=project_in, user_id=user.id) return project -@router.get("/projects/{project_id}", response_model=ProjectResponse) -async def get_project(project_id: int, db: Session = Depends(get_db)): - project = db.get(models.Project, project_id) + +@router.get("/{project_id}", response_model=ProjectResponse) +async def get_project_for_subject( + project_id: int, + db: AsyncSession = Depends(get_db) +): + project = await get_project(db, project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") return project + +@router.delete("/{project_id}") +async def delete_project_for_subject( + subject_id: int, + project_id: int, + user: User = Depends(get_authenticated_user), + db: AsyncSession = Depends(get_db) +): + if not await is_teacher_of_subject(db, user.id, subject_id): + raise HTTPException(status_code=403, detail="User is not authorized to delete this project") + + await delete_project(db, project_id) + return {"message": "Project deleted successfully"} + + + + + + diff --git a/backend/src/project/service.py b/backend/src/project/service.py index ca888768..6a4bef1c 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -1,14 +1,38 @@ from sqlalchemy.orm import Session from . import models, schemas +from .models import Project +from .schemas import ProjectCreate + async def create_project(db: Session, project_in: ProjectCreate, user_id: str) -> Project: new_project = Project( name=project_in.name, deadline=project_in.deadline, - subject_id=project_in.subject_id + subject_id=project_in.subject_id, description=project_in.description ) db.add(new_project) db.commit() db.refresh(new_project) return new_project + +async def get_project(db: Session, project_id: int) -> Project: + return db.query(Project).filter(Project.id == project_id).first() + +async def delete_project(db: Session, project_id: int): + project = db.query(Project).filter(Project.id == project_id).first() + if project: + db.delete(project) + db.commit() + +async def update_project(db: Session, project_id: int, project_update: ProjectCreate) -> Project: + project = db.query(Project).filter(Project.id == project_id).first() + if project: + project.name = project_update.name + project.deadline = project_update.deadline + project.description = project_update.description + db.commit() + db.refresh(project) + return project + + From eaac9057ac5d30472ac1b969e6d41ac4e5378c63 Mon Sep 17 00:00:00 2001 From: driesaster Date: Mon, 4 Mar 2024 20:23:41 +0100 Subject: [PATCH 03/29] cleanup + all projects for certain subject overview added --- backend/src/project/router.py | 11 +++++++++++ backend/src/project/service.py | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index b0883a3f..550934ff 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -13,6 +13,17 @@ responses={404: {"description": "Not found"}}, ) +@router.get("/", response_model=list[ProjectResponse]) +async def list_projects_for_subject( + subject_id: int, + user: User = Depends(get_authenticated_user), + db: AsyncSession = Depends(get_db) +): + # Optional: You may want to check if the user has access to the subject (e.g., is a teacher or a student of the subject) + projects = await get_projects_for_subject(db, subject_id) + if not projects: + raise HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") + return projects @router.post("/", response_model=ProjectResponse) async def create_project_for_subject( diff --git a/backend/src/project/service.py b/backend/src/project/service.py index 6a4bef1c..33ed32a2 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -19,6 +19,14 @@ async def create_project(db: Session, project_in: ProjectCreate, user_id: str) - async def get_project(db: Session, project_id: int) -> Project: return db.query(Project).filter(Project.id == project_id).first() +async def get_projects_for_subject(db: Session, subject_id: int) -> list[models.Project]: + projects = ( + db.query(models.Project) + .filter(models.Project.subject_id == subject_id) + .all() + ) + return projects + async def delete_project(db: Session, project_id: int): project = db.query(Project).filter(Project.id == project_id).first() if project: From 448fe6d043ffc79a3c52819679048aeade288a4d Mon Sep 17 00:00:00 2001 From: driesaster Date: Mon, 4 Mar 2024 20:31:26 +0100 Subject: [PATCH 04/29] project update --- backend/src/project/router.py | 20 +++++++++++++++++++- backend/src/project/schemas.py | 7 +++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 550934ff..73dd7cce 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -3,7 +3,7 @@ from src.user.models import User from src.dependencies import get_db from src.user.dependencies import get_authenticated_user -from .schemas import ProjectCreate, ProjectResponse +from .schemas import ProjectCreate, ProjectResponse, ProjectUpdate from .service import create_project, get_project, delete_project, update_project from ..subject.service import is_teacher_of_subject @@ -64,6 +64,24 @@ async def delete_project_for_subject( return {"message": "Project deleted successfully"} +@router.patch("/{project_id}", response_model=ProjectResponse) +async def patch_project_for_subject( + subject_id: int, + project_id: int, + project_update: ProjectUpdate, + user: User = Depends(get_authenticated_user), + db: AsyncSession = Depends(get_db) +): + # Check if the user is authorized to update the project + if not await is_teacher_of_subject(db, user.id, subject_id): + raise HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") + + updated_project = await update_project(db, project_id, project_update) + if not updated_project: + raise HTTPException(status_code=404, detail="Project not found") + return updated_project + + diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py index cc8c9b7d..be5bc5ea 100644 --- a/backend/src/project/schemas.py +++ b/backend/src/project/schemas.py @@ -1,4 +1,6 @@ from datetime import date +from typing import Optional + from pydantic import BaseModel, Field, validator class ProjectCreate(BaseModel): @@ -24,3 +26,8 @@ class ProjectResponse(BaseModel): class Config: orm_mode = True +class ProjectUpdate(BaseModel): + name: Optional[str] = None + deadline: Optional[date] = None + description: Optional[str] = None + From c2ee3ff6d8445e002fc7086517a910d9df82e870 Mon Sep 17 00:00:00 2001 From: driesaster Date: Tue, 5 Mar 2024 19:41:29 +0100 Subject: [PATCH 05/29] exceptions split up, cleanup --- backend/src/project/dependencies.py | 2 -- backend/src/project/exceptions.py | 18 ++++++++++++++++++ backend/src/project/models.py | 2 +- backend/src/project/router.py | 15 ++++++++------- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py index 18df27db..0f0712d9 100644 --- a/backend/src/project/dependencies.py +++ b/backend/src/project/dependencies.py @@ -9,8 +9,6 @@ from . import service -# from .exceptions import SubjectNotFound -# from .schemas import Subject, SubjectList diff --git a/backend/src/project/exceptions.py b/backend/src/project/exceptions.py index e69de29b..6c0d5358 100644 --- a/backend/src/project/exceptions.py +++ b/backend/src/project/exceptions.py @@ -0,0 +1,18 @@ +# exceptions.py + +from fastapi import HTTPException + +def NoProjectsFoundException(subject_id: int): + return HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") + +def UnauthorizedToCreateProjectException(): + return HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") + +def ProjectNotFoundException(): + return HTTPException(status_code=404, detail="Project not found") + +def UnauthorizedToDeleteProjectException(): + return HTTPException(status_code=403, detail="User is not authorized to delete this project") + +def UnauthorizedToUpdateProjectException(): + return HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 1db2ace3..9b53b0bd 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -4,7 +4,7 @@ class Project(Base): - __tablename__ = 'Project' + __tablename__ = 'project' id = Column(BigInteger, primary_key=True, autoincrement=True, index=True) deadline = Column(Date, nullable=False, check_constraint='deadline >= CURRENT_DATE') diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 73dd7cce..5e61b518 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -4,8 +4,9 @@ from src.dependencies import get_db from src.user.dependencies import get_authenticated_user from .schemas import ProjectCreate, ProjectResponse, ProjectUpdate -from .service import create_project, get_project, delete_project, update_project +from .service import create_project, get_project, delete_project, update_project, get_projects_for_subject from ..subject.service import is_teacher_of_subject +from . import exceptions router = APIRouter( prefix="/subjects/{subject_id}/projects", @@ -22,7 +23,7 @@ async def list_projects_for_subject( # Optional: You may want to check if the user has access to the subject (e.g., is a teacher or a student of the subject) projects = await get_projects_for_subject(db, subject_id) if not projects: - raise HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") + raise NoProjectsFoundException(subject_id) return projects @router.post("/", response_model=ProjectResponse) @@ -33,7 +34,7 @@ async def create_project_for_subject( db: AsyncSession = Depends(get_db) ): if not await is_teacher_of_subject(db, user.id, subject_id): - raise HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") + raise UnauthorizedToCreateProjectException() project = await create_project(db=db, project_in=project_in, user_id=user.id) return project @@ -46,7 +47,7 @@ async def get_project_for_subject( ): project = await get_project(db, project_id) if not project: - raise HTTPException(status_code=404, detail="Project not found") + raise ProjectNotFoundException() return project @@ -58,7 +59,7 @@ async def delete_project_for_subject( db: AsyncSession = Depends(get_db) ): if not await is_teacher_of_subject(db, user.id, subject_id): - raise HTTPException(status_code=403, detail="User is not authorized to delete this project") + raise UnauthorizedToUpdateProjectException() await delete_project(db, project_id) return {"message": "Project deleted successfully"} @@ -74,11 +75,11 @@ async def patch_project_for_subject( ): # Check if the user is authorized to update the project if not await is_teacher_of_subject(db, user.id, subject_id): - raise HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") + raise UnauthorizedToUpdateProjectException() updated_project = await update_project(db, project_id, project_update) if not updated_project: - raise HTTPException(status_code=404, detail="Project not found") + raise ProjectNotFoundException() return updated_project From 2ed95d5c6e5298d7ca44a8b2533e7ce9c8d731df Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Tue, 5 Mar 2024 20:57:44 +0100 Subject: [PATCH 06/29] actual patches/fixes now that im running on linux --- backend/src/main.py | 2 ++ backend/src/project/dependencies.py | 3 --- backend/src/project/exceptions.py | 5 +++++ backend/src/project/models.py | 4 ++-- backend/src/project/router.py | 8 ++------ backend/src/project/schemas.py | 4 +++- backend/src/project/service.py | 6 ++++-- backend/src/subject/service.py | 6 ++++++ 8 files changed, 24 insertions(+), 14 deletions(-) diff --git a/backend/src/main.py b/backend/src/main.py index 71c78d55..57d9ac2b 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -2,6 +2,7 @@ from starlette.middleware.sessions import SessionMiddleware from src.subject.router import router as subject_router from src.user.router import router as user_router +from src.project.router import router as project_router app = FastAPI() @@ -9,6 +10,7 @@ app.include_router(subject_router) app.include_router(user_router) +app.include_router(project_router) @app.get("/api") diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py index 0f0712d9..1a605b30 100644 --- a/backend/src/project/dependencies.py +++ b/backend/src/project/dependencies.py @@ -11,9 +11,6 @@ from . import service - - - async def retrieve_subjects( user: User = Depends(get_authenticated_user), db: Session = Depends(get_db) ) -> SubjectList: diff --git a/backend/src/project/exceptions.py b/backend/src/project/exceptions.py index 6c0d5358..6ecc8164 100644 --- a/backend/src/project/exceptions.py +++ b/backend/src/project/exceptions.py @@ -2,17 +2,22 @@ from fastapi import HTTPException + def NoProjectsFoundException(subject_id: int): return HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") + def UnauthorizedToCreateProjectException(): return HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") + def ProjectNotFoundException(): return HTTPException(status_code=404, detail="Project not found") + def UnauthorizedToDeleteProjectException(): return HTTPException(status_code=403, detail="User is not authorized to delete this project") + def UnauthorizedToUpdateProjectException(): return HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 9b53b0bd..240de490 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -9,9 +9,9 @@ class Project(Base): id = Column(BigInteger, primary_key=True, autoincrement=True, index=True) deadline = Column(Date, nullable=False, check_constraint='deadline >= CURRENT_DATE') name = Column(String, nullable=False) - subjectId = Column(String, ForeignKey('Subject.subjectId', ondelete="SET NULL"), nullable=True) + subjectId = Column(String, ForeignKey( + 'Subject.subjectId', ondelete="SET NULL"), nullable=True) description = Column(String, nullable=True) # Relationships teams = relationship("Team", back_populates="project") - diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 5e61b518..4077f573 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -14,6 +14,7 @@ responses={404: {"description": "Not found"}}, ) + @router.get("/", response_model=list[ProjectResponse]) async def list_projects_for_subject( subject_id: int, @@ -26,6 +27,7 @@ async def list_projects_for_subject( raise NoProjectsFoundException(subject_id) return projects + @router.post("/", response_model=ProjectResponse) async def create_project_for_subject( subject_id: int, @@ -81,9 +83,3 @@ async def patch_project_for_subject( if not updated_project: raise ProjectNotFoundException() return updated_project - - - - - - diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py index be5bc5ea..a7ea8e0f 100644 --- a/backend/src/project/schemas.py +++ b/backend/src/project/schemas.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, Field, validator + class ProjectCreate(BaseModel): name: str = Field(..., min_length=1) deadline: date @@ -16,6 +17,7 @@ def validate_deadline(cls, value): raise ValueError('The deadline cannot be in the past') return value + class ProjectResponse(BaseModel): id: int name: str @@ -26,8 +28,8 @@ class ProjectResponse(BaseModel): class Config: orm_mode = True + class ProjectUpdate(BaseModel): name: Optional[str] = None deadline: Optional[date] = None description: Optional[str] = None - diff --git a/backend/src/project/service.py b/backend/src/project/service.py index 33ed32a2..5fd6c0fd 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -16,9 +16,11 @@ async def create_project(db: Session, project_in: ProjectCreate, user_id: str) - db.refresh(new_project) return new_project + async def get_project(db: Session, project_id: int) -> Project: return db.query(Project).filter(Project.id == project_id).first() + async def get_projects_for_subject(db: Session, subject_id: int) -> list[models.Project]: projects = ( db.query(models.Project) @@ -27,12 +29,14 @@ async def get_projects_for_subject(db: Session, subject_id: int) -> list[models. ) return projects + async def delete_project(db: Session, project_id: int): project = db.query(Project).filter(Project.id == project_id).first() if project: db.delete(project) db.commit() + async def update_project(db: Session, project_id: int, project_update: ProjectCreate) -> Project: project = db.query(Project).filter(Project.id == project_id).first() if project: @@ -42,5 +46,3 @@ async def update_project(db: Session, project_id: int, project_update: ProjectCr db.commit() db.refresh(project) return project - - diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index 421c0392..c519a51a 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -49,3 +49,9 @@ async def delete_subject(db: Session, subject_id: int): """Remove a subject""" db.query(models.Subject).filter_by(id=subject_id).delete() db.commit() + + +async def is_teacher_of_subject(db: Session, user_id: str, subject_id: int) -> bool: + """Check if a user is a teacher of the subject.""" + teachers = await get_subject_teachers(db, subject_id) + return any(teacher.uid == user_id for teacher in teachers) From d87943a9be497a4750fe5040a8b38f3f3de107de Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Sun, 10 Mar 2024 16:39:09 +0100 Subject: [PATCH 07/29] api voor router gezet --- backend/src/project/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 4077f573..0b36a549 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -9,7 +9,7 @@ from . import exceptions router = APIRouter( - prefix="/subjects/{subject_id}/projects", + prefix="/api/subjects/{subject_id}/projects", tags=["projects"], responses={404: {"description": "Not found"}}, ) From 43520c9b8fb173fd77b3e0b414896c7cee983f8f Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Mon, 11 Mar 2024 19:11:49 +0100 Subject: [PATCH 08/29] alembic added --- backend/alembic.ini | 115 ++++++++ backend/alembic/README | 1 + backend/alembic/env.py | 97 +++++++ backend/alembic/script.py.mako | 26 ++ .../29a17fa183de_enroll_deadline_added.py | 30 +++ .../2cc559f8470c_allign_with_new_schema.py | 159 +++++++++++ .../ae4432f8b5b7_initial_migration.py | 252 ++++++++++++++++++ .../versions/eb472f05f70e_recreate_tables.py | 68 +++++ ...7e930257_correct_project_subjectid_type.py | 67 +++++ backend/src/project/models.py | 18 +- backend/src/project/router.py | 1 - backend/src/subject/models.py | 4 +- 12 files changed, 828 insertions(+), 10 deletions(-) create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/README create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/29a17fa183de_enroll_deadline_added.py create mode 100644 backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py create mode 100644 backend/alembic/versions/ae4432f8b5b7_initial_migration.py create mode 100644 backend/alembic/versions/eb472f05f70e_recreate_tables.py create mode 100644 backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 00000000..d40db921 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,115 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql://selab:2UJd24Z6aUm85ZExEri@sel2-5.ugent.be:2002/selab + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/README b/backend/alembic/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 00000000..de4ae87d --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,97 @@ +from src.user.models import Base as UserBase +from src.subject.models import Base as SubjectBase +from src.project.models import Base as ProjectBase +import os +import sys +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from sqlalchemy import MetaData + +from alembic import context +from src.database import Base + +# Calculate the path based on the location of the env.py file +d = os.path.dirname +parent_dir = d(d(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +# Import the Base from each of your model submodules + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +combined_metadata = MetaData() +for base in [ProjectBase, SubjectBase, UserBase]: + for table in base.metadata.tables.values(): + combined_metadata._add_table(table.name, table.schema, table) + +target_metadata = combined_metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 00000000..fbc4b07d --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py b/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py new file mode 100644 index 00000000..048c10f8 --- /dev/null +++ b/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py @@ -0,0 +1,30 @@ +"""enroll_deadline_added + +Revision ID: 29a17fa183de +Revises: fc317e930257 +Create Date: 2024-03-11 19:09:35.872572 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '29a17fa183de' +down_revision: Union[str, None] = 'fc317e930257' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('project', sa.Column('enroll_deadline', sa.Date(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('project', 'enroll_deadline') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py new file mode 100644 index 00000000..0e6c837f --- /dev/null +++ b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py @@ -0,0 +1,159 @@ +"""Allign with new schema + +Revision ID: 2cc559f8470c +Revises: ae4432f8b5b7 +Create Date: 2024-03-11 16:20:20.260148 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '2cc559f8470c' +down_revision: Union[str, None] = 'ae4432f8b5b7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Drop tables that are dependent on other tables + op.execute('DROP TABLE IF EXISTS student_subject CASCADE') + op.execute('DROP TABLE IF EXISTS teacher_subject CASCADE') + op.execute('DROP TABLE IF EXISTS student_group CASCADE') + op.execute('DROP TABLE IF EXISTS submission CASCADE') + op.execute('DROP TABLE IF EXISTS file CASCADE') + op.execute('DROP TABLE IF EXISTS website_user CASCADE') + op.execute('DROP TABLE IF EXISTS status CASCADE') + op.execute('DROP TABLE IF EXISTS team CASCADE') + op.execute('DROP TABLE IF EXISTS subject CASCADE') + op.execute('DROP TABLE IF EXISTS project CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('file', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('submission_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='file_project_id_fkey', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['submission_id'], ['submission.id'], + name='file_submission_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='file_pkey') + ) + op.create_table('project', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('project_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('deadline', sa.DATE(), + autoincrement=False, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), server_default=sa.text( + "nextval('project_subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('description', sa.TEXT(), + autoincrement=False, nullable=True), + sa.Column('max_team_size', sa.INTEGER(), server_default=sa.text( + '4'), autoincrement=False, nullable=False), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='project_subject_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='project_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='teacher_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='teacher_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='teacher_subject_pkey') + ) + op.create_table('student_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='student_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='student_subject_pkey') + ) + op.create_table('submission', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='submission_project_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['status_id'], ['status.id'], + name='submission_status_id_fkey', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='submission_team_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='submission_pkey') + ) + op.create_table('subject', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='subject_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('team', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('team_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('team_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='team_project_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='team_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('student_group', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='student_group_team_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_group_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'team_id', name='student_group_pkey') + ) + op.create_table('status', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('status_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='status_pkey'), + sa.UniqueConstraint('status_name', name='status_status_name_key') + ) + op.create_table('website_user', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=False), + sa.Column('given_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('mail', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('uid', name='website_user_pkey') + ) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/ae4432f8b5b7_initial_migration.py b/backend/alembic/versions/ae4432f8b5b7_initial_migration.py new file mode 100644 index 00000000..dd1ab367 --- /dev/null +++ b/backend/alembic/versions/ae4432f8b5b7_initial_migration.py @@ -0,0 +1,252 @@ +"""Initial migration + +Revision ID: ae4432f8b5b7 +Revises: +Create Date: 2024-03-11 16:00:23.837847 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'ae4432f8b5b7' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('student_subject', cascade='CASCADE') + op.drop_table('studentvak', cascade='CASCADE') + op.drop_table('team', cascade='CASCADE') + op.drop_table('groep', cascade='CASCADE') + op.drop_table('indiening', cascade='CASCADE') + op.drop_table('lesgevervak', cascade='CASCADE') + op.drop_table('teacher_subject', cascade='CASCADE') + op.drop_table('student_group', cascade='CASCADE') + op.drop_table('website_user', cascade='CASCADE') + op.drop_table('bestand', cascade='CASCADE') + op.drop_table('project', cascade='CASCADE') + op.drop_table('submission', cascade='CASCADE') + op.drop_table('status', cascade='CASCADE') + op.drop_table('studentgroep', cascade='CASCADE') + op.drop_table('file', cascade='CASCADE') + op.drop_table('subject', cascade='CASCADE') + op.drop_table('vak', cascade='CASCADE') + op.drop_table('website_user', cascade='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('website_user', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=False), + sa.Column('given_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('mail', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('uid', name='website_user_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('vak', + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('naam', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('vak_id', name='vak_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('subject', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='subject_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('file', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('submission_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='file_project_id_fkey', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['submission_id'], ['submission.id'], + name='file_submission_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='file_pkey') + ) + op.create_table('studentgroep', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('groep_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='studentgroep_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['groep_id'], ['groep.groep_id'], + name='studentgroep_groep_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'groep_id', name='studentgroep_pkey') + ) + op.create_table('status', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('status_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('status_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='status_pkey'), + sa.UniqueConstraint('status_name', name='status_status_name_key'), + postgresql_ignore_search_path=False + ) + op.create_table('submission', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='submission_project_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['status_id'], ['status.id'], + name='submission_status_id_fkey', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='submission_team_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='submission_pkey') + ) + op.create_table('project', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('project_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('deadline', sa.DATE(), + autoincrement=False, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), server_default=sa.text( + "nextval('project_subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('description', sa.TEXT(), + autoincrement=False, nullable=True), + sa.Column('max_team_size', sa.INTEGER(), server_default=sa.text( + '4'), autoincrement=False, nullable=False), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='project_subject_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='project_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('bestand', + sa.Column('bestand_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('indiening_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['indiening_id'], [ + 'indiening.indiening_id'], name='bestand_indiening_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('bestand_id', name='bestand_pkey') + ) + op.create_table('websiteuser', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('azureobjectid', name='websiteuser_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('student_group', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='student_group_team_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_group_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'team_id', name='student_group_pkey') + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='teacher_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='teacher_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='teacher_subject_pkey') + ) + op.create_table('lesgevervak', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='lesgevervak_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['vak_id'], ['vak.vak_id'], + name='lesgevervak_vak_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'vak_id', name='lesgevervak_pkey') + ) + op.create_table('indiening', + sa.Column('indiening_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('datum', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('groep_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['groep_id'], ['groep.groep_id'], + name='indiening_groep_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('indiening_id', name='indiening_pkey') + ) + op.create_table('groep', + sa.Column('groep_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('groep', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.PrimaryKeyConstraint('groep_id', name='groep_pkey') + ) + op.create_table('team', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('team_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='team_project_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='team_pkey') + ) + op.create_table('studentvak', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='studentvak_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['vak_id'], ['vak.vak_id'], + name='studentvak_vak_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'vak_id', name='studentvak_pkey') + ) + op.create_table('student_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='student_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='student_subject_pkey') + ) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/eb472f05f70e_recreate_tables.py b/backend/alembic/versions/eb472f05f70e_recreate_tables.py new file mode 100644 index 00000000..98cf2a2c --- /dev/null +++ b/backend/alembic/versions/eb472f05f70e_recreate_tables.py @@ -0,0 +1,68 @@ +"""recreate tables + +Revision ID: eb472f05f70e +Revises: 3756e9987aa1 +Create Date: 2024-03-11 18:51:19.501228 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'eb472f05f70e' +down_revision: Union[str, None] = '3756e9987aa1' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('subject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('website_user', + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) + op.create_table('project', + sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False, check_constraint='deadline >= CURRENT_DATE'), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subjectId', sa.String(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['subjectId'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_project_id'), 'project', ['id'], unique=False) + op.create_table('student_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('teacher_subject') + op.drop_table('student_subject') + op.drop_index(op.f('ix_project_id'), table_name='project') + op.drop_table('project') + op.drop_table('website_user') + op.drop_table('subject') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py new file mode 100644 index 00000000..fbf2e9fe --- /dev/null +++ b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py @@ -0,0 +1,67 @@ +"""Correct project subjectId type + +Revision ID: fc317e930257 +Revises: eb472f05f70e +Create Date: 2024-03-11 19:07:21.468978 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'fc317e930257' +down_revision: Union[str, None] = 'eb472f05f70e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('subject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('website_user', + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) + op.create_table('project', + sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subject_id', sa.BigInteger(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('student_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('teacher_subject') + op.drop_table('student_subject') + op.drop_table('project') + op.drop_table('website_user') + op.drop_table('subject') + # ### end Alembic commands ### diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 240de490..f4a6d18a 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -1,17 +1,21 @@ -from sqlalchemy import Column, BigInteger, String, Date, ForeignKey -from sqlalchemy.orm import relationship +from sqlalchemy import Column, BigInteger, String, Date, ForeignKey, CheckConstraint +from sqlalchemy.orm import relationship, Mapped from src.database import Base class Project(Base): __tablename__ = 'project' - id = Column(BigInteger, primary_key=True, autoincrement=True, index=True) - deadline = Column(Date, nullable=False, check_constraint='deadline >= CURRENT_DATE') + id = Column(BigInteger, primary_key=True, autoincrement=True) + deadline = Column(Date, nullable=False) name = Column(String, nullable=False) - subjectId = Column(String, ForeignKey( - 'Subject.subjectId', ondelete="SET NULL"), nullable=True) + subject_id = Column(BigInteger, ForeignKey('subject.id', ondelete="SET NULL"), nullable=True) # Adjusted to BigInteger description = Column(String, nullable=True) + enroll_deadline = Column(Date, nullable=True) # Relationships - teams = relationship("Team", back_populates="project") + subject = relationship("Subject") + + __table_args__ = ( + CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), + ) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 0b36a549..22df0313 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -21,7 +21,6 @@ async def list_projects_for_subject( user: User = Depends(get_authenticated_user), db: AsyncSession = Depends(get_db) ): - # Optional: You may want to check if the user has access to the subject (e.g., is a teacher or a student of the subject) projects = await get_projects_for_subject(db, subject_id) if not projects: raise NoProjectsFoundException(subject_id) diff --git a/backend/src/subject/models.py b/backend/src/subject/models.py index 1b864e13..ad7371db 100644 --- a/backend/src/subject/models.py +++ b/backend/src/subject/models.py @@ -5,14 +5,14 @@ StudentSubject = Table( "student_subject", Base.metadata, - Column("uid", ForeignKey("user.uid")), + Column("uid", ForeignKey("website_user.uid")), Column("subject_id", ForeignKey("subject.id")), ) TeacherSubject = Table( "teacher_subject", Base.metadata, - Column("uid", ForeignKey("user.uid")), + Column("uid", ForeignKey("website_user.uid")), Column("subject_id", ForeignKey("subject.id")), ) From 2a1ff7c1797871b70924ead9f3aa47bb4601720e Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Mon, 11 Mar 2024 19:12:16 +0100 Subject: [PATCH 09/29] alembic added --- .../versions/3756e9987aa1_recreate_tables.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 backend/alembic/versions/3756e9987aa1_recreate_tables.py diff --git a/backend/alembic/versions/3756e9987aa1_recreate_tables.py b/backend/alembic/versions/3756e9987aa1_recreate_tables.py new file mode 100644 index 00000000..7353acde --- /dev/null +++ b/backend/alembic/versions/3756e9987aa1_recreate_tables.py @@ -0,0 +1,30 @@ +"""Recreate tables + +Revision ID: 3756e9987aa1 +Revises: 2cc559f8470c +Create Date: 2024-03-11 16:40:14.053191 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '3756e9987aa1' +down_revision: Union[str, None] = '2cc559f8470c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### From ecbfa62eab404e32fa1ec06f29940357d9b7c646 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Mon, 11 Mar 2024 19:14:12 +0100 Subject: [PATCH 10/29] autopep8 --- .../versions/eb472f05f70e_recreate_tables.py | 59 +++++++++--------- ...7e930257_correct_project_subjectid_type.py | 61 ++++++++++--------- backend/src/main.py | 1 + backend/src/project/models.py | 3 +- 4 files changed, 66 insertions(+), 58 deletions(-) diff --git a/backend/alembic/versions/eb472f05f70e_recreate_tables.py b/backend/alembic/versions/eb472f05f70e_recreate_tables.py index 98cf2a2c..eee42a68 100644 --- a/backend/alembic/versions/eb472f05f70e_recreate_tables.py +++ b/backend/alembic/versions/eb472f05f70e_recreate_tables.py @@ -21,39 +21,42 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('subject', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) op.create_table('website_user', - sa.Column('uid', sa.String(), nullable=False), - sa.Column('given_name', sa.String(), nullable=False), - sa.Column('mail', sa.String(), nullable=False), - sa.Column('is_admin', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('uid') - ) + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) op.create_table('project', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('deadline', sa.Date(), nullable=False, check_constraint='deadline >= CURRENT_DATE'), - sa.Column('name', sa.String(), nullable=False), - sa.Column('subjectId', sa.String(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['subjectId'], ['subject.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('id', sa.BigInteger(), + autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False, + check_constraint='deadline >= CURRENT_DATE'), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subjectId', sa.String(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ['subjectId'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) op.create_index(op.f('ix_project_id'), 'project', ['id'], unique=False) op.create_table('student_subject', - sa.Column('uid', sa.String(), nullable=True), - sa.Column('subject_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), - sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) - ) + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) op.create_table('teacher_subject', - sa.Column('uid', sa.String(), nullable=True), - sa.Column('subject_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), - sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) - ) + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) # ### end Alembic commands ### diff --git a/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py index fbf2e9fe..367b0dee 100644 --- a/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py +++ b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py @@ -21,39 +21,42 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('subject', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) op.create_table('website_user', - sa.Column('uid', sa.String(), nullable=False), - sa.Column('given_name', sa.String(), nullable=False), - sa.Column('mail', sa.String(), nullable=False), - sa.Column('is_admin', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('uid') - ) + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) op.create_table('project', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('deadline', sa.Date(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('subject_id', sa.BigInteger(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('id', sa.BigInteger(), + autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subject_id', sa.BigInteger(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint( + ['subject_id'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) op.create_table('student_subject', - sa.Column('uid', sa.String(), nullable=True), - sa.Column('subject_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), - sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) - ) + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) op.create_table('teacher_subject', - sa.Column('uid', sa.String(), nullable=True), - sa.Column('subject_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), - sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) - ) + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) # ### end Alembic commands ### diff --git a/backend/src/main.py b/backend/src/main.py index 7de4b462..c2194d6c 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -25,6 +25,7 @@ app.include_router(project_router) app.include_router(auth_router) + @app.get("/api") async def root(): return {"message": "Hello World"} diff --git a/backend/src/project/models.py b/backend/src/project/models.py index f4a6d18a..ad4906a4 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -9,7 +9,8 @@ class Project(Base): id = Column(BigInteger, primary_key=True, autoincrement=True) deadline = Column(Date, nullable=False) name = Column(String, nullable=False) - subject_id = Column(BigInteger, ForeignKey('subject.id', ondelete="SET NULL"), nullable=True) # Adjusted to BigInteger + subject_id = Column(BigInteger, ForeignKey( + 'subject.id', ondelete="SET NULL"), nullable=True) # Adjusted to BigInteger description = Column(String, nullable=True) enroll_deadline = Column(Date, nullable=True) From 6ae0215e6f51de6d7f7abc27ef6fb2664acfda04 Mon Sep 17 00:00:00 2001 From: driesaster Date: Mon, 4 Mar 2024 15:45:53 +0100 Subject: [PATCH 11/29] adds project schemes,routes,services into backend (not complete) --- backend/src/project/dependencies.py | 34 +++++++++++++++++++++++++++++ backend/src/project/exceptions.py | 0 backend/src/project/models.py | 17 +++++++++++++++ backend/src/project/router.py | 33 ++++++++++++++++++++++++++++ backend/src/project/schemas.py | 26 ++++++++++++++++++++++ backend/src/project/service.py | 14 ++++++++++++ backend/src/subject/service.py | 6 ++++- 7 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 backend/src/project/dependencies.py create mode 100644 backend/src/project/exceptions.py create mode 100644 backend/src/project/models.py create mode 100644 backend/src/project/router.py create mode 100644 backend/src/project/schemas.py create mode 100644 backend/src/project/service.py diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py new file mode 100644 index 00000000..18df27db --- /dev/null +++ b/backend/src/project/dependencies.py @@ -0,0 +1,34 @@ +from fastapi import Depends +from sqlalchemy.orm import Session +from src.dependencies import get_db +from src.user.dependencies import get_authenticated_user +from src.user.exceptions import NotAuthorized +from src.user.schemas import User +from src.subject.schemas import Subject +from src.subject.schemas import SubjectList + + +from . import service +# from .exceptions import SubjectNotFound +# from .schemas import Subject, SubjectList + + + + + +async def retrieve_subjects( + user: User = Depends(get_authenticated_user), db: Session = Depends(get_db) +) -> SubjectList: + teacher_subjects, student_subjects = await service.get_subjects(db, user.uid) + return SubjectList(as_teacher=teacher_subjects, as_student=student_subjects) + + +async def user_permission_validation( + subject_id: int, + user: User = Depends(get_authenticated_user), + db: Session = Depends(get_db), +): + if not user.is_admin: + teachers = await service.get_teachers(db, subject_id) + if not list(filter(lambda teacher: teacher.id == user.uid, teachers)): + raise NotAuthorized() diff --git a/backend/src/project/exceptions.py b/backend/src/project/exceptions.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/src/project/models.py b/backend/src/project/models.py new file mode 100644 index 00000000..1db2ace3 --- /dev/null +++ b/backend/src/project/models.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, BigInteger, String, Date, ForeignKey +from sqlalchemy.orm import relationship +from src.database import Base + + +class Project(Base): + __tablename__ = 'Project' + + id = Column(BigInteger, primary_key=True, autoincrement=True, index=True) + deadline = Column(Date, nullable=False, check_constraint='deadline >= CURRENT_DATE') + name = Column(String, nullable=False) + subjectId = Column(String, ForeignKey('Subject.subjectId', ondelete="SET NULL"), nullable=True) + description = Column(String, nullable=True) + + # Relationships + teams = relationship("Team", back_populates="project") + diff --git a/backend/src/project/router.py b/backend/src/project/router.py new file mode 100644 index 00000000..87d5e942 --- /dev/null +++ b/backend/src/project/router.py @@ -0,0 +1,33 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from src.user.schemas import User +from src.user.dependencies import get_authenticated_user, get_db +from .schemas import ProjectCreate, ProjectResponse +from .service import create_project +from ..subject.service import is_teacher_of_subject + +router = APIRouter( + prefix="/subject/{subject_id}", + tags=["subject_overview"], + responses={404: {"description": "Not found"}}, +) + +@router.post("/subjects/{subject_id}/projects/create") +async def create_project( + subject_id: int, + project_in: ProjectCreate, + user: User = Depends(get_authenticated_user), + db: Session = Depends(get_db) +): + # Check if the user is a teacher of the subject + if not await is_teacher_of_subject(db, user.uid, subject_id): + raise HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") + + project = create_project(db=db, project_in=project_in, user_id=user.id) + return project + +@router.get("/projects/{project_id}", response_model=ProjectResponse) +async def get_project(project_id: int, db: Session = Depends(get_db)): + project = db.get(models.Project, project_id) + return project + diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py new file mode 100644 index 00000000..cc8c9b7d --- /dev/null +++ b/backend/src/project/schemas.py @@ -0,0 +1,26 @@ +from datetime import date +from pydantic import BaseModel, Field, validator + +class ProjectCreate(BaseModel): + name: str = Field(..., min_length=1) + deadline: date + subject_id: int + description: str + + # Check if deadline is not in the past + @validator('deadline', pre=True, always=True) + def validate_deadline(cls, value): + if value < date.today(): + raise ValueError('The deadline cannot be in the past') + return value + +class ProjectResponse(BaseModel): + id: int + name: str + deadline: date + subject_id: int + description: str + + class Config: + orm_mode = True + diff --git a/backend/src/project/service.py b/backend/src/project/service.py new file mode 100644 index 00000000..ca888768 --- /dev/null +++ b/backend/src/project/service.py @@ -0,0 +1,14 @@ +from sqlalchemy.orm import Session +from . import models, schemas + +async def create_project(db: Session, project_in: ProjectCreate, user_id: str) -> Project: + new_project = Project( + name=project_in.name, + deadline=project_in.deadline, + subject_id=project_in.subject_id + description=project_in.description + ) + db.add(new_project) + db.commit() + db.refresh(new_project) + return new_project diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index 421c0392..1c2bec5e 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -31,7 +31,6 @@ async def create_subject(db: Session, subject: schemas.SubjectCreate) -> models. db.refresh(db_subject) return db_subject - async def create_subject_teacher(db: Session, subject_id: int, user_id: str): insert_stmnt = models.TeacherSubject.insert().values( subject_id=subject_id, uid=user_id) @@ -49,3 +48,8 @@ async def delete_subject(db: Session, subject_id: int): """Remove a subject""" db.query(models.Subject).filter_by(id=subject_id).delete() db.commit() + +async def is_teacher_of_subject(db: Session, user_id: str, subject_id: int) -> bool: + """Check if a user is a teacher of the subject.""" + teachers = await get_subject_teachers(db, subject_id) + return any(teacher.uid == user_id for teacher in teachers) From edf2606eabf6f07a14f15be86abd3791bca88770 Mon Sep 17 00:00:00 2001 From: driesaster Date: Mon, 4 Mar 2024 16:34:58 +0100 Subject: [PATCH 12/29] router + services, added create delete, get not yet update --- backend/src/project/router.py | 56 +++++++++++++++++++++++++--------- backend/src/project/service.py | 26 +++++++++++++++- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 87d5e942..b0883a3f 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -1,33 +1,59 @@ from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session -from src.user.schemas import User -from src.user.dependencies import get_authenticated_user, get_db +from sqlalchemy.ext.asyncio import AsyncSession +from src.user.models import User +from src.dependencies import get_db +from src.user.dependencies import get_authenticated_user from .schemas import ProjectCreate, ProjectResponse -from .service import create_project +from .service import create_project, get_project, delete_project, update_project from ..subject.service import is_teacher_of_subject router = APIRouter( - prefix="/subject/{subject_id}", - tags=["subject_overview"], + prefix="/subjects/{subject_id}/projects", + tags=["projects"], responses={404: {"description": "Not found"}}, ) -@router.post("/subjects/{subject_id}/projects/create") -async def create_project( + +@router.post("/", response_model=ProjectResponse) +async def create_project_for_subject( subject_id: int, project_in: ProjectCreate, user: User = Depends(get_authenticated_user), - db: Session = Depends(get_db) + db: AsyncSession = Depends(get_db) ): - # Check if the user is a teacher of the subject - if not await is_teacher_of_subject(db, user.uid, subject_id): + if not await is_teacher_of_subject(db, user.id, subject_id): raise HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") - project = create_project(db=db, project_in=project_in, user_id=user.id) + project = await create_project(db=db, project_in=project_in, user_id=user.id) return project -@router.get("/projects/{project_id}", response_model=ProjectResponse) -async def get_project(project_id: int, db: Session = Depends(get_db)): - project = db.get(models.Project, project_id) + +@router.get("/{project_id}", response_model=ProjectResponse) +async def get_project_for_subject( + project_id: int, + db: AsyncSession = Depends(get_db) +): + project = await get_project(db, project_id) + if not project: + raise HTTPException(status_code=404, detail="Project not found") return project + +@router.delete("/{project_id}") +async def delete_project_for_subject( + subject_id: int, + project_id: int, + user: User = Depends(get_authenticated_user), + db: AsyncSession = Depends(get_db) +): + if not await is_teacher_of_subject(db, user.id, subject_id): + raise HTTPException(status_code=403, detail="User is not authorized to delete this project") + + await delete_project(db, project_id) + return {"message": "Project deleted successfully"} + + + + + + diff --git a/backend/src/project/service.py b/backend/src/project/service.py index ca888768..6a4bef1c 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -1,14 +1,38 @@ from sqlalchemy.orm import Session from . import models, schemas +from .models import Project +from .schemas import ProjectCreate + async def create_project(db: Session, project_in: ProjectCreate, user_id: str) -> Project: new_project = Project( name=project_in.name, deadline=project_in.deadline, - subject_id=project_in.subject_id + subject_id=project_in.subject_id, description=project_in.description ) db.add(new_project) db.commit() db.refresh(new_project) return new_project + +async def get_project(db: Session, project_id: int) -> Project: + return db.query(Project).filter(Project.id == project_id).first() + +async def delete_project(db: Session, project_id: int): + project = db.query(Project).filter(Project.id == project_id).first() + if project: + db.delete(project) + db.commit() + +async def update_project(db: Session, project_id: int, project_update: ProjectCreate) -> Project: + project = db.query(Project).filter(Project.id == project_id).first() + if project: + project.name = project_update.name + project.deadline = project_update.deadline + project.description = project_update.description + db.commit() + db.refresh(project) + return project + + From 278024e5743dbef57052241c8d35d7dc21b333dd Mon Sep 17 00:00:00 2001 From: driesaster Date: Mon, 4 Mar 2024 20:23:41 +0100 Subject: [PATCH 13/29] cleanup + all projects for certain subject overview added --- backend/src/project/router.py | 11 +++++++++++ backend/src/project/service.py | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index b0883a3f..550934ff 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -13,6 +13,17 @@ responses={404: {"description": "Not found"}}, ) +@router.get("/", response_model=list[ProjectResponse]) +async def list_projects_for_subject( + subject_id: int, + user: User = Depends(get_authenticated_user), + db: AsyncSession = Depends(get_db) +): + # Optional: You may want to check if the user has access to the subject (e.g., is a teacher or a student of the subject) + projects = await get_projects_for_subject(db, subject_id) + if not projects: + raise HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") + return projects @router.post("/", response_model=ProjectResponse) async def create_project_for_subject( diff --git a/backend/src/project/service.py b/backend/src/project/service.py index 6a4bef1c..33ed32a2 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -19,6 +19,14 @@ async def create_project(db: Session, project_in: ProjectCreate, user_id: str) - async def get_project(db: Session, project_id: int) -> Project: return db.query(Project).filter(Project.id == project_id).first() +async def get_projects_for_subject(db: Session, subject_id: int) -> list[models.Project]: + projects = ( + db.query(models.Project) + .filter(models.Project.subject_id == subject_id) + .all() + ) + return projects + async def delete_project(db: Session, project_id: int): project = db.query(Project).filter(Project.id == project_id).first() if project: From 56d35dbba526365637be3f93eba1f7fb4c511a70 Mon Sep 17 00:00:00 2001 From: driesaster Date: Mon, 4 Mar 2024 20:31:26 +0100 Subject: [PATCH 14/29] project update --- backend/src/project/router.py | 20 +++++++++++++++++++- backend/src/project/schemas.py | 7 +++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 550934ff..73dd7cce 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -3,7 +3,7 @@ from src.user.models import User from src.dependencies import get_db from src.user.dependencies import get_authenticated_user -from .schemas import ProjectCreate, ProjectResponse +from .schemas import ProjectCreate, ProjectResponse, ProjectUpdate from .service import create_project, get_project, delete_project, update_project from ..subject.service import is_teacher_of_subject @@ -64,6 +64,24 @@ async def delete_project_for_subject( return {"message": "Project deleted successfully"} +@router.patch("/{project_id}", response_model=ProjectResponse) +async def patch_project_for_subject( + subject_id: int, + project_id: int, + project_update: ProjectUpdate, + user: User = Depends(get_authenticated_user), + db: AsyncSession = Depends(get_db) +): + # Check if the user is authorized to update the project + if not await is_teacher_of_subject(db, user.id, subject_id): + raise HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") + + updated_project = await update_project(db, project_id, project_update) + if not updated_project: + raise HTTPException(status_code=404, detail="Project not found") + return updated_project + + diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py index cc8c9b7d..be5bc5ea 100644 --- a/backend/src/project/schemas.py +++ b/backend/src/project/schemas.py @@ -1,4 +1,6 @@ from datetime import date +from typing import Optional + from pydantic import BaseModel, Field, validator class ProjectCreate(BaseModel): @@ -24,3 +26,8 @@ class ProjectResponse(BaseModel): class Config: orm_mode = True +class ProjectUpdate(BaseModel): + name: Optional[str] = None + deadline: Optional[date] = None + description: Optional[str] = None + From c7df7b98aeee103d05d064461b7be07657d158a3 Mon Sep 17 00:00:00 2001 From: driesaster Date: Tue, 5 Mar 2024 19:41:29 +0100 Subject: [PATCH 15/29] exceptions split up, cleanup --- backend/src/project/dependencies.py | 2 -- backend/src/project/exceptions.py | 18 ++++++++++++++++++ backend/src/project/models.py | 2 +- backend/src/project/router.py | 15 ++++++++------- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py index 18df27db..0f0712d9 100644 --- a/backend/src/project/dependencies.py +++ b/backend/src/project/dependencies.py @@ -9,8 +9,6 @@ from . import service -# from .exceptions import SubjectNotFound -# from .schemas import Subject, SubjectList diff --git a/backend/src/project/exceptions.py b/backend/src/project/exceptions.py index e69de29b..6c0d5358 100644 --- a/backend/src/project/exceptions.py +++ b/backend/src/project/exceptions.py @@ -0,0 +1,18 @@ +# exceptions.py + +from fastapi import HTTPException + +def NoProjectsFoundException(subject_id: int): + return HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") + +def UnauthorizedToCreateProjectException(): + return HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") + +def ProjectNotFoundException(): + return HTTPException(status_code=404, detail="Project not found") + +def UnauthorizedToDeleteProjectException(): + return HTTPException(status_code=403, detail="User is not authorized to delete this project") + +def UnauthorizedToUpdateProjectException(): + return HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 1db2ace3..9b53b0bd 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -4,7 +4,7 @@ class Project(Base): - __tablename__ = 'Project' + __tablename__ = 'project' id = Column(BigInteger, primary_key=True, autoincrement=True, index=True) deadline = Column(Date, nullable=False, check_constraint='deadline >= CURRENT_DATE') diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 73dd7cce..5e61b518 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -4,8 +4,9 @@ from src.dependencies import get_db from src.user.dependencies import get_authenticated_user from .schemas import ProjectCreate, ProjectResponse, ProjectUpdate -from .service import create_project, get_project, delete_project, update_project +from .service import create_project, get_project, delete_project, update_project, get_projects_for_subject from ..subject.service import is_teacher_of_subject +from . import exceptions router = APIRouter( prefix="/subjects/{subject_id}/projects", @@ -22,7 +23,7 @@ async def list_projects_for_subject( # Optional: You may want to check if the user has access to the subject (e.g., is a teacher or a student of the subject) projects = await get_projects_for_subject(db, subject_id) if not projects: - raise HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") + raise NoProjectsFoundException(subject_id) return projects @router.post("/", response_model=ProjectResponse) @@ -33,7 +34,7 @@ async def create_project_for_subject( db: AsyncSession = Depends(get_db) ): if not await is_teacher_of_subject(db, user.id, subject_id): - raise HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") + raise UnauthorizedToCreateProjectException() project = await create_project(db=db, project_in=project_in, user_id=user.id) return project @@ -46,7 +47,7 @@ async def get_project_for_subject( ): project = await get_project(db, project_id) if not project: - raise HTTPException(status_code=404, detail="Project not found") + raise ProjectNotFoundException() return project @@ -58,7 +59,7 @@ async def delete_project_for_subject( db: AsyncSession = Depends(get_db) ): if not await is_teacher_of_subject(db, user.id, subject_id): - raise HTTPException(status_code=403, detail="User is not authorized to delete this project") + raise UnauthorizedToUpdateProjectException() await delete_project(db, project_id) return {"message": "Project deleted successfully"} @@ -74,11 +75,11 @@ async def patch_project_for_subject( ): # Check if the user is authorized to update the project if not await is_teacher_of_subject(db, user.id, subject_id): - raise HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") + raise UnauthorizedToUpdateProjectException() updated_project = await update_project(db, project_id, project_update) if not updated_project: - raise HTTPException(status_code=404, detail="Project not found") + raise ProjectNotFoundException() return updated_project From 96ce5cc5e46e846c305fc4bbc380371e346b82a7 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Tue, 5 Mar 2024 20:57:44 +0100 Subject: [PATCH 16/29] actual patches/fixes now that im running on linux --- backend/src/main.py | 2 ++ backend/src/project/dependencies.py | 3 --- backend/src/project/exceptions.py | 5 +++++ backend/src/project/models.py | 4 ++-- backend/src/project/router.py | 8 ++------ backend/src/project/schemas.py | 4 +++- backend/src/project/service.py | 6 ++++-- 7 files changed, 18 insertions(+), 14 deletions(-) diff --git a/backend/src/main.py b/backend/src/main.py index 18010993..45972696 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -5,6 +5,7 @@ from src.auth.router import router as auth_router from fastapi.middleware.cors import CORSMiddleware from src import config +from src.project.router import router as project_router app = FastAPI() @@ -22,6 +23,7 @@ app.include_router(subject_router) app.include_router(user_router) app.include_router(auth_router) +app.include_router(project_router) @app.get("/api") diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py index 0f0712d9..1a605b30 100644 --- a/backend/src/project/dependencies.py +++ b/backend/src/project/dependencies.py @@ -11,9 +11,6 @@ from . import service - - - async def retrieve_subjects( user: User = Depends(get_authenticated_user), db: Session = Depends(get_db) ) -> SubjectList: diff --git a/backend/src/project/exceptions.py b/backend/src/project/exceptions.py index 6c0d5358..6ecc8164 100644 --- a/backend/src/project/exceptions.py +++ b/backend/src/project/exceptions.py @@ -2,17 +2,22 @@ from fastapi import HTTPException + def NoProjectsFoundException(subject_id: int): return HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") + def UnauthorizedToCreateProjectException(): return HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") + def ProjectNotFoundException(): return HTTPException(status_code=404, detail="Project not found") + def UnauthorizedToDeleteProjectException(): return HTTPException(status_code=403, detail="User is not authorized to delete this project") + def UnauthorizedToUpdateProjectException(): return HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 9b53b0bd..240de490 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -9,9 +9,9 @@ class Project(Base): id = Column(BigInteger, primary_key=True, autoincrement=True, index=True) deadline = Column(Date, nullable=False, check_constraint='deadline >= CURRENT_DATE') name = Column(String, nullable=False) - subjectId = Column(String, ForeignKey('Subject.subjectId', ondelete="SET NULL"), nullable=True) + subjectId = Column(String, ForeignKey( + 'Subject.subjectId', ondelete="SET NULL"), nullable=True) description = Column(String, nullable=True) # Relationships teams = relationship("Team", back_populates="project") - diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 5e61b518..4077f573 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -14,6 +14,7 @@ responses={404: {"description": "Not found"}}, ) + @router.get("/", response_model=list[ProjectResponse]) async def list_projects_for_subject( subject_id: int, @@ -26,6 +27,7 @@ async def list_projects_for_subject( raise NoProjectsFoundException(subject_id) return projects + @router.post("/", response_model=ProjectResponse) async def create_project_for_subject( subject_id: int, @@ -81,9 +83,3 @@ async def patch_project_for_subject( if not updated_project: raise ProjectNotFoundException() return updated_project - - - - - - diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py index be5bc5ea..a7ea8e0f 100644 --- a/backend/src/project/schemas.py +++ b/backend/src/project/schemas.py @@ -3,6 +3,7 @@ from pydantic import BaseModel, Field, validator + class ProjectCreate(BaseModel): name: str = Field(..., min_length=1) deadline: date @@ -16,6 +17,7 @@ def validate_deadline(cls, value): raise ValueError('The deadline cannot be in the past') return value + class ProjectResponse(BaseModel): id: int name: str @@ -26,8 +28,8 @@ class ProjectResponse(BaseModel): class Config: orm_mode = True + class ProjectUpdate(BaseModel): name: Optional[str] = None deadline: Optional[date] = None description: Optional[str] = None - diff --git a/backend/src/project/service.py b/backend/src/project/service.py index 33ed32a2..5fd6c0fd 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -16,9 +16,11 @@ async def create_project(db: Session, project_in: ProjectCreate, user_id: str) - db.refresh(new_project) return new_project + async def get_project(db: Session, project_id: int) -> Project: return db.query(Project).filter(Project.id == project_id).first() + async def get_projects_for_subject(db: Session, subject_id: int) -> list[models.Project]: projects = ( db.query(models.Project) @@ -27,12 +29,14 @@ async def get_projects_for_subject(db: Session, subject_id: int) -> list[models. ) return projects + async def delete_project(db: Session, project_id: int): project = db.query(Project).filter(Project.id == project_id).first() if project: db.delete(project) db.commit() + async def update_project(db: Session, project_id: int, project_update: ProjectCreate) -> Project: project = db.query(Project).filter(Project.id == project_id).first() if project: @@ -42,5 +46,3 @@ async def update_project(db: Session, project_id: int, project_update: ProjectCr db.commit() db.refresh(project) return project - - From 6a3373ee94f183eba3799e7a25921ee08ff07c7e Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Sun, 10 Mar 2024 16:39:09 +0100 Subject: [PATCH 17/29] api voor router gezet --- backend/src/project/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 4077f573..0b36a549 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -9,7 +9,7 @@ from . import exceptions router = APIRouter( - prefix="/subjects/{subject_id}/projects", + prefix="/api/subjects/{subject_id}/projects", tags=["projects"], responses={404: {"description": "Not found"}}, ) From c5bd3b5c18ece6cb83ab262413097531180b5372 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Mon, 11 Mar 2024 19:11:49 +0100 Subject: [PATCH 18/29] alembic added --- backend/alembic.ini | 115 ++++++++ backend/alembic/README | 1 + backend/alembic/env.py | 97 +++++++ backend/alembic/script.py.mako | 26 ++ .../29a17fa183de_enroll_deadline_added.py | 30 +++ .../2cc559f8470c_allign_with_new_schema.py | 159 +++++++++++ .../ae4432f8b5b7_initial_migration.py | 252 ++++++++++++++++++ .../versions/eb472f05f70e_recreate_tables.py | 68 +++++ ...7e930257_correct_project_subjectid_type.py | 67 +++++ backend/src/project/models.py | 18 +- backend/src/project/router.py | 1 - backend/src/subject/models.py | 4 +- 12 files changed, 828 insertions(+), 10 deletions(-) create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/README create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/29a17fa183de_enroll_deadline_added.py create mode 100644 backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py create mode 100644 backend/alembic/versions/ae4432f8b5b7_initial_migration.py create mode 100644 backend/alembic/versions/eb472f05f70e_recreate_tables.py create mode 100644 backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 00000000..d40db921 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,115 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql://selab:2UJd24Z6aUm85ZExEri@sel2-5.ugent.be:2002/selab + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/README b/backend/alembic/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 00000000..de4ae87d --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,97 @@ +from src.user.models import Base as UserBase +from src.subject.models import Base as SubjectBase +from src.project.models import Base as ProjectBase +import os +import sys +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from sqlalchemy import MetaData + +from alembic import context +from src.database import Base + +# Calculate the path based on the location of the env.py file +d = os.path.dirname +parent_dir = d(d(os.path.abspath(__file__))) +sys.path.append(parent_dir) + +# Import the Base from each of your model submodules + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +combined_metadata = MetaData() +for base in [ProjectBase, SubjectBase, UserBase]: + for table in base.metadata.tables.values(): + combined_metadata._add_table(table.name, table.schema, table) + +target_metadata = combined_metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 00000000..fbc4b07d --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py b/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py new file mode 100644 index 00000000..048c10f8 --- /dev/null +++ b/backend/alembic/versions/29a17fa183de_enroll_deadline_added.py @@ -0,0 +1,30 @@ +"""enroll_deadline_added + +Revision ID: 29a17fa183de +Revises: fc317e930257 +Create Date: 2024-03-11 19:09:35.872572 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '29a17fa183de' +down_revision: Union[str, None] = 'fc317e930257' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('project', sa.Column('enroll_deadline', sa.Date(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('project', 'enroll_deadline') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py new file mode 100644 index 00000000..0e6c837f --- /dev/null +++ b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py @@ -0,0 +1,159 @@ +"""Allign with new schema + +Revision ID: 2cc559f8470c +Revises: ae4432f8b5b7 +Create Date: 2024-03-11 16:20:20.260148 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '2cc559f8470c' +down_revision: Union[str, None] = 'ae4432f8b5b7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Drop tables that are dependent on other tables + op.execute('DROP TABLE IF EXISTS student_subject CASCADE') + op.execute('DROP TABLE IF EXISTS teacher_subject CASCADE') + op.execute('DROP TABLE IF EXISTS student_group CASCADE') + op.execute('DROP TABLE IF EXISTS submission CASCADE') + op.execute('DROP TABLE IF EXISTS file CASCADE') + op.execute('DROP TABLE IF EXISTS website_user CASCADE') + op.execute('DROP TABLE IF EXISTS status CASCADE') + op.execute('DROP TABLE IF EXISTS team CASCADE') + op.execute('DROP TABLE IF EXISTS subject CASCADE') + op.execute('DROP TABLE IF EXISTS project CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('file', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('submission_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='file_project_id_fkey', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['submission_id'], ['submission.id'], + name='file_submission_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='file_pkey') + ) + op.create_table('project', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('project_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('deadline', sa.DATE(), + autoincrement=False, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), server_default=sa.text( + "nextval('project_subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('description', sa.TEXT(), + autoincrement=False, nullable=True), + sa.Column('max_team_size', sa.INTEGER(), server_default=sa.text( + '4'), autoincrement=False, nullable=False), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='project_subject_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='project_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='teacher_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='teacher_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='teacher_subject_pkey') + ) + op.create_table('student_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='student_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='student_subject_pkey') + ) + op.create_table('submission', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='submission_project_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['status_id'], ['status.id'], + name='submission_status_id_fkey', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='submission_team_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='submission_pkey') + ) + op.create_table('subject', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='subject_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('team', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('team_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('team_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='team_project_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='team_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('student_group', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='student_group_team_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_group_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'team_id', name='student_group_pkey') + ) + op.create_table('status', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('status_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='status_pkey'), + sa.UniqueConstraint('status_name', name='status_status_name_key') + ) + op.create_table('website_user', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=False), + sa.Column('given_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('mail', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('uid', name='website_user_pkey') + ) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/ae4432f8b5b7_initial_migration.py b/backend/alembic/versions/ae4432f8b5b7_initial_migration.py new file mode 100644 index 00000000..dd1ab367 --- /dev/null +++ b/backend/alembic/versions/ae4432f8b5b7_initial_migration.py @@ -0,0 +1,252 @@ +"""Initial migration + +Revision ID: ae4432f8b5b7 +Revises: +Create Date: 2024-03-11 16:00:23.837847 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'ae4432f8b5b7' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('student_subject', cascade='CASCADE') + op.drop_table('studentvak', cascade='CASCADE') + op.drop_table('team', cascade='CASCADE') + op.drop_table('groep', cascade='CASCADE') + op.drop_table('indiening', cascade='CASCADE') + op.drop_table('lesgevervak', cascade='CASCADE') + op.drop_table('teacher_subject', cascade='CASCADE') + op.drop_table('student_group', cascade='CASCADE') + op.drop_table('website_user', cascade='CASCADE') + op.drop_table('bestand', cascade='CASCADE') + op.drop_table('project', cascade='CASCADE') + op.drop_table('submission', cascade='CASCADE') + op.drop_table('status', cascade='CASCADE') + op.drop_table('studentgroep', cascade='CASCADE') + op.drop_table('file', cascade='CASCADE') + op.drop_table('subject', cascade='CASCADE') + op.drop_table('vak', cascade='CASCADE') + op.drop_table('website_user', cascade='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('website_user', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=False), + sa.Column('given_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('mail', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('uid', name='website_user_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('vak', + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('naam', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('vak_id', name='vak_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('subject', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='subject_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('file', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('submission_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='file_project_id_fkey', ondelete='SET NULL'), + sa.ForeignKeyConstraint(['submission_id'], ['submission.id'], + name='file_submission_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='file_pkey') + ) + op.create_table('studentgroep', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('groep_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='studentgroep_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['groep_id'], ['groep.groep_id'], + name='studentgroep_groep_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'groep_id', name='studentgroep_pkey') + ) + op.create_table('status', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('status_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('status_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint('id', name='status_pkey'), + sa.UniqueConstraint('status_name', name='status_status_name_key'), + postgresql_ignore_search_path=False + ) + op.create_table('submission', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='submission_project_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['status_id'], ['status.id'], + name='submission_status_id_fkey', ondelete='RESTRICT'), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='submission_team_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='submission_pkey') + ) + op.create_table('project', + sa.Column('id', sa.BIGINT(), server_default=sa.text( + "nextval('project_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('deadline', sa.DATE(), + autoincrement=False, nullable=False), + sa.Column('name', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), server_default=sa.text( + "nextval('project_subject_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('description', sa.TEXT(), + autoincrement=False, nullable=True), + sa.Column('max_team_size', sa.INTEGER(), server_default=sa.text( + '4'), autoincrement=False, nullable=False), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='project_subject_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id', name='project_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('bestand', + sa.Column('bestand_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('indiening_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['indiening_id'], [ + 'indiening.indiening_id'], name='bestand_indiening_id_fkey', ondelete='SET NULL'), + sa.PrimaryKeyConstraint('bestand_id', name='bestand_pkey') + ) + op.create_table('websiteuser', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text( + 'false'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('azureobjectid', name='websiteuser_pkey'), + postgresql_ignore_search_path=False + ) + op.create_table('student_group', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('team_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['team_id'], ['team.id'], + name='student_group_team_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_group_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'team_id', name='student_group_pkey') + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='teacher_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='teacher_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='teacher_subject_pkey') + ) + op.create_table('lesgevervak', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='lesgevervak_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['vak_id'], ['vak.vak_id'], + name='lesgevervak_vak_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'vak_id', name='lesgevervak_pkey') + ) + op.create_table('indiening', + sa.Column('indiening_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('datum', postgresql.TIMESTAMP(), server_default=sa.text( + 'CURRENT_TIMESTAMP'), autoincrement=False, nullable=False), + sa.Column('groep_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('status_id', sa.BIGINT(), server_default=sa.text( + '1'), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['groep_id'], ['groep.groep_id'], + name='indiening_groep_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('indiening_id', name='indiening_pkey') + ) + op.create_table('groep', + sa.Column('groep_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.Column('groep', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), autoincrement=False, nullable=True), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=True), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.PrimaryKeyConstraint('groep_id', name='groep_pkey') + ) + op.create_table('team', + sa.Column('id', sa.BIGINT(), autoincrement=True, nullable=False), + sa.Column('team_name', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('score', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.Column('project_id', sa.BIGINT(), + autoincrement=False, nullable=False), + sa.CheckConstraint('score >= 0 AND score <= 20', + name='score_check'), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], + name='team_project_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='team_pkey') + ) + op.create_table('studentvak', + sa.Column('azureobjectid', sa.TEXT(), + autoincrement=False, nullable=False), + sa.Column('vak_id', sa.TEXT(), autoincrement=False, nullable=False), + sa.ForeignKeyConstraint(['azureobjectid'], [ + 'websiteuser.azureobjectid'], name='studentvak_azureobjectid_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['vak_id'], ['vak.vak_id'], + name='studentvak_vak_id_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint( + 'azureobjectid', 'vak_id', name='studentvak_pkey') + ) + op.create_table('student_subject', + sa.Column('uid', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('subject_id', sa.BIGINT(), + autoincrement=True, nullable=False), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], + name='student_subject_subject_id_fkey', ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], + name='student_subject_uid_fkey', ondelete='CASCADE'), + sa.PrimaryKeyConstraint('uid', 'subject_id', + name='student_subject_pkey') + ) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/eb472f05f70e_recreate_tables.py b/backend/alembic/versions/eb472f05f70e_recreate_tables.py new file mode 100644 index 00000000..98cf2a2c --- /dev/null +++ b/backend/alembic/versions/eb472f05f70e_recreate_tables.py @@ -0,0 +1,68 @@ +"""recreate tables + +Revision ID: eb472f05f70e +Revises: 3756e9987aa1 +Create Date: 2024-03-11 18:51:19.501228 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'eb472f05f70e' +down_revision: Union[str, None] = '3756e9987aa1' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('subject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('website_user', + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) + op.create_table('project', + sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False, check_constraint='deadline >= CURRENT_DATE'), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subjectId', sa.String(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['subjectId'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_project_id'), 'project', ['id'], unique=False) + op.create_table('student_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('teacher_subject') + op.drop_table('student_subject') + op.drop_index(op.f('ix_project_id'), table_name='project') + op.drop_table('project') + op.drop_table('website_user') + op.drop_table('subject') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py new file mode 100644 index 00000000..fbf2e9fe --- /dev/null +++ b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py @@ -0,0 +1,67 @@ +"""Correct project subjectId type + +Revision ID: fc317e930257 +Revises: eb472f05f70e +Create Date: 2024-03-11 19:07:21.468978 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'fc317e930257' +down_revision: Union[str, None] = 'eb472f05f70e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('subject', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('website_user', + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) + op.create_table('project', + sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subject_id', sa.BigInteger(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('student_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + op.create_table('teacher_subject', + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('teacher_subject') + op.drop_table('student_subject') + op.drop_table('project') + op.drop_table('website_user') + op.drop_table('subject') + # ### end Alembic commands ### diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 240de490..f4a6d18a 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -1,17 +1,21 @@ -from sqlalchemy import Column, BigInteger, String, Date, ForeignKey -from sqlalchemy.orm import relationship +from sqlalchemy import Column, BigInteger, String, Date, ForeignKey, CheckConstraint +from sqlalchemy.orm import relationship, Mapped from src.database import Base class Project(Base): __tablename__ = 'project' - id = Column(BigInteger, primary_key=True, autoincrement=True, index=True) - deadline = Column(Date, nullable=False, check_constraint='deadline >= CURRENT_DATE') + id = Column(BigInteger, primary_key=True, autoincrement=True) + deadline = Column(Date, nullable=False) name = Column(String, nullable=False) - subjectId = Column(String, ForeignKey( - 'Subject.subjectId', ondelete="SET NULL"), nullable=True) + subject_id = Column(BigInteger, ForeignKey('subject.id', ondelete="SET NULL"), nullable=True) # Adjusted to BigInteger description = Column(String, nullable=True) + enroll_deadline = Column(Date, nullable=True) # Relationships - teams = relationship("Team", back_populates="project") + subject = relationship("Subject") + + __table_args__ = ( + CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), + ) diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 0b36a549..22df0313 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -21,7 +21,6 @@ async def list_projects_for_subject( user: User = Depends(get_authenticated_user), db: AsyncSession = Depends(get_db) ): - # Optional: You may want to check if the user has access to the subject (e.g., is a teacher or a student of the subject) projects = await get_projects_for_subject(db, subject_id) if not projects: raise NoProjectsFoundException(subject_id) diff --git a/backend/src/subject/models.py b/backend/src/subject/models.py index 1b864e13..ad7371db 100644 --- a/backend/src/subject/models.py +++ b/backend/src/subject/models.py @@ -5,14 +5,14 @@ StudentSubject = Table( "student_subject", Base.metadata, - Column("uid", ForeignKey("user.uid")), + Column("uid", ForeignKey("website_user.uid")), Column("subject_id", ForeignKey("subject.id")), ) TeacherSubject = Table( "teacher_subject", Base.metadata, - Column("uid", ForeignKey("user.uid")), + Column("uid", ForeignKey("website_user.uid")), Column("subject_id", ForeignKey("subject.id")), ) From 6cee923b0c51c5a32fca9a83cbab4a446a08eed6 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Mon, 11 Mar 2024 19:12:16 +0100 Subject: [PATCH 19/29] alembic added --- .../versions/3756e9987aa1_recreate_tables.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 backend/alembic/versions/3756e9987aa1_recreate_tables.py diff --git a/backend/alembic/versions/3756e9987aa1_recreate_tables.py b/backend/alembic/versions/3756e9987aa1_recreate_tables.py new file mode 100644 index 00000000..7353acde --- /dev/null +++ b/backend/alembic/versions/3756e9987aa1_recreate_tables.py @@ -0,0 +1,30 @@ +"""Recreate tables + +Revision ID: 3756e9987aa1 +Revises: 2cc559f8470c +Create Date: 2024-03-11 16:40:14.053191 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '3756e9987aa1' +down_revision: Union[str, None] = '2cc559f8470c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### From 7a37a92b0ab137c8a39989d8edb8f5000938d493 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Mon, 11 Mar 2024 19:14:12 +0100 Subject: [PATCH 20/29] autopep8 --- .../versions/eb472f05f70e_recreate_tables.py | 59 +++++++++--------- ...7e930257_correct_project_subjectid_type.py | 61 ++++++++++--------- backend/src/main.py | 1 + backend/src/project/models.py | 3 +- 4 files changed, 66 insertions(+), 58 deletions(-) diff --git a/backend/alembic/versions/eb472f05f70e_recreate_tables.py b/backend/alembic/versions/eb472f05f70e_recreate_tables.py index 98cf2a2c..eee42a68 100644 --- a/backend/alembic/versions/eb472f05f70e_recreate_tables.py +++ b/backend/alembic/versions/eb472f05f70e_recreate_tables.py @@ -21,39 +21,42 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('subject', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) op.create_table('website_user', - sa.Column('uid', sa.String(), nullable=False), - sa.Column('given_name', sa.String(), nullable=False), - sa.Column('mail', sa.String(), nullable=False), - sa.Column('is_admin', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('uid') - ) + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) op.create_table('project', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('deadline', sa.Date(), nullable=False, check_constraint='deadline >= CURRENT_DATE'), - sa.Column('name', sa.String(), nullable=False), - sa.Column('subjectId', sa.String(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['subjectId'], ['subject.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('id', sa.BigInteger(), + autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False, + check_constraint='deadline >= CURRENT_DATE'), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subjectId', sa.String(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.ForeignKeyConstraint( + ['subjectId'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) op.create_index(op.f('ix_project_id'), 'project', ['id'], unique=False) op.create_table('student_subject', - sa.Column('uid', sa.String(), nullable=True), - sa.Column('subject_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), - sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) - ) + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) op.create_table('teacher_subject', - sa.Column('uid', sa.String(), nullable=True), - sa.Column('subject_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), - sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) - ) + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) # ### end Alembic commands ### diff --git a/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py index fbf2e9fe..367b0dee 100644 --- a/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py +++ b/backend/alembic/versions/fc317e930257_correct_project_subjectid_type.py @@ -21,39 +21,42 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### op.create_table('subject', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) op.create_table('website_user', - sa.Column('uid', sa.String(), nullable=False), - sa.Column('given_name', sa.String(), nullable=False), - sa.Column('mail', sa.String(), nullable=False), - sa.Column('is_admin', sa.Boolean(), nullable=False), - sa.PrimaryKeyConstraint('uid') - ) + sa.Column('uid', sa.String(), nullable=False), + sa.Column('given_name', sa.String(), nullable=False), + sa.Column('mail', sa.String(), nullable=False), + sa.Column('is_admin', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('uid') + ) op.create_table('project', - sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column('deadline', sa.Date(), nullable=False), - sa.Column('name', sa.String(), nullable=False), - sa.Column('subject_id', sa.BigInteger(), nullable=True), - sa.Column('description', sa.String(), nullable=True), - sa.CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') - ) + sa.Column('id', sa.BigInteger(), + autoincrement=True, nullable=False), + sa.Column('deadline', sa.Date(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('subject_id', sa.BigInteger(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.CheckConstraint('deadline >= CURRENT_DATE', + name='deadline_check'), + sa.ForeignKeyConstraint( + ['subject_id'], ['subject.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) op.create_table('student_subject', - sa.Column('uid', sa.String(), nullable=True), - sa.Column('subject_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), - sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) - ) + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) op.create_table('teacher_subject', - sa.Column('uid', sa.String(), nullable=True), - sa.Column('subject_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), - sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) - ) + sa.Column('uid', sa.String(), nullable=True), + sa.Column('subject_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['subject_id'], ['subject.id'], ), + sa.ForeignKeyConstraint(['uid'], ['website_user.uid'], ) + ) # ### end Alembic commands ### diff --git a/backend/src/main.py b/backend/src/main.py index 45972696..20b870fa 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -26,6 +26,7 @@ app.include_router(project_router) + @app.get("/api") async def root(): return {"message": "Hello World"} diff --git a/backend/src/project/models.py b/backend/src/project/models.py index f4a6d18a..ad4906a4 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -9,7 +9,8 @@ class Project(Base): id = Column(BigInteger, primary_key=True, autoincrement=True) deadline = Column(Date, nullable=False) name = Column(String, nullable=False) - subject_id = Column(BigInteger, ForeignKey('subject.id', ondelete="SET NULL"), nullable=True) # Adjusted to BigInteger + subject_id = Column(BigInteger, ForeignKey( + 'subject.id', ondelete="SET NULL"), nullable=True) # Adjusted to BigInteger description = Column(String, nullable=True) enroll_deadline = Column(Date, nullable=True) From 91e701fdb46c8e5aed0e552c1f060f91d7e69e2c Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Mon, 11 Mar 2024 23:54:43 +0100 Subject: [PATCH 21/29] style --- backend/src/main.py | 1 - backend/src/subject/service.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main.py b/backend/src/main.py index 42bc777e..548a3ce3 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -28,7 +28,6 @@ app.include_router(project_router) - @app.get("/api") async def root(): return {"message": "Hello World"} diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index 103ce05e..d3b15680 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -30,6 +30,7 @@ async def create_subject(db: Session, subject: schemas.SubjectCreate) -> models. db.refresh(db_subject) return db_subject + async def create_subject_teacher(db: Session, subject_id: int, user_id: str): insert_stmnt = models.TeacherSubject.insert().values( subject_id=subject_id, uid=user_id) From 31bf4b5ac48605606ed534b538afa2ed15bc5894 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Tue, 12 Mar 2024 00:00:41 +0100 Subject: [PATCH 22/29] alembic import op ignore (style error) --- backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py index 0e6c837f..e0ec6fc4 100644 --- a/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py +++ b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py @@ -7,7 +7,7 @@ """ from typing import Sequence, Union -from alembic import op +from alembic import op #type : ignore import sqlalchemy as sa from sqlalchemy.dialects import postgresql From d73dfa7614f613f2daf2503bde47d470341964f9 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Tue, 12 Mar 2024 00:35:38 +0100 Subject: [PATCH 23/29] pyright fixes --- backend/src/project/models.py | 17 +++++++++------ backend/src/project/router.py | 3 +++ backend/src/project/schemas.py | 9 +++++++- backend/src/project/service.py | 39 ++++++++++++++++++++++------------ backend/src/subject/service.py | 3 +-- 5 files changed, 47 insertions(+), 24 deletions(-) diff --git a/backend/src/project/models.py b/backend/src/project/models.py index ad4906a4..50bee174 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -1,3 +1,7 @@ +from datetime import date +from typing import Optional + +from pydantic import BaseModel from sqlalchemy import Column, BigInteger, String, Date, ForeignKey, CheckConstraint from sqlalchemy.orm import relationship, Mapped from src.database import Base @@ -6,13 +10,12 @@ class Project(Base): __tablename__ = 'project' - id = Column(BigInteger, primary_key=True, autoincrement=True) - deadline = Column(Date, nullable=False) - name = Column(String, nullable=False) - subject_id = Column(BigInteger, ForeignKey( - 'subject.id', ondelete="SET NULL"), nullable=True) # Adjusted to BigInteger - description = Column(String, nullable=True) - enroll_deadline = Column(Date, nullable=True) + id: Mapped[int] = Column(BigInteger, primary_key=True, autoincrement=True) # type: ignore + deadline: Mapped[date] = Column(Date, nullable=False) # type: ignore + name: Mapped[str] = Column(String, nullable=False) # type: ignore + subject_id: Mapped[int] = Column(BigInteger, ForeignKey('subject.id', ondelete="SET NULL"),nullable=True) # type: ignore + description: Mapped[str] = Column(String, nullable=True) # type: ignore + enroll_deadline: Mapped[date] = Column(Date, nullable=True) # Relationships subject = relationship("Subject") diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 22df0313..3fcf3551 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -3,6 +3,9 @@ from src.user.models import User from src.dependencies import get_db from src.user.dependencies import get_authenticated_user + +from .exceptions import UnauthorizedToCreateProjectException, ProjectNotFoundException, \ + UnauthorizedToUpdateProjectException, NoProjectsFoundException from .schemas import ProjectCreate, ProjectResponse, ProjectUpdate from .service import create_project, get_project, delete_project, update_project, get_projects_for_subject from ..subject.service import is_teacher_of_subject diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py index a7ea8e0f..67b0d280 100644 --- a/backend/src/project/schemas.py +++ b/backend/src/project/schemas.py @@ -30,6 +30,13 @@ class Config: class ProjectUpdate(BaseModel): - name: Optional[str] = None + name: Optional[str] = Field(None, min_length=1) deadline: Optional[date] = None + subject_id: Optional[int] = None description: Optional[str] = None + + @validator('deadline', pre=True, always=True) + def validate_deadline(cls, value): + if value is not None and value < date.today(): + raise ValueError('The deadline cannot be in the past') + return value diff --git a/backend/src/project/service.py b/backend/src/project/service.py index 5fd6c0fd..9f0bb171 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -1,24 +1,25 @@ from sqlalchemy.orm import Session from . import models, schemas +from .exceptions import ProjectNotFoundException from .models import Project -from .schemas import ProjectCreate +from .schemas import ProjectCreate, ProjectUpdate -async def create_project(db: Session, project_in: ProjectCreate, user_id: str) -> Project: +def create_project(db: Session, project_in: ProjectCreate, user_id: str) -> Project: + # SQLAlchemy does magic that Pyright doesn't understand. Using type: ignore new_project = Project( - name=project_in.name, - deadline=project_in.deadline, - subject_id=project_in.subject_id, - description=project_in.description + name=project_in.name, # type: ignore + deadline=project_in.deadline, # type: ignore + subject_id=project_in.subject_id, # type: ignore + description=project_in.description # type: ignore ) db.add(new_project) db.commit() db.refresh(new_project) return new_project - -async def get_project(db: Session, project_id: int) -> Project: - return db.query(Project).filter(Project.id == project_id).first() +async def get_project(db: Session, project_id: int) -> models.Project: + return db.query(models.Project).filter(models.Project.id == project_id).first() async def get_projects_for_subject(db: Session, subject_id: int) -> list[models.Project]: @@ -31,18 +32,28 @@ async def get_projects_for_subject(db: Session, subject_id: int) -> list[models. async def delete_project(db: Session, project_id: int): - project = db.query(Project).filter(Project.id == project_id).first() + project = db.query(models.Project).filter(models.Project.id == project_id).first() if project: db.delete(project) db.commit() -async def update_project(db: Session, project_id: int, project_update: ProjectCreate) -> Project: +async def update_project(db: Session, project_id: int, project_update: ProjectUpdate) -> Project: project = db.query(Project).filter(Project.id == project_id).first() - if project: + if not project: + # Handle the case where the project doesn't exist + raise ProjectNotFoundException() + + # Update fields only if they're provided in the update payload + if project_update.name is not None: project.name = project_update.name + if project_update.deadline is not None: project.deadline = project_update.deadline + if project_update.subject_id is not None: + project.subject_id = project_update.subject_id + if project_update.description is not None: project.description = project_update.description - db.commit() - db.refresh(project) + + db.commit() + db.refresh(project) return project diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index d3b15680..b6ebffec 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -52,5 +52,4 @@ async def delete_subject(db: Session, subject_id: int): async def is_teacher_of_subject(db: Session, user_id: str, subject_id: int) -> bool: """Check if a user is a teacher of the subject.""" - teachers = await get_subject_teachers(db, subject_id) - return any(teacher.uid == user_id for teacher in teachers) + return db.query(models.TeacherSubject).filter_by(uid=user_id, subject_id=subject_id).count() > 0 From 366a7a8eb6df8530e9b8848415872b193db89b69 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Tue, 12 Mar 2024 00:55:59 +0100 Subject: [PATCH 24/29] pyright fixes --- backend/src/project/models.py | 17 ++++++---- backend/src/project/service.py | 61 +++++++++++++++++----------------- backend/src/subject/service.py | 9 +++-- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 50bee174..7136e821 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -6,19 +6,22 @@ from sqlalchemy.orm import relationship, Mapped from src.database import Base +from backend.src.subject.models import Subject + class Project(Base): __tablename__ = 'project' - id: Mapped[int] = Column(BigInteger, primary_key=True, autoincrement=True) # type: ignore - deadline: Mapped[date] = Column(Date, nullable=False) # type: ignore - name: Mapped[str] = Column(String, nullable=False) # type: ignore - subject_id: Mapped[int] = Column(BigInteger, ForeignKey('subject.id', ondelete="SET NULL"),nullable=True) # type: ignore - description: Mapped[str] = Column(String, nullable=True) # type: ignore - enroll_deadline: Mapped[date] = Column(Date, nullable=True) + id = Column(Integer, primary_key=True) # type: Mapped[int] + deadline = Column(Date, nullable=False) # type: Mapped[date] + name = Column(String, nullable=False) # type: Mapped[str] + subject_id = Column(Integer, ForeignKey('subject.id'), nullable=True) # type: Mapped[int] + description = Column(String, nullable=True) # type: Mapped[str] + subject = relationship("Subject") # type: Mapped[Subject] + enroll_deadline: Mapped[date] = Column(Date, nullable=True) # type: Mapped[date] # Relationships - subject = relationship("Subject") + subject = relationship("Subject") # type: Mapped[Subject] __table_args__ = ( CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), diff --git a/backend/src/project/service.py b/backend/src/project/service.py index 9f0bb171..136d3abd 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -1,45 +1,42 @@ -from sqlalchemy.orm import Session -from . import models, schemas +from typing import List +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from . import models from .exceptions import ProjectNotFoundException from .models import Project from .schemas import ProjectCreate, ProjectUpdate - -def create_project(db: Session, project_in: ProjectCreate, user_id: str) -> Project: - # SQLAlchemy does magic that Pyright doesn't understand. Using type: ignore +async def create_project(db: AsyncSession, project_in: ProjectCreate, user_id: str) -> Project: new_project = Project( - name=project_in.name, # type: ignore - deadline=project_in.deadline, # type: ignore - subject_id=project_in.subject_id, # type: ignore - description=project_in.description # type: ignore + name=project_in.name, + deadline=project_in.deadline, + subject_id=project_in.subject_id, + description=project_in.description ) db.add(new_project) - db.commit() - db.refresh(new_project) + await db.commit() + await db.refresh(new_project) return new_project -async def get_project(db: Session, project_id: int) -> models.Project: - return db.query(models.Project).filter(models.Project.id == project_id).first() - - -async def get_projects_for_subject(db: Session, subject_id: int) -> list[models.Project]: - projects = ( - db.query(models.Project) - .filter(models.Project.subject_id == subject_id) - .all() - ) - return projects +async def get_project(db: AsyncSession, project_id: int) -> models.Project: + result = await db.execute(select(models.Project).filter(models.Project.id == project_id)) + return result.scalars().first() +async def get_projects_for_subject(db: AsyncSession, subject_id: int) -> List[models.Project]: + result = await db.execute(select(models.Project).filter(models.Project.subject_id == subject_id)) + projects = result.scalars().all() + return list(projects) # Explicitly convert to list -async def delete_project(db: Session, project_id: int): - project = db.query(models.Project).filter(models.Project.id == project_id).first() +async def delete_project(db: AsyncSession, project_id: int): + result = await db.execute(select(models.Project).filter(models.Project.id == project_id)) + project = result.scalars().first() if project: - db.delete(project) - db.commit() + await db.delete(project) + await db.commit() - -async def update_project(db: Session, project_id: int, project_update: ProjectUpdate) -> Project: - project = db.query(Project).filter(Project.id == project_id).first() +async def update_project(db: AsyncSession, project_id: int, project_update: ProjectUpdate) -> Project: + result = await db.execute(select(Project).filter(Project.id == project_id)) + project = result.scalars().first() if not project: # Handle the case where the project doesn't exist raise ProjectNotFoundException() @@ -54,6 +51,8 @@ async def update_project(db: Session, project_id: int, project_update: ProjectUp if project_update.description is not None: project.description = project_update.description - db.commit() - db.refresh(project) + await db.commit() + await db.refresh(project) return project + + diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index b6ebffec..d6ae204b 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -1,4 +1,7 @@ from typing import Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session from . import models, schemas from src.user.models import User @@ -50,6 +53,8 @@ async def delete_subject(db: Session, subject_id: int): db.commit() -async def is_teacher_of_subject(db: Session, user_id: str, subject_id: int) -> bool: +async def is_teacher_of_subject(db: AsyncSession, user_id: str, subject_id: int) -> bool: """Check if a user is a teacher of the subject.""" - return db.query(models.TeacherSubject).filter_by(uid=user_id, subject_id=subject_id).count() > 0 + query = select(models.TeacherSubject).filter_by(uid=user_id, subject_id=subject_id) + result = await db.execute(query) + return result.scalars().first() is not None From e5b4010eb252f90b7a1b256dfec6ba5fa851a3bf Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Tue, 12 Mar 2024 00:58:12 +0100 Subject: [PATCH 25/29] style fixes --- .../versions/2cc559f8470c_allign_with_new_schema.py | 2 +- backend/src/project/dependencies.py | 2 +- backend/src/project/models.py | 8 +++++--- backend/src/project/service.py | 7 +++++-- backend/src/subject/service.py | 3 ++- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py index e0ec6fc4..0ebc9588 100644 --- a/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py +++ b/backend/alembic/versions/2cc559f8470c_allign_with_new_schema.py @@ -7,7 +7,7 @@ """ from typing import Sequence, Union -from alembic import op #type : ignore +from alembic import op # type : ignore import sqlalchemy as sa from sqlalchemy.dialects import postgresql diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py index 1a605b30..567e4847 100644 --- a/backend/src/project/dependencies.py +++ b/backend/src/project/dependencies.py @@ -2,13 +2,13 @@ from sqlalchemy.orm import Session from src.dependencies import get_db from src.user.dependencies import get_authenticated_user -from src.user.exceptions import NotAuthorized from src.user.schemas import User from src.subject.schemas import Subject from src.subject.schemas import SubjectList from . import service +from ..auth.exceptions import NotAuthorized async def retrieve_subjects( diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 7136e821..5d1cbab8 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -15,13 +15,15 @@ class Project(Base): id = Column(Integer, primary_key=True) # type: Mapped[int] deadline = Column(Date, nullable=False) # type: Mapped[date] name = Column(String, nullable=False) # type: Mapped[str] - subject_id = Column(Integer, ForeignKey('subject.id'), nullable=True) # type: Mapped[int] + subject_id = Column(Integer, ForeignKey('subject.id'), + nullable=True) # type: Mapped[int] description = Column(String, nullable=True) # type: Mapped[str] subject = relationship("Subject") # type: Mapped[Subject] - enroll_deadline: Mapped[date] = Column(Date, nullable=True) # type: Mapped[date] + enroll_deadline: Mapped[date] = Column( + Date, nullable=True) # type: Mapped[date] # Relationships - subject = relationship("Subject") # type: Mapped[Subject] + subject = relationship("Subject") # type: Mapped[Subject] __table_args__ = ( CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), diff --git a/backend/src/project/service.py b/backend/src/project/service.py index 136d3abd..b9b7475c 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -6,6 +6,7 @@ from .models import Project from .schemas import ProjectCreate, ProjectUpdate + async def create_project(db: AsyncSession, project_in: ProjectCreate, user_id: str) -> Project: new_project = Project( name=project_in.name, @@ -18,15 +19,18 @@ async def create_project(db: AsyncSession, project_in: ProjectCreate, user_id: s await db.refresh(new_project) return new_project + async def get_project(db: AsyncSession, project_id: int) -> models.Project: result = await db.execute(select(models.Project).filter(models.Project.id == project_id)) return result.scalars().first() + async def get_projects_for_subject(db: AsyncSession, subject_id: int) -> List[models.Project]: result = await db.execute(select(models.Project).filter(models.Project.subject_id == subject_id)) projects = result.scalars().all() return list(projects) # Explicitly convert to list + async def delete_project(db: AsyncSession, project_id: int): result = await db.execute(select(models.Project).filter(models.Project.id == project_id)) project = result.scalars().first() @@ -34,6 +38,7 @@ async def delete_project(db: AsyncSession, project_id: int): await db.delete(project) await db.commit() + async def update_project(db: AsyncSession, project_id: int, project_update: ProjectUpdate) -> Project: result = await db.execute(select(Project).filter(Project.id == project_id)) project = result.scalars().first() @@ -54,5 +59,3 @@ async def update_project(db: AsyncSession, project_id: int, project_update: Proj await db.commit() await db.refresh(project) return project - - diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index d6ae204b..c17efc8b 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -55,6 +55,7 @@ async def delete_subject(db: Session, subject_id: int): async def is_teacher_of_subject(db: AsyncSession, user_id: str, subject_id: int) -> bool: """Check if a user is a teacher of the subject.""" - query = select(models.TeacherSubject).filter_by(uid=user_id, subject_id=subject_id) + query = select(models.TeacherSubject).filter_by( + uid=user_id, subject_id=subject_id) result = await db.execute(query) return result.scalars().first() is not None From 009962a727f4338cbbd95cca7a6d02d23343884c Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Tue, 12 Mar 2024 01:03:19 +0100 Subject: [PATCH 26/29] style fixes --- backend/src/project/dependencies.py | 6 +++--- backend/src/project/models.py | 22 ++++++++++------------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py index 567e4847..3168583c 100644 --- a/backend/src/project/dependencies.py +++ b/backend/src/project/dependencies.py @@ -7,14 +7,14 @@ from src.subject.schemas import SubjectList -from . import service from ..auth.exceptions import NotAuthorized +from ..subject.service import get_subjects, get_teachers async def retrieve_subjects( user: User = Depends(get_authenticated_user), db: Session = Depends(get_db) ) -> SubjectList: - teacher_subjects, student_subjects = await service.get_subjects(db, user.uid) + teacher_subjects, student_subjects = await get_subjects(db, user.uid) return SubjectList(as_teacher=teacher_subjects, as_student=student_subjects) @@ -24,6 +24,6 @@ async def user_permission_validation( db: Session = Depends(get_db), ): if not user.is_admin: - teachers = await service.get_teachers(db, subject_id) + teachers = await get_teachers(db, subject_id) if not list(filter(lambda teacher: teacher.id == user.uid, teachers)): raise NotAuthorized() diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 5d1cbab8..e48a6b26 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -2,28 +2,26 @@ from typing import Optional from pydantic import BaseModel -from sqlalchemy import Column, BigInteger, String, Date, ForeignKey, CheckConstraint +from sqlalchemy import Column, BigInteger, String, Date, ForeignKey, CheckConstraint, Integer from sqlalchemy.orm import relationship, Mapped from src.database import Base -from backend.src.subject.models import Subject +f class Project(Base): __tablename__ = 'project' - id = Column(Integer, primary_key=True) # type: Mapped[int] - deadline = Column(Date, nullable=False) # type: Mapped[date] - name = Column(String, nullable=False) # type: Mapped[str] - subject_id = Column(Integer, ForeignKey('subject.id'), - nullable=True) # type: Mapped[int] - description = Column(String, nullable=True) # type: Mapped[str] - subject = relationship("Subject") # type: Mapped[Subject] - enroll_deadline: Mapped[date] = Column( - Date, nullable=True) # type: Mapped[date] + id = Column(BigInteger, primary_key=True) + deadline = Column(Date, nullable=False) + name = Column(String, nullable=False) + subject_id = Column(BigInteger, ForeignKey('subject.id'), nullable=True) + description = Column(String, nullable=True) + subject = relationship("Subject") + enroll_deadline: Mapped[date] = Column(Date, nullable=True) # Relationships - subject = relationship("Subject") # type: Mapped[Subject] + subject = relationship("Subject") __table_args__ = ( CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), From 25afa962eaddef8f875bf36634cea3f121ed8306 Mon Sep 17 00:00:00 2001 From: drieshuybens Date: Tue, 12 Mar 2024 01:07:34 +0100 Subject: [PATCH 27/29] linterrrrrrrrrrr --- backend/src/project/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/project/models.py b/backend/src/project/models.py index e48a6b26..4d651ebe 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -6,8 +6,6 @@ from sqlalchemy.orm import relationship, Mapped from src.database import Base -f - class Project(Base): __tablename__ = 'project' From 9137de1eea732c2a09976f60c57d1c63574961ae Mon Sep 17 00:00:00 2001 From: Xander Bil Date: Tue, 12 Mar 2024 11:05:10 +0100 Subject: [PATCH 28/29] requirements.txt --- backend/requirements.txt | 3 +++ backend/src/project/models.py | 25 ++++++++++--------------- backend/src/project/schemas.py | 10 +++++----- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 959c0992..7d586d7c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,4 @@ +alembic==1.13.1 annotated-types==0.6.0 anyio==4.3.0 attrs==23.1.0 @@ -20,6 +21,8 @@ iniconfig==2.0.0 itsdangerous==2.1.2 lsprotocol==2023.0.0a2 lxml==5.1.0 +Mako==1.3.2 +MarkupSafe==2.1.5 nodeenv==1.8.0 packaging==24.0 pluggy==1.4.0 diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 4d651ebe..902e433e 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -1,25 +1,20 @@ -from datetime import date -from typing import Optional - -from pydantic import BaseModel -from sqlalchemy import Column, BigInteger, String, Date, ForeignKey, CheckConstraint, Integer -from sqlalchemy.orm import relationship, Mapped +from datetime import datetime +from sqlalchemy import BigInteger, DateTime, ForeignKey, CheckConstraint +from sqlalchemy.orm import Mapped, mapped_column from src.database import Base class Project(Base): __tablename__ = 'project' - id = Column(BigInteger, primary_key=True) - deadline = Column(Date, nullable=False) - name = Column(String, nullable=False) - subject_id = Column(BigInteger, ForeignKey('subject.id'), nullable=True) - description = Column(String, nullable=True) - subject = relationship("Subject") - enroll_deadline: Mapped[date] = Column(Date, nullable=True) + id: Mapped[int] = mapped_column(primary_key=True) + deadline: Mapped[datetime] = mapped_column(nullable=False) + name: Mapped[str] = mapped_column(nullable=False) + subject_id: Mapped[int] = mapped_column(ForeignKey( + 'subject.id', ondelete="CASCADE"), nullable=True) + description: Mapped[str] = mapped_column(nullable=True) - # Relationships - subject = relationship("Subject") + enroll_deadline: Mapped[datetime] = mapped_column(nullable=True) __table_args__ = ( CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py index 67b0d280..0d8cfce2 100644 --- a/backend/src/project/schemas.py +++ b/backend/src/project/schemas.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import datetime, date from typing import Optional from pydantic import BaseModel, Field, validator @@ -6,7 +6,7 @@ class ProjectCreate(BaseModel): name: str = Field(..., min_length=1) - deadline: date + deadline: datetime subject_id: int description: str @@ -21,7 +21,7 @@ def validate_deadline(cls, value): class ProjectResponse(BaseModel): id: int name: str - deadline: date + deadline: datetime subject_id: int description: str @@ -31,12 +31,12 @@ class Config: class ProjectUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1) - deadline: Optional[date] = None + deadline: Optional[datetime] = None subject_id: Optional[int] = None description: Optional[str] = None @validator('deadline', pre=True, always=True) def validate_deadline(cls, value): - if value is not None and value < date.today(): + if value is not None and value < datetime.now(): raise ValueError('The deadline cannot be in the past') return value From 74787fe02c4c718d79f8b504dc5df3b24855dfbe Mon Sep 17 00:00:00 2001 From: Xander Bil Date: Tue, 12 Mar 2024 17:15:41 +0100 Subject: [PATCH 29/29] Fix problems with project --- .../ecbdc859aca6_update_project_scheme.py | 68 +++++++++++++++++++ backend/requirements.txt | 3 + backend/src/database.py | 5 ++ backend/src/dependencies.py | 11 ++- backend/src/main.py | 1 - backend/src/project/dependencies.py | 12 +--- backend/src/project/exceptions.py | 18 ----- backend/src/project/models.py | 5 +- backend/src/project/router.py | 58 ++++++---------- backend/src/project/schemas.py | 24 +++---- backend/src/project/service.py | 16 ++--- backend/src/subject/service.py | 8 --- 12 files changed, 125 insertions(+), 104 deletions(-) create mode 100644 backend/alembic/versions/ecbdc859aca6_update_project_scheme.py diff --git a/backend/alembic/versions/ecbdc859aca6_update_project_scheme.py b/backend/alembic/versions/ecbdc859aca6_update_project_scheme.py new file mode 100644 index 00000000..8cf4a47b --- /dev/null +++ b/backend/alembic/versions/ecbdc859aca6_update_project_scheme.py @@ -0,0 +1,68 @@ +"""update project scheme + +Revision ID: ecbdc859aca6 +Revises: 29a17fa183de +Create Date: 2024-03-12 16:43:51.794097 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ecbdc859aca6' +down_revision: Union[str, None] = '29a17fa183de' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('project', 'id', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=False, + autoincrement=True) + op.alter_column('project', 'deadline', + existing_type=sa.DATE(), + type_=sa.DateTime(), + existing_nullable=False) + op.alter_column('project', 'subject_id', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=True) + op.alter_column('project', 'enroll_deadline', + existing_type=sa.DATE(), + type_=sa.DateTime(), + existing_nullable=True) + op.drop_constraint('project_subject_id_fkey', 'project', type_='foreignkey') + op.create_foreign_key(None, 'project', 'subject', [ + 'subject_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'project', type_='foreignkey') # type: ignore + op.create_foreign_key('project_subject_id_fkey', 'project', 'subject', [ + 'subject_id'], ['id'], ondelete='SET NULL') + op.alter_column('project', 'enroll_deadline', + existing_type=sa.DateTime(), + type_=sa.DATE(), + existing_nullable=True) + op.alter_column('project', 'subject_id', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=True) + op.alter_column('project', 'deadline', + existing_type=sa.DateTime(), + type_=sa.DATE(), + existing_nullable=False) + op.alter_column('project', 'id', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=False, + autoincrement=True) + # ### end Alembic commands ### diff --git a/backend/requirements.txt b/backend/requirements.txt index 7d586d7c..bc56e132 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,7 @@ alembic==1.13.1 annotated-types==0.6.0 anyio==4.3.0 +asyncpg==0.29.0 attrs==23.1.0 autopep8==2.0.4 cattrs==23.1.2 @@ -26,6 +27,8 @@ MarkupSafe==2.1.5 nodeenv==1.8.0 packaging==24.0 pluggy==1.4.0 +psycopg-binary==3.1.18 +psycopg-pool==3.2.1 psycopg2-binary==2.9.9 pycodestyle==2.11.1 pycparser==2.21 diff --git a/backend/src/database.py b/backend/src/database.py index 21bbb78e..02b1c5c6 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -1,12 +1,17 @@ from sqlalchemy import MetaData, create_engine +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.orm import declarative_base from sqlalchemy.orm import sessionmaker from src import config SQLALCHEMY_DATABASE_URL = config.CONFIG.database_uri +# TODO: migrate full codebase to async engine = create_engine(SQLALCHEMY_DATABASE_URL) +async_engine = create_async_engine(SQLALCHEMY_DATABASE_URL[:len( + "postgresql")] + "+asyncpg" + SQLALCHEMY_DATABASE_URL[len("postgresql"):]) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +AsyncSessionLocal = async_sessionmaker(async_engine, autoflush=False) Base = declarative_base() diff --git a/backend/src/dependencies.py b/backend/src/dependencies.py index b1c37e2d..e3a24eba 100644 --- a/backend/src/dependencies.py +++ b/backend/src/dependencies.py @@ -1,4 +1,4 @@ -from .database import SessionLocal +from .database import SessionLocal, AsyncSessionLocal def get_db(): @@ -8,3 +8,12 @@ def get_db(): yield db finally: db.close() + + +async def get_async_db(): + """Creates new async database session per request, which is closed afterwards""" + db = AsyncSessionLocal() + try: + yield db + finally: + await db.close() diff --git a/backend/src/main.py b/backend/src/main.py index 548a3ce3..6e649a70 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -25,7 +25,6 @@ app.include_router(user_router) app.include_router(project_router) app.include_router(auth_router) -app.include_router(project_router) @app.get("/api") diff --git a/backend/src/project/dependencies.py b/backend/src/project/dependencies.py index 3168583c..ecbc71d2 100644 --- a/backend/src/project/dependencies.py +++ b/backend/src/project/dependencies.py @@ -3,19 +3,9 @@ from src.dependencies import get_db from src.user.dependencies import get_authenticated_user from src.user.schemas import User -from src.subject.schemas import Subject -from src.subject.schemas import SubjectList - from ..auth.exceptions import NotAuthorized -from ..subject.service import get_subjects, get_teachers - - -async def retrieve_subjects( - user: User = Depends(get_authenticated_user), db: Session = Depends(get_db) -) -> SubjectList: - teacher_subjects, student_subjects = await get_subjects(db, user.uid) - return SubjectList(as_teacher=teacher_subjects, as_student=student_subjects) +from ..subject.service import get_teachers async def user_permission_validation( diff --git a/backend/src/project/exceptions.py b/backend/src/project/exceptions.py index 6ecc8164..ed3c4d5c 100644 --- a/backend/src/project/exceptions.py +++ b/backend/src/project/exceptions.py @@ -1,23 +1,5 @@ -# exceptions.py - from fastapi import HTTPException -def NoProjectsFoundException(subject_id: int): - return HTTPException(status_code=404, detail=f"No projects found for subject {subject_id}") - - -def UnauthorizedToCreateProjectException(): - return HTTPException(status_code=403, detail="User is not authorized to create projects for this subject") - - def ProjectNotFoundException(): return HTTPException(status_code=404, detail="Project not found") - - -def UnauthorizedToDeleteProjectException(): - return HTTPException(status_code=403, detail="User is not authorized to delete this project") - - -def UnauthorizedToUpdateProjectException(): - return HTTPException(status_code=403, detail="User is not authorized to update projects for this subject") diff --git a/backend/src/project/models.py b/backend/src/project/models.py index 902e433e..237b12d9 100644 --- a/backend/src/project/models.py +++ b/backend/src/project/models.py @@ -8,13 +8,14 @@ class Project(Base): __tablename__ = 'project' id: Mapped[int] = mapped_column(primary_key=True) - deadline: Mapped[datetime] = mapped_column(nullable=False) + deadline: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) name: Mapped[str] = mapped_column(nullable=False) subject_id: Mapped[int] = mapped_column(ForeignKey( 'subject.id', ondelete="CASCADE"), nullable=True) description: Mapped[str] = mapped_column(nullable=True) - enroll_deadline: Mapped[datetime] = mapped_column(nullable=True) + enroll_deadline: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=True) __table_args__ = ( CheckConstraint('deadline >= CURRENT_DATE', name='deadline_check'), diff --git a/backend/src/project/router.py b/backend/src/project/router.py index 3fcf3551..bccad258 100644 --- a/backend/src/project/router.py +++ b/backend/src/project/router.py @@ -1,15 +1,11 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession -from src.user.models import User -from src.dependencies import get_db -from src.user.dependencies import get_authenticated_user +from src.subject.dependencies import user_permission_validation +from src.dependencies import get_async_db, get_db -from .exceptions import UnauthorizedToCreateProjectException, ProjectNotFoundException, \ - UnauthorizedToUpdateProjectException, NoProjectsFoundException +from .exceptions import ProjectNotFoundException from .schemas import ProjectCreate, ProjectResponse, ProjectUpdate from .service import create_project, get_project, delete_project, update_project, get_projects_for_subject -from ..subject.service import is_teacher_of_subject -from . import exceptions router = APIRouter( prefix="/api/subjects/{subject_id}/projects", @@ -21,33 +17,29 @@ @router.get("/", response_model=list[ProjectResponse]) async def list_projects_for_subject( subject_id: int, - user: User = Depends(get_authenticated_user), - db: AsyncSession = Depends(get_db) + db: AsyncSession = Depends(get_async_db) ): projects = await get_projects_for_subject(db, subject_id) - if not projects: - raise NoProjectsFoundException(subject_id) return projects -@router.post("/", response_model=ProjectResponse) +@router.post("/", + response_model=ProjectResponse, + dependencies=[Depends(user_permission_validation)], + status_code=201) async def create_project_for_subject( subject_id: int, project_in: ProjectCreate, - user: User = Depends(get_authenticated_user), - db: AsyncSession = Depends(get_db) + db: AsyncSession = Depends(get_async_db) ): - if not await is_teacher_of_subject(db, user.id, subject_id): - raise UnauthorizedToCreateProjectException() - - project = await create_project(db=db, project_in=project_in, user_id=user.id) + project = await create_project(db, project_in, subject_id) return project @router.get("/{project_id}", response_model=ProjectResponse) async def get_project_for_subject( project_id: int, - db: AsyncSession = Depends(get_db) + db: AsyncSession = Depends(get_async_db) ): project = await get_project(db, project_id) if not project: @@ -55,33 +47,21 @@ async def get_project_for_subject( return project -@router.delete("/{project_id}") +@router.delete("/{project_id}", dependencies=[Depends(user_permission_validation)]) async def delete_project_for_subject( - subject_id: int, project_id: int, - user: User = Depends(get_authenticated_user), - db: AsyncSession = Depends(get_db) + db: AsyncSession = Depends(get_async_db) ): - if not await is_teacher_of_subject(db, user.id, subject_id): - raise UnauthorizedToUpdateProjectException() - await delete_project(db, project_id) return {"message": "Project deleted successfully"} -@router.patch("/{project_id}", response_model=ProjectResponse) +@router.patch("/{project_id}", + response_model=ProjectResponse, + dependencies=[Depends(user_permission_validation)]) async def patch_project_for_subject( - subject_id: int, project_id: int, project_update: ProjectUpdate, - user: User = Depends(get_authenticated_user), - db: AsyncSession = Depends(get_db) + db: AsyncSession = Depends(get_async_db) ): - # Check if the user is authorized to update the project - if not await is_teacher_of_subject(db, user.id, subject_id): - raise UnauthorizedToUpdateProjectException() - - updated_project = await update_project(db, project_id, project_update) - if not updated_project: - raise ProjectNotFoundException() - return updated_project + return await update_project(db, project_id, project_update) diff --git a/backend/src/project/schemas.py b/backend/src/project/schemas.py index 0d8cfce2..d7a09e2a 100644 --- a/backend/src/project/schemas.py +++ b/backend/src/project/schemas.py @@ -1,42 +1,38 @@ -from datetime import datetime, date +from datetime import datetime, date, timezone from typing import Optional -from pydantic import BaseModel, Field, validator +from pydantic import BaseModel, Field, validator, ConfigDict, field_validator class ProjectCreate(BaseModel): name: str = Field(..., min_length=1) deadline: datetime - subject_id: int description: str # Check if deadline is not in the past - @validator('deadline', pre=True, always=True) - def validate_deadline(cls, value): - if value < date.today(): + @field_validator('deadline') + def validate_deadline(cls, value: datetime) -> datetime: + if value < datetime.now(value.tzinfo): raise ValueError('The deadline cannot be in the past') return value class ProjectResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + id: int name: str deadline: datetime - subject_id: int description: str - class Config: - orm_mode = True - class ProjectUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1) deadline: Optional[datetime] = None - subject_id: Optional[int] = None description: Optional[str] = None - @validator('deadline', pre=True, always=True) - def validate_deadline(cls, value): - if value is not None and value < datetime.now(): + @field_validator('deadline') + def validate_deadline(cls, value: datetime) -> datetime: + if value is not None and value < datetime.now(value.tzinfo): raise ValueError('The deadline cannot be in the past') return value diff --git a/backend/src/project/service.py b/backend/src/project/service.py index b9b7475c..a2329002 100644 --- a/backend/src/project/service.py +++ b/backend/src/project/service.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Sequence from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from . import models @@ -7,11 +7,11 @@ from .schemas import ProjectCreate, ProjectUpdate -async def create_project(db: AsyncSession, project_in: ProjectCreate, user_id: str) -> Project: +async def create_project(db: AsyncSession, project_in: ProjectCreate, subject_id: int) -> Project: new_project = Project( name=project_in.name, deadline=project_in.deadline, - subject_id=project_in.subject_id, + subject_id=subject_id, description=project_in.description ) db.add(new_project) @@ -25,10 +25,10 @@ async def get_project(db: AsyncSession, project_id: int) -> models.Project: return result.scalars().first() -async def get_projects_for_subject(db: AsyncSession, subject_id: int) -> List[models.Project]: - result = await db.execute(select(models.Project).filter(models.Project.subject_id == subject_id)) +async def get_projects_for_subject(db: AsyncSession, subject_id: int) -> Sequence[models.Project]: + result = await db.execute(select(models.Project).filter_by(subject_id=subject_id)) projects = result.scalars().all() - return list(projects) # Explicitly convert to list + return projects async def delete_project(db: AsyncSession, project_id: int): @@ -43,16 +43,12 @@ async def update_project(db: AsyncSession, project_id: int, project_update: Proj result = await db.execute(select(Project).filter(Project.id == project_id)) project = result.scalars().first() if not project: - # Handle the case where the project doesn't exist raise ProjectNotFoundException() - # Update fields only if they're provided in the update payload if project_update.name is not None: project.name = project_update.name if project_update.deadline is not None: project.deadline = project_update.deadline - if project_update.subject_id is not None: - project.subject_id = project_update.subject_id if project_update.description is not None: project.description = project_update.description diff --git a/backend/src/subject/service.py b/backend/src/subject/service.py index c17efc8b..b42ad276 100644 --- a/backend/src/subject/service.py +++ b/backend/src/subject/service.py @@ -51,11 +51,3 @@ async def delete_subject(db: Session, subject_id: int): """Remove a subject""" db.query(models.Subject).filter_by(id=subject_id).delete() db.commit() - - -async def is_teacher_of_subject(db: AsyncSession, user_id: str, subject_id: int) -> bool: - """Check if a user is a teacher of the subject.""" - query = select(models.TeacherSubject).filter_by( - uid=user_id, subject_id=subject_id) - result = await db.execute(query) - return result.scalars().first() is not None