From d1a620b2e2219bf814be2f057b80d3f220429166 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 15 Apr 2024 22:36:02 +0200 Subject: [PATCH] feat: init CourseUpdates backend CRUD --- apps/api/src/db/course_updates.py | 41 +++++++ apps/api/src/routers/courses/courses.py | 71 ++++++++++- apps/api/src/security/rbac/utils.py | 2 +- apps/api/src/services/courses/updates.py | 148 +++++++++++++++++++++++ 4 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 apps/api/src/db/course_updates.py create mode 100644 apps/api/src/services/courses/updates.py diff --git a/apps/api/src/db/course_updates.py b/apps/api/src/db/course_updates.py new file mode 100644 index 00000000..fd1126a3 --- /dev/null +++ b/apps/api/src/db/course_updates.py @@ -0,0 +1,41 @@ +from typing import Optional +from sqlalchemy import Column, ForeignKey, Integer +from sqlmodel import Field, SQLModel + + +class CourseUpdate(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + courseupdate_uuid: str + title: str + content: str + course_id: int = Field( + sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE")) + ) + linked_activity_uuids: Optional[str] = Field(default=None) + org_id: int = Field(default=None, foreign_key="organization.id") + creation_date: str + update_date: str + +class CourseUpdateCreate(SQLModel): + title: str + content: str + linked_activity_uuids: Optional[str] = Field(default=None) + org_id: int + +class CourseUpdateRead(SQLModel): + id: int + title: str + content: str + course_id: int + courseupdate_uuid: str + linked_activity_uuids: Optional[str] = Field(default=None) + org_id: int + creation_date: str + update_date: str + +class CourseUpdateUpdate(SQLModel): + title: Optional[str] = None + content: Optional[str] = None + linked_activity_uuids: Optional[str] = Field(default=None) + + \ No newline at end of file diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index 6244c838..cd145ac3 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -2,6 +2,11 @@ from typing import List from fastapi import APIRouter, Depends, UploadFile, Form, Request from sqlmodel import Session from src.core.events.database import get_db_session +from src.db.course_updates import ( + CourseUpdateCreate, + CourseUpdateRead, + CourseUpdateUpdate, +) from src.db.users import PublicUser from src.db.courses import ( CourseCreate, @@ -19,6 +24,7 @@ from src.services.courses.courses import ( delete_course, update_course_thumbnail, ) +from src.services.courses.updates import create_update, delete_update, get_updates_by_course_uuid, update_update router = APIRouter() @@ -51,7 +57,9 @@ async def api_create_course( learnings=learnings, tags=tags, ) - return await create_course(request, org_id, course, current_user, db_session, thumbnail) + return await create_course( + request, org_id, course, current_user, db_session, thumbnail + ) @router.put("/{course_uuid}/thumbnail") @@ -145,3 +153,64 @@ async def api_delete_course( """ return await delete_course(request, course_uuid, current_user, db_session) + +@ router.get("/{course_uuid}/updates") +async def api_get_course_updates( + request: Request, + course_uuid: str, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +) -> List[CourseUpdateRead]: + """ + Get Course Updates by course_uuid + """ + + return await get_updates_by_course_uuid(request, course_uuid, current_user, db_session) + +@router.post("/{course_uuid}/update") +async def api_create_course_update( + request: Request, + course_uuid: str, + update_object: CourseUpdateCreate, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +) -> CourseUpdateRead: + """ + Create new Course Update + """ + + return await create_update( + request, course_uuid, update_object, current_user, db_session + ) + +@router.put("/{course_uuid}/update/{courseupdate_uuid}") +async def api_update_course_update( + request: Request, + course_uuid: str, + courseupdate_uuid: str, + update_object: CourseUpdateUpdate, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +) -> CourseUpdateRead: + """ + Update Course Update by courseupdate_uuid + """ + + return await update_update( + request, courseupdate_uuid, update_object, current_user, db_session + ) + +@router.delete("/{course_uuid}/update/{courseupdate_uuid}") +async def api_delete_course_update( + request: Request, + course_uuid: str, + courseupdate_uuid: str, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): + """ + Delete Course Update by courseupdate_uuid + """ + + return await delete_update(request, courseupdate_uuid, current_user, db_session) + diff --git a/apps/api/src/security/rbac/utils.py b/apps/api/src/security/rbac/utils.py index aa6560cd..0717b452 100644 --- a/apps/api/src/security/rbac/utils.py +++ b/apps/api/src/security/rbac/utils.py @@ -5,7 +5,7 @@ async def check_element_type(element_uuid): """ Check if the element is a course, a user, a house or a collection, by checking its prefix """ - if element_uuid.startswith("course_"): + if element_uuid.startswith("course_") or element_uuid.startswith("courseupdate_"): return "courses" elif element_uuid.startswith("user_"): return "users" diff --git a/apps/api/src/services/courses/updates.py b/apps/api/src/services/courses/updates.py new file mode 100644 index 00000000..5ce33e55 --- /dev/null +++ b/apps/api/src/services/courses/updates.py @@ -0,0 +1,148 @@ +from datetime import datetime +from typing import List +from uuid import uuid4 +from fastapi import HTTPException, Request, status +from sqlmodel import Session, select +from src.db.course_updates import ( + CourseUpdate, + CourseUpdateCreate, + CourseUpdateRead, + CourseUpdateUpdate, +) +from src.db.courses import Course +from src.db.organizations import Organization +from src.db.users import AnonymousUser, PublicUser +from src.services.courses.courses import rbac_check + + +async def create_update( + request: Request, + course_uuid: str, + update_object: CourseUpdateCreate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +) -> CourseUpdateRead: + + # CHekc if org exists + statement_org = select(Organization).where(Organization.id == update_object.org_id) + org = db_session.exec(statement_org).first() + + if not org or org.id is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + ) + + statement = select(Course).where(Course.course_uuid == course_uuid) + course = db_session.exec(statement).first() + + if not course or course.id is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + + # Generate UUID + courseupdate_uuid = str(f"courseupdate_{uuid4()}") + + update = CourseUpdate( + **update_object.model_dump(), + course_id=course.id, + courseupdate_uuid=courseupdate_uuid, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + + db_session.add(update) + + db_session.commit() + db_session.refresh(update) + + return CourseUpdateRead(**update.model_dump()) + + +# Update Course Update +async def update_update( + request: Request, + courseupdate_uuid: str, + update_object: CourseUpdateUpdate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +) -> CourseUpdateRead: + statement = select(CourseUpdate).where( + CourseUpdate.courseupdate_uuid == courseupdate_uuid + ) + update = db_session.exec(statement).first() + + if not update or update.id is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Update does not exist" + ) + + # RBAC check + await rbac_check( + request, update.courseupdate_uuid, current_user, "update", db_session + ) + + for key, value in update_object.model_dump().items(): + if value is not None: + setattr(update, key, value) + + + db_session.add(update) + + db_session.commit() + db_session.refresh(update) + + return CourseUpdateRead(**update.model_dump()) + + +# Delete Course Update +async def delete_update( + request: Request, + courseupdate_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + statement = select(CourseUpdate).where( + CourseUpdate.courseupdate_uuid == courseupdate_uuid + ) + update = db_session.exec(statement).first() + + if not update or update.id is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Update does not exist" + ) + + # RBAC check + await rbac_check( + request, update.courseupdate_uuid, current_user, "delete", db_session + ) + + db_session.delete(update) + db_session.commit() + + return {"message": "Update deleted successfully"} + + +# Get Course Updates by Course ID +async def get_updates_by_course_uuid( + request: Request, + course_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +) -> List[CourseUpdateRead]: + # FInd if course exists + statement = select(Course).where(Course.course_uuid == course_uuid) + course = db_session.exec(statement).first() + + if not course or course.id is None: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + ) + + statement = select(CourseUpdate).where(CourseUpdate.course_id == course.id) + updates = db_session.exec(statement).all() + + return [CourseUpdateRead(**update.model_dump()) for update in updates]