From e6adbca5627620d263fcb197b9f0fa42a43821f3 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 16 Nov 2023 21:30:01 +0100 Subject: [PATCH] feat: init collections --- apps/api/src/db/collections.py | 39 ++- apps/api/src/db/collections_courses.py | 11 + apps/api/src/routers/courses/collections.py | 24 +- apps/api/src/services/courses/collections.py | 323 +++++++++---------- 4 files changed, 219 insertions(+), 178 deletions(-) create mode 100644 apps/api/src/db/collections_courses.py diff --git a/apps/api/src/db/collections.py b/apps/api/src/db/collections.py index 63960f70..26085f2e 100644 --- a/apps/api/src/db/collections.py +++ b/apps/api/src/db/collections.py @@ -1,3 +1,40 @@ from typing import Optional from sqlmodel import Field, SQLModel -from enum import Enum \ No newline at end of file + + +class CollectionBase(SQLModel): + name: str + public: bool + description: Optional[str] = "" + + +class Collection(CollectionBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + org_id: int = Field(default=None, foreign_key="organization.id") + collection_uuid: str = "" + creation_date: str = "" + update_date: str = "" + + +class CollectionCreate(CollectionBase): + courses: list + org_id: int = Field(default=None, foreign_key="organization.id") + + pass + + +class CollectionUpdate(CollectionBase): + collection_id: int + courses: Optional[list] + name: Optional[str] + public: Optional[bool] + description: Optional[str] + + +class CollectionRead(CollectionBase): + id: int + courses: list + collection_uuid: str + creation_date: str + update_date: str + pass diff --git a/apps/api/src/db/collections_courses.py b/apps/api/src/db/collections_courses.py new file mode 100644 index 00000000..7ec5ff1b --- /dev/null +++ b/apps/api/src/db/collections_courses.py @@ -0,0 +1,11 @@ +from typing import Optional +from sqlmodel import Field, SQLModel + + +class CollectionCourse(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + collection_id: int = Field(default=None, foreign_key="collection.id") + course_id: int = Field(default=None, foreign_key="course.id") + org_id: int = Field(default=None, foreign_key="organization.id") + creation_date: str + update_date: str diff --git a/apps/api/src/routers/courses/collections.py b/apps/api/src/routers/courses/collections.py index 87dfe81c..d691401f 100644 --- a/apps/api/src/routers/courses/collections.py +++ b/apps/api/src/routers/courses/collections.py @@ -1,4 +1,6 @@ from fastapi import APIRouter, Depends, Request +from src.core.events.database import get_db_session +from src.db.collections import CollectionCreate, CollectionUpdate from src.security.auth import get_current_user from src.services.users.users import PublicUser from src.services.courses.collections import ( @@ -17,13 +19,14 @@ router = APIRouter() @router.post("/") async def api_create_collection( request: Request, - collection_object: Collection, + collection_object: CollectionCreate, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Create new Collection """ - return await create_collection(request, collection_object, current_user) + return await create_collection(request, collection_object, current_user, db_session) @router.get("/{collection_id}") @@ -31,11 +34,12 @@ async def api_get_collection( request: Request, collection_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Get single collection by ID """ - return await get_collection(request, collection_id, current_user) + return await get_collection(request, collection_id, current_user, db_session) @router.get("/org_id/{org_id}/page/{page}/limit/{limit}") @@ -45,26 +49,25 @@ async def api_get_collections_by( limit: int, org_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Get collections by page and limit """ - return await get_collections(request, org_id, current_user, page, limit) + return await get_collections(request, org_id, current_user, db_session, page, limit) @router.put("/{collection_id}") async def api_update_collection( request: Request, - collection_object: Collection, - collection_id: str, + collection_object: CollectionUpdate, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Update collection by ID """ - return await update_collection( - request, collection_object, collection_id, current_user - ) + return await update_collection(request, collection_object, current_user, db_session) @router.delete("/{collection_id}") @@ -72,9 +75,10 @@ async def api_delete_collection( request: Request, collection_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Delete collection by ID """ - return await delete_collection(request, collection_id, current_user) + return await delete_collection(request, collection_id, current_user, db_session) diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index 05e133bf..38269e53 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -1,27 +1,31 @@ +from datetime import datetime +from gc import collect from typing import List, Literal from uuid import uuid4 from pydantic import BaseModel -from src.security.rbac.rbac import authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon +from sqlmodel import Session, select +from src.db.collections import ( + Collection, + CollectionCreate, + CollectionRead, + CollectionUpdate, +) +from src.db.collections_courses import CollectionCourse +from src.db.courses import Course +from src.security.rbac.rbac import ( + authorization_verify_based_on_roles_and_authorship, + authorization_verify_if_user_is_anon, +) from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request +from typing import List +from fastapi import HTTPException, Request +from sqlmodel import Session, select +from src.db.collections import Collection +from src.db.courses import Course +from src.db.collections_courses import CollectionCourse +from src.services.users.users import PublicUser -#### Classes #################################################### - - -class Collection(BaseModel): - name: str - description: str - courses: List[str] # course_id - public: bool - org_id: str # org_id - - -class CollectionInDB(Collection): - collection_id: str - authors: List[str] # user_id - - -#### Classes #################################################### #################################################### # CRUD @@ -29,134 +33,164 @@ class CollectionInDB(Collection): async def get_collection( - request: Request, collection_id: str, current_user: PublicUser -): - collections = request.app.db["collections"] - - collection = await collections.find_one({"collection_id": collection_id}) - - # verify collection rights - await verify_collection_rights( - request, collection_id, current_user, "read", collection["org_id"] - ) + request: Request, collection_id: str, current_user: PublicUser, db_session: Session +) -> CollectionRead: + statement = select(Collection).where(Collection.id == collection_id) + collection = db_session.exec(statement).first() if not collection: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" ) - collection = Collection(**collection) + # get courses in collection + statement = ( + select(Course) + .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .distinct(Course.id) + ) + courses = db_session.exec(statement).all() - # add courses to collection - courses = request.app.db["courses"] - courseids = [course for course in collection.courses] - - collection.courses = [] - collection.courses = courses.find({"course_id": {"$in": courseids}}, {"_id": 0}) - - collection.courses = [ - course for course in await collection.courses.to_list(length=100) - ] + collection = CollectionRead(**collection.dict(), courses=courses) return collection async def create_collection( - request: Request, collection_object: Collection, current_user: PublicUser -): - collections = request.app.db["collections"] + request: Request, + collection_object: CollectionCreate, + current_user: PublicUser, + db_session: Session, +) -> CollectionRead: + collection = Collection.from_orm(collection_object) - # find if collection already exists using name - isCollectionNameAvailable = await collections.find_one( - {"name": collection_object.name} - ) + # Complete the collection object + collection.collection_uuid = f"collection_{uuid4()}" + collection.creation_date = str(datetime.now()) + collection.update_date = str(datetime.now()) - # TODO - # await verify_collection_rights("*", current_user, "create") + # Add collection to database + db_session.add(collection) + db_session.commit() - if isCollectionNameAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Collection name already exists", + db_session.refresh(collection) + + # Link courses to collection + for course in collection_object.courses: + collection_course = CollectionCourse( + collection_id=int(collection.id is not None), + course_id=int(course), + org_id=int(collection_object.org_id), + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) + # Add collection_course to database + db_session.add(collection_course) - # generate collection_id with uuid4 - collection_id = str(f"collection_{uuid4()}") + db_session.commit() + db_session.refresh(collection) - collection = CollectionInDB( - collection_id=collection_id, - authors=[current_user.user_id], - **collection_object.dict(), + # Get courses once again + statement = ( + select(Course) + .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .distinct(Course.id) ) + courses = db_session.exec(statement).all() - collection_in_db = await collections.insert_one(collection.dict()) + collection = CollectionRead(**collection.dict(), courses=courses) - if not collection_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) - - return collection.dict() + return CollectionRead.from_orm(collection) async def update_collection( request: Request, - collection_object: Collection, - collection_id: str, + collection_object: CollectionUpdate, current_user: PublicUser, -): - # verify collection rights - - collections = request.app.db["collections"] - - collection = await collections.find_one({"collection_id": collection_id}) - - await verify_collection_rights( - request, collection_id, current_user, "update", collection["org_id"] + db_session: Session, +) -> CollectionRead: + statement = select(Collection).where( + Collection.id == collection_object.collection_id ) + collection = db_session.exec(statement).first() if not collection: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" ) - updated_collection = CollectionInDB( - collection_id=collection_id, **collection_object.dict() + courses = collection_object.courses + + del collection_object.collection_id + del collection_object.courses + + # Update only the fields that were passed in + for var, value in vars(collection_object).items(): + if value is not None: + setattr(collection, var, value) + + collection.update_date = str(datetime.now()) + + # Update only the fields that were passed in + for var, value in vars(collection_object).items(): + if value is not None: + setattr(collection, var, value) + + statement = select(CollectionCourse).where( + CollectionCourse.collection_id == collection.id + ) + collection_courses = db_session.exec(statement).all() + + # Delete all collection_courses + for collection_course in collection_courses: + db_session.delete(collection_course) + + # Add new collection_courses + for course in courses or []: + collection_course = CollectionCourse( + collection_id=int(collection.id is not None), + course_id=int(course), + org_id=int(collection.org_id), + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + # Add collection_course to database + db_session.add(collection_course) + + db_session.commit() + db_session.refresh(collection) + + # Get courses once again + statement = ( + select(Course) + .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .distinct(Course.id) ) - await collections.update_one( - {"collection_id": collection_id}, {"$set": updated_collection.dict()} - ) + courses = db_session.exec(statement).all() - return Collection(**updated_collection.dict()) + collection = CollectionRead(**collection.dict(), courses=courses) + + return collection async def delete_collection( - request: Request, collection_id: str, current_user: PublicUser + request: Request, collection_id: str, current_user: PublicUser, db_session: Session ): - collections = request.app.db["collections"] - - collection = await collections.find_one({"collection_id": collection_id}) - - await verify_collection_rights( - request, collection_id, current_user, "delete", collection["org_id"] - ) + statement = select(Collection).where(Collection.id == collection_id) + collection = db_session.exec(statement).first() if not collection: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" + status_code=404, + detail="Collection not found", ) - isDeleted = await collections.delete_one({"collection_id": collection_id}) + # delete collection from database + db_session.delete(collection) + db_session.commit() - if isDeleted: - return {"detail": "collection deleted"} - else: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) + return {"detail": "Collection deleted"} #################################################### @@ -168,75 +202,30 @@ async def get_collections( request: Request, org_id: str, current_user: PublicUser, + db_session: Session, page: int = 1, limit: int = 10, -): - collections = request.app.db["collections"] - - - if current_user.user_id == "anonymous": - all_collections = collections.find( - {"org_id": org_id, "public": True}, {"_id": 0} - ) - else: - # get all collections from database without ObjectId - all_collections = ( - collections.find({"org_id": org_id}) - .sort("name", 1) - .skip(10 * (page - 1)) - .limit(limit) - ) - - # create list of collections and include courses in each collection - collections_list = [] - for collection in await all_collections.to_list(length=100): - collection = CollectionInDB(**collection) - collections_list.append(collection) - - collection_courses = [course for course in collection.courses] - # add courses to collection - courses = request.app.db["courses"] - collection.courses = [] - collection.courses = courses.find( - {"course_id": {"$in": collection_courses}}, {"_id": 0} - ) - - collection.courses = [ - course for course in await collection.courses.to_list(length=100) - ] - - return collections_list - - -#### Security #################################################### - - -async def verify_collection_rights( - request: Request, - collection_id: str, - current_user: PublicUser, - action: Literal["create", "read", "update", "delete"], - org_id: str, -): - collections = request.app.db["collections"] - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) - collection = await collections.find_one({"collection_id": collection_id}) - - if not collection and action != "create" and collection_id != "*": - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" - ) - - # Collections are public by default for now - if current_user.user_id == "anonymous" and action == "read": - return True - - await authorization_verify_if_user_is_anon(current_user.user_id) - - await authorization_verify_based_on_roles_and_authorship( - request, current_user.user_id, action, user["roles"], collection_id +) -> List[CollectionRead]: + statement = ( + select(Collection).where(Collection.org_id == org_id).distinct(Collection.id) ) + collections = db_session.exec(statement).all() + if not collections: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="No collections found" + ) -#### Security #################################################### + collections_with_courses = [] + for collection in collections: + statement = ( + select(Course) + .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .distinct(Course.id) + ) + courses = db_session.exec(statement).all() + + collection = CollectionRead(**collection.dict(), courses=courses) + collections_with_courses.append(collection) + + return collections_with_courses