From d95497e804214140e98a15ff3d82c48af343caef Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 20 Nov 2023 19:53:39 +0100 Subject: [PATCH] init : more chapters --- apps/api/src/services/courses/chapters.py | 706 ++++++++++++---------- 1 file changed, 403 insertions(+), 303 deletions(-) diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 596b825a..09403480 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -1,7 +1,19 @@ from datetime import datetime -from typing import List, Literal +from typing import List from uuid import uuid4 -from pydantic import BaseModel +from sqlmodel import Session, select +from src import db +from src.db.course_chapters import CourseChapter +from src.db.activities import Activity, ActivityRead +from src.db.chapter_activities import ChapterActivity +from src.db.chapters import ( + Chapter, + ChapterCreate, + ChapterRead, + ChapterUpdate, + ChapterUpdateOrder, + DepreceatedChaptersRead, +) from src.security.auth import non_public_endpoint from src.security.rbac.rbac import ( authorization_verify_based_on_roles, @@ -14,366 +26,454 @@ from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request -class Activity(BaseModel): - name: str - type: str - content: object - -class ActivityInDB(Activity): - activity_id: str - course_id: str - coursechapter_id: str - org_id: str - creationDate: str - updateDate: str - -class CourseChapter(BaseModel): - name: str - description: str - activities: list - - -class CourseChapterInDB(CourseChapter): - coursechapter_id: str - course_id: str - creationDate: str - updateDate: str - - -# Frontend -class CourseChapterMetaData(BaseModel): - chapterOrder: List[str] - chapters: dict - activities: object - - -#### Classes #################################################### - #################################################### # CRUD #################################################### -async def create_coursechapter( +async def create_chapter( request: Request, - coursechapter_object: CourseChapter, - course_id: str, + chapter_object: ChapterCreate, current_user: PublicUser, -): - courses = request.app.db["courses"] - users = request.app.db["users"] - # get course org_id and verify rights - await courses.find_one({"course_id": course_id}) - user = await users.find_one({"user_id": current_user.user_id}) + db_session: Session, +) -> ChapterRead: + chapter = Chapter.from_orm(chapter_object) - # generate coursechapter_id with uuid4 - coursechapter_id = str(f"coursechapter_{uuid4()}") + # complete chapter object + chapter.course_id = chapter_object.course_id + chapter.chapter_uuid = f"chapter_{uuid4()}" + chapter.creation_date = str(datetime.now()) + chapter.update_date = str(datetime.now()) - hasRoleRights = await authorization_verify_based_on_roles( - request, current_user.user_id, "create", user["roles"], course_id + # Find the last chapter in the course and add it to the list + statement = ( + select(CourseChapter) + .where(CourseChapter.course_id == chapter.course_id) + .order_by(CourseChapter.order) ) + course_chapters = db_session.exec(statement).all() - if not hasRoleRights: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Roles : Insufficient rights to perform this action", + # get last chapter order + last_order = course_chapters[-1].order if course_chapters else 0 + to_be_used_order = last_order + 1 + + # Add chapter to database + db_session.add(chapter) + db_session.commit() + db_session.refresh(chapter) + + chapter = ChapterRead(**chapter.dict(), activities=[]) + + # Check if COurseChapter link exists + + statement = ( + select(CourseChapter) + .where(CourseChapter.chapter_id == chapter.id) + .where(CourseChapter.course_id == chapter.course_id) + .where(CourseChapter.order == to_be_used_order) + ) + course_chapter = db_session.exec(statement).first() + + if not course_chapter: + # Add CourseChapter link + course_chapter = CourseChapter( + course_id=chapter.course_id, + chapter_id=chapter.id, + org_id=chapter.org_id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + order=to_be_used_order, ) - coursechapter = CourseChapterInDB( - coursechapter_id=coursechapter_id, - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), - course_id=course_id, - **coursechapter_object.dict(), - ) + # Insert CourseChapter link in DB + db_session.add(course_chapter) + db_session.commit() - courses.update_one( - {"course_id": course_id}, - { - "$addToSet": { - "chapters": coursechapter_id, - "chapters_content": coursechapter.dict(), - } - }, - ) - - return coursechapter.dict() + return chapter -async def get_coursechapter( - request: Request, coursechapter_id: str, current_user: PublicUser -): - courses = request.app.db["courses"] - - coursechapter = await courses.find_one( - {"chapters_content.coursechapter_id": coursechapter_id} - ) - - if coursechapter: - # verify course rights - await verify_rights(request, coursechapter["course_id"], current_user, "read") - coursechapter = CourseChapter(**coursechapter) - - return coursechapter - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="CourseChapter does not exist" - ) - - -async def update_coursechapter( +async def get_chapter( request: Request, - coursechapter_object: CourseChapter, - coursechapter_id: str, + chapter_id: int, current_user: PublicUser, -): - courses = request.app.db["courses"] + db_session: Session, +) -> ChapterRead: + statement = select(Chapter).where(Chapter.id == chapter_id) + chapter = db_session.exec(statement).first() - coursechapter = await courses.find_one( - {"chapters_content.coursechapter_id": coursechapter_id} + if not chapter: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist" + ) + + # Get activities for this chapter + statement = ( + select(Activity) + .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) + .where(ChapterActivity.chapter_id == chapter_id) + .distinct(Activity.id) ) - if coursechapter: - # verify course rights - await verify_rights(request, coursechapter["course_id"], current_user, "update") + activities = db_session.exec(statement).all() - coursechapter = CourseChapterInDB( - coursechapter_id=coursechapter_id, - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), - course_id=coursechapter["course_id"], - **coursechapter_object.dict(), - ) - - courses.update_one( - {"chapters_content.coursechapter_id": coursechapter_id}, - {"$set": {"chapters_content.$": coursechapter.dict()}}, - ) - - return coursechapter - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Coursechapter does not exist" - ) - - -async def delete_coursechapter( - request: Request, coursechapter_id: str, current_user: PublicUser -): - courses = request.app.db["courses"] - - course = await courses.find_one( - {"chapters_content.coursechapter_id": coursechapter_id} + chapter = ChapterRead( + **chapter.dict(), + activities=[ActivityRead(**activity.dict()) for activity in activities], ) - if course: - # verify course rights - await verify_rights(request, course["course_id"], current_user, "delete") + return chapter - # Remove coursechapter from course - await courses.update_one( - {"course_id": course["course_id"]}, - {"$pull": {"chapters": coursechapter_id}}, - ) - await courses.update_one( - {"chapters_content.coursechapter_id": coursechapter_id}, - {"$pull": {"chapters_content": {"coursechapter_id": coursechapter_id}}}, - ) +async def update_chapter( + request: Request, + chapter_object: ChapterUpdate, + current_user: PublicUser, + db_session: Session, +) -> ChapterRead: + statement = select(Chapter).where(Chapter.id == chapter_object.chapter_id) + chapter = db_session.exec(statement).first() - return {"message": "Coursechapter deleted"} - - else: + if not chapter: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist" ) + # Update only the fields that were passed in + for var, value in vars(chapter_object).items(): + if value is not None: + setattr(chapter, var, value) + + chapter.update_date = str(datetime.now()) + + db_session.commit() + db_session.refresh(chapter) + + chapter = ChapterRead(**chapter.dict()) + + return chapter + + +async def delete_chapter( + request: Request, + chapter_id: str, + current_user: PublicUser, + db_session: Session, +): + statement = select(Chapter).where(Chapter.id == chapter_id) + chapter = db_session.exec(statement).first() + + if not chapter: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist" + ) + + db_session.delete(chapter) + db_session.commit() + + # Remove all linked activities + statement = select(ChapterActivity).where(ChapterActivity.chapter_id == chapter_id) + chapter_activities = db_session.exec(statement).all() + + for chapter_activity in chapter_activities: + db_session.delete(chapter_activity) + db_session.commit() + + return {"detail": "chapter deleted"} + #################################################### # Misc #################################################### -async def get_coursechapters( - request: Request, course_id: str, page: int = 1, limit: int = 10 -): - courses = request.app.db["courses"] +async def get_course_chapters( + request: Request, + course_id: int, + db_session: Session, + page: int = 1, + limit: int = 10, +) -> List[ChapterRead]: + statement = select(Chapter).where(Chapter.course_id == course_id) + chapters = db_session.exec(statement).all() - course = await courses.find_one({"course_id": course_id}) - - if course: - course = Course(**course) - coursechapters = course.chapters_content - - return coursechapters - - -async def get_coursechapters_meta( - request: Request, course_id: str, current_user: PublicUser -): - courses = request.app.db["courses"] - activities = request.app.db["activities"] - - await non_public_endpoint(current_user) - - await verify_rights(request, course_id, current_user, "read") - - coursechapters = await courses.find_one( - {"course_id": course_id}, {"chapters": 1, "chapters_content": 1, "_id": 0} - ) - - coursechapters = coursechapters - - if not coursechapters: + if not chapters: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=status.HTTP_409_CONFLICT, detail="Course do not have chapters" ) - # activities - coursechapter_activityIds_global = [] + chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] - # chapters - chapters = {} - if coursechapters["chapters_content"]: - for coursechapter in coursechapters["chapters_content"]: - coursechapter = CourseChapterInDB(**coursechapter) - coursechapter_activityIds = [] + # Get activities for each chapter + for chapter in chapters: + statement = ( + select(ChapterActivity) + .where(ChapterActivity.chapter_id == chapter.id) + .order_by(ChapterActivity.order) + .distinct(ChapterActivity.id, ChapterActivity.order) + ) + chapter_activities = db_session.exec(statement).all() - for activity in coursechapter.activities: - coursechapter_activityIds.append(activity) - coursechapter_activityIds_global.append(activity) + for chapter_activity in chapter_activities: + statement = ( + select(Activity) + .where(Activity.id == chapter_activity.activity_id) + .distinct(Activity.id) + ) + activity = db_session.exec(statement).first() - chapters[coursechapter.coursechapter_id] = { - "id": coursechapter.coursechapter_id, - "name": coursechapter.name, - "activityIds": coursechapter_activityIds, - } + if activity: + chapter.activities.append(ActivityRead(**activity.dict())) - # activities - activities_list = {} - for activity in await activities.find( - {"activity_id": {"$in": coursechapter_activityIds_global}} - ).to_list(length=100): - activity = ActivityInDB(**activity) - activities_list[activity.activity_id] = { - "id": activity.activity_id, - "name": activity.name, - "type": activity.type, - "content": activity.content, - } - - final = { - "chapters": chapters, - "chapterOrder": coursechapters["chapters"], - "activities": activities_list, - } - - return final + return chapters -async def update_coursechapters_meta( +async def get_depreceated_course_chapters( request: Request, - course_id: str, - coursechapters_metadata: CourseChapterMetaData, + course_id: int, current_user: PublicUser, -): - courses = request.app.db["courses"] - - await verify_rights(request, course_id, current_user, "update") - - # update chapters in course - await courses.update_one( - {"course_id": course_id}, - {"$set": {"chapters": coursechapters_metadata.chapterOrder}}, - ) - - if coursechapters_metadata.chapters is not None: - for ( - coursechapter_id, - chapter_metadata, - ) in coursechapters_metadata.chapters.items(): - filter_query = {"chapters_content.coursechapter_id": coursechapter_id} - update_query = { - "$set": { - "chapters_content.$.activities": chapter_metadata["activityIds"] - } - } - result = await courses.update_one(filter_query, update_query) - if result.matched_count == 0: - # handle error when no documents are matched by the filter query - print(f"No documents found for course chapter ID {coursechapter_id}") - - # update activities in coursechapters - activity = request.app.db["activities"] - if coursechapters_metadata.chapters is not None: - for ( - coursechapter_id, - chapter_metadata, - ) in coursechapters_metadata.chapters.items(): - # Update coursechapter_id in activities - filter_query = {"activity_id": {"$in": chapter_metadata["activityIds"]}} - update_query = {"$set": {"coursechapter_id": coursechapter_id}} - - result = await activity.update_many(filter_query, update_query) - if result.matched_count == 0: - # handle error when no documents are matched by the filter query - print(f"No documents found for course chapter ID {coursechapter_id}") - - return {"detail": "coursechapters metadata updated"} - - -#### Security #################################################### - - -async def verify_rights( - request: Request, - course_id: str, - current_user: PublicUser, - action: Literal["read", "update", "delete"], -): - courses = request.app.db["courses"] - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) - course = await courses.find_one({"course_id": course_id}) + db_session: Session, +) -> DepreceatedChaptersRead: + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() if not course: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" ) - if action == "read": - if current_user.user_id == "anonymous": - await authorization_verify_if_element_is_public( - request, course_id, current_user.user_id, action + # Get chapters that are linked to his course and order them by order, using the order field in the CourseChapter table + statement = ( + select(Chapter) + .join(CourseChapter, Chapter.id == CourseChapter.chapter_id) + .where(CourseChapter.course_id == course_id) + .order_by(CourseChapter.order) + .group_by(Chapter.id, CourseChapter.order) + ) + print("ded", statement) + chapters = db_session.exec(statement).all() + + chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] + + # Get activities for each chapter + for chapter in chapters: + statement = ( + select(Activity) + .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) + .where(ChapterActivity.chapter_id == chapter.id) + .order_by(ChapterActivity.order) + .distinct(Activity.id, ChapterActivity.order) + ) + chapter_activities = db_session.exec(statement).all() + + for chapter_activity in chapter_activities: + statement = ( + select(Activity) + .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) + .where(Activity.id == chapter_activity.id) + .distinct(Activity.id, ChapterActivity.order) + .order_by(ChapterActivity.order) ) - else: - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) + activity = db_session.exec(statement).first() - await authorization_verify_if_user_is_anon(current_user.user_id) + if activity: + chapter.activities.append(ActivityRead(**activity.dict())) - await authorization_verify_based_on_roles_and_authorship( - request, - current_user.user_id, - action, - user["roles"], - course_id, - ) - else: - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) + # Get a list of chapter ids + chapter_order: List[str] = [str(chapter.id) for chapter in chapters] - await authorization_verify_if_user_is_anon(current_user.user_id) + # Get activities for each chapter + activities = [] + for chapter_id in chapter_order: + # order by activity order + statement = ( + select(Activity) + .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) + .where(ChapterActivity.chapter_id == chapter_id) + .order_by(ChapterActivity.order) + .distinct(Activity.id, ChapterActivity.order) + ) + chapter_activities = db_session.exec(statement).all() - await authorization_verify_based_on_roles_and_authorship( - request, - current_user.user_id, - action, - user["roles"], - course_id, + activities.extend(chapter_activities) + + result = DepreceatedChaptersRead( + chapter_order=chapter_order, chapters=chapters, activities=activities + ) + + return result + + +async def reorder_chapters_and_activities( + request: Request, + course_id: int, + chapters_order: ChapterUpdateOrder, + current_user: PublicUser, + db_session: Session, +): + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" ) + ########### + # Chapters + ########### -#### Security #################################################### + # Delete CourseChapters that are not linked to chapter_id and activity_id and org_id and course_id + statement = ( + select(CourseChapter) + .where( + CourseChapter.course_id == course_id, CourseChapter.org_id == course.org_id + ) + .order_by(CourseChapter.order) + ) + course_chapters = db_session.exec(statement).all() + + chapter_ids_to_keep = [ + chapter_order.chapter_id + for chapter_order in chapters_order.chapter_order_by_ids + ] + for course_chapter in course_chapters: + if course_chapter.chapter_id not in chapter_ids_to_keep: + db_session.delete(course_chapter) + db_session.commit() + + # Delete Chapters that are not in the list of chapters_order + statement = select(Chapter).where(Chapter.course_id == course_id) + chapters = db_session.exec(statement).all() + + chapter_ids_to_keep = [ + chapter_order.chapter_id + for chapter_order in chapters_order.chapter_order_by_ids + ] + + for chapter in chapters: + if chapter.id not in chapter_ids_to_keep: + db_session.delete(chapter) + db_session.commit() + + # If links do not exists, create them + for chapter_order in chapters_order.chapter_order_by_ids: + statement = ( + select(CourseChapter) + .where( + CourseChapter.chapter_id == chapter_order.chapter_id, + CourseChapter.course_id == course_id, + ) + .order_by(CourseChapter.order) + ) + course_chapter = db_session.exec(statement).first() + + if not course_chapter: + # Add CourseChapter link + course_chapter = CourseChapter( + chapter_id=chapter_order.chapter_id, + course_id=course_id, + org_id=course.org_id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + order=chapter_order.chapter_id, + ) + + # Insert CourseChapter link in DB + db_session.add(course_chapter) + db_session.commit() + + # Update order of chapters + for chapter_order in chapters_order.chapter_order_by_ids: + statement = ( + select(CourseChapter) + .where( + CourseChapter.chapter_id == chapter_order.chapter_id, + CourseChapter.course_id == course_id, + ) + .order_by(CourseChapter.order) + ) + course_chapter = db_session.exec(statement).first() + + if course_chapter: + # Get the order from the index of the chapter_order_by_ids list + course_chapter.order = chapters_order.chapter_order_by_ids.index( + chapter_order + ) + db_session.commit() + + ########### + # Activities + ########### + + # Delete ChapterActivities that are not linked to chapter_id and activity_id and org_id and course_id + statement = ( + select(ChapterActivity) + .where( + ChapterActivity.course_id == course_id, + ChapterActivity.org_id == course.org_id, + ) + .order_by(ChapterActivity.order) + ) + chapter_activities = db_session.exec(statement).all() + + activity_ids_to_keep = [ + activity_order.activity_id + for chapter_order in chapters_order.chapter_order_by_ids + for activity_order in chapter_order.activities_order_by_ids + ] + + for chapter_activity in chapter_activities: + if chapter_activity.activity_id not in activity_ids_to_keep: + db_session.delete(chapter_activity) + db_session.commit() + + # If links do not exists, create them + for chapter_order in chapters_order.chapter_order_by_ids: + for activity_order in chapter_order.activities_order_by_ids: + statement = ( + select(ChapterActivity) + .where( + ChapterActivity.chapter_id == chapter_order.chapter_id, + ChapterActivity.activity_id == activity_order.activity_id, + ) + .order_by(ChapterActivity.order) + ) + chapter_activity = db_session.exec(statement).first() + + if not chapter_activity: + # Add ChapterActivity link + chapter_activity = ChapterActivity( + chapter_id=chapter_order.chapter_id, + activity_id=activity_order.activity_id, + org_id=course.org_id, + course_id=course_id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + order=activity_order.activity_id, + ) + + # Insert ChapterActivity link in DB + db_session.add(chapter_activity) + db_session.commit() + + # Update order of activities + for chapter_order in chapters_order.chapter_order_by_ids: + for activity_order in chapter_order.activities_order_by_ids: + statement = ( + select(ChapterActivity) + .where( + ChapterActivity.chapter_id == chapter_order.chapter_id, + ChapterActivity.activity_id == activity_order.activity_id, + ) + .order_by(ChapterActivity.order) + ) + chapter_activity = db_session.exec(statement).first() + + if chapter_activity: + # Get the order from the index of the chapter_order_by_ids list + chapter_activity.order = chapter_order.activities_order_by_ids.index( + activity_order + ) + db_session.commit() + + return {"detail": "Chapters reordered"}