From 187f75e58377b2a5f7839e85b0737b99c9fb54da Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 29 Nov 2023 22:29:48 +0100 Subject: [PATCH] feat: various improvements wip: frontend feat: enable cascade on foreign keys wip1 wip2 fix chapters issues wip4 --- apps/api/src/core/events/database.py | 2 +- apps/api/src/db/activities.py | 20 ++- apps/api/src/db/blocks.py | 8 +- apps/api/src/db/chapter_activities.py | 7 +- apps/api/src/db/chapters.py | 21 ++- apps/api/src/db/collections.py | 1 - apps/api/src/db/collections_courses.py | 5 +- apps/api/src/db/course_chapters.py | 14 +- apps/api/src/db/courses.py | 7 +- apps/api/src/db/organization_settings.py | 6 +- apps/api/src/db/user_organizations.py | 5 +- apps/api/src/routers/courses/chapters.py | 20 +-- apps/api/src/routers/courses/collections.py | 19 +- apps/api/src/routers/courses/courses.py | 38 ++-- apps/api/src/routers/trail.py | 14 +- apps/api/src/routers/users.py | 18 ++ .../services/courses/activities/activities.py | 14 +- .../src/services/courses/activities/pdf.py | 2 +- .../src/services/courses/activities/video.py | 6 + apps/api/src/services/courses/chapters.py | 165 ++++++++++-------- apps/api/src/services/courses/collections.py | 19 +- apps/api/src/services/courses/courses.py | 118 ++++++++++--- apps/api/src/services/orgs/orgs.py | 2 - apps/api/src/services/trail/trail.py | 26 +-- apps/api/src/services/users/users.py | 33 +++- .../collection/[collectionid]/page.tsx | 6 +- .../(withmenu)/collections/new/page.tsx | 45 ++--- .../[orgslug]/(withmenu)/collections/page.tsx | 14 +- .../activity/[activityid]/activity.tsx | 4 +- .../activity/[activityid]/error.tsx | 0 .../activity/[activityid]/loading.tsx | 0 .../activity/[activityid]/page.tsx | 0 .../{[courseid] => [courseuuid]}/course.tsx | 80 +++++---- .../edit/[[...subpage]]/edit.tsx | 65 +++++-- .../edit/[[...subpage]]/page.tsx | 15 +- .../edit/subpages/CourseContentEdition.tsx | 80 ++++----- .../edit/subpages/CourseEdition.tsx | 20 +-- .../{[courseid] => [courseuuid]}/error.tsx | 0 .../{[courseid] => [courseuuid]}/loading.tsx | 0 .../{[courseid] => [courseuuid]}/page.tsx | 24 +-- .../[orgslug]/(withmenu)/courses/courses.tsx | 12 +- .../app/orgs/[orgslug]/(withmenu)/page.tsx | 18 +- .../app/orgs/[orgslug]/settings/layout.tsx | 12 +- .../Activities/DocumentPdf/DocumentPdf.tsx | 2 +- .../Objects/Activities/Video/Video.tsx | 2 +- apps/web/components/Objects/Editor/Editor.tsx | 10 +- .../Extensions/Image/ImageBlockComponent.tsx | 2 +- .../Extensions/PDF/PDFBlockComponent.tsx | 2 +- .../Extensions/Video/VideoBlockComponent.tsx | 2 +- .../Modals/Activities/Create/NewActivity.tsx | 8 +- .../Create/NewActivityModal/DocumentPdf.tsx | 10 +- .../Create/NewActivityModal/DynamicCanva.tsx | 11 +- .../Create/NewActivityModal/Video.tsx | 23 ++- .../Objects/Modals/Chapters/NewChapter.tsx | 20 ++- .../Modals/Course/Create/CreateCourse.tsx | 44 ++++- .../Objects/Other/CollectionThumbnail.tsx | 15 +- .../Objects/Other/CourseThumbnail.tsx | 21 ++- .../Pages/CourseEdit/Draggables/Activity.tsx | 8 +- .../Pages/CourseEdit/Draggables/Chapter.tsx | 8 +- .../Pages/Courses/ActivityIndicators.tsx | 26 +-- .../Pages/Trail/TrailCourseElement.tsx | 10 +- .../Security/AuthenticatedClientElement.tsx | 57 +++--- .../components/Security/HeaderProfileBox.tsx | 4 +- apps/web/package-lock.json | 96 +++++----- apps/web/package.json | 6 +- apps/web/services/auth/auth.ts | 2 +- apps/web/services/courses/activities.ts | 9 +- apps/web/services/courses/activity.ts | 12 +- apps/web/services/courses/chapters.ts | 14 +- apps/web/services/courses/collections.ts | 14 +- apps/web/services/courses/courses.ts | 24 +-- 71 files changed, 879 insertions(+), 568 deletions(-) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/activity/[activityid]/activity.tsx (96%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/activity/[activityid]/error.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/activity/[activityid]/loading.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/activity/[activityid]/page.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/course.tsx (77%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/edit/[[...subpage]]/edit.tsx (66%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/edit/[[...subpage]]/page.tsx (64%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/edit/subpages/CourseContentEdition.tsx (82%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/edit/subpages/CourseEdition.tsx (82%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/error.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/loading.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/page.tsx (68%) diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index deb88120..43f6e1af 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -5,7 +5,7 @@ from sqlmodel import SQLModel, Session, create_engine engine = create_engine( - "postgresql://learnhouse:learnhouse@db:5432/learnhouse", echo=True + "postgresql://learnhouse:learnhouse@db:5432/learnhouse", echo=False ) SQLModel.metadata.create_all(engine) diff --git a/apps/api/src/db/activities.py b/apps/api/src/db/activities.py index bae65eb4..ada0d56d 100644 --- a/apps/api/src/db/activities.py +++ b/apps/api/src/db/activities.py @@ -1,5 +1,5 @@ from typing import Optional -from sqlalchemy import JSON, Column +from sqlalchemy import JSON, BigInteger, Column, ForeignKey from sqlmodel import Field, SQLModel from enum import Enum @@ -34,20 +34,32 @@ class ActivityBase(SQLModel): content: dict = Field(default={}, sa_column=Column(JSON)) published_version: int version: int - course_id: int = Field(default=None, foreign_key="course.id") + course_id: int = Field( + default=None, + sa_column=Column( + BigInteger, ForeignKey("course.id", ondelete="CASCADE") + ), + ) class Activity(ActivityBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) org_id: int = Field(default=None, foreign_key="organization.id") + course_id: int = Field( + default=None, + sa_column=Column( + BigInteger, ForeignKey("course.id", ondelete="CASCADE") + ), + ) activity_uuid: str = "" creation_date: str = "" update_date: str = "" class ActivityCreate(ActivityBase): - org_id: int = Field(default=None, foreign_key="organization.id") - course_id: int = Field(default=None, foreign_key="course.id") + course_id: int = Field( + sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) + ) chapter_id: int pass diff --git a/apps/api/src/db/blocks.py b/apps/api/src/db/blocks.py index 7fb2adb4..59972a04 100644 --- a/apps/api/src/db/blocks.py +++ b/apps/api/src/db/blocks.py @@ -1,5 +1,5 @@ from typing import Optional -from sqlalchemy import JSON, Column +from sqlalchemy import JSON, Column, ForeignKey from sqlmodel import Field, SQLModel from enum import Enum @@ -22,9 +22,9 @@ class Block(BlockBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) content: dict = Field(default={}, sa_column=Column(JSON)) org_id: int = Field(default=None, foreign_key="organization.id") - course_id: int = Field(default=None, foreign_key="course.id") - chapter_id: int = Field(default=None, foreign_key="chapter.id") - activity_id: int = Field(default=None, foreign_key="activity.id") + course_id: int = Field(sa_column= Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))) + chapter_id: int = Field(sa_column= Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE"))) + activity_id: int = Field(sa_column= Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE"))) block_uuid: str creation_date: str update_date: str diff --git a/apps/api/src/db/chapter_activities.py b/apps/api/src/db/chapter_activities.py index b567e0de..936078d9 100644 --- a/apps/api/src/db/chapter_activities.py +++ b/apps/api/src/db/chapter_activities.py @@ -1,12 +1,13 @@ from typing import Optional +from sqlalchemy import BigInteger, Column, ForeignKey from sqlmodel import Field, SQLModel class ChapterActivity(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) order: int - chapter_id: int = Field(default=None, foreign_key="chapter.id", ) - activity_id: int = Field(default=None, foreign_key="activity.id") - course_id : int = Field(default=None, foreign_key="course.id") + chapter_id: int = Field(sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE"))) + activity_id: int = Field(sa_column=Column(BigInteger, ForeignKey("activity.id", ondelete="CASCADE"))) + course_id : int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE"))) org_id : int = Field(default=None, foreign_key="organization.id") creation_date: str update_date: str \ No newline at end of file diff --git a/apps/api/src/db/chapters.py b/apps/api/src/db/chapters.py index d44efe67..4e94dc62 100644 --- a/apps/api/src/db/chapters.py +++ b/apps/api/src/db/chapters.py @@ -1,5 +1,6 @@ -from typing import List, Optional +from typing import Any, List, Optional from pydantic import BaseModel +from sqlalchemy import Column, ForeignKey from sqlmodel import Field, SQLModel from src.db.activities import ActivityRead @@ -9,19 +10,21 @@ class ChapterBase(SQLModel): description: Optional[str] = "" thumbnail_image: Optional[str] = "" org_id: int = Field(default=None, foreign_key="organization.id") - course_id: int = Field(default=None, foreign_key="course.id") - creation_date: str - update_date: str + course_id: int = Field( + sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) + ) class Chapter(ChapterBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) + course_id: int = Field( + sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) + ) chapter_uuid: str = "" creation_date: str = "" update_date: str = "" - class ChapterCreate(ChapterBase): # referenced order here will be ignored and just used for validation # used order will be the next available. @@ -32,6 +35,8 @@ class ChapterUpdate(ChapterBase): name: Optional[str] description: Optional[str] thumbnail_image: Optional[str] + course_id: Optional[int] + org_id: Optional[int] class ChapterRead(ChapterBase): @@ -57,7 +62,7 @@ class ChapterUpdateOrder(BaseModel): class DepreceatedChaptersRead(BaseModel): - chapter_order: list[str] - chapters: List[ChapterRead] - activities: List[ActivityRead] + chapterOrder: Any + chapters: Any + activities: Any pass diff --git a/apps/api/src/db/collections.py b/apps/api/src/db/collections.py index f3cbd850..9b191c8d 100644 --- a/apps/api/src/db/collections.py +++ b/apps/api/src/db/collections.py @@ -24,7 +24,6 @@ class CollectionCreate(CollectionBase): class CollectionUpdate(CollectionBase): - collection_id: int courses: Optional[list] name: Optional[str] public: Optional[bool] diff --git a/apps/api/src/db/collections_courses.py b/apps/api/src/db/collections_courses.py index 7ec5ff1b..6b30c2ee 100644 --- a/apps/api/src/db/collections_courses.py +++ b/apps/api/src/db/collections_courses.py @@ -1,11 +1,12 @@ from typing import Optional +from sqlalchemy import BigInteger, Column, ForeignKey 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") + collection_id: int = Field(sa_column=Column(BigInteger, ForeignKey("collection.id", ondelete="CASCADE"))) + course_id: int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE"))) org_id: int = Field(default=None, foreign_key="organization.id") creation_date: str update_date: str diff --git a/apps/api/src/db/course_chapters.py b/apps/api/src/db/course_chapters.py index 1d9f0990..dec820c6 100644 --- a/apps/api/src/db/course_chapters.py +++ b/apps/api/src/db/course_chapters.py @@ -1,11 +1,17 @@ from typing import Optional +from sqlalchemy import BigInteger, Column, ForeignKey from sqlmodel import Field, SQLModel + class CourseChapter(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) order: int - course_id: int = Field(default=None, foreign_key="course.id") - chapter_id: int = Field(default=None, foreign_key="chapter.id") - org_id : int = Field(default=None, foreign_key="organization.id") + course_id: int = Field( + sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")) + ) + chapter_id: int = Field( + sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE")) + ) + org_id: int = Field(default=None, foreign_key="organization.id") creation_date: str - update_date: str \ No newline at end of file + update_date: str diff --git a/apps/api/src/db/courses.py b/apps/api/src/db/courses.py index bb726803..686538a2 100644 --- a/apps/api/src/db/courses.py +++ b/apps/api/src/db/courses.py @@ -1,5 +1,6 @@ from typing import List, Optional from sqlmodel import Field, SQLModel +from src.db.users import User, UserRead from src.db.trails import TrailRead from src.db.chapters import ChapterRead @@ -22,7 +23,6 @@ class Course(CourseBase, table=True): update_date: str = "" - class CourseCreate(CourseBase): org_id: int = Field(default=None, foreign_key="organization.id") pass @@ -40,6 +40,7 @@ class CourseUpdate(CourseBase): class CourseRead(CourseBase): id: int org_id: int = Field(default=None, foreign_key="organization.id") + authors: List[UserRead] course_uuid: str creation_date: str update_date: str @@ -53,6 +54,7 @@ class FullCourseRead(CourseBase): update_date: str # Chapters, Activities chapters: List[ChapterRead] + authors: List[UserRead] pass @@ -61,8 +63,9 @@ class FullCourseReadWithTrail(CourseBase): course_uuid: str creation_date: str update_date: str + authors: List[UserRead] # Chapters, Activities chapters: List[ChapterRead] # Trail - trail: TrailRead + trail: TrailRead | None pass diff --git a/apps/api/src/db/organization_settings.py b/apps/api/src/db/organization_settings.py index 4c464b5d..babdef08 100644 --- a/apps/api/src/db/organization_settings.py +++ b/apps/api/src/db/organization_settings.py @@ -1,7 +1,9 @@ from typing import Optional +from sqlalchemy import BigInteger, Column, ForeignKey from sqlmodel import Field, SQLModel from enum import Enum + class HeaderTypeEnum(str, Enum): LOGO_MENU_SETTINGS = "LOGO_MENU_SETTINGS" MENU_LOGO_SETTINGS = "MENU_LOGO_SETTINGS" @@ -9,7 +11,9 @@ class HeaderTypeEnum(str, Enum): class OrganizationSettings(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - org_id: int = Field(default=None, foreign_key="organization.id") + org_id: int = Field( + sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE")) + ) logo_image: Optional[str] = "" header_type: HeaderTypeEnum = HeaderTypeEnum.LOGO_MENU_SETTINGS color: str = "" diff --git a/apps/api/src/db/user_organizations.py b/apps/api/src/db/user_organizations.py index bb70a5fd..c842d41c 100644 --- a/apps/api/src/db/user_organizations.py +++ b/apps/api/src/db/user_organizations.py @@ -1,11 +1,14 @@ from typing import Optional +from sqlalchemy import BigInteger, Column, ForeignKey from sqlmodel import Field, SQLModel class UserOrganization(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) user_id: int = Field(default=None, foreign_key="user.id") - org_id: int = Field(default=None, foreign_key="organization.id") + org_id: int = Field( + sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE")) + ) role_id: int = Field(default=None, foreign_key="role.id") creation_date: str update_date: str diff --git a/apps/api/src/routers/courses/chapters.py b/apps/api/src/routers/courses/chapters.py index bcd3c34c..be87247d 100644 --- a/apps/api/src/routers/courses/chapters.py +++ b/apps/api/src/routers/courses/chapters.py @@ -9,11 +9,11 @@ from src.db.chapters import ( DepreceatedChaptersRead, ) from src.services.courses.chapters import ( + DEPRECEATED_get_course_chapters, create_chapter, delete_chapter, get_chapter, get_course_chapters, - get_depreceated_course_chapters, reorder_chapters_and_activities, update_chapter, ) @@ -50,25 +50,25 @@ async def api_get_coursechapter( return await get_chapter(request, chapter_id, current_user, db_session) -@router.get("/course/{course_id}/meta") +@router.get("/course/{course_uuid}/meta", deprecated=True) async def api_get_chapter_meta( request: Request, - course_id: int, + course_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -) -> DepreceatedChaptersRead: +): """ Get Chapters metadata """ - return await get_depreceated_course_chapters( - request, course_id, current_user, db_session + return await DEPRECEATED_get_course_chapters( + request, course_uuid, current_user, db_session ) -@router.put("/course/{course_id}/order") +@router.put("/course/{course_uuid}/order") async def api_update_chapter_meta( request: Request, - course_id: int, + course_uuid: str, order: ChapterUpdateOrder, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), @@ -77,7 +77,7 @@ async def api_update_chapter_meta( Update Chapter metadata """ return await reorder_chapters_and_activities( - request, course_id, order, current_user, db_session + request, course_uuid, order, current_user, db_session ) @@ -117,7 +117,7 @@ async def api_update_coursechapter( @router.delete("/{chapter_id}") async def api_delete_coursechapter( request: Request, - chapter_id: int, + chapter_id: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ): diff --git a/apps/api/src/routers/courses/collections.py b/apps/api/src/routers/courses/collections.py index ba960848..12fcfb66 100644 --- a/apps/api/src/routers/courses/collections.py +++ b/apps/api/src/routers/courses/collections.py @@ -29,17 +29,17 @@ async def api_create_collection( return await create_collection(request, collection_object, current_user, db_session) -@router.get("/{collection_id}") +@router.get("/{collection_uuid}") async def api_get_collection( request: Request, - collection_id: str, + collection_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ) -> CollectionRead: """ Get single collection by ID """ - return await get_collection(request, collection_id, current_user, db_session) + return await get_collection(request, collection_uuid, current_user, db_session) @router.get("/org/{org_id}/page/{page}/limit/{limit}") @@ -57,23 +57,26 @@ async def api_get_collections_by( return await get_collections(request, org_id, current_user, db_session, page, limit) -@router.put("/{collection_id}") +@router.put("/{collection_uuid}") async def api_update_collection( request: Request, collection_object: CollectionUpdate, + collection_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ) -> CollectionRead: """ Update collection by ID """ - return await update_collection(request, collection_object, current_user, db_session) + return await update_collection( + request, collection_object, collection_uuid, current_user, db_session + ) -@router.delete("/{collection_id}") +@router.delete("/{collection_uuid}") async def api_delete_collection( request: Request, - collection_id: str, + collection_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ): @@ -81,4 +84,4 @@ async def api_delete_collection( Delete collection by ID """ - return await delete_collection(request, collection_id, current_user, db_session) + return await delete_collection(request, collection_uuid, current_user, db_session) diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index e6669738..f9485e80 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -51,13 +51,13 @@ async def api_create_course( learnings=learnings, tags=tags, ) - return await create_course(request, course, current_user, db_session, thumbnail) + return await create_course(request, org_id, course, current_user, db_session, thumbnail) -@router.put("/{course_id}/thumbnail") +@router.put("/{course_uuid}/thumbnail") async def api_create_course_thumbnail( request: Request, - course_id: str, + course_uuid: str, thumbnail: UploadFile | None = None, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), @@ -66,37 +66,37 @@ async def api_create_course_thumbnail( Update new Course Thumbnail """ return await update_course_thumbnail( - request, course_id, current_user, db_session, thumbnail + request, course_uuid, current_user, db_session, thumbnail ) -@router.get("/{course_id}") +@router.get("/{course_uuid}") async def api_get_course( request: Request, - course_id: str, + course_uuid: str, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), ) -> CourseRead: """ - Get single Course by course_id + Get single Course by course_uuid """ return await get_course( - request, course_id, current_user=current_user, db_session=db_session + request, course_uuid, current_user=current_user, db_session=db_session ) -@router.get("/{course_id}/meta") +@router.get("/{course_uuid}/meta") async def api_get_course_meta( request: Request, - course_id: int, + course_uuid: str, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), ) -> FullCourseReadWithTrail: """ - Get single Course Metadata (chapters, activities) by course_id + Get single Course Metadata (chapters, activities) by course_uuid """ return await get_course_meta( - request, course_id, current_user=current_user, db_session=db_session + request, course_uuid, current_user=current_user, db_session=db_session ) @@ -117,26 +117,26 @@ async def api_get_course_by_orgslug( ) -@router.put("/{course_id}") +@router.put("/{course_uuid}") async def api_update_course( request: Request, course_object: CourseUpdate, - course_id: int, + course_uuid: str, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), ) -> CourseRead: """ - Update Course by course_id + Update Course by course_uuid """ return await update_course( - request, course_object, course_id, current_user, db_session + request, course_object, course_uuid, current_user, db_session ) -@router.delete("/{course_id}") +@router.delete("/{course_uuid}") async def api_delete_course( request: Request, - course_id: str, + course_uuid: str, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), ): @@ -144,4 +144,4 @@ async def api_delete_course( Delete Course by ID """ - return await delete_course(request, course_id, current_user, db_session) + return await delete_course(request, course_uuid, current_user, db_session) diff --git a/apps/api/src/routers/trail.py b/apps/api/src/routers/trail.py index 0c6455e5..1c1c2fa5 100644 --- a/apps/api/src/routers/trail.py +++ b/apps/api/src/routers/trail.py @@ -56,33 +56,33 @@ async def api_get_trail_by_org_id( ) -@router.post("/add_course/{course_id}") +@router.post("/add_course/{course_uuid}") async def api_add_course_to_trail( request: Request, - course_id: str, + course_uuid: str, user=Depends(get_current_user), db_session=Depends(get_db_session), ) -> TrailRead: """ Add Course to trail """ - return await add_course_to_trail(request, user, course_id, db_session) + return await add_course_to_trail(request, user, course_uuid, db_session) -@router.delete("/remove_course/{course_id}") +@router.delete("/remove_course/{course_uuid}") async def api_remove_course_to_trail( request: Request, - course_id: str, + course_uuid: str, user=Depends(get_current_user), db_session=Depends(get_db_session), ) -> TrailRead: """ Remove Course from trail """ - return await remove_course_from_trail(request, user, course_id, db_session) + return await remove_course_from_trail(request, user, course_uuid, db_session) -@router.post("/add_activity/{activity_id}") +@router.post("/add_activity/{activity_uuid}") async def api_add_activity_to_trail( request: Request, activity_id: int, diff --git a/apps/api/src/routers/users.py b/apps/api/src/routers/users.py index cf6e81f9..71ec2741 100644 --- a/apps/api/src/routers/users.py +++ b/apps/api/src/routers/users.py @@ -1,3 +1,4 @@ +from typing import Literal from fastapi import APIRouter, Depends, Request from sqlmodel import Session from src.security.auth import get_current_user @@ -12,6 +13,7 @@ from src.db.users import ( UserUpdatePassword, ) from src.services.users.users import ( + authorize_user_action, create_user, create_user_without_org, delete_user_by_id, @@ -33,6 +35,22 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)): return current_user.dict() +@router.get("/authorize/ressource/{ressource_uuid}/action/{action}") +async def api_get_authorization_status( + request: Request, + ressource_uuid: str, + action: Literal["create", "read", "update", "delete"], + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): + """ + Get current user authorization status + """ + return await authorize_user_action( + request, db_session, current_user, ressource_uuid, action + ) + + @router.post("/{org_id}", response_model=UserRead, tags=["users"]) async def api_create_user_with_orgid( *, diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index dc4dfda5..0912937d 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -1,5 +1,6 @@ from typing import Literal from sqlmodel import Session, select +from src.db.chapters import Chapter from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, @@ -27,21 +28,22 @@ async def create_activity( activity = Activity.from_orm(activity_object) # CHeck if org exists - statement = select(Organization).where(Organization.id == activity_object.org_id) - org = db_session.exec(statement).first() + statement = select(Chapter).where(Chapter.id == activity_object.chapter_id) + chapter = db_session.exec(statement).first() - if not org: + if not chapter: raise HTTPException( status_code=404, - detail="Organization not found", + detail="Chapter not found", ) # RBAC check - await rbac_check(request, org.org_uuid, current_user, "create", db_session) + await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session) activity.activity_uuid = str(f"activity_{uuid4()}") activity.creation_date = str(datetime.now()) activity.update_date = str(datetime.now()) + activity.org_id = chapter.org_id # Insert Activity in DB db_session.add(activity) @@ -64,7 +66,7 @@ async def create_activity( chapter_id=activity_object.chapter_id, activity_id=activity.id if activity.id else 0, course_id=activity_object.course_id, - org_id=activity_object.org_id, + org_id=chapter.org_id, creation_date=str(datetime.now()), update_date=str(datetime.now()), order=to_be_used_order, diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index 5d728709..cc9c9a8f 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -51,7 +51,7 @@ async def create_documentpdf_activity( ) # get org_id - org_id = coursechapter.id + org_id = coursechapter.org_id # create activity uuid activity_uuid = f"activity_{uuid4()}" diff --git a/apps/api/src/services/courses/activities/video.py b/apps/api/src/services/courses/activities/video.py index a1d3beda..8e25b9de 100644 --- a/apps/api/src/services/courses/activities/video.py +++ b/apps/api/src/services/courses/activities/video.py @@ -82,6 +82,8 @@ async def create_video_activity( activity_type=ActivityTypeEnum.TYPE_VIDEO, activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_HOSTED, activity_uuid=activity_uuid, + org_id=coursechapter.org_id, + course_id=coursechapter.course_id, published_version=1, content={ "filename": "video." + video_format, @@ -171,6 +173,8 @@ async def create_external_video_activity( activity_type=ActivityTypeEnum.TYPE_VIDEO, activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_YOUTUBE, activity_uuid=activity_uuid, + course_id=coursechapter.course_id, + org_id=coursechapter.org_id, published_version=1, content={ "uri": data.uri, @@ -192,6 +196,8 @@ async def create_external_video_activity( chapter_activity_object = ChapterActivity( chapter_id=coursechapter.id, # type: ignore activity_id=activity.id, # type: ignore + course_id=coursechapter.course_id, + org_id=coursechapter.org_id, creation_date=str(datetime.now()), update_date=str(datetime.now()), order=1, diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 51830815..f610d6fe 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -36,6 +36,11 @@ async def create_chapter( ) -> ChapterRead: chapter = Chapter.from_orm(chapter_object) + # Get COurse + statement = select(Course).where(Course.id == chapter_object.course_id) + + course = db_session.exec(statement).one() + # RBAC check await rbac_check(request, "chapter_x", current_user, "create", db_session) @@ -44,6 +49,7 @@ async def create_chapter( chapter.chapter_uuid = f"chapter_{uuid4()}" chapter.creation_date = str(datetime.now()) chapter.update_date = str(datetime.now()) + chapter.org_id = course.org_id # Find the last chapter in the course and add it to the list statement = ( @@ -155,14 +161,17 @@ async def update_chapter( db_session.commit() db_session.refresh(chapter) - chapter = ChapterRead(**chapter.dict()) + if chapter: + chapter = await get_chapter( + request, chapter.id, current_user, db_session # type: ignore + ) return chapter async def delete_chapter( request: Request, - chapter_id: int, + chapter_id: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): @@ -181,7 +190,7 @@ async def delete_chapter( db_session.commit() # Remove all linked activities - statement = select(ChapterActivity).where(ChapterActivity.chapter_id == chapter_id) + statement = select(ChapterActivity).where(ChapterActivity.id == chapter.id) chapter_activities = db_session.exec(statement).all() for chapter_activity in chapter_activities: @@ -199,14 +208,16 @@ async def get_course_chapters( page: int = 1, limit: int = 10, ) -> List[ChapterRead]: - statement = select(Chapter).where(Chapter.course_id == course_id) + statement = ( + select(Chapter) + .join(CourseChapter, Chapter.id == CourseChapter.chapter_id) + .where(CourseChapter.course_id == course_id) + .where(Chapter.course_id == course_id) + .order_by(CourseChapter.order) + .group_by(Chapter.id, CourseChapter.order) + ) chapters = db_session.exec(statement).all() - if not chapters: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course do not have chapters" - ) - chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] # RBAC check @@ -236,13 +247,16 @@ async def get_course_chapters( return chapters -async def get_depreceated_course_chapters( +# Important Note : this is legacy code that has been used because +# the frontend is still not adapted for the new data structure, this implementation is absolutely not the best one +# and should not be used for future features +async def DEPRECEATED_get_course_chapters( request: Request, - course_id: int, + course_uuid: str, current_user: PublicUser, db_session: Session, -) -> DepreceatedChaptersRead: - statement = select(Course).where(Course.id == course_id) +): + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -253,76 +267,79 @@ async def get_depreceated_course_chapters( # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) - # Get chapters that are linked to his course and order them by order, using the order field in the CourseChapter table + chapters_in_db = await get_course_chapters(request, course.id, db_session, current_user) # type: ignore + + # activities + chapter_activityIdsGlobal = [] + + # chapters + chapters = {} + + for chapter in chapters_in_db: + chapter_activityIds = [] + + for activity in chapter.activities: + print("test", activity) + chapter_activityIds.append(activity.activity_uuid) + + chapters[chapter.chapter_uuid] = { + "uuid": chapter.chapter_uuid, + "id": chapter.id, + "name": chapter.name, + "activityIds": chapter_activityIds, + } + + # activities + activities_list = {} + statement = ( + select(Activity) + .join(ChapterActivity, ChapterActivity.activity_id == Activity.id) + .where(ChapterActivity.activity_id == Activity.id) + .group_by(Activity.id) + ) + activities_in_db = db_session.exec(statement).all() + + for activity in activities_in_db: + activities_list[activity.activity_uuid] = { + "uuid": activity.activity_uuid, + "id": activity.id, + "name": activity.name, + "type": activity.activity_type, + "content": activity.content, + } + + # get chapter order statement = ( select(Chapter) - .join(CourseChapter, Chapter.id == CourseChapter.chapter_id) - .where(CourseChapter.course_id == course_id) - .order_by(CourseChapter.order) + .join(CourseChapter, CourseChapter.chapter_id == Chapter.id) + .where(CourseChapter.chapter_id == Chapter.id) .group_by(Chapter.id, CourseChapter.order) + .order_by(CourseChapter.order) ) - print("ded", statement) - chapters = db_session.exec(statement).all() + chapters_in_db = db_session.exec(statement).all() - chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] + chapterOrder = [] - # 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 in chapters_in_db: + chapterOrder.append(chapter.chapter_uuid) - 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) - ) - activity = db_session.exec(statement).first() + final = { + "chapters": chapters, + "chapterOrder": chapterOrder, + "activities": activities_list, + } - if activity: - chapter.activities.append(ActivityRead(**activity.dict())) - - # Get a list of chapter ids - chapter_order: List[str] = [str(chapter.id) for chapter in chapters] - - # 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() - - activities.extend(chapter_activities) - - result = DepreceatedChaptersRead( - chapter_order=chapter_order, chapters=chapters, activities=activities - ) - - return result + return final async def reorder_chapters_and_activities( request: Request, - course_id: int, + course_uuid: str, chapters_order: ChapterUpdateOrder, current_user: PublicUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -341,7 +358,7 @@ async def reorder_chapters_and_activities( statement = ( select(CourseChapter) .where( - CourseChapter.course_id == course_id, CourseChapter.org_id == course.org_id + CourseChapter.course_id == course.id, CourseChapter.org_id == course.org_id ) .order_by(CourseChapter.order) ) @@ -357,7 +374,7 @@ async def reorder_chapters_and_activities( db_session.commit() # Delete Chapters that are not in the list of chapters_order - statement = select(Chapter).where(Chapter.course_id == course_id) + statement = select(Chapter).where(Chapter.course_id == course.id) chapters = db_session.exec(statement).all() chapter_ids_to_keep = [ @@ -376,7 +393,7 @@ async def reorder_chapters_and_activities( select(CourseChapter) .where( CourseChapter.chapter_id == chapter_order.chapter_id, - CourseChapter.course_id == course_id, + CourseChapter.course_id == course.id, ) .order_by(CourseChapter.order) ) @@ -386,7 +403,7 @@ async def reorder_chapters_and_activities( # Add CourseChapter link course_chapter = CourseChapter( chapter_id=chapter_order.chapter_id, - course_id=course_id, + course_id=course.id, # type: ignore org_id=course.org_id, creation_date=str(datetime.now()), update_date=str(datetime.now()), @@ -403,7 +420,7 @@ async def reorder_chapters_and_activities( select(CourseChapter) .where( CourseChapter.chapter_id == chapter_order.chapter_id, - CourseChapter.course_id == course_id, + CourseChapter.course_id == course.id, ) .order_by(CourseChapter.order) ) @@ -424,7 +441,7 @@ async def reorder_chapters_and_activities( statement = ( select(ChapterActivity) .where( - ChapterActivity.course_id == course_id, + ChapterActivity.course_id == course.id, ChapterActivity.org_id == course.org_id, ) .order_by(ChapterActivity.order) @@ -461,7 +478,7 @@ async def reorder_chapters_and_activities( chapter_id=chapter_order.chapter_id, activity_id=activity_order.activity_id, org_id=course.org_id, - course_id=course_id, + course_id=course.id, # type: ignore creation_date=str(datetime.now()), update_date=str(datetime.now()), order=activity_order.activity_id, diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index e19ef879..3a8c8810 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -25,9 +25,9 @@ from fastapi import HTTPException, status, Request async def get_collection( - request: Request, collection_id: str, current_user: PublicUser, db_session: Session + request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session ) -> CollectionRead: - statement = select(Collection).where(Collection.id == collection_id) + statement = select(Collection).where(Collection.collection_uuid == collection_uuid) collection = db_session.exec(statement).first() if not collection: @@ -107,12 +107,11 @@ async def create_collection( async def update_collection( request: Request, collection_object: CollectionUpdate, + collection_uuid: str, current_user: PublicUser, db_session: Session, ) -> CollectionRead: - statement = select(Collection).where( - Collection.id == collection_object.collection_id - ) + statement = select(Collection).where(Collection.collection_uuid == collection_uuid) collection = db_session.exec(statement).first() if not collection: @@ -127,7 +126,6 @@ async def update_collection( courses = collection_object.courses - del collection_object.collection_id del collection_object.courses # Update only the fields that were passed in @@ -181,9 +179,9 @@ async def update_collection( async def delete_collection( - request: Request, collection_id: str, current_user: PublicUser, db_session: Session + request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session ): - statement = select(Collection).where(Collection.id == collection_id) + statement = select(Collection).where(Collection.collection_uuid == collection_uuid) collection = db_session.exec(statement).first() if not collection: @@ -225,10 +223,7 @@ async def get_collections( ) collections = db_session.exec(statement).all() - if not collections: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="No collections found" - ) + collections_with_courses = [] for collection in collections: diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index e36eb93e..3cc05dff 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -6,7 +6,7 @@ from src.db.trails import TrailRead from src.services.trail.trail import get_user_trail_with_orgid from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum -from src.db.users import PublicUser, AnonymousUser +from src.db.users import PublicUser, AnonymousUser, User, UserRead from src.db.courses import ( Course, CourseCreate, @@ -26,11 +26,11 @@ from datetime import datetime async def get_course( request: Request, - course_id: str, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -42,21 +42,32 @@ async def get_course( # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) - course = CourseRead.from_orm(course) + # Get course authors + authors_statement = ( + select(User) + .join(ResourceAuthor) + .where(ResourceAuthor.resource_uuid == course.course_uuid) + ) + authors = db_session.exec(authors_statement).all() + + # convert from User to UserRead + authors = [UserRead.from_orm(author) for author in authors] + + course = CourseRead(**course.dict(), authors=authors) return course async def get_course_meta( request: Request, - course_id: int, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ) -> FullCourseReadWithTrail: # Avoid circular import from src.services.courses.chapters import get_course_chapters - course_statement = select(Course).where(Course.id == course_id) + course_statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(course_statement).first() if not course: @@ -65,12 +76,21 @@ async def get_course_meta( detail="Course not found", ) - print('cd',course.course_uuid) - # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) - course = CourseRead.from_orm(course) + # Get course authors + authors_statement = ( + select(User) + .join(ResourceAuthor) + .where(ResourceAuthor.resource_uuid == course.course_uuid) + ) + authors = db_session.exec(authors_statement).all() + + # convert from User to UserRead + authors = [UserRead.from_orm(author) for author in authors] + + course = CourseRead(**course.dict(), authors=authors) # Get course chapters chapters = await get_course_chapters(request, course.id, db_session, current_user) @@ -85,12 +105,13 @@ async def get_course_meta( return FullCourseReadWithTrail( **course.dict(), chapters=chapters, - trail=trail, + trail=trail if trail else None, ) async def create_course( request: Request, + org_id: int, course_object: CourseCreate, current_user: PublicUser | AnonymousUser, db_session: Session, @@ -111,9 +132,9 @@ async def create_course( if thumbnail_file and thumbnail_file.filename: name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" await upload_thumbnail( - thumbnail_file, name_in_disk, course_object.org_id, course.course_uuid + thumbnail_file, name_in_disk, org_id, course.course_uuid ) - course_object.thumbnail = name_in_disk + course_object.thumbnail_image = name_in_disk # Insert course db_session.add(course) @@ -134,17 +155,30 @@ async def create_course( db_session.commit() db_session.refresh(resource_author) + # Get course authors + authors_statement = ( + select(User) + .join(ResourceAuthor) + .where(ResourceAuthor.resource_uuid == course.course_uuid) + ) + authors = db_session.exec(authors_statement).all() + + # convert from User to UserRead + authors = [UserRead.from_orm(author) for author in authors] + + course = CourseRead(**course.dict(), authors=authors) + return CourseRead.from_orm(course) async def update_course_thumbnail( request: Request, - course_id: str, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, thumbnail_file: UploadFile | None = None, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() name_in_disk = None @@ -160,9 +194,7 @@ async def update_course_thumbnail( # Upload thumbnail if thumbnail_file and thumbnail_file.filename: - name_in_disk = ( - f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" - ) + name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" await upload_thumbnail( thumbnail_file, name_in_disk, course.org_id, course.course_uuid ) @@ -183,7 +215,20 @@ async def update_course_thumbnail( db_session.commit() db_session.refresh(course) - course = CourseRead.from_orm(course) + # Get course authors + authors_statement = ( + select(User) + .join(ResourceAuthor) + .where(ResourceAuthor.resource_uuid == course.course_uuid) + ) + authors = db_session.exec(authors_statement).all() + + + + # convert from User to UserRead + authors = [UserRead.from_orm(author) for author in authors] + + course = CourseRead(**course.dict(), authors=authors) return course @@ -191,11 +236,11 @@ async def update_course_thumbnail( async def update_course( request: Request, course_object: CourseUpdate, - course_id: int, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -219,18 +264,29 @@ async def update_course( db_session.commit() db_session.refresh(course) - course = CourseRead.from_orm(course) + # Get course authors + authors_statement = ( + select(User) + .join(ResourceAuthor) + .where(ResourceAuthor.resource_uuid == course.course_uuid) + ) + authors = db_session.exec(authors_statement).all() + + # convert from User to UserRead + authors = [UserRead.from_orm(author) for author in authors] + + course = CourseRead(**course.dict(), authors=authors) return course async def delete_course( request: Request, - course_id: str, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -275,7 +331,21 @@ async def get_courses_orgslug( courses = db_session.exec(statement) - courses = [CourseRead.from_orm(course) for course in courses] + courses = [CourseRead(**course.dict(),authors=[]) for course in courses] + + # for every course, get the authors + for course in courses: + authors_statement = ( + select(User) + .join(ResourceAuthor) + .where(ResourceAuthor.resource_uuid == course.course_uuid) + ) + authors = db_session.exec(authors_statement).all() + + # convert from User to UserRead + authors = [UserRead.from_orm(author) for author in authors] + + course.authors = authors return courses diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index 0f67d08c..c151b1b9 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -136,8 +136,6 @@ async def update_org( # RBAC check await rbac_check(request, org.org_uuid, current_user, "update", db_session) - org = Organization.from_orm(org_object) - # Verify if the new slug is already in use statement = select(Organization).where(Organization.slug == org_object.slug) result = db_session.exec(statement) diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 019520a2..51a81384 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -230,19 +230,10 @@ async def add_activity_to_trail( async def add_course_to_trail( request: Request, user: PublicUser, - course_id: str, + course_uuid: str, db_session: Session, ) -> TrailRead: - # check if run already exists - statement = select(TrailRun).where(TrailRun.course_id == course_id) - trailrun = db_session.exec(statement).first() - - if trailrun: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" - ) - - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -250,6 +241,15 @@ async def add_course_to_trail( status_code=status.HTTP_404_NOT_FOUND, detail="Course not found" ) + # check if run already exists + statement = select(TrailRun).where(TrailRun.course_id == course.id) + trailrun = db_session.exec(statement).first() + + if trailrun: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" + ) + statement = select(Trail).where( Trail.org_id == course.org_id, Trail.user_id == user.id ) @@ -308,10 +308,10 @@ async def add_course_to_trail( async def remove_course_from_trail( request: Request, user: PublicUser, - course_id: str, + course_uuid: str, db_session: Session, ) -> TrailRead: - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index e36b7040..4a9c5ee6 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -279,6 +279,37 @@ async def read_user_by_uuid( return user +async def authorize_user_action( + request: Request, + db_session: Session, + current_user: PublicUser | AnonymousUser, + ressource_uuid: str, + action: Literal["create", "read", "update", "delete"], +): + # Get user + statement = select(User).where(User.user_uuid == current_user.user_uuid) + user = db_session.exec(statement).first() + + if not user: + raise HTTPException( + status_code=400, + detail="User does not exist", + ) + + # RBAC check + authorized = await authorization_verify_based_on_roles_and_authorship( + request, current_user.id, action, ressource_uuid, db_session + ) + + if authorized: + return True + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You are not authorized to perform this action", + ) + + async def delete_user_by_id( request: Request, db_session: Session, @@ -350,7 +381,7 @@ async def rbac_check( return True await authorization_verify_based_on_roles_and_authorship( - request, current_user.id, "read", action, db_session + request, current_user.id, action, user_uuid, db_session ) diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx index ad1e8569..c51601ba 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx @@ -62,9 +62,9 @@ const CollectionPage = async (params: any) => {
{col.courses.map((course: any) => ( -
- -
+
+ +

{course.name}

diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx index e338f86e..85922e14 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx @@ -41,7 +41,7 @@ function NewCollection(params: any) { description: description, courses: selectedCourses, public: true, - org_id: org.org_id, + org_id: org.id, }; await createCollection(collection); await revalidateTags(["collections"], orgslug); @@ -69,26 +69,29 @@ function NewCollection(params: any) { ) : (
{courses.map((course: any) => ( -
- { - const courseId = e.target.value; - setSelectedCourses((prevSelectedCourses: string[]) => { - if (e.target.checked) { - return [...prevSelectedCourses, courseId]; - } else { - return prevSelectedCourses.filter((selectedCourse) => selectedCourse !== courseId); - } - }); - }} - className="mr-2 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - +
+ + { + if (e.target.checked) { + setSelectedCourses([...selectedCourses, course.id]); + } + else { + setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid)); + } + } + } + className="mr-2" +/> + +
))} diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx index 864450fd..86ae0018 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx @@ -49,14 +49,17 @@ const CollectionsPage = async (params: any) => { const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const orgslug = params.params.orgslug; const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); - const org_id = org.org_id; + const org_id = org.id; const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] }); return (
- + @@ -64,7 +67,7 @@ const CollectionsPage = async (params: any) => {
{collections.map((collection: any) => ( -
+
))} @@ -81,7 +84,10 @@ const CollectionsPage = async (params: any) => {

No collections yet

Create a collection to group courses together

- + diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx similarity index 96% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index 596dcf6f..c076e09c 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -47,7 +47,7 @@ function ActivityClient(props: ActivityClientProps) {
- +
@@ -55,7 +55,7 @@ function ActivityClient(props: ActivityClientProps) {

{course.course.name}

- +
diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/error.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/error.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/error.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/error.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/loading.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/loading.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/loading.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/loading.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/page.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/page.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/page.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx similarity index 77% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx index f4ceb215..a73654d4 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx @@ -15,23 +15,24 @@ import { getUser } from "@services/users/users"; const CourseClient = (props: any) => { const [user, setUser] = useState({}); - const courseid = props.courseid; + const [learnings, setLearnings] = useState([]); + const courseuuid = props.courseuuid; const orgslug = props.orgslug; const course = props.course; const router = useRouter(); + function getLearningTags() { + // create array of learnings from a string object (comma separated) + let learnings = course.learnings.split(","); + setLearnings(learnings); - - async function getUserUI() { - let user_id = course.course.authors[0]; - const user = await getUser(user_id); - setUser(user); - console.log(user); } + console.log(course); + async function startCourseUI() { // Create activity - await startCourse("course_" + courseid, orgslug); + await startCourse("course_" + courseuuid, orgslug); await revalidateTags(['courses'], orgslug); router.refresh(); @@ -39,17 +40,23 @@ const CourseClient = (props: any) => { // window.location.reload(); } + function isCourseStarted() { + const runs = course.trail.runs; + // checks if one of the obejcts in the array has the property "STATUS_IN_PROGRESS" + return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS"); + } + async function quitCourse() { // Close activity - let activity = await removeCourse("course_" + courseid, orgslug); + let activity = await removeCourse("course_" + courseuuid, orgslug); // Mutate course await revalidateTags(['courses'], orgslug); router.refresh(); } - useEffect(() => { - getUserUI(); - } + useEffect(() => { + + } , []); return ( @@ -61,26 +68,26 @@ const CourseClient = (props: any) => {

Course

- {course.course.name} + {course.name}

-
+
- +

Description

-

{course.course.description}

+

{course.description}

What you will learn

- {course.course.learnings.map((learning: any) => { + {learnings.map((learning: any) => { return (
@@ -118,48 +125,48 @@ const CourseClient = (props: any) => {

- {activity.type === "dynamic" && + {activity.activity_type === "TYPE_DYNAMIC" &&
} - {activity.type === "video" && + {activity.activity_type === "TYPE_VIDEO" &&
} - {activity.type === "documentpdf" && + {activity.activity_type === "TYPE_DOCUMENT" &&
}
- +

{activity.name}

- {activity.type === "dynamic" && + {activity.activity_type === "TYPE_DYNAMIC" && <> - +

Page

} - {activity.type === "video" && + {activity.activity_type === "TYPE_VIDEO" && <> - +

Video

} - {activity.type === "documentpdf" && + {activity.activity_type === "TYPE_DOCUMENT" && <> - +

Document

@@ -178,19 +185,20 @@ const CourseClient = (props: any) => {
- { user && + {user &&
-
- +
+ +
+
+
Author
+
{course.authors[0].first_name} {course.authors[0].last_name}
+
-
-
Author
-
{user.full_name}
-
-
} + {console.log(course)} - {course.trail.status == "ongoing" ? ( + {isCourseStarted() ? ( diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/edit.tsx similarity index 66% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/edit.tsx index ed292f48..294400c6 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/edit.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { FC, use, useEffect, useReducer } from 'react' +import React, { FC, useEffect, useReducer } from 'react' import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; import { getAPIUrl, getUriWithOrg } from '@services/config/config'; import useSWR, { mutate } from 'swr'; @@ -14,15 +14,40 @@ import Loading from '../../loading'; import { updateCourse } from '@services/courses/courses'; import { useRouter } from 'next/navigation'; -function CourseEditClient({ courseid, subpage, params }: { courseid: string, subpage: string, params: any }) { - const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/meta/course_${courseid}`, swrFetcher); - const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseid}`, swrFetcher); +function CourseEditClient({ courseuuid, courseid, subpage, params }: { courseid: any, courseuuid: string, subpage: string, params: any }) { + const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`, swrFetcher); + const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseuuid}/meta`, swrFetcher); const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {}); const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {}); const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true); const router = useRouter(); + // This function is a quick fix to transform the payload object from what was used before to the new and improved format + // The entire course edition frontend code will be remade in the future in a proper way. + const ConvertToNewAPIOrderUpdatePayload = (courseChaptersMetadata: any) => { + const old_format = courseChaptersMetadata + console.log() + + // Convert originalObject to the desired format + const convertedObject = { + "chapter_order_by_ids": old_format.chapterOrder.map((chapterId: string | number, chapterIndex: any) => { + const chapter = old_format.chapters[chapterId]; + return { + "chapter_id": chapter.id, + "activities_order_by_ids": chapter.activityIds.map((activityId: any, activityIndex: any) => { + return { + "activity_id": activityIndex + }; + }) + }; + }) + }; + + return convertedObject + } + + function courseChaptersReducer(state: any, action: any) { switch (action.type) { @@ -57,22 +82,25 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub async function saveCourse() { if (subpage.toString() === 'content') { - await updateChaptersMetadata(courseid, courseChaptersMetadata) + let payload = ConvertToNewAPIOrderUpdatePayload(courseChaptersMetadata) + await updateChaptersMetadata(courseuuid, payload) dispatchSavedContent({ type: 'saved_content' }) - await mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`) + await mutate(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`) await revalidateTags(['courses'], params.params.orgslug) router.refresh() } else if (subpage.toString() === 'general') { - await updateCourse(courseid, courseState) + await updateCourse(courseuuid, courseState) dispatchSavedContent({ type: 'saved_content' }) - await mutate(`${getAPIUrl()}courses/course_${courseid}`) + await mutate(`${getAPIUrl()}courses/course_${courseuuid}`) + await mutate(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`) await revalidateTags(['courses'], params.params.orgslug) router.refresh() } } useEffect(() => { + if (chapters_meta) { dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta }) dispatchSavedContent({ type: 'saved_content' }) @@ -91,8 +119,8 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub {course && <>
- - + +
@@ -118,27 +146,28 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
}
- +
General
- +
Content
- + ) } -const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch, dispatchCourseMetadata: React.Dispatch, dispatchSavedContent: React.Dispatch, courseChaptersMetadata: any, courseState: any }) => { - if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) { - return +const CoursePageViewer = ({ subpage, course, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseuuid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch, dispatchCourseMetadata: React.Dispatch, dispatchSavedContent: React.Dispatch, courseChaptersMetadata: any, courseState: any, course: any }) => { + + if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0 && course) { + return } - else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) { - return + else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0 && course) { + return } else if (subpage.toString() === 'content' || subpage.toString() === 'general') { return diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/page.tsx similarity index 64% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/page.tsx index 28e4a8d4..bc2fb786 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/page.tsx @@ -6,7 +6,7 @@ import { Metadata } from 'next'; import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth"; type MetadataProps = { - params: { orgslug: string, courseid: string }; + params: { orgslug: string, courseuuid: string }; searchParams: { [key: string]: string | string[] | undefined }; }; @@ -19,20 +19,23 @@ export async function generateMetadata( // Get Org context information const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); - const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) + const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) return { - title: `Edit Course - ` + course_meta.course.name, - description: course_meta.course.mini_description, + title: `Edit Course - ` + course_meta.name, + description: course_meta.mini_description, }; } -function CourseEdit(params: any) { +async function CourseEdit(params: any) { + const cookieStore = cookies(); + const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) let subpage = params.params.subpage ? params.params.subpage : 'general'; + const course_meta = await getCourseMetadataWithAuthHeader(params.params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) return ( <> - + ); } diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseContentEdition.tsx similarity index 82% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseContentEdition.tsx index b3634d5f..a5269054 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseContentEdition.tsx @@ -14,40 +14,43 @@ import { denyAccessToUser } from "@services/utils/react/middlewares/views"; import { Folders, Hexagon, SaveIcon } from "lucide-react"; import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper"; import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; -import { mutate } from "swr"; import { getAPIUrl } from "@services/config/config"; +import { mutate } from "swr"; function CourseContentEdition(props: any) { const router = useRouter(); - // Initial Course State - const data = props.data; + // Initial Course Chapters State + const course_chapters_with_orders_and_activities = props.course_chapters_with_orders_and_activities; // New Chapter Modal State const [newChapterModal, setNewChapterModal] = useState(false) as any; // New Activity Modal State const [newActivityModal, setNewActivityModal] = useState(false) as any; - const [newActivityModalData, setNewActivityModalData] = useState("") as any; + const [selectedChapterToAddActivityTo, setSelectedChapterToAddActivityTo] = useState("") as any; // Check window availability const [winReady, setwinReady] = useState(false); - const courseid = props.courseid; + const course = props.course; + const course_uuid = props.course ? props.course.course_uuid : '' const orgslug = props.orgslug; + // + useEffect(() => { setwinReady(true); - }, [courseid, orgslug]); + }, [course_uuid, orgslug]); // get a list of chapters order by chapter order const getChapters = () => { - const chapterOrder = data.chapterOrder ? data.chapterOrder : []; + const chapterOrder = course_chapters_with_orders_and_activities.chapterOrder ? course_chapters_with_orders_and_activities.chapterOrder : []; return chapterOrder.map((chapterId: any) => { - const chapter = data.chapters[chapterId]; + const chapter = course_chapters_with_orders_and_activities.chapters[chapterId]; let activities = []; - if (data.activities) { - activities = chapter.activityIds.map((activityId: any) => data.activities[activityId]) - ? chapter.activityIds.map((activityId: any) => data.activities[activityId]) + if (course_chapters_with_orders_and_activities.activities) { + activities = chapter.activityIds.map((activityId: any) => course_chapters_with_orders_and_activities.activities[activityId]) + ? chapter.activityIds.map((activityId: any) => course_chapters_with_orders_and_activities.activities[activityId]) : []; } return { @@ -61,9 +64,9 @@ function CourseContentEdition(props: any) { // Submit new chapter const submitChapter = async (chapter: any) => { - await createChapter(chapter, courseid); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); - // await getCourseChapters(); + await createChapter(chapter); + + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`,true); await revalidateTags(['courses'], orgslug); router.refresh(); setNewChapterModal(false); @@ -72,22 +75,21 @@ function CourseContentEdition(props: any) { // Submit new activity const submitActivity = async (activity: any) => { let org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 1800 }); - await updateChaptersMetadata(courseid, data); await createActivity(activity, activity.chapterId, org.org_id); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`); // await getCourseChapters(); setNewActivityModal(false); await revalidateTags(['courses'], orgslug); router.refresh(); }; - + // Submit File Upload const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => { - await updateChaptersMetadata(courseid, data); + //await updateChaptersMetadata(course_uuid, course_chapters_with_orders_and_activities); await createFileActivity(file, type, activity, chapterId); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`); // await getCourseChapters(); setNewActivityModal(false); await revalidateTags(['courses'], orgslug); @@ -96,9 +98,9 @@ function CourseContentEdition(props: any) { // Submit YouTube Video Upload const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => { - await updateChaptersMetadata(courseid, data); + //await updateChaptersMetadata(course_uuid, course_chapters_with_orders_and_activities); await createExternalVideoActivity(external_video_data, activity, chapterId); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`); // await getCourseChapters(); setNewActivityModal(false); await revalidateTags(['courses'], orgslug); @@ -106,19 +108,15 @@ function CourseContentEdition(props: any) { }; const deleteChapterUI = async (chapterId: any) => { - await deleteChapter(chapterId); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); + + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`,true); // await getCourseChapters(); await revalidateTags(['courses'], orgslug); router.refresh(); }; - const updateChapters = () => { - updateChaptersMetadata(courseid, data); - revalidateTags(['courses'], orgslug); - router.refresh(); - }; + /* Modals @@ -126,7 +124,7 @@ function CourseContentEdition(props: any) { const openNewActivityModal = async (chapterId: any) => { setNewActivityModal(true); - setNewActivityModalData(chapterId); + setSelectedChapterToAddActivityTo(chapterId); }; // Close new chapter modal @@ -157,12 +155,12 @@ function CourseContentEdition(props: any) { } //////////////////////////// CHAPTERS //////////////////////////// if (type === "chapter") { - const newChapterOrder = Array.from(data.chapterOrder); + const newChapterOrder = Array.from(course_chapters_with_orders_and_activities.chapterOrder); newChapterOrder.splice(source.index, 1); newChapterOrder.splice(destination.index, 0, draggableId); const newState = { - ...data, + ...course_chapters_with_orders_and_activities, chapterOrder: newChapterOrder, }; @@ -174,13 +172,13 @@ function CourseContentEdition(props: any) { //////////////////////// ACTIVITIES IN SAME CHAPTERS //////////////////////////// // check if the activity is dropped in the same chapter - const start = data.chapters[source.droppableId]; - const finish = data.chapters[destination.droppableId]; + const start = course_chapters_with_orders_and_activities.chapters[source.droppableId]; + const finish = course_chapters_with_orders_and_activities.chapters[destination.droppableId]; // check if the activity is dropped in the same chapter if (start === finish) { // create new arrays for chapters and activities - const chapter = data.chapters[source.droppableId]; + const chapter = course_chapters_with_orders_and_activities.chapters[source.droppableId]; const newActivityIds = Array.from(chapter.activityIds); // remove the activity from the old position @@ -195,9 +193,9 @@ function CourseContentEdition(props: any) { }; const newState = { - ...data, + ...course_chapters_with_orders_and_activities, chapters: { - ...data.chapters, + ...course_chapters_with_orders_and_activities.chapters, [newChapter.id]: newChapter, }, }; @@ -229,9 +227,9 @@ function CourseContentEdition(props: any) { }; const newState = { - ...data, + ...course_chapters_with_orders_and_activities, chapters: { - ...data.chapters, + ...course_chapters_with_orders_and_activities.chapters, [newStart.id]: newStart, [newFinish.id]: newFinish, }, @@ -259,7 +257,8 @@ function CourseContentEdition(props: any) { submitFileActivity={submitFileActivity} submitExternalVideo={submitExternalVideo} submitActivity={submitActivity} - chapterId={newActivityModalData} + chapterId={selectedChapterToAddActivityTo} + course={course} >} dialogTitle="Create Activity" dialogDescription="Choose between types of activities to add to the course" @@ -276,7 +275,7 @@ function CourseContentEdition(props: any) { <> } diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseEdition.tsx similarity index 82% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseEdition.tsx index 23b77f85..3684daab 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseEdition.tsx @@ -45,10 +45,10 @@ function CourseEdition(props: any) { const [error, setError] = React.useState(''); const formik = useFormik({ initialValues: { - name: String(props.data.name), - mini_description: String(props.data.mini_description), - description: String(props.data.description), - learnings: String(props.data.learnings), + name: String(props.course_chapters_with_orders_and_activities.name), + mini_description: String(props.course_chapters_with_orders_and_activities.mini_description), + description: String(props.course_chapters_with_orders_and_activities.description), + learnings: String(props.course_chapters_with_orders_and_activities.learnings), }, validate, onSubmit: async values => { @@ -61,11 +61,10 @@ function CourseEdition(props: any) { if (formik.values !== formik.initialValues) { props.dispatchSavedContent({ type: 'unsaved_content' }); const updatedCourse = { - ...props.data, + ...props.course_chapters_with_orders_and_activities, name: formik.values.name, - mini_description: formik.values.mini_description, description: formik.values.description, - learnings: formik.values.learnings.split(", "), + learnings: formik.values.learnings, }; props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse }); } @@ -88,12 +87,7 @@ function CourseEdition(props: any) { - - - - - - + diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/error.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/error.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/error.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/error.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/loading.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/loading.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/loading.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/loading.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx similarity index 68% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx index dac4a9ba..efae450b 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx @@ -7,7 +7,7 @@ import { Metadata } from 'next'; import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth'; type MetadataProps = { - params: { orgslug: string, courseid: string }; + params: { orgslug: string, courseuuid: string }; searchParams: { [key: string]: string | string[] | undefined }; }; @@ -19,14 +19,14 @@ export async function generateMetadata( // Get Org context information const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); - const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) + const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) // SEO return { - title: course_meta.course.name + ` — ${org.name}`, - description: course_meta.course.mini_description, - keywords: course_meta.course.learnings, + title: course_meta.name + ` — ${org.name}`, + description: course_meta.description, + keywords: course_meta.learnings, robots: { index: true, follow: true, @@ -38,11 +38,11 @@ export async function generateMetadata( } }, openGraph: { - title: course_meta.course.name + ` — ${org.name}`, - description: course_meta.course.mini_description, + title: course_meta.name + ` — ${org.name}`, + description: course_meta.description, type: 'article', - publishedTime: course_meta.course.creationDate, - tags: course_meta.course.learnings, + publishedTime: course_meta.creation_date, + tags: course_meta.learnings, }, }; } @@ -50,14 +50,14 @@ export async function generateMetadata( const CoursePage = async (params: any) => { const cookieStore = cookies(); - const courseid = params.params.courseid + const courseuuid = params.params.courseuuid const orgslug = params.params.orgslug; const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) - const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) + const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) return (
- +
) } diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx index d115dc0d..3b786301 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx @@ -32,7 +32,10 @@ function Courses(props: CourseProps) {
- + {courses.map((course: any) => ( -
+
))} @@ -73,7 +76,10 @@ function Courses(props: CourseProps) {

No courses yet

Create a course to add content

- + { const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null); const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); - const org_id = org.org_id; - const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] }); + const org_id = org.id; + const collections = await getOrgCollectionsWithAuthHeader(org.id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] }); return (
@@ -67,7 +67,11 @@ const OrgHomePage = async (params: any) => {
- + @@ -105,7 +109,11 @@ const OrgHomePage = async (params: any) => {
- + @@ -113,7 +121,7 @@ const OrgHomePage = async (params: any) => {
{courses.map((course: any) => ( -
+
))} diff --git a/apps/web/app/orgs/[orgslug]/settings/layout.tsx b/apps/web/app/orgs/[orgslug]/settings/layout.tsx index 3e2ff305..b5e4a401 100644 --- a/apps/web/app/orgs/[orgslug]/settings/layout.tsx +++ b/apps/web/app/orgs/[orgslug]/settings/layout.tsx @@ -8,12 +8,15 @@ import Avvvatars from 'avvvatars-react'; import Image from 'next/image'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import { getOrganizationContextInfo } from '@services/organizations/orgs'; +import useSWR, { mutate } from "swr"; +import { getAPIUrl } from '@services/config/config'; +import { swrFetcher } from '@services/utils/ts/requests'; -async function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) { +function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) { const auth: any = React.useContext(AuthContext); const orgslug = params.orgslug; - let org = await getOrganizationContextInfo(orgslug, {}); + const { data: org, error: error } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher); return ( <> @@ -33,7 +36,10 @@ async function SettingsLayout({ children, params }: { children: React.ReactNode,
  • Profile
  • Passwords
  • - + Organization
    • General
    • diff --git a/apps/web/components/Objects/Activities/DocumentPdf/DocumentPdf.tsx b/apps/web/components/Objects/Activities/DocumentPdf/DocumentPdf.tsx index 78bd4f4b..e3859254 100644 --- a/apps/web/components/Objects/Activities/DocumentPdf/DocumentPdf.tsx +++ b/apps/web/components/Objects/Activities/DocumentPdf/DocumentPdf.tsx @@ -8,7 +8,7 @@ function DocumentPdfActivity({ activity, course }: { activity: any; course: any