feat: various improvements

wip: frontend

feat: enable cascade on foreign keys

wip1

wip2

fix chapters issues

wip4
This commit is contained in:
swve 2023-11-29 22:29:48 +01:00
parent 2bf80030d7
commit 187f75e583
71 changed files with 879 additions and 568 deletions

View file

@ -5,7 +5,7 @@ from sqlmodel import SQLModel, Session, create_engine
engine = 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) SQLModel.metadata.create_all(engine)

View file

@ -1,5 +1,5 @@
from typing import Optional from typing import Optional
from sqlalchemy import JSON, Column from sqlalchemy import JSON, BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from enum import Enum from enum import Enum
@ -34,20 +34,32 @@ class ActivityBase(SQLModel):
content: dict = Field(default={}, sa_column=Column(JSON)) content: dict = Field(default={}, sa_column=Column(JSON))
published_version: int published_version: int
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): class Activity(ActivityBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id") 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 = "" activity_uuid: str = ""
creation_date: str = "" creation_date: str = ""
update_date: str = "" update_date: str = ""
class ActivityCreate(ActivityBase): class ActivityCreate(ActivityBase):
org_id: int = Field(default=None, foreign_key="organization.id") course_id: int = Field(
course_id: int = Field(default=None, foreign_key="course.id") sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
)
chapter_id: int chapter_id: int
pass pass

View file

@ -1,5 +1,5 @@
from typing import Optional from typing import Optional
from sqlalchemy import JSON, Column from sqlalchemy import JSON, Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from enum import Enum from enum import Enum
@ -22,9 +22,9 @@ class Block(BlockBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
content: dict = Field(default={}, sa_column=Column(JSON)) content: dict = Field(default={}, sa_column=Column(JSON))
org_id: int = Field(default=None, foreign_key="organization.id") 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 = Field(default=None, foreign_key="chapter.id") chapter_id: int = Field(sa_column= Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")))
activity_id: int = Field(default=None, foreign_key="activity.id") activity_id: int = Field(sa_column= Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")))
block_uuid: str block_uuid: str
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -1,12 +1,13 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class ChapterActivity(SQLModel, table=True): class ChapterActivity(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
order: int order: int
chapter_id: int = Field(default=None, foreign_key="chapter.id", ) chapter_id: int = Field(sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE")))
activity_id: int = Field(default=None, foreign_key="activity.id") activity_id: int = Field(sa_column=Column(BigInteger, ForeignKey("activity.id", ondelete="CASCADE")))
course_id : int = Field(default=None, foreign_key="course.id") course_id : int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
org_id : int = Field(default=None, foreign_key="organization.id") org_id : int = Field(default=None, foreign_key="organization.id")
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -1,5 +1,6 @@
from typing import List, Optional from typing import Any, List, Optional
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.activities import ActivityRead from src.db.activities import ActivityRead
@ -9,19 +10,21 @@ class ChapterBase(SQLModel):
description: Optional[str] = "" description: Optional[str] = ""
thumbnail_image: Optional[str] = "" thumbnail_image: Optional[str] = ""
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(default=None, foreign_key="course.id") course_id: int = Field(
creation_date: str sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
update_date: str )
class Chapter(ChapterBase, table=True): class Chapter(ChapterBase, table=True):
id: Optional[int] = Field(default=None, primary_key=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 = "" chapter_uuid: str = ""
creation_date: str = "" creation_date: str = ""
update_date: str = "" update_date: str = ""
class ChapterCreate(ChapterBase): class ChapterCreate(ChapterBase):
# referenced order here will be ignored and just used for validation # referenced order here will be ignored and just used for validation
# used order will be the next available. # used order will be the next available.
@ -32,6 +35,8 @@ class ChapterUpdate(ChapterBase):
name: Optional[str] name: Optional[str]
description: Optional[str] description: Optional[str]
thumbnail_image: Optional[str] thumbnail_image: Optional[str]
course_id: Optional[int]
org_id: Optional[int]
class ChapterRead(ChapterBase): class ChapterRead(ChapterBase):
@ -57,7 +62,7 @@ class ChapterUpdateOrder(BaseModel):
class DepreceatedChaptersRead(BaseModel): class DepreceatedChaptersRead(BaseModel):
chapter_order: list[str] chapterOrder: Any
chapters: List[ChapterRead] chapters: Any
activities: List[ActivityRead] activities: Any
pass pass

View file

@ -24,7 +24,6 @@ class CollectionCreate(CollectionBase):
class CollectionUpdate(CollectionBase): class CollectionUpdate(CollectionBase):
collection_id: int
courses: Optional[list] courses: Optional[list]
name: Optional[str] name: Optional[str]
public: Optional[bool] public: Optional[bool]

View file

@ -1,11 +1,12 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class CollectionCourse(SQLModel, table=True): class CollectionCourse(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
collection_id: int = Field(default=None, foreign_key="collection.id") collection_id: int = Field(sa_column=Column(BigInteger, ForeignKey("collection.id", ondelete="CASCADE")))
course_id: int = Field(default=None, foreign_key="course.id") course_id: int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -1,11 +1,17 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class CourseChapter(SQLModel, table=True): class CourseChapter(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
order: int order: int
course_id: int = Field(default=None, foreign_key="course.id") course_id: int = Field(
chapter_id: int = Field(default=None, foreign_key="chapter.id") sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE"))
org_id : int = Field(default=None, foreign_key="organization.id") )
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 creation_date: str
update_date: str update_date: str

View file

@ -1,5 +1,6 @@
from typing import List, Optional from typing import List, Optional
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.users import User, UserRead
from src.db.trails import TrailRead from src.db.trails import TrailRead
from src.db.chapters import ChapterRead from src.db.chapters import ChapterRead
@ -22,7 +23,6 @@ class Course(CourseBase, table=True):
update_date: str = "" update_date: str = ""
class CourseCreate(CourseBase): class CourseCreate(CourseBase):
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
pass pass
@ -40,6 +40,7 @@ class CourseUpdate(CourseBase):
class CourseRead(CourseBase): class CourseRead(CourseBase):
id: int id: int
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
authors: List[UserRead]
course_uuid: str course_uuid: str
creation_date: str creation_date: str
update_date: str update_date: str
@ -53,6 +54,7 @@ class FullCourseRead(CourseBase):
update_date: str update_date: str
# Chapters, Activities # Chapters, Activities
chapters: List[ChapterRead] chapters: List[ChapterRead]
authors: List[UserRead]
pass pass
@ -61,8 +63,9 @@ class FullCourseReadWithTrail(CourseBase):
course_uuid: str course_uuid: str
creation_date: str creation_date: str
update_date: str update_date: str
authors: List[UserRead]
# Chapters, Activities # Chapters, Activities
chapters: List[ChapterRead] chapters: List[ChapterRead]
# Trail # Trail
trail: TrailRead trail: TrailRead | None
pass pass

View file

@ -1,7 +1,9 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from enum import Enum from enum import Enum
class HeaderTypeEnum(str, Enum): class HeaderTypeEnum(str, Enum):
LOGO_MENU_SETTINGS = "LOGO_MENU_SETTINGS" LOGO_MENU_SETTINGS = "LOGO_MENU_SETTINGS"
MENU_LOGO_SETTINGS = "MENU_LOGO_SETTINGS" MENU_LOGO_SETTINGS = "MENU_LOGO_SETTINGS"
@ -9,7 +11,9 @@ class HeaderTypeEnum(str, Enum):
class OrganizationSettings(SQLModel, table=True): class OrganizationSettings(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=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] = "" logo_image: Optional[str] = ""
header_type: HeaderTypeEnum = HeaderTypeEnum.LOGO_MENU_SETTINGS header_type: HeaderTypeEnum = HeaderTypeEnum.LOGO_MENU_SETTINGS
color: str = "" color: str = ""

View file

@ -1,11 +1,14 @@
from typing import Optional from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
class UserOrganization(SQLModel, table=True): class UserOrganization(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(default=None, foreign_key="user.id") 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") role_id: int = Field(default=None, foreign_key="role.id")
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -9,11 +9,11 @@ from src.db.chapters import (
DepreceatedChaptersRead, DepreceatedChaptersRead,
) )
from src.services.courses.chapters import ( from src.services.courses.chapters import (
DEPRECEATED_get_course_chapters,
create_chapter, create_chapter,
delete_chapter, delete_chapter,
get_chapter, get_chapter,
get_course_chapters, get_course_chapters,
get_depreceated_course_chapters,
reorder_chapters_and_activities, reorder_chapters_and_activities,
update_chapter, update_chapter,
) )
@ -50,25 +50,25 @@ async def api_get_coursechapter(
return await get_chapter(request, chapter_id, current_user, db_session) 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( async def api_get_chapter_meta(
request: Request, request: Request,
course_id: int, course_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
) -> DepreceatedChaptersRead: ):
""" """
Get Chapters metadata Get Chapters metadata
""" """
return await get_depreceated_course_chapters( return await DEPRECEATED_get_course_chapters(
request, course_id, current_user, db_session 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( async def api_update_chapter_meta(
request: Request, request: Request,
course_id: int, course_uuid: str,
order: ChapterUpdateOrder, order: ChapterUpdateOrder,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
@ -77,7 +77,7 @@ async def api_update_chapter_meta(
Update Chapter metadata Update Chapter metadata
""" """
return await reorder_chapters_and_activities( 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}") @router.delete("/{chapter_id}")
async def api_delete_coursechapter( async def api_delete_coursechapter(
request: Request, request: Request,
chapter_id: int, chapter_id: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
): ):

View file

@ -29,17 +29,17 @@ async def api_create_collection(
return await create_collection(request, collection_object, current_user, db_session) return await create_collection(request, collection_object, current_user, db_session)
@router.get("/{collection_id}") @router.get("/{collection_uuid}")
async def api_get_collection( async def api_get_collection(
request: Request, request: Request,
collection_id: str, collection_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
) -> CollectionRead: ) -> CollectionRead:
""" """
Get single collection by ID 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}") @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) 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( async def api_update_collection(
request: Request, request: Request,
collection_object: CollectionUpdate, collection_object: CollectionUpdate,
collection_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
) -> CollectionRead: ) -> CollectionRead:
""" """
Update collection by ID 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( async def api_delete_collection(
request: Request, request: Request,
collection_id: str, collection_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
): ):
@ -81,4 +84,4 @@ async def api_delete_collection(
Delete collection by ID 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)

View file

@ -51,13 +51,13 @@ async def api_create_course(
learnings=learnings, learnings=learnings,
tags=tags, 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( async def api_create_course_thumbnail(
request: Request, request: Request,
course_id: str, course_uuid: str,
thumbnail: UploadFile | None = None, thumbnail: UploadFile | None = None,
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
@ -66,37 +66,37 @@ async def api_create_course_thumbnail(
Update new Course Thumbnail Update new Course Thumbnail
""" """
return await update_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( async def api_get_course(
request: Request, request: Request,
course_id: str, course_uuid: str,
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
) -> CourseRead: ) -> CourseRead:
""" """
Get single Course by course_id Get single Course by course_uuid
""" """
return await get_course( 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( async def api_get_course_meta(
request: Request, request: Request,
course_id: int, course_uuid: str,
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
) -> FullCourseReadWithTrail: ) -> FullCourseReadWithTrail:
""" """
Get single Course Metadata (chapters, activities) by course_id Get single Course Metadata (chapters, activities) by course_uuid
""" """
return await get_course_meta( 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( async def api_update_course(
request: Request, request: Request,
course_object: CourseUpdate, course_object: CourseUpdate,
course_id: int, course_uuid: str,
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
) -> CourseRead: ) -> CourseRead:
""" """
Update Course by course_id Update Course by course_uuid
""" """
return await update_course( 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( async def api_delete_course(
request: Request, request: Request,
course_id: str, course_uuid: str,
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
): ):
@ -144,4 +144,4 @@ async def api_delete_course(
Delete Course by ID 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)

View file

@ -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( async def api_add_course_to_trail(
request: Request, request: Request,
course_id: str, course_uuid: str,
user=Depends(get_current_user), user=Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
) -> TrailRead: ) -> TrailRead:
""" """
Add Course to trail 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( async def api_remove_course_to_trail(
request: Request, request: Request,
course_id: str, course_uuid: str,
user=Depends(get_current_user), user=Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
) -> TrailRead: ) -> TrailRead:
""" """
Remove Course from trail 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( async def api_add_activity_to_trail(
request: Request, request: Request,
activity_id: int, activity_id: int,

View file

@ -1,3 +1,4 @@
from typing import Literal
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from sqlmodel import Session from sqlmodel import Session
from src.security.auth import get_current_user from src.security.auth import get_current_user
@ -12,6 +13,7 @@ from src.db.users import (
UserUpdatePassword, UserUpdatePassword,
) )
from src.services.users.users import ( from src.services.users.users import (
authorize_user_action,
create_user, create_user,
create_user_without_org, create_user_without_org,
delete_user_by_id, 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() 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"]) @router.post("/{org_id}", response_model=UserRead, tags=["users"])
async def api_create_user_with_orgid( async def api_create_user_with_orgid(
*, *,

View file

@ -1,5 +1,6 @@
from typing import Literal from typing import Literal
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.chapters import Chapter
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
@ -27,21 +28,22 @@ async def create_activity(
activity = Activity.from_orm(activity_object) activity = Activity.from_orm(activity_object)
# CHeck if org exists # CHeck if org exists
statement = select(Organization).where(Organization.id == activity_object.org_id) statement = select(Chapter).where(Chapter.id == activity_object.chapter_id)
org = db_session.exec(statement).first() chapter = db_session.exec(statement).first()
if not org: if not chapter:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="Organization not found", detail="Chapter not found",
) )
# RBAC check # 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.activity_uuid = str(f"activity_{uuid4()}")
activity.creation_date = str(datetime.now()) activity.creation_date = str(datetime.now())
activity.update_date = str(datetime.now()) activity.update_date = str(datetime.now())
activity.org_id = chapter.org_id
# Insert Activity in DB # Insert Activity in DB
db_session.add(activity) db_session.add(activity)
@ -64,7 +66,7 @@ async def create_activity(
chapter_id=activity_object.chapter_id, chapter_id=activity_object.chapter_id,
activity_id=activity.id if activity.id else 0, activity_id=activity.id if activity.id else 0,
course_id=activity_object.course_id, course_id=activity_object.course_id,
org_id=activity_object.org_id, org_id=chapter.org_id,
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
order=to_be_used_order, order=to_be_used_order,

View file

@ -51,7 +51,7 @@ async def create_documentpdf_activity(
) )
# get org_id # get org_id
org_id = coursechapter.id org_id = coursechapter.org_id
# create activity uuid # create activity uuid
activity_uuid = f"activity_{uuid4()}" activity_uuid = f"activity_{uuid4()}"

View file

@ -82,6 +82,8 @@ async def create_video_activity(
activity_type=ActivityTypeEnum.TYPE_VIDEO, activity_type=ActivityTypeEnum.TYPE_VIDEO,
activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_HOSTED, activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_HOSTED,
activity_uuid=activity_uuid, activity_uuid=activity_uuid,
org_id=coursechapter.org_id,
course_id=coursechapter.course_id,
published_version=1, published_version=1,
content={ content={
"filename": "video." + video_format, "filename": "video." + video_format,
@ -171,6 +173,8 @@ async def create_external_video_activity(
activity_type=ActivityTypeEnum.TYPE_VIDEO, activity_type=ActivityTypeEnum.TYPE_VIDEO,
activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_YOUTUBE, activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_YOUTUBE,
activity_uuid=activity_uuid, activity_uuid=activity_uuid,
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
published_version=1, published_version=1,
content={ content={
"uri": data.uri, "uri": data.uri,
@ -192,6 +196,8 @@ async def create_external_video_activity(
chapter_activity_object = ChapterActivity( chapter_activity_object = ChapterActivity(
chapter_id=coursechapter.id, # type: ignore chapter_id=coursechapter.id, # type: ignore
activity_id=activity.id, # type: ignore activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
order=1, order=1,

View file

@ -36,6 +36,11 @@ async def create_chapter(
) -> ChapterRead: ) -> ChapterRead:
chapter = Chapter.from_orm(chapter_object) 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 # RBAC check
await rbac_check(request, "chapter_x", current_user, "create", db_session) 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.chapter_uuid = f"chapter_{uuid4()}"
chapter.creation_date = str(datetime.now()) chapter.creation_date = str(datetime.now())
chapter.update_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 # Find the last chapter in the course and add it to the list
statement = ( statement = (
@ -155,14 +161,17 @@ async def update_chapter(
db_session.commit() db_session.commit()
db_session.refresh(chapter) 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 return chapter
async def delete_chapter( async def delete_chapter(
request: Request, request: Request,
chapter_id: int, chapter_id: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
): ):
@ -181,7 +190,7 @@ async def delete_chapter(
db_session.commit() db_session.commit()
# Remove all linked activities # 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() chapter_activities = db_session.exec(statement).all()
for chapter_activity in chapter_activities: for chapter_activity in chapter_activities:
@ -199,14 +208,16 @@ async def get_course_chapters(
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
) -> List[ChapterRead]: ) -> 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() 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] chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
# RBAC check # RBAC check
@ -236,13 +247,16 @@ async def get_course_chapters(
return 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, request: Request,
course_id: int, course_uuid: str,
current_user: PublicUser, current_user: PublicUser,
db_session: Session, 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() course = db_session.exec(statement).first()
if not course: if not course:
@ -253,76 +267,79 @@ async def get_depreceated_course_chapters(
# RBAC check # RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session) 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 = ( statement = (
select(Chapter) select(Chapter)
.join(CourseChapter, Chapter.id == CourseChapter.chapter_id) .join(CourseChapter, CourseChapter.chapter_id == Chapter.id)
.where(CourseChapter.course_id == course_id) .where(CourseChapter.chapter_id == Chapter.id)
.order_by(CourseChapter.order)
.group_by(Chapter.id, CourseChapter.order) .group_by(Chapter.id, CourseChapter.order)
.order_by(CourseChapter.order)
) )
print("ded", statement) chapters_in_db = db_session.exec(statement).all()
chapters = db_session.exec(statement).all()
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] chapterOrder = []
# Get activities for each chapter for chapter in chapters_in_db:
for chapter in chapters: chapterOrder.append(chapter.chapter_uuid)
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: final = {
statement = ( "chapters": chapters,
select(Activity) "chapterOrder": chapterOrder,
.join(ChapterActivity, Activity.id == ChapterActivity.activity_id) "activities": activities_list,
.where(Activity.id == chapter_activity.id) }
.distinct(Activity.id, ChapterActivity.order)
.order_by(ChapterActivity.order)
)
activity = db_session.exec(statement).first()
if activity: return final
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
async def reorder_chapters_and_activities( async def reorder_chapters_and_activities(
request: Request, request: Request,
course_id: int, course_uuid: str,
chapters_order: ChapterUpdateOrder, chapters_order: ChapterUpdateOrder,
current_user: PublicUser, current_user: PublicUser,
db_session: Session, 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() course = db_session.exec(statement).first()
if not course: if not course:
@ -341,7 +358,7 @@ async def reorder_chapters_and_activities(
statement = ( statement = (
select(CourseChapter) select(CourseChapter)
.where( .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) .order_by(CourseChapter.order)
) )
@ -357,7 +374,7 @@ async def reorder_chapters_and_activities(
db_session.commit() db_session.commit()
# Delete Chapters that are not in the list of chapters_order # 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() chapters = db_session.exec(statement).all()
chapter_ids_to_keep = [ chapter_ids_to_keep = [
@ -376,7 +393,7 @@ async def reorder_chapters_and_activities(
select(CourseChapter) select(CourseChapter)
.where( .where(
CourseChapter.chapter_id == chapter_order.chapter_id, CourseChapter.chapter_id == chapter_order.chapter_id,
CourseChapter.course_id == course_id, CourseChapter.course_id == course.id,
) )
.order_by(CourseChapter.order) .order_by(CourseChapter.order)
) )
@ -386,7 +403,7 @@ async def reorder_chapters_and_activities(
# Add CourseChapter link # Add CourseChapter link
course_chapter = CourseChapter( course_chapter = CourseChapter(
chapter_id=chapter_order.chapter_id, chapter_id=chapter_order.chapter_id,
course_id=course_id, course_id=course.id, # type: ignore
org_id=course.org_id, org_id=course.org_id,
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
@ -403,7 +420,7 @@ async def reorder_chapters_and_activities(
select(CourseChapter) select(CourseChapter)
.where( .where(
CourseChapter.chapter_id == chapter_order.chapter_id, CourseChapter.chapter_id == chapter_order.chapter_id,
CourseChapter.course_id == course_id, CourseChapter.course_id == course.id,
) )
.order_by(CourseChapter.order) .order_by(CourseChapter.order)
) )
@ -424,7 +441,7 @@ async def reorder_chapters_and_activities(
statement = ( statement = (
select(ChapterActivity) select(ChapterActivity)
.where( .where(
ChapterActivity.course_id == course_id, ChapterActivity.course_id == course.id,
ChapterActivity.org_id == course.org_id, ChapterActivity.org_id == course.org_id,
) )
.order_by(ChapterActivity.order) .order_by(ChapterActivity.order)
@ -461,7 +478,7 @@ async def reorder_chapters_and_activities(
chapter_id=chapter_order.chapter_id, chapter_id=chapter_order.chapter_id,
activity_id=activity_order.activity_id, activity_id=activity_order.activity_id,
org_id=course.org_id, org_id=course.org_id,
course_id=course_id, course_id=course.id, # type: ignore
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
order=activity_order.activity_id, order=activity_order.activity_id,

View file

@ -25,9 +25,9 @@ from fastapi import HTTPException, status, Request
async def get_collection( 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: ) -> CollectionRead:
statement = select(Collection).where(Collection.id == collection_id) statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collection = db_session.exec(statement).first() collection = db_session.exec(statement).first()
if not collection: if not collection:
@ -107,12 +107,11 @@ async def create_collection(
async def update_collection( async def update_collection(
request: Request, request: Request,
collection_object: CollectionUpdate, collection_object: CollectionUpdate,
collection_uuid: str,
current_user: PublicUser, current_user: PublicUser,
db_session: Session, db_session: Session,
) -> CollectionRead: ) -> CollectionRead:
statement = select(Collection).where( statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
Collection.id == collection_object.collection_id
)
collection = db_session.exec(statement).first() collection = db_session.exec(statement).first()
if not collection: if not collection:
@ -127,7 +126,6 @@ async def update_collection(
courses = collection_object.courses courses = collection_object.courses
del collection_object.collection_id
del collection_object.courses del collection_object.courses
# Update only the fields that were passed in # Update only the fields that were passed in
@ -181,9 +179,9 @@ async def update_collection(
async def delete_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() collection = db_session.exec(statement).first()
if not collection: if not collection:
@ -225,10 +223,7 @@ async def get_collections(
) )
collections = db_session.exec(statement).all() collections = db_session.exec(statement).all()
if not collections:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="No collections found"
)
collections_with_courses = [] collections_with_courses = []
for collection in collections: for collection in collections:

View file

@ -6,7 +6,7 @@ from src.db.trails import TrailRead
from src.services.trail.trail import get_user_trail_with_orgid from src.services.trail.trail import get_user_trail_with_orgid
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum 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 ( from src.db.courses import (
Course, Course,
CourseCreate, CourseCreate,
@ -26,11 +26,11 @@ from datetime import datetime
async def get_course( async def get_course(
request: Request, request: Request,
course_id: str, course_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, 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() course = db_session.exec(statement).first()
if not course: if not course:
@ -42,21 +42,32 @@ async def get_course(
# RBAC check # RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session) 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 return course
async def get_course_meta( async def get_course_meta(
request: Request, request: Request,
course_id: int, course_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
) -> FullCourseReadWithTrail: ) -> FullCourseReadWithTrail:
# Avoid circular import # Avoid circular import
from src.services.courses.chapters import get_course_chapters 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() course = db_session.exec(course_statement).first()
if not course: if not course:
@ -65,12 +76,21 @@ async def get_course_meta(
detail="Course not found", detail="Course not found",
) )
print('cd',course.course_uuid)
# RBAC check # RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session) 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 # Get course chapters
chapters = await get_course_chapters(request, course.id, db_session, current_user) chapters = await get_course_chapters(request, course.id, db_session, current_user)
@ -85,12 +105,13 @@ async def get_course_meta(
return FullCourseReadWithTrail( return FullCourseReadWithTrail(
**course.dict(), **course.dict(),
chapters=chapters, chapters=chapters,
trail=trail, trail=trail if trail else None,
) )
async def create_course( async def create_course(
request: Request, request: Request,
org_id: int,
course_object: CourseCreate, course_object: CourseCreate,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
@ -111,9 +132,9 @@ async def create_course(
if thumbnail_file and thumbnail_file.filename: if thumbnail_file and thumbnail_file.filename:
name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
await upload_thumbnail( 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 # Insert course
db_session.add(course) db_session.add(course)
@ -134,17 +155,30 @@ async def create_course(
db_session.commit() db_session.commit()
db_session.refresh(resource_author) 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) return CourseRead.from_orm(course)
async def update_course_thumbnail( async def update_course_thumbnail(
request: Request, request: Request,
course_id: str, course_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
thumbnail_file: UploadFile | None = None, 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() course = db_session.exec(statement).first()
name_in_disk = None name_in_disk = None
@ -160,9 +194,7 @@ async def update_course_thumbnail(
# Upload thumbnail # Upload thumbnail
if thumbnail_file and thumbnail_file.filename: if thumbnail_file and thumbnail_file.filename:
name_in_disk = ( name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
)
await upload_thumbnail( await upload_thumbnail(
thumbnail_file, name_in_disk, course.org_id, course.course_uuid 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.commit()
db_session.refresh(course) 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 return course
@ -191,11 +236,11 @@ async def update_course_thumbnail(
async def update_course( async def update_course(
request: Request, request: Request,
course_object: CourseUpdate, course_object: CourseUpdate,
course_id: int, course_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, 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() course = db_session.exec(statement).first()
if not course: if not course:
@ -219,18 +264,29 @@ async def update_course(
db_session.commit() db_session.commit()
db_session.refresh(course) 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 return course
async def delete_course( async def delete_course(
request: Request, request: Request,
course_id: str, course_uuid: str,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, 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() course = db_session.exec(statement).first()
if not course: if not course:
@ -275,7 +331,21 @@ async def get_courses_orgslug(
courses = db_session.exec(statement) 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 return courses

View file

@ -136,8 +136,6 @@ async def update_org(
# RBAC check # RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session) 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 # Verify if the new slug is already in use
statement = select(Organization).where(Organization.slug == org_object.slug) statement = select(Organization).where(Organization.slug == org_object.slug)
result = db_session.exec(statement) result = db_session.exec(statement)

View file

@ -230,19 +230,10 @@ async def add_activity_to_trail(
async def add_course_to_trail( async def add_course_to_trail(
request: Request, request: Request,
user: PublicUser, user: PublicUser,
course_id: str, course_uuid: str,
db_session: Session, db_session: Session,
) -> TrailRead: ) -> TrailRead:
# check if run already exists statement = select(Course).where(Course.course_uuid == course_uuid)
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)
course = db_session.exec(statement).first() course = db_session.exec(statement).first()
if not course: if not course:
@ -250,6 +241,15 @@ async def add_course_to_trail(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found" 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( statement = select(Trail).where(
Trail.org_id == course.org_id, Trail.user_id == user.id 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( async def remove_course_from_trail(
request: Request, request: Request,
user: PublicUser, user: PublicUser,
course_id: str, course_uuid: str,
db_session: Session, db_session: Session,
) -> TrailRead: ) -> TrailRead:
statement = select(Course).where(Course.id == course_id) statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first() course = db_session.exec(statement).first()
if not course: if not course:

View file

@ -279,6 +279,37 @@ async def read_user_by_uuid(
return user 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( async def delete_user_by_id(
request: Request, request: Request,
db_session: Session, db_session: Session,
@ -350,7 +381,7 @@ async def rbac_check(
return True return True
await authorization_verify_based_on_roles_and_authorship( 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
) )

View file

@ -62,9 +62,9 @@ const CollectionPage = async (params: any) => {
<br /> <br />
<div className="home_courses flex flex-wrap"> <div className="home_courses flex flex-wrap">
{col.courses.map((course: any) => ( {col.courses.map((course: any) => (
<div className="pr-8" key={course.course_id}> <div className="pr-8" key={course.course_uuid}>
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}> <Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_uuid))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_id, course.thumbnail)})` }}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_uuid, course.thumbnail)})` }}>
</div> </div>
</Link> </Link>
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2> <h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>

View file

@ -41,7 +41,7 @@ function NewCollection(params: any) {
description: description, description: description,
courses: selectedCourses, courses: selectedCourses,
public: true, public: true,
org_id: org.org_id, org_id: org.id,
}; };
await createCollection(collection); await createCollection(collection);
await revalidateTags(["collections"], orgslug); await revalidateTags(["collections"], orgslug);
@ -69,26 +69,29 @@ function NewCollection(params: any) {
) : ( ) : (
<div> <div>
{courses.map((course: any) => ( {courses.map((course: any) => (
<div key={course.course_id} className="flex items-center mb-2"> <div key={course.course_uuid} className="flex items-center mb-2">
<input
type="checkbox" <input
id={course.course_id}
name={course.course_id} type="checkbox"
value={course.course_id} id={course.id}
checked={selectedCourses.includes(course.course_id)} name={course.name}
onChange={(e) => { value={course.id}
const courseId = e.target.value; // id is an integer, not a string
setSelectedCourses((prevSelectedCourses: string[]) => {
if (e.target.checked) { onChange={(e) => {
return [...prevSelectedCourses, courseId]; if (e.target.checked) {
} else { setSelectedCourses([...selectedCourses, course.id]);
return prevSelectedCourses.filter((selectedCourse) => selectedCourse !== courseId); }
} else {
}); setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
}} }
className="mr-2 focus:outline-none focus:ring-2 focus:ring-blue-500" }
/> }
<label htmlFor={course.course_id} className="text-sm">{course.name}</label> className="mr-2"
/>
<label htmlFor={course.course_uuid} className="text-sm">{course.name}</label>
</div> </div>
))} ))}

View file

@ -49,14 +49,17 @@ const CollectionsPage = async (params: any) => {
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); 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'] }); const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
return ( return (
<GeneralWrapperStyled> <GeneralWrapperStyled>
<div className="flex justify-between" > <div className="flex justify-between" >
<TypeOfContentTitle title="Collections" type="col" /> <TypeOfContentTitle title="Collections" type="col" />
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}> <AuthenticatedClientElement
ressourceType="collection"
action="create"
checkMethod='roles' orgId={org_id}>
<Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}> <Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton /> <NewCollectionButton />
</Link> </Link>
@ -64,7 +67,7 @@ const CollectionsPage = async (params: any) => {
</div> </div>
<div className="home_collections flex flex-wrap"> <div className="home_collections flex flex-wrap">
{collections.map((collection: any) => ( {collections.map((collection: any) => (
<div className="flex flex-col py-1 px-3" key={collection.collection_id}> <div className="flex flex-col py-1 px-3" key={collection.collection_uuid}>
<CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org_id} /> <CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org_id} />
</div> </div>
))} ))}
@ -81,7 +84,10 @@ const CollectionsPage = async (params: any) => {
<h1 className="text-3xl font-bold text-gray-600">No collections yet</h1> <h1 className="text-3xl font-bold text-gray-600">No collections yet</h1>
<p className="text-lg text-gray-400">Create a collection to group courses together</p> <p className="text-lg text-gray-400">Create a collection to group courses together</p>
</div> </div>
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}> <AuthenticatedClientElement checkMethod='roles'
ressourceType="collection"
action="create"
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}> <Link href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton /> <NewCollectionButton />
</Link> </Link>

View file

@ -47,7 +47,7 @@ function ActivityClient(props: ActivityClientProps) {
<div className="flex space-x-6"> <div className="flex space-x-6">
<div className="flex"> <div className="flex">
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}> <Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_id, course.course.thumbnail)}`} alt="" /> <img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_uuid, course.course.thumbnail)}`} alt="" />
</Link> </Link>
</div> </div>
<div className="flex flex-col -space-y-1"> <div className="flex flex-col -space-y-1">
@ -55,7 +55,7 @@ function ActivityClient(props: ActivityClientProps) {
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.course.name}</h1> <h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.course.name}</h1>
</div> </div>
</div> </div>
<ActivityIndicators course_id={courseid} current_activity={activityid} orgslug={orgslug} course={course} /> <ActivityIndicators course_uuid={courseid} current_activity={activityid} orgslug={orgslug} course={course} />
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex flex-col -space-y-1"> <div className="flex flex-col -space-y-1">

View file

@ -15,23 +15,24 @@ import { getUser } from "@services/users/users";
const CourseClient = (props: any) => { const CourseClient = (props: any) => {
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
const courseid = props.courseid; const [learnings, setLearnings] = useState<any>([]);
const courseuuid = props.courseuuid;
const orgslug = props.orgslug; const orgslug = props.orgslug;
const course = props.course; const course = props.course;
const router = useRouter(); 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() { async function startCourseUI() {
// Create activity // Create activity
await startCourse("course_" + courseid, orgslug); await startCourse("course_" + courseuuid, orgslug);
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
@ -39,17 +40,23 @@ const CourseClient = (props: any) => {
// window.location.reload(); // 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() { async function quitCourse() {
// Close activity // Close activity
let activity = await removeCourse("course_" + courseid, orgslug); let activity = await removeCourse("course_" + courseuuid, orgslug);
// Mutate course // Mutate course
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
} }
useEffect(() => { useEffect(() => {
getUserUI();
} }
, []); , []);
return ( return (
@ -61,26 +68,26 @@ const CourseClient = (props: any) => {
<div className="pb-3"> <div className="pb-3">
<p className="text-md font-bold text-gray-400 pb-2">Course</p> <p className="text-md font-bold text-gray-400 pb-2">Course</p>
<h1 className="text-3xl -mt-3 font-bold"> <h1 className="text-3xl -mt-3 font-bold">
{course.course.name} {course.name}
</h1> </h1>
</div> </div>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_id, course.course.thumbnail)})` }}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_uuid, course.thumbnail)})` }}>
</div> </div>
<ActivityIndicators course_id={props.course.course.course_id} orgslug={orgslug} course={course} /> <ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} />
<div className="flex flex-row pt-10"> <div className="flex flex-row pt-10">
<div className="course_metadata_left grow space-y-2"> <div className="course_metadata_left grow space-y-2">
<h2 className="py-3 text-2xl font-bold">Description</h2> <h2 className="py-3 text-2xl font-bold">Description</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
<p className="py-5 px-5">{course.course.description}</p> <p className="py-5 px-5">{course.description}</p>
</div> </div>
<h2 className="py-3 text-2xl font-bold">What you will learn</h2> <h2 className="py-3 text-2xl font-bold">What you will learn</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2"> <div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2">
{course.course.learnings.map((learning: any) => { {learnings.map((learning: any) => {
return ( return (
<div key={learning} <div key={learning}
className="flex space-x-2 items-center font-semibold text-gray-500 capitalize"> className="flex space-x-2 items-center font-semibold text-gray-500 capitalize">
@ -118,48 +125,48 @@ const CourseClient = (props: any) => {
</p> </p>
<div className="flex space-x-1 py-2 px-4 items-center"> <div className="flex space-x-1 py-2 px-4 items-center">
<div className="courseicon items-center flex space-x-2 text-neutral-400"> <div className="courseicon items-center flex space-x-2 text-neutral-400">
{activity.type === "dynamic" && {activity.activity_type === "TYPE_DYNAMIC" &&
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<Sparkles className="text-gray-400" size={13} /> <Sparkles className="text-gray-400" size={13} />
</div> </div>
} }
{activity.type === "video" && {activity.activity_type === "TYPE_VIDEO" &&
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<Video className="text-gray-400" size={13} /> <Video className="text-gray-400" size={13} />
</div> </div>
} }
{activity.type === "documentpdf" && {activity.activity_type === "TYPE_DOCUMENT" &&
<div className="bg-gray-100 px-2 py-2 rounded-full"> <div className="bg-gray-100 px-2 py-2 rounded-full">
<File className="text-gray-400" size={13} /> <File className="text-gray-400" size={13} />
</div> </div>
} }
</div> </div>
<Link className="flex font-semibold grow pl-2 text-neutral-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer"> <Link className="flex font-semibold grow pl-2 text-neutral-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<p>{activity.name}</p> <p>{activity.name}</p>
</Link> </Link>
<div className="flex "> <div className="flex ">
{activity.type === "dynamic" && {activity.activity_type === "TYPE_DYNAMIC" &&
<> <>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer"> <Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center"> <div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Page</p> <p>Page</p>
<ArrowRight size={13} /></div> <ArrowRight size={13} /></div>
</Link> </Link>
</> </>
} }
{activity.type === "video" && {activity.activity_type === "TYPE_VIDEO" &&
<> <>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer"> <Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center"> <div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Video</p> <p>Video</p>
<ArrowRight size={13} /></div> <ArrowRight size={13} /></div>
</Link> </Link>
</> </>
} }
{activity.type === "documentpdf" && {activity.activity_type === "TYPE_DOCUMENT" &&
<> <>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer"> <Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center"> <div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Document</p> <p>Document</p>
<ArrowRight size={13} /></div> <ArrowRight size={13} /></div>
@ -178,19 +185,20 @@ const CourseClient = (props: any) => {
</div> </div>
<div className="course_metadata_right space-y-3 w-64 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden"> <div className="course_metadata_right space-y-3 w-64 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
{ user && {user &&
<div className="flex mx-auto space-x-3 px-2 py-2 items-center"> <div className="flex mx-auto space-x-3 px-2 py-2 items-center">
<div className=""> <div className="">
<Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.course.authors[0]} style='shape' /> <Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.authors[0].username} style='shape' />
</div>
<div className="-space-y-2 ">
<div className="text-[12px] text-neutral-400 font-semibold">Author</div>
<div className="text-xl font-bold text-neutral-800">{course.authors[0].first_name} {course.authors[0].last_name}</div>
</div>
</div> </div>
<div className="-space-y-2 ">
<div className="text-[12px] text-neutral-400 font-semibold">Author</div>
<div className="text-xl font-bold text-neutral-800">{user.full_name}</div>
</div>
</div>
} }
{console.log(course)}
{course.trail.status == "ongoing" ? ( {isCourseStarted() ? (
<button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" onClick={quitCourse}> <button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" onClick={quitCourse}>
Quit Course Quit Course
</button> </button>

View file

@ -1,5 +1,5 @@
"use client"; "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 { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { getAPIUrl, getUriWithOrg } from '@services/config/config'; import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
@ -14,15 +14,40 @@ import Loading from '../../loading';
import { updateCourse } from '@services/courses/courses'; import { updateCourse } from '@services/courses/courses';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
function CourseEditClient({ courseid, subpage, params }: { courseid: string, subpage: string, params: any }) { 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/meta/course_${courseid}`, swrFetcher); 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_${courseid}`, swrFetcher); const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseuuid}/meta`, swrFetcher);
const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {}); const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {});
const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {}); const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {});
const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true); const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true);
const router = useRouter(); 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) { function courseChaptersReducer(state: any, action: any) {
switch (action.type) { switch (action.type) {
@ -57,22 +82,25 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
async function saveCourse() { async function saveCourse() {
if (subpage.toString() === 'content') { if (subpage.toString() === 'content') {
await updateChaptersMetadata(courseid, courseChaptersMetadata) let payload = ConvertToNewAPIOrderUpdatePayload(courseChaptersMetadata)
await updateChaptersMetadata(courseuuid, payload)
dispatchSavedContent({ type: 'saved_content' }) 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) await revalidateTags(['courses'], params.params.orgslug)
router.refresh() router.refresh()
} }
else if (subpage.toString() === 'general') { else if (subpage.toString() === 'general') {
await updateCourse(courseid, courseState) await updateCourse(courseuuid, courseState)
dispatchSavedContent({ type: 'saved_content' }) 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) await revalidateTags(['courses'], params.params.orgslug)
router.refresh() router.refresh()
} }
} }
useEffect(() => { useEffect(() => {
if (chapters_meta) { if (chapters_meta) {
dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta }) dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta })
dispatchSavedContent({ type: 'saved_content' }) dispatchSavedContent({ type: 'saved_content' })
@ -91,8 +119,8 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
{course && <> {course && <>
<div className='flex items-center'><div className='info flex space-x-5 items-center grow'> <div className='flex items-center'><div className='info flex space-x-5 items-center grow'>
<div className='flex'> <div className='flex'>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}`}> <Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseuuid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.org_id, "course_" + courseid, course.thumbnail)}`} alt="" /> <img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.org_id, "course_" + courseuuid, course.thumbnail)}`} alt="" />
</Link> </Link>
</div> </div>
<div className="flex flex-col "> <div className="flex flex-col ">
@ -118,27 +146,28 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
</div> </div>
</>} </>}
<div className='flex space-x-5 pt-3 font-black text-sm'> <div className='flex space-x-5 pt-3 font-black text-sm'>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/general`}> <Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseuuid}/edit/general`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>General</div> <div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>General</div>
</Link> </Link>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/content`}> <Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseuuid}/edit/content`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>Content</div> <div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>Content</div>
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
<CoursePageViewer dispatchSavedContent={dispatchSavedContent} courseState={courseState} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseMetadata={dispatchCourseMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseid={courseid} orgslug={params.params.orgslug} /> <CoursePageViewer course={course} dispatchSavedContent={dispatchSavedContent} courseState={courseState} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseMetadata={dispatchCourseMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseuuid={courseuuid} orgslug={params.params.orgslug} />
</> </>
) )
} }
const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchCourseMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any, courseState: any }) => { const CoursePageViewer = ({ subpage, course, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseuuid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchCourseMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any, courseState: any, course: any }) => {
if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) {
return <CourseEdition data={courseState} dispatchCourseMetadata={dispatchCourseMetadata} dispatchSavedContent={dispatchSavedContent} /> if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0 && course) {
return <CourseEdition course={course} orgslug={orgslug} course_chapters_with_orders_and_activities={courseState} dispatchCourseMetadata={dispatchCourseMetadata} dispatchSavedContent={dispatchSavedContent} />
} }
else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) { else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0 && course) {
return <CourseContentEdition data={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} courseid={courseid} orgslug={orgslug} /> return <CourseContentEdition course={course} orgslug={orgslug} course_chapters_with_orders_and_activities={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} />
} }
else if (subpage.toString() === 'content' || subpage.toString() === 'general') { else if (subpage.toString() === 'content' || subpage.toString() === 'general') {
return <Loading /> return <Loading />

View file

@ -6,7 +6,7 @@ import { Metadata } from 'next';
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth"; import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
type MetadataProps = { type MetadataProps = {
params: { orgslug: string, courseid: string }; params: { orgslug: string, courseuuid: string };
searchParams: { [key: string]: string | string[] | undefined }; searchParams: { [key: string]: string | string[] | undefined };
}; };
@ -19,20 +19,23 @@ export async function generateMetadata(
// Get Org context information // Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); 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 { return {
title: `Edit Course - ` + course_meta.course.name, title: `Edit Course - ` + course_meta.name,
description: course_meta.course.mini_description, 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'; 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 ( return (
<> <>
<CourseEditClient params={params} subpage={subpage} courseid={params.params.courseid} /> <CourseEditClient params={params} subpage={subpage} courseid={course_meta.id} courseuuid={params.params.courseuuid} />
</> </>
); );
} }

View file

@ -14,40 +14,43 @@ import { denyAccessToUser } from "@services/utils/react/middlewares/views";
import { Folders, Hexagon, SaveIcon } from "lucide-react"; import { Folders, Hexagon, SaveIcon } from "lucide-react";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper"; import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { mutate } from "swr";
import { getAPIUrl } from "@services/config/config"; import { getAPIUrl } from "@services/config/config";
import { mutate } from "swr";
function CourseContentEdition(props: any) { function CourseContentEdition(props: any) {
const router = useRouter(); const router = useRouter();
// Initial Course State // Initial Course Chapters State
const data = props.data; const course_chapters_with_orders_and_activities = props.course_chapters_with_orders_and_activities;
// New Chapter Modal State // New Chapter Modal State
const [newChapterModal, setNewChapterModal] = useState(false) as any; const [newChapterModal, setNewChapterModal] = useState(false) as any;
// New Activity Modal State // New Activity Modal State
const [newActivityModal, setNewActivityModal] = useState(false) as any; const [newActivityModal, setNewActivityModal] = useState(false) as any;
const [newActivityModalData, setNewActivityModalData] = useState("") as any; const [selectedChapterToAddActivityTo, setSelectedChapterToAddActivityTo] = useState("") as any;
// Check window availability // Check window availability
const [winReady, setwinReady] = useState(false); 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; const orgslug = props.orgslug;
//
useEffect(() => { useEffect(() => {
setwinReady(true); setwinReady(true);
}, [courseid, orgslug]); }, [course_uuid, orgslug]);
// get a list of chapters order by chapter order // get a list of chapters order by chapter order
const getChapters = () => { 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) => { return chapterOrder.map((chapterId: any) => {
const chapter = data.chapters[chapterId]; const chapter = course_chapters_with_orders_and_activities.chapters[chapterId];
let activities = []; let activities = [];
if (data.activities) { if (course_chapters_with_orders_and_activities.activities) {
activities = chapter.activityIds.map((activityId: any) => data.activities[activityId]) activities = chapter.activityIds.map((activityId: any) => course_chapters_with_orders_and_activities.activities[activityId])
? chapter.activityIds.map((activityId: any) => data.activities[activityId]) ? chapter.activityIds.map((activityId: any) => course_chapters_with_orders_and_activities.activities[activityId])
: []; : [];
} }
return { return {
@ -61,9 +64,9 @@ function CourseContentEdition(props: any) {
// Submit new chapter // Submit new chapter
const submitChapter = async (chapter: any) => { const submitChapter = async (chapter: any) => {
await createChapter(chapter, courseid); await createChapter(chapter);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters(); mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`,true);
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
setNewChapterModal(false); setNewChapterModal(false);
@ -72,9 +75,8 @@ function CourseContentEdition(props: any) {
// Submit new activity // Submit new activity
const submitActivity = async (activity: any) => { const submitActivity = async (activity: any) => {
let org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 1800 }); let org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 1800 });
await updateChaptersMetadata(courseid, data);
await createActivity(activity, activity.chapterId, org.org_id); await createActivity(activity, activity.chapterId, org.org_id);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`);
// await getCourseChapters(); // await getCourseChapters();
setNewActivityModal(false); setNewActivityModal(false);
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
@ -85,9 +87,9 @@ function CourseContentEdition(props: any) {
// Submit File Upload // Submit File Upload
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => { 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); await createFileActivity(file, type, activity, chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`);
// await getCourseChapters(); // await getCourseChapters();
setNewActivityModal(false); setNewActivityModal(false);
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
@ -96,9 +98,9 @@ function CourseContentEdition(props: any) {
// Submit YouTube Video Upload // Submit YouTube Video Upload
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => { 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); await createExternalVideoActivity(external_video_data, activity, chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`);
// await getCourseChapters(); // await getCourseChapters();
setNewActivityModal(false); setNewActivityModal(false);
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
@ -106,19 +108,15 @@ function CourseContentEdition(props: any) {
}; };
const deleteChapterUI = async (chapterId: any) => { const deleteChapterUI = async (chapterId: any) => {
await deleteChapter(chapterId); await deleteChapter(chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`,true);
// await getCourseChapters(); // await getCourseChapters();
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
}; };
const updateChapters = () => {
updateChaptersMetadata(courseid, data);
revalidateTags(['courses'], orgslug);
router.refresh();
};
/* /*
Modals Modals
@ -126,7 +124,7 @@ function CourseContentEdition(props: any) {
const openNewActivityModal = async (chapterId: any) => { const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true); setNewActivityModal(true);
setNewActivityModalData(chapterId); setSelectedChapterToAddActivityTo(chapterId);
}; };
// Close new chapter modal // Close new chapter modal
@ -157,12 +155,12 @@ function CourseContentEdition(props: any) {
} }
//////////////////////////// CHAPTERS //////////////////////////// //////////////////////////// CHAPTERS ////////////////////////////
if (type === "chapter") { 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(source.index, 1);
newChapterOrder.splice(destination.index, 0, draggableId); newChapterOrder.splice(destination.index, 0, draggableId);
const newState = { const newState = {
...data, ...course_chapters_with_orders_and_activities,
chapterOrder: newChapterOrder, chapterOrder: newChapterOrder,
}; };
@ -174,13 +172,13 @@ function CourseContentEdition(props: any) {
//////////////////////// ACTIVITIES IN SAME CHAPTERS //////////////////////////// //////////////////////// ACTIVITIES IN SAME CHAPTERS ////////////////////////////
// check if the activity is dropped in the same chapter // check if the activity is dropped in the same chapter
const start = data.chapters[source.droppableId]; const start = course_chapters_with_orders_and_activities.chapters[source.droppableId];
const finish = data.chapters[destination.droppableId]; const finish = course_chapters_with_orders_and_activities.chapters[destination.droppableId];
// check if the activity is dropped in the same chapter // check if the activity is dropped in the same chapter
if (start === finish) { if (start === finish) {
// create new arrays for chapters and activities // 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); const newActivityIds = Array.from(chapter.activityIds);
// remove the activity from the old position // remove the activity from the old position
@ -195,9 +193,9 @@ function CourseContentEdition(props: any) {
}; };
const newState = { const newState = {
...data, ...course_chapters_with_orders_and_activities,
chapters: { chapters: {
...data.chapters, ...course_chapters_with_orders_and_activities.chapters,
[newChapter.id]: newChapter, [newChapter.id]: newChapter,
}, },
}; };
@ -229,9 +227,9 @@ function CourseContentEdition(props: any) {
}; };
const newState = { const newState = {
...data, ...course_chapters_with_orders_and_activities,
chapters: { chapters: {
...data.chapters, ...course_chapters_with_orders_and_activities.chapters,
[newStart.id]: newStart, [newStart.id]: newStart,
[newFinish.id]: newFinish, [newFinish.id]: newFinish,
}, },
@ -259,7 +257,8 @@ function CourseContentEdition(props: any) {
submitFileActivity={submitFileActivity} submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo} submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity} submitActivity={submitActivity}
chapterId={newActivityModalData} chapterId={selectedChapterToAddActivityTo}
course={course}
></NewActivityModal>} ></NewActivityModal>}
dialogTitle="Create Activity" dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course" dialogDescription="Choose between types of activities to add to the course"
@ -276,7 +275,7 @@ function CourseContentEdition(props: any) {
<> <>
<Chapter <Chapter
orgslug={orgslug} orgslug={orgslug}
courseid={courseid} course_uuid={course_uuid}
openNewActivityModal={openNewActivityModal} openNewActivityModal={openNewActivityModal}
deleteChapter={deleteChapterUI} deleteChapter={deleteChapterUI}
key={index} key={index}
@ -296,6 +295,7 @@ function CourseContentEdition(props: any) {
onOpenChange={setNewChapterModal} onOpenChange={setNewChapterModal}
minHeight="sm" minHeight="sm"
dialogContent={<NewChapterModal dialogContent={<NewChapterModal
course={props.course ? props.course : null}
closeModal={closeNewChapterModal} closeModal={closeNewChapterModal}
submitChapter={submitChapter} submitChapter={submitChapter}
></NewChapterModal>} ></NewChapterModal>}

View file

@ -45,10 +45,10 @@ function CourseEdition(props: any) {
const [error, setError] = React.useState(''); const [error, setError] = React.useState('');
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
name: String(props.data.name), name: String(props.course_chapters_with_orders_and_activities.name),
mini_description: String(props.data.mini_description), mini_description: String(props.course_chapters_with_orders_and_activities.mini_description),
description: String(props.data.description), description: String(props.course_chapters_with_orders_and_activities.description),
learnings: String(props.data.learnings), learnings: String(props.course_chapters_with_orders_and_activities.learnings),
}, },
validate, validate,
onSubmit: async values => { onSubmit: async values => {
@ -61,11 +61,10 @@ function CourseEdition(props: any) {
if (formik.values !== formik.initialValues) { if (formik.values !== formik.initialValues) {
props.dispatchSavedContent({ type: 'unsaved_content' }); props.dispatchSavedContent({ type: 'unsaved_content' });
const updatedCourse = { const updatedCourse = {
...props.data, ...props.course_chapters_with_orders_and_activities,
name: formik.values.name, name: formik.values.name,
mini_description: formik.values.mini_description,
description: formik.values.description, description: formik.values.description,
learnings: formik.values.learnings.split(", "), learnings: formik.values.learnings,
}; };
props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse }); props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse });
} }
@ -88,12 +87,7 @@ function CourseEdition(props: any) {
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required /> <Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control> </Form.Control>
</FormField> </FormField>
<FormField name="mini_description">
<FormLabelAndMessage label='Mini description' message={formik.errors.mini_description} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.mini_description} type="text" required />
</Form.Control>
</FormField>
<FormField name="description"> <FormField name="description">
<FormLabelAndMessage label='Description' message={formik.errors.description} /> <FormLabelAndMessage label='Description' message={formik.errors.description} />
<Form.Control asChild> <Form.Control asChild>

View file

@ -7,7 +7,7 @@ import { Metadata } from 'next';
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth'; import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth';
type MetadataProps = { type MetadataProps = {
params: { orgslug: string, courseid: string }; params: { orgslug: string, courseuuid: string };
searchParams: { [key: string]: string | string[] | undefined }; searchParams: { [key: string]: string | string[] | undefined };
}; };
@ -19,14 +19,14 @@ export async function generateMetadata(
// Get Org context information // Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); 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 // SEO
return { return {
title: course_meta.course.name + `${org.name}`, title: course_meta.name + `${org.name}`,
description: course_meta.course.mini_description, description: course_meta.description,
keywords: course_meta.course.learnings, keywords: course_meta.learnings,
robots: { robots: {
index: true, index: true,
follow: true, follow: true,
@ -38,11 +38,11 @@ export async function generateMetadata(
} }
}, },
openGraph: { openGraph: {
title: course_meta.course.name + `${org.name}`, title: course_meta.name + `${org.name}`,
description: course_meta.course.mini_description, description: course_meta.description,
type: 'article', type: 'article',
publishedTime: course_meta.course.creationDate, publishedTime: course_meta.creation_date,
tags: course_meta.course.learnings, tags: course_meta.learnings,
}, },
}; };
} }
@ -50,14 +50,14 @@ export async function generateMetadata(
const CoursePage = async (params: any) => { const CoursePage = async (params: any) => {
const cookieStore = cookies(); const cookieStore = cookies();
const courseid = params.params.courseid const courseuuid = params.params.courseuuid
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) 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 ( return (
<div> <div>
<CourseClient courseid={courseid} orgslug={orgslug} course={course_meta} /> <CourseClient courseuuid={courseuuid} orgslug={orgslug} course={course_meta} />
</div> </div>
) )
} }

View file

@ -32,7 +32,10 @@ function Courses(props: CourseProps) {
<div className='flex flex-wrap justify-between'> <div className='flex flex-wrap justify-between'>
<TypeOfContentTitle title="Courses" type="cou" /> <TypeOfContentTitle title="Courses" type="cou" />
<AuthenticatedClientElement checkMethod='roles' orgId={props.org_id}> <AuthenticatedClientElement checkMethod='roles'
action='create'
ressourceType='course'
orgId={props.org_id}>
<Modal <Modal
isDialogOpen={newCourseModal} isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal} onOpenChange={setNewCourseModal}
@ -56,7 +59,7 @@ function Courses(props: CourseProps) {
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{courses.map((course: any) => ( {courses.map((course: any) => (
<div className="px-3" key={course.course_id}> <div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} /> <CourseThumbnail course={course} orgslug={orgslug} />
</div> </div>
))} ))}
@ -73,7 +76,10 @@ function Courses(props: CourseProps) {
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1> <h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
<p className="text-lg text-gray-400">Create a course to add content</p> <p className="text-lg text-gray-400">Create a course to add content</p>
</div> </div>
<AuthenticatedClientElement checkMethod='roles' orgId={props.org_id}> <AuthenticatedClientElement
action='create'
ressourceType='course'
checkMethod='roles' orgId={props.org_id}>
<Modal <Modal
isDialogOpen={newCourseModal} isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal} onOpenChange={setNewCourseModal}

View file

@ -56,8 +56,8 @@ const OrgHomePage = async (params: any) => {
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null); 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 = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const org_id = org.org_id; const org_id = org.id;
const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] }); const collections = await getOrgCollectionsWithAuthHeader(org.id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] });
return ( return (
<div> <div>
@ -67,7 +67,11 @@ const OrgHomePage = async (params: any) => {
<div className='flex grow'> <div className='flex grow'>
<TypeOfContentTitle title="Collections" type="col" /> <TypeOfContentTitle title="Collections" type="col" />
</div> </div>
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}> <AuthenticatedClientElement
checkMethod='roles'
ressourceType='collection'
action='create'
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}> <Link href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton /> <NewCollectionButton />
</Link> </Link>
@ -105,7 +109,11 @@ const OrgHomePage = async (params: any) => {
<div className='flex grow'> <div className='flex grow'>
<TypeOfContentTitle title="Courses" type="cou" /> <TypeOfContentTitle title="Courses" type="cou" />
</div> </div>
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}> <AuthenticatedClientElement
ressourceType='course'
action='create'
checkMethod='roles'
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/courses?new=true")}> <Link href={getUriWithOrg(orgslug, "/courses?new=true")}>
<NewCourseButton /> <NewCourseButton />
</Link> </Link>
@ -113,7 +121,7 @@ const OrgHomePage = async (params: any) => {
</div> </div>
<div className="home_courses flex flex-wrap"> <div className="home_courses flex flex-wrap">
{courses.map((course: any) => ( {courses.map((course: any) => (
<div className="py-3 px-3" key={course.course_id}> <div className="py-3 px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} /> <CourseThumbnail course={course} orgslug={orgslug} />
</div> </div>
))} ))}

View file

@ -8,12 +8,15 @@ import Avvvatars from 'avvvatars-react';
import Image from 'next/image'; import Image from 'next/image';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import { getOrganizationContextInfo } from '@services/organizations/orgs'; 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 auth: any = React.useContext(AuthContext);
const orgslug = params.orgslug; const orgslug = params.orgslug;
let org = await getOrganizationContextInfo(orgslug, {}); const { data: org, error: error } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher);
return ( return (
<> <>
@ -33,7 +36,10 @@ async function SettingsLayout({ children, params }: { children: React.ReactNode,
<li><Link href="/settings/account/profile">Profile</Link></li> <li><Link href="/settings/account/profile">Profile</Link></li>
<li><Link href="/settings/account/passwords">Passwords</Link></li> <li><Link href="/settings/account/passwords">Passwords</Link></li>
</ul> </ul>
<AuthenticatedClientElement checkMethod='roles' orgId={org.org_id} > <AuthenticatedClientElement
ressourceType='organization'
action='update'
checkMethod='roles' >
<MenuTitle>Organization</MenuTitle> <MenuTitle>Organization</MenuTitle>
<ul> <ul>
<li><Link href="/settings/organization/general">General</Link></li> <li><Link href="/settings/organization/general">General</Link></li>

View file

@ -8,7 +8,7 @@ function DocumentPdfActivity({ activity, course }: { activity: any; course: any
<div className="m-8 bg-zinc-900 rounded-md mt-14"> <div className="m-8 bg-zinc-900 rounded-md mt-14">
<iframe <iframe
className="rounded-lg w-full h-[900px]" className="rounded-lg w-full h-[900px]"
src={getActivityMediaDirectory(activity.org_id, activity.course_id, activity.activity_id, activity.content.documentpdf.filename, 'documentpdf')} src={getActivityMediaDirectory(activity.org_id, activity.course_uuid, activity.activity_id, activity.content.documentpdf.filename, 'documentpdf')}
/> />
</div> </div>
); );

View file

@ -38,7 +38,7 @@ function VideoActivity({ activity, course }: { activity: any; course: any }) {
{videoType === 'video' && ( {videoType === 'video' && (
<div className="m-8 bg-zinc-900 rounded-md mt-14"> <div className="m-8 bg-zinc-900 rounded-md mt-14">
<video className="rounded-lg w-full h-[500px]" controls <video className="rounded-lg w-full h-[500px]" controls
src={getActivityMediaDirectory(activity.org_id, activity.course_id, activity.activity_id, activity.content.video.filename, 'video')} src={getActivityMediaDirectory(activity.org_id, activity.course_uuid, activity.activity_id, activity.content.video.filename, 'video')}
></video> ></video>
</div> </div>

View file

@ -50,8 +50,8 @@ interface Editor {
function Editor(props: Editor) { function Editor(props: Editor) {
const auth: any = React.useContext(AuthContext); const auth: any = React.useContext(AuthContext);
// remove course_ from course_id // remove course_ from course_uuid
const course_id = props.course.course.course_id.substring(7); const course_uuid = props.course.course.course_uuid.substring(7);
// remove activity_ from activity_id // remove activity_ from activity_id
const activity_id = props.activity.activity_id.substring(9); const activity_id = props.activity.activity_id.substring(9);
@ -145,8 +145,8 @@ function Editor(props: Editor) {
<Link href="/"> <Link href="/">
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" /> <EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
</Link> </Link>
<Link target="_blank" href={`/course/${course_id}/edit`}> <Link target="_blank" href={`/course/${course_uuid}/edit`}>
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.course.course.org_id, props.course.course.course_id, props.course.course.thumbnail)}`} alt=""></EditorInfoThumbnail> <EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.course.course.org_id, props.course.course.course_uuid, props.course.course.thumbnail)}`} alt=""></EditorInfoThumbnail>
</Link> </Link>
<EditorInfoDocName> <EditorInfoDocName>
{" "} {" "}
@ -167,7 +167,7 @@ function Editor(props: Editor) {
<EditorLeftOptionsSection className="space-x-2 pl-2 pr-3"> <EditorLeftOptionsSection className="space-x-2 pl-2 pr-3">
<div className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer" onClick={() => props.setContent(editor.getJSON())}> Save </div> <div className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer" onClick={() => props.setContent(editor.getJSON())}> Save </div>
<ToolTip content="Preview"> <ToolTip content="Preview">
<Link target="_blank" href={`/course/${course_id}/activity/${activity_id}`}> <Link target="_blank" href={`/course/${course_uuid}/activity/${activity_id}`}>
<div className="flex bg-neutral-600 hover:bg-neutral-700 transition-all ease-linear h-9 px-3 py-2 font-black justify-center items-center text-sm shadow text-neutral-100 rounded-lg hover:cursor-pointer"> <div className="flex bg-neutral-600 hover:bg-neutral-700 transition-all ease-linear h-9 px-3 py-2 font-black justify-center items-center text-sm shadow text-neutral-100 rounded-lg hover:cursor-pointer">
<Eye className="mx-auto items-center" size={15} /> <Eye className="mx-auto items-center" size={15} />
</div> </div>

View file

@ -70,7 +70,7 @@ function ImageBlockComponent(props: any) {
<img <img
src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id, src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
props.extension.options.activity.course_id, props.extension.options.activity.course_uuid,
props.extension.options.activity.activity_id, props.extension.options.activity.activity_id,
blockObject.block_id, blockObject.block_id,
blockObject ? fileId : ' ', 'imageBlock')}`} blockObject ? fileId : ' ', 'imageBlock')}`}

View file

@ -50,7 +50,7 @@ function PDFBlockComponent(props: any) {
<iframe <iframe
className="shadow rounded-lg h-96 w-full object-scale-down bg-black" className="shadow rounded-lg h-96 w-full object-scale-down bg-black"
src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id, src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
props.extension.options.activity.course_id, props.extension.options.activity.course_uuid,
props.extension.options.activity.activity_id, props.extension.options.activity.activity_id,
blockObject.block_id, blockObject.block_id,
blockObject ? fileId : ' ', 'pdfBlock')}`} blockObject ? fileId : ' ', 'pdfBlock')}`}

View file

@ -51,7 +51,7 @@ function VideoBlockComponents(props: any) {
controls controls
className="rounded-lg shadow h-96 w-full object-scale-down bg-black" className="rounded-lg shadow h-96 w-full object-scale-down bg-black"
src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id, src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
props.extension.options.activity.course_id, props.extension.options.activity.course_uuid,
props.extension.options.activity.activity_id, props.extension.options.activity.activity_id,
blockObject.block_id, blockObject.block_id,
blockObject ? fileId : ' ', 'videoBlock')}`} blockObject ? fileId : ' ', 'videoBlock')}`}

View file

@ -9,7 +9,7 @@ import VideoModal from "./NewActivityModal/Video";
import Image from "next/image"; import Image from "next/image";
import DocumentPdfModal from "./NewActivityModal/DocumentPdf"; import DocumentPdfModal from "./NewActivityModal/DocumentPdf";
function NewActivityModal({ closeModal, submitActivity, submitFileActivity, submitExternalVideo, chapterId }: any) { function NewActivityModal({ closeModal, submitActivity, submitFileActivity, submitExternalVideo, chapterId, course }: any) {
const [selectedView, setSelectedView] = useState("home"); const [selectedView, setSelectedView] = useState("home");
@ -39,16 +39,16 @@ function NewActivityModal({ closeModal, submitActivity, submitFileActivity, subm
)} )}
{selectedView === "dynamic" && ( {selectedView === "dynamic" && (
<DynamicCanvaModal submitActivity={submitActivity} chapterId={chapterId} /> <DynamicCanvaModal submitActivity={submitActivity} chapterId={chapterId} course={course} />
)} )}
{selectedView === "video" && ( {selectedView === "video" && (
<VideoModal submitFileActivity={submitFileActivity} submitExternalVideo={submitExternalVideo} <VideoModal submitFileActivity={submitFileActivity} submitExternalVideo={submitExternalVideo}
chapterId={chapterId} /> chapterId={chapterId} course={course} />
)} )}
{selectedView === "documentpdf" && ( {selectedView === "documentpdf" && (
<DocumentPdfModal submitFileActivity={submitFileActivity} chapterId={chapterId} /> <DocumentPdfModal submitFileActivity={submitFileActivity} chapterId={chapterId} course={course} />
)} )}
</div> </div>
); );

View file

@ -3,7 +3,7 @@ import React, { useState } from "react";
import * as Form from '@radix-ui/react-form'; import * as Form from '@radix-ui/react-form';
import BarLoader from "react-spinners/BarLoader"; import BarLoader from "react-spinners/BarLoader";
function DocumentPdfModal({ submitFileActivity, chapterId }: any) { function DocumentPdfModal({ submitFileActivity, chapterId, course }: any) {
const [documentpdf, setDocumentPdf] = React.useState(null) as any; const [documentpdf, setDocumentPdf] = React.useState(null) as any;
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [name, setName] = React.useState(""); const [name, setName] = React.useState("");
@ -19,7 +19,13 @@ function DocumentPdfModal({ submitFileActivity, chapterId }: any) {
const handleSubmit = async (e: any) => { const handleSubmit = async (e: any) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
let status = await submitFileActivity(documentpdf, "documentpdf", { name, type: "documentpdf" }, chapterId); let status = await submitFileActivity(documentpdf, "documentpdf", { name: name,
chapter_id: chapterId,
activity_type: "TYPE_DOCUMENT",
activity_sub_type:"SUBTYPE_DOCUMENT_PDF",
published_version:1,
version:1,
course_id: course.id, }, chapterId);
setIsSubmitting(false); setIsSubmitting(false);
}; };

View file

@ -3,7 +3,7 @@ import React, { useState } from "react";
import * as Form from '@radix-ui/react-form'; import * as Form from '@radix-ui/react-form';
import BarLoader from "react-spinners/BarLoader"; import BarLoader from "react-spinners/BarLoader";
function DynamicCanvaModal({ submitActivity, chapterId }: any) { function DynamicCanvaModal({ submitActivity, chapterId, course }: any) {
const [activityName, setActivityName] = useState(""); const [activityName, setActivityName] = useState("");
const [activityDescription, setActivityDescription] = useState(""); const [activityDescription, setActivityDescription] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -21,9 +21,12 @@ function DynamicCanvaModal({ submitActivity, chapterId }: any) {
setIsSubmitting(true); setIsSubmitting(true);
await submitActivity({ await submitActivity({
name: activityName, name: activityName,
chapterId: chapterId, chapter_id: chapterId,
type: "dynamic", activity_type: "TYPE_DYNAMIC",
org_id : "test", activity_sub_type:"SUBTYPE_DYNAMIC_PAGE",
published_version:1,
version:1,
course_id: course.id,
}); });
setIsSubmitting(false); setIsSubmitting(false);
}; };

View file

@ -8,10 +8,11 @@ interface ExternalVideoObject {
name: string, name: string,
type: string, type: string,
uri: string uri: string
chapter_id: string
} }
function VideoModal({ submitFileActivity, submitExternalVideo, chapterId }: any) { function VideoModal({ submitFileActivity, submitExternalVideo, chapterId, course }: any) {
const [video, setVideo] = React.useState(null) as any; const [video, setVideo] = React.useState(null) as any;
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [name, setName] = React.useState(""); const [name, setName] = React.useState("");
@ -35,16 +36,28 @@ function VideoModal({ submitFileActivity, submitExternalVideo, chapterId }: any)
setIsSubmitting(true); setIsSubmitting(true);
if (selectedView === "file") { if (selectedView === "file") {
let status = await submitFileActivity(video, "video", { name, type: "video" }, chapterId); let status = await submitFileActivity(video, "video", {
name: name,
chapter_id: chapterId,
activity_type: "TYPE_VIDEO",
activity_sub_type: "SUBTYPE_VIDEO_HOSTED",
published_version: 1,
version: 1,
course_id: course.id,
}, chapterId);
setIsSubmitting(false); setIsSubmitting(false);
} }
if (selectedView === "youtube") { if (selectedView === "youtube") {
let external_video_object: ExternalVideoObject = { let external_video_object: ExternalVideoObject = {
name, name,
type: "youtube", type: "youtube",
uri: youtubeUrl uri: youtubeUrl,
chapter_id: chapterId
} }
let status = await submitExternalVideo(external_video_object, 'activity' ,chapterId);
let status = await submitExternalVideo(external_video_object, 'activity', chapterId);
setIsSubmitting(false); setIsSubmitting(false);
} }

View file

@ -4,7 +4,7 @@ import * as Form from '@radix-ui/react-form';
import React, { useState } from "react"; import React, { useState } from "react";
import BarLoader from "react-spinners/BarLoader"; import BarLoader from "react-spinners/BarLoader";
function NewChapterModal({ submitChapter, closeModal }: any) { function NewChapterModal({ submitChapter, closeModal, course }: any) {
const [chapterName, setChapterName] = useState(""); const [chapterName, setChapterName] = useState("");
const [chapterDescription, setChapterDescription] = useState(""); const [chapterDescription, setChapterDescription] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -21,7 +21,15 @@ function NewChapterModal({ submitChapter, closeModal }: any) {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
await submitChapter({ name: chapterName, description: chapterDescription, activities: [] }); const chapter_object = {
name: chapterName,
description: chapterDescription,
thumbnail_image: "",
course_id: course.id,
org_id: course.org_id
};
await submitChapter(chapter_object);
setIsSubmitting(false); setIsSubmitting(false);
}; };
@ -49,9 +57,9 @@ function NewChapterModal({ submitChapter, closeModal }: any) {
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}> <Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
<Form.Submit asChild> <Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}> <ButtonBlack type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? <BarLoader cssOverride={{borderRadius:60,}} width={60} color="#ffffff" /> {isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
: "Create Chapter"} : "Create Chapter"}
</ButtonBlack> </ButtonBlack>
</Form.Submit> </Form.Submit>
</Flex> </Flex>
</FormLayout> </FormLayout>

View file

@ -1,3 +1,4 @@
'use client';
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, Input, Textarea } from '@components/StyledElements/Form/Form' import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, Input, Textarea } from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form' import * as Form from '@radix-ui/react-form'
import { FormMessage } from "@radix-ui/react-form"; import { FormMessage } from "@radix-ui/react-form";
@ -12,16 +13,21 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [name, setName] = React.useState(""); const [name, setName] = React.useState("");
const [description, setDescription] = React.useState(""); const [description, setDescription] = React.useState("");
const [learnings, setLearnings] = React.useState("");
const [visibility, setVisibility] = React.useState("");
const [tags, setTags] = React.useState("");
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [thumbnail, setThumbnail] = React.useState(null) as any; const [thumbnail, setThumbnail] = React.useState(null) as any;
const router = useRouter(); const router = useRouter();
const [orgId, setOrgId] = React.useState(null) as any; const [orgId, setOrgId] = React.useState(null) as any;
const [org, setOrg] = React.useState(null) as any;
const getOrgMetadata = async () => { const getOrgMetadata = async () => {
const org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 360, tags: ['organizations'] }); const org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 360, tags: ['organizations'] });
setOrgId(org.org_id);
setOrgId(org.id);
} }
@ -33,6 +39,20 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
setDescription(event.target.value); setDescription(event.target.value);
}; };
const handleLearningsChange = (event: React.ChangeEvent<any>) => {
setLearnings(event.target.value);
}
const handleVisibilityChange = (event: React.ChangeEvent<any>) => {
setVisibility(event.target.value);
console.log(event.target.value);
}
const handleTagsChange = (event: React.ChangeEvent<any>) => {
setTags(event.target.value);
}
const handleThumbnailChange = (event: React.ChangeEvent<any>) => { const handleThumbnailChange = (event: React.ChangeEvent<any>) => {
setThumbnail(event.target.files[0]); setThumbnail(event.target.files[0]);
}; };
@ -40,7 +60,9 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
const handleSubmit = async (e: any) => { const handleSubmit = async (e: any) => {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
let status = await createNewCourse(orgId, { name, description }, thumbnail);
let status = await createNewCourse(orgId, { name, description, tags, visibility }, thumbnail);
await revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
setIsSubmitting(false); setIsSubmitting(false);
@ -92,13 +114,25 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
<Input onChange={handleThumbnailChange} type="file" required /> <Input onChange={handleThumbnailChange} type="file" required />
</Form.Control> </Form.Control>
</FormField> </FormField>
<FormField name="course-learnings"> <FormField name="course-tags">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}> <Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Course keywords</FormLabel> <FormLabel>Course tags (separated by comma)</FormLabel>
<FormMessage match="valueMissing">Please provide learning elements, separated by comma (,)</FormMessage> <FormMessage match="valueMissing">Please provide learning elements, separated by comma (,)</FormMessage>
</Flex> </Flex>
<Form.Control asChild> <Form.Control asChild>
<Textarea required /> <Textarea onChange={handleTagsChange} required />
</Form.Control>
</FormField>
<FormField name="course-visibility">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Course Visibility</FormLabel>
<FormMessage match="valueMissing">Please choose cours visibility</FormMessage>
</Flex>
<Form.Control asChild>
<select onChange={handleVisibilityChange} className='border border-gray-300 rounded-md p-2' required>
<option value="true">Public (Available to see on the internet) </option>
<option value="false">Private (Private to users) </option>
</select>
</Form.Control> </Form.Control>
</FormField> </FormField>

View file

@ -27,17 +27,17 @@ function CollectionThumbnail(props: PropsType) {
<div className="flex -space-x-5"> <div className="flex -space-x-5">
{props.collection.courses.slice(0, 2).map((course: any) => ( {props.collection.courses.slice(0, 2).map((course: any) => (
<> <>
<Link href={getUriWithOrg(props.orgslug, "/collection/" + removeCollectionPrefix(props.collection.collection_id))}> <Link href={getUriWithOrg(props.orgslug, "/collection/" + removeCollectionPrefix(props.collection.collection_uuid))}>
<div className="inset-0 rounded-full shadow-2xl bg-cover w-12 h-8 justify-center ring-indigo-800 ring-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.collection.org_id, course.course_id, course.thumbnail)})` }}> <div className="inset-0 rounded-full shadow-2xl bg-cover w-12 h-8 justify-center ring-indigo-800 ring-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.collection.org_id, course.course_uuid, course.thumbnail)})` }}>
</div> </div>
</Link> </Link>
</> </>
))} ))}
</div> </div>
<Link href={getUriWithOrg(props.orgslug, "/collection/" + removeCollectionPrefix(props.collection.collection_id))}> <Link href={getUriWithOrg(props.orgslug, "/collection/" + removeCollectionPrefix(props.collection.collection_uuid))}>
<h1 className="font-bold text-md justify-center">{props.collection.name}</h1> <h1 className="font-bold text-md justify-center">{props.collection.name}</h1>
</Link> </Link>
<CollectionAdminEditsArea orgslug={props.orgslug} org_id={props.org_id} collection_id={props.collection.collection_id} collection={props.collection} /> <CollectionAdminEditsArea orgslug={props.orgslug} org_id={props.org_id} collection_uuid={props.collection.collection_uuid} collection={props.collection} />
</div> </div>
</div> </div>
) )
@ -54,7 +54,10 @@ const CollectionAdminEditsArea = (props: any) => {
} }
return ( return (
<AuthenticatedClientElement orgId={props.org_id} checkMethod='roles'> <AuthenticatedClientElement
action="delete"
ressourceType="collection"
orgId={props.org_id} checkMethod='roles'>
<div className="flex space-x-1 justify-center mx-auto z-20 "> <div className="flex space-x-1 justify-center mx-auto z-20 ">
<ConfirmationModal <ConfirmationModal
confirmationMessage="Are you sure you want to delete this collection?" confirmationMessage="Are you sure you want to delete this collection?"
@ -66,7 +69,7 @@ const CollectionAdminEditsArea = (props: any) => {
rel="noopener noreferrer"> rel="noopener noreferrer">
<X size={10} className="text-rose-200 font-bold" /> <X size={10} className="text-rose-200 font-bold" />
</div>} </div>}
functionToExecute={() => deleteCollectionUI(props.collection_id)} functionToExecute={() => deleteCollectionUI(props.collection_uuid)}
status='warning' status='warning'
></ConfirmationModal> ></ConfirmationModal>
</div> </div>

View file

@ -15,16 +15,16 @@ type PropsType = {
orgslug: string orgslug: string
} }
// function to remove "course_" from the course_id // function to remove "course_" from the course_uuid
function removeCoursePrefix(course_id: string) { function removeCoursePrefix(course_uuid: string) {
return course_id.replace("course_", ""); return course_uuid.replace("course_", "");
} }
function CourseThumbnail(props: PropsType) { function CourseThumbnail(props: PropsType) {
const router = useRouter(); const router = useRouter();
async function deleteCourses(course_id: any) { async function deleteCourses(course_uuid: any) {
await deleteCourseFromBackend(course_id); await deleteCourseFromBackend(course_uuid);
await revalidateTags(['courses'], props.orgslug); await revalidateTags(['courses'], props.orgslug);
router.refresh(); router.refresh();
@ -32,9 +32,9 @@ function CourseThumbnail(props: PropsType) {
return ( return (
<div className='relative'> <div className='relative'>
<AdminEditsArea course={props.course} orgSlug={props.orgslug} courseId={props.course.course_id} deleteCourses={deleteCourses} /> <AdminEditsArea course={props.course} orgSlug={props.orgslug} courseId={props.course.course_uuid} deleteCourses={deleteCourses} />
<Link href={getUriWithOrg(props.orgslug, "/course/" + removeCoursePrefix(props.course.course_id))}> <Link href={getUriWithOrg(props.orgslug, "/course/" + removeCoursePrefix(props.course.course_uuid))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.course.org_id, props.course.course_id, props.course.thumbnail)})` }}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-xl shadow-xl w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.course.org_id, props.course.course_uuid, props.course.thumbnail)})` }}>
</div> </div>
</Link> </Link>
@ -45,7 +45,10 @@ function CourseThumbnail(props: PropsType) {
const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any, deleteCourses: any }) => { const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any, deleteCourses: any }) => {
return ( return (
<AuthenticatedClientElement checkMethod='roles' orgId={props.course.org_id}> <AuthenticatedClientElement
action="update"
ressourceType="course"
checkMethod='roles' orgId={props.course.org_id}>
<div className="flex space-x-1 absolute justify-center mx-auto z-20 bottom-14 left-1/2 transform -translate-x-1/2"> <div className="flex space-x-1 absolute justify-center mx-auto z-20 bottom-14 left-1/2 transform -translate-x-1/2">
<Link href={getUriWithOrg(props.orgSlug, "/course/" + removeCoursePrefix(props.courseId) + "/edit")}> <Link href={getUriWithOrg(props.orgSlug, "/course/" + removeCoursePrefix(props.courseId) + "/edit")}>
<div <div

View file

@ -44,7 +44,7 @@ function Activity(props: any) {
} }
return ( return (
<Draggable key={props.activity.id} draggableId={props.activity.id} index={props.index}> <Draggable key={props.activity.uuid} draggableId={String(props.activity.uuid)} index={props.index}>
{(provided) => ( {(provided) => (
<div <div
className="flex flex-row py-2 my-2 rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 w-auto items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear" key={props.activity.id} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}> className="flex flex-row py-2 my-2 rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 w-auto items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear" key={props.activity.id} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
@ -69,16 +69,16 @@ function Activity(props: any) {
</div> </div>
<div className="flex flex-row space-x-2"> <div className="flex flex-row space-x-2">
{props.activity.type === "dynamic" && <> {props.activity.type === "TYPE_DYNAMIC" && <>
<Link <Link
href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.id.replace("activity_", "")}/edit`} href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.uuid.replace("activity_", "")}/edit`}
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center" className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
rel="noopener noreferrer"> rel="noopener noreferrer">
<div className="text-sky-100 font-bold text-xs" >Edit </div> <div className="text-sky-100 font-bold text-xs" >Edit </div>
</Link> </Link>
</>} </>}
<Link <Link
href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.id.replace("activity_", "")}`} href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.uuid.replace("activity_", "")}`}
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md" className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md"
rel="noopener noreferrer"> rel="noopener noreferrer">
<Eye strokeWidth={2} size={15} className="text-gray-600" /> <Eye strokeWidth={2} size={15} className="text-gray-600" />

View file

@ -25,18 +25,16 @@ function Chapter(props: any) {
setSelectedChapter(undefined); setSelectedChapter(undefined);
let modifiedChapterCopy = { let modifiedChapterCopy = {
name: modifiedChapter.chapterName, name: modifiedChapter.chapterName,
description: '',
activities: props.info.list.chapter.activityIds,
} }
await updateChapter(chapterId, modifiedChapterCopy) await updateChapter(chapterId, modifiedChapterCopy)
await mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}`) await mutate(`${getAPIUrl()}chapters/course/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug) await revalidateTags(['courses'], props.orgslug)
router.refresh(); router.refresh();
} }
} }
return ( return (
<Draggable key={props.info.list.chapter.id} draggableId={props.info.list.chapter.id} index={props.index}> <Draggable key={props.info.list.chapter.uuid} draggableId={String(props.info.list.chapter.uuid)} index={props.index}>
{(provided, snapshot) => ( {(provided, snapshot) => (
<ChapterWrapper <ChapterWrapper
{...provided.dragHandleProps} {...provided.dragHandleProps}
@ -81,7 +79,7 @@ function Chapter(props: any) {
></ConfirmationModal> ></ConfirmationModal>
</div> </div>
<Droppable key={props.info.list.chapter.id} droppableId={props.info.list.chapter.id} type="activity"> <Droppable key={props.info.list.chapter.id} droppableId={String(props.info.list.chapter.id)} type="activity">
{(provided) => ( {(provided) => (
<ActivitiesList {...provided.droppableProps} ref={provided.innerRef}> <ActivitiesList {...provided.droppableProps} ref={provided.innerRef}>
<div className="flex flex-col"> <div className="flex flex-col">

View file

@ -7,30 +7,37 @@ import React from 'react'
interface Props { interface Props {
course: any course: any
orgslug: string orgslug: string
course_id: string course_uuid: string
current_activity?: any current_activity?: any
} }
function ActivityIndicators(props: Props) { function ActivityIndicators(props: Props) {
const course = props.course const course = props.course
const orgslug = props.orgslug const orgslug = props.orgslug
const courseid = props.course_id.replace("course_", "") const courseid = props.course_uuid.replace("course_", "")
const done_activity_style = 'bg-teal-600 hover:bg-teal-700' const done_activity_style = 'bg-teal-600 hover:bg-teal-700'
const black_activity_style = 'bg-black hover:bg-gray-700' const black_activity_style = 'bg-black hover:bg-gray-700'
const current_activity_style = 'bg-gray-600 animate-pulse hover:bg-gray-700' const current_activity_style = 'bg-gray-600 animate-pulse hover:bg-gray-700'
const trail = props.course.trail
function isActivityDone(activity: any) { function isActivityDone(activity: any) {
if (course.trail.activities_marked_complete && course.trail.activities_marked_complete.includes(activity.id) && course.trail.status == "ongoing") { const runs = course.trail.runs;
return true for (let run of runs) {
for (let step of run.steps) {
if (step.activity_id === activity.id && step.complete === true) {
return false;
}
}
} }
return false return false;
} }
function isActivityCurrent(activity: any) { function isActivityCurrent(activity: any) {
let activityid = activity.id.replace("activity_", "") let activity_uuid = activity.activity_uuid.replace("activity_", "")
if (props.current_activity && props.current_activity == activityid) { if (props.current_activity && props.current_activity == activity_uuid) {
return true return true
} }
return false return false
@ -46,7 +53,6 @@ function ActivityIndicators(props: Props) {
return black_activity_style return black_activity_style
} }
return ( return (
<div className='grid grid-flow-col justify-stretch space-x-6'> <div className='grid grid-flow-col justify-stretch space-x-6'>
{course.chapters.map((chapter: any) => { {course.chapters.map((chapter: any) => {
@ -55,8 +61,8 @@ function ActivityIndicators(props: Props) {
<div className='grid grid-flow-col justify-stretch space-x-2'> <div className='grid grid-flow-col justify-stretch space-x-2'>
{chapter.activities.map((activity: any) => { {chapter.activities.map((activity: any) => {
return ( return (
<ToolTip sideOffset={8} slateBlack content={activity.name} key={activity.id}> <ToolTip sideOffset={8} slateBlack content={activity.name} key={activity.activity_uuid}>
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`}> <Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.activity_uuid.replace("activity_", "")}`}>
<div className={`h-[7px] w-auto ${getActivityClass(activity)} rounded-lg shadow-md`}></div> <div className={`h-[7px] w-auto ${getActivityClass(activity)} rounded-lg shadow-md`}></div>
</Link> </Link>

View file

@ -13,13 +13,13 @@ interface TrailCourseElementProps {
} }
function TrailCourseElement(props: TrailCourseElementProps) { function TrailCourseElement(props: TrailCourseElementProps) {
const courseid = props.course.course_id.replace("course_", "") const courseid = props.course.course_uuid.replace("course_", "")
const course = props.course const course = props.course
const router = useRouter(); const router = useRouter();
async function quitCourse(course_id: string) { async function quitCourse(course_uuid: string) {
// Close activity // Close activity
let activity = await removeCourse(course_id, props.orgslug); let activity = await removeCourse(course_uuid, props.orgslug);
// Mutate course // Mutate course
await revalidateTags(['courses'], props.orgslug); await revalidateTags(['courses'], props.orgslug);
router.refresh(); router.refresh();
@ -32,7 +32,7 @@ function TrailCourseElement(props: TrailCourseElementProps) {
<div className='trailcoursebox flex p-3 bg-white rounded-xl' style={{ boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}> <div className='trailcoursebox flex p-3 bg-white rounded-xl' style={{ boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}>
<Link href={getUriWithOrg(props.orgslug, "/course/" + courseid)}> <Link href={getUriWithOrg(props.orgslug, "/course/" + courseid)}>
<div className="course_tumbnail inset-0 ring-1 ring-inset ring-black/10 rounded-lg relative h-[50px] w-[72px] bg-cover bg-center" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.course.course_object.org_id, props.course.course_object.course_id, props.course.course_object.thumbnail)})`, boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}></div> <div className="course_tumbnail inset-0 ring-1 ring-inset ring-black/10 rounded-lg relative h-[50px] w-[72px] bg-cover bg-center" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.course.course_object.org_id, props.course.course_object.course_uuid, props.course.course_object.thumbnail)})`, boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}></div>
</Link> </Link>
<div className="course_meta pl-5 flex-grow space-y-1"> <div className="course_meta pl-5 flex-grow space-y-1">
<div className="course_top"> <div className="course_top">
@ -46,7 +46,7 @@ function TrailCourseElement(props: TrailCourseElementProps) {
</div> </div>
</div> </div>
<div className="course_actions flex-grow flex flex-row-reverse"> <div className="course_actions flex-grow flex flex-row-reverse">
<button onClick={() => quitCourse(course.course_id)} className="bg-red-200 text-red-700 hover:bg-red-300 rounded-full text-xs h-5 px-2 font-bold">Quit Course</button> <button onClick={() => quitCourse(course.course_uuid)} className="bg-red-200 text-red-700 hover:bg-red-300 rounded-full text-xs h-5 px-2 font-bold">Quit Course</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,45 +1,46 @@
'use client'; 'use client';
import React from "react"; import React from "react";
import { AuthContext } from "./AuthProvider"; import { AuthContext } from "./AuthProvider";
import useSWR, { mutate } from "swr";
import { getAPIUrl } from "@services/config/config";
import { swrFetcher } from "@services/utils/ts/requests";
interface AuthenticatedClientElementProps { interface AuthenticatedClientElementProps {
children: React.ReactNode; children: React.ReactNode;
checkMethod: 'authentication' | 'roles'; checkMethod: 'authentication' | 'roles';
orgId?: string; orgId?: string;
ressourceType?: 'collection' | 'course' | 'activity' | 'user' | 'organization';
action?: 'create' | 'update' | 'delete' | 'read';
}
function generateRessourceId(ressourceType: string) {
// for every type of ressource, we need to generate a ressource id, example for a collection: col_XXXXX
if (ressourceType == 'collection') {
return `collection_xxxx`
}
else if (ressourceType == 'course') {
return `course_xxxx`
}
else if (ressourceType == 'activity') {
return `activity_xxxx`
}
else if (ressourceType == 'user') {
return `user_xxxx`
}
else if (ressourceType == 'organization') {
return `org_xxxx`
}
else if (ressourceType === null) {
return `n/a`
}
} }
export const AuthenticatedClientElement = (props: AuthenticatedClientElementProps) => { export const AuthenticatedClientElement = (props: AuthenticatedClientElementProps) => {
const auth: any = React.useContext(AuthContext); const auth: any = React.useContext(AuthContext);
const { data: authorization_status, error: error } = useSWR(props.checkMethod == 'roles' && props.ressourceType ? `${getAPIUrl()}users/authorize/ressource/${generateRessourceId(props.ressourceType)}/action/${props.action}` : null, swrFetcher);
console.log(authorization_status);
// Available roles if ((props.checkMethod == 'authentication' && auth.isAuthenticated) || (auth.isAuthenticated && props.checkMethod == 'roles' && authorization_status)) {
const org_roles_values = ["admin", "owner"];
const user_roles_values = ["role_admin"];
function checkRoles() {
const org_id = props.orgId;
const org_roles = auth.userInfo.user_object.orgs;
const user_roles = auth.userInfo.user_object.roles;
const org_role = org_roles.find((org: any) => org.org_id == org_id);
const user_role = user_roles.find((role: any) => role.org_id == org_id);
if (org_role && user_role) {
if (org_roles_values.includes(org_role.org_role) || user_roles_values.includes(user_role.role_id)) {
return true;
}
else {
return false;
}
} else {
return false;
}
}
if ((props.checkMethod == 'authentication' && auth.isAuthenticated) || (auth.isAuthenticated && props.checkMethod == 'roles' && checkRoles())) {
return <>{props.children}</>; return <>{props.children}</>;
} }
return <></>; return <></>;

View file

@ -29,10 +29,10 @@ export const HeaderProfileBox = () => {
)} )}
{auth.isAuthenticated && ( {auth.isAuthenticated && (
<AccountArea className="-space-x-2"> <AccountArea className="-space-x-2">
<div className="text-xs px-4 text-gray-600 p-1.5 rounded-full bg-gray-50">{auth.userInfo.user_object.full_name}</div> <div className="text-xs px-4 text-gray-600 p-1.5 rounded-full bg-gray-50">{auth.userInfo.username}</div>
<div className="flex -space-x-2 items-center"> <div className="flex -space-x-2 items-center">
<div className="py-4"> <div className="py-4">
<Avvvatars size={26} value={auth.userInfo.user_object.user_id} style="shape" /> <Avvvatars size={26} value={auth.userInfo.user_uuid} style="shape" />
</div> </div>
<Link className="bg-gray-50 p-1.5 rounded-full" href={"/settings"}><GearIcon fontSize={26} /></Link> <Link className="bg-gray-50 p-1.5 rounded-full" href={"/settings"}><GearIcon fontSize={26} /></Link>
</div> </div>

View file

@ -1,11 +1,11 @@
{ {
"name": "learnhouse-web", "name": "learnhouse",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "learnhouse-web", "name": "learnhouse",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@radix-ui/colors": "^0.1.8", "@radix-ui/colors": "^0.1.8",
@ -28,7 +28,7 @@
"framer-motion": "^10.16.1", "framer-motion": "^10.16.1",
"lowlight": "^3.0.0", "lowlight": "^3.0.0",
"lucide-react": "^0.268.0", "lucide-react": "^0.268.0",
"next": "^13.5.4", "next": "^14.0.3",
"re-resizable": "^6.9.9", "re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
@ -39,7 +39,7 @@
"react-spinners": "^0.13.8", "react-spinners": "^0.13.8",
"react-youtube": "^10.1.0", "react-youtube": "^10.1.0",
"styled-components": "^6.0.0-beta.9", "styled-components": "^6.0.0-beta.9",
"swr": "^2.0.1", "swr": "^2.2.4",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"y-indexeddb": "^9.0.9", "y-indexeddb": "^9.0.9",
@ -2266,9 +2266,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.3.tgz",
"integrity": "sha512-LGegJkMvRNw90WWphGJ3RMHMVplYcOfRWf2Be3td3sUa+1AaxmsYyANsA+znrGCBjXJNi4XAQlSoEfUxs/4kIQ==" "integrity": "sha512-7xRqh9nMvP5xrW4/+L0jgRRX+HoNRGnfJpD+5Wq6/13j3dsdzxO3BCXn7D3hMqsDb+vjZnJq+vI7+EtgrYZTeA=="
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
"version": "13.5.4", "version": "13.5.4",
@ -2300,9 +2300,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.3.tgz",
"integrity": "sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==", "integrity": "sha512-64JbSvi3nbbcEtyitNn2LEDS/hcleAFpHdykpcnrstITFlzFgB/bW0ER5/SJJwUPj+ZPY+z3e+1jAfcczRLVGw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2315,9 +2315,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.3.tgz",
"integrity": "sha512-siPuUwO45PnNRMeZnSa8n/Lye5ZX93IJom9wQRB5DEOdFrw0JjOMu1GINB8jAEdwa7Vdyn1oJ2xGNaQpdQQ9Pw==", "integrity": "sha512-RkTf+KbAD0SgYdVn1XzqE/+sIxYGB7NLMZRn9I4Z24afrhUpVJx6L8hsRnIwxz3ERE2NFURNliPjJ2QNfnWicQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2330,9 +2330,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.3.tgz",
"integrity": "sha512-l/k/fvRP/zmB2jkFMfefmFkyZbDkYW0mRM/LB+tH5u9pB98WsHXC0WvDHlGCYp3CH/jlkJPL7gN8nkTQVrQ/2w==", "integrity": "sha512-3tBWGgz7M9RKLO6sPWC6c4pAw4geujSwQ7q7Si4d6bo0l6cLs4tmO+lnSwFp1Tm3lxwfMk0SgkJT7EdwYSJvcg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2345,9 +2345,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.3.tgz",
"integrity": "sha512-YYGb7SlLkI+XqfQa8VPErljb7k9nUnhhRrVaOdfJNCaQnHBcvbT7cx/UjDQLdleJcfyg1Hkn5YSSIeVfjgmkTg==", "integrity": "sha512-v0v8Kb8j8T23jvVUWZeA2D8+izWspeyeDGNaT2/mTHWp7+37fiNfL8bmBWiOmeumXkacM/AB0XOUQvEbncSnHA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2360,9 +2360,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.3.tgz",
"integrity": "sha512-uE61vyUSClnCH18YHjA8tE1prr/PBFlBFhxBZis4XBRJoR+txAky5d7gGNUIbQ8sZZ7LVkSVgm/5Fc7mwXmRAg==", "integrity": "sha512-VM1aE1tJKLBwMGtyBR21yy+STfl0MapMQnNrXkxeyLs0GFv/kZqXS5Jw/TQ3TSUnbv0QPDf/X8sDXuMtSgG6eg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2375,9 +2375,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.3.tgz",
"integrity": "sha512-qVEKFYML/GvJSy9CfYqAdUexA6M5AklYcQCW+8JECmkQHGoPxCf04iMh7CPR7wkHyWWK+XLt4Ja7hhsPJtSnhg==", "integrity": "sha512-64EnmKy18MYFL5CzLaSuUn561hbO1Gk16jM/KHznYP3iCIfF9e3yULtHaMy0D8zbHfxset9LTOv6cuYKJgcOxg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2390,9 +2390,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.3.tgz",
"integrity": "sha512-mDSQfqxAlfpeZOLPxLymZkX0hYF3juN57W6vFHTvwKlnHfmh12Pt7hPIRLYIShk8uYRsKPtMTth/EzpwRI+u8w==", "integrity": "sha512-WRDp8QrmsL1bbGtsh5GqQ/KWulmrnMBgbnb+59qNTW1kVi1nG/2ndZLkcbs2GX7NpFLlToLRMWSQXmPzQm4tog==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -2405,9 +2405,9 @@
} }
}, },
"node_modules/@next/swc-win32-ia32-msvc": { "node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.3.tgz",
"integrity": "sha512-aoqAT2XIekIWoriwzOmGFAvTtVY5O7JjV21giozBTP5c6uZhpvTWRbmHXbmsjZqY4HnEZQRXWkSAppsIBweKqw==", "integrity": "sha512-EKffQeqCrj+t6qFFhIFTRoqb2QwX1mU7iTOvMyLbYw3QtqTw9sMwjykyiMlZlrfm2a4fA84+/aeW+PMg1MjuTg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -2420,9 +2420,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.3.tgz",
"integrity": "sha512-cyRvlAxwlddlqeB9xtPSfNSCRy8BOa4wtMo0IuI9P7Y0XT2qpDrpFKRyZ7kUngZis59mPVla5k8X1oOJ8RxDYg==", "integrity": "sha512-ERhKPSJ1vQrPiwrs15Pjz/rvDHZmkmvbf/BjPN/UCOI++ODftT0GtasDPi0j+y6PPJi5HsXw+dpRaXUaw4vjuQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7117,11 +7117,11 @@
"dev": true "dev": true
}, },
"node_modules/next": { "node_modules/next": {
"version": "13.5.4", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/next/-/next-13.5.4.tgz", "resolved": "https://registry.npmjs.org/next/-/next-14.0.3.tgz",
"integrity": "sha512-+93un5S779gho8y9ASQhb/bTkQF17FNQOtXLKAj3lsNgltEcF0C5PMLLncDmH+8X1EnJH1kbqAERa29nRXqhjA==", "integrity": "sha512-AbYdRNfImBr3XGtvnwOxq8ekVCwbFTv/UJoLwmaX89nk9i051AEY4/HAWzU0YpaTDw8IofUpmuIlvzWF13jxIw==",
"dependencies": { "dependencies": {
"@next/env": "13.5.4", "@next/env": "14.0.3",
"@swc/helpers": "0.5.2", "@swc/helpers": "0.5.2",
"busboy": "1.6.0", "busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406", "caniuse-lite": "^1.0.30001406",
@ -7133,18 +7133,18 @@
"next": "dist/bin/next" "next": "dist/bin/next"
}, },
"engines": { "engines": {
"node": ">=16.14.0" "node": ">=18.17.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "13.5.4", "@next/swc-darwin-arm64": "14.0.3",
"@next/swc-darwin-x64": "13.5.4", "@next/swc-darwin-x64": "14.0.3",
"@next/swc-linux-arm64-gnu": "13.5.4", "@next/swc-linux-arm64-gnu": "14.0.3",
"@next/swc-linux-arm64-musl": "13.5.4", "@next/swc-linux-arm64-musl": "14.0.3",
"@next/swc-linux-x64-gnu": "13.5.4", "@next/swc-linux-x64-gnu": "14.0.3",
"@next/swc-linux-x64-musl": "13.5.4", "@next/swc-linux-x64-musl": "14.0.3",
"@next/swc-win32-arm64-msvc": "13.5.4", "@next/swc-win32-arm64-msvc": "14.0.3",
"@next/swc-win32-ia32-msvc": "13.5.4", "@next/swc-win32-ia32-msvc": "14.0.3",
"@next/swc-win32-x64-msvc": "13.5.4" "@next/swc-win32-x64-msvc": "14.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",

View file

@ -29,7 +29,7 @@
"framer-motion": "^10.16.1", "framer-motion": "^10.16.1",
"lowlight": "^3.0.0", "lowlight": "^3.0.0",
"lucide-react": "^0.268.0", "lucide-react": "^0.268.0",
"next": "^13.5.4", "next": "^14.0.3",
"re-resizable": "^6.9.9", "re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
@ -40,7 +40,7 @@
"react-spinners": "^0.13.8", "react-spinners": "^0.13.8",
"react-youtube": "^10.1.0", "react-youtube": "^10.1.0",
"styled-components": "^6.0.0-beta.9", "styled-components": "^6.0.0-beta.9",
"swr": "^2.0.1", "swr": "^2.2.4",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"y-indexeddb": "^9.0.9", "y-indexeddb": "^9.0.9",

View file

@ -40,7 +40,7 @@ export async function getUserInfo(token: string): Promise<any> {
credentials: "include", credentials: "include",
}; };
return fetch(`${getAPIUrl()}users/profile_metadata`, requestOptions) return fetch(`${getAPIUrl()}users/profile`, requestOptions)
.then((result) => result.json()) .then((result) => result.json())
.catch((error) => console.log("error", error)); .catch((error) => console.log("error", error));
} }

View file

@ -14,19 +14,18 @@ export async function createActivity(data: any, chapter_id: any, org_id: any) {
export async function createFileActivity(file: File, type: string, data: any, chapter_id: any) { export async function createFileActivity(file: File, type: string, data: any, chapter_id: any) {
// Send file thumbnail as form data // Send file thumbnail as form data
const formData = new FormData(); const formData = new FormData();
formData.append("coursechapter_id", chapter_id); formData.append("chapter_id", chapter_id);
let org_id = "test";
let endpoint = ""; let endpoint = "";
if (type === "video") { if (type === "video") {
formData.append("name", data.name); formData.append("name", data.name);
formData.append("video_file", file); formData.append("video_file", file);
endpoint = `${getAPIUrl()}activities/video?org_id=${org_id}`; endpoint = `${getAPIUrl()}activities/video`;
} else if (type === "documentpdf") { } else if (type === "documentpdf") {
formData.append("pdf_file", file); formData.append("pdf_file", file);
formData.append("name", data.name); formData.append("name", data.name);
endpoint = `${getAPIUrl()}activities/documentpdf?org_id=${org_id}`; endpoint = `${getAPIUrl()}activities/documentpdf`;
} else { } else {
// Handle other file types here // Handle other file types here
} }
@ -38,7 +37,7 @@ export async function createFileActivity(file: File, type: string, data: any, ch
export async function createExternalVideoActivity(data: any, activity: any, chapter_id: any) { export async function createExternalVideoActivity(data: any, activity: any, chapter_id: any) {
// add coursechapter_id to data // add coursechapter_id to data
data.coursechapter_id = chapter_id; data.chapter_id = chapter_id;
data.activity_id = activity.id; data.activity_id = activity.id;
const result = await fetch(`${getAPIUrl()}activities/external_video?coursechapter_id=${chapter_id}`, RequestBody("POST", data, null)); const result = await fetch(`${getAPIUrl()}activities/external_video?coursechapter_id=${chapter_id}`, RequestBody("POST", data, null));

View file

@ -6,20 +6,20 @@ import { getAPIUrl } from "@services/config/config";
GET requests are called from the frontend using SWR (https://swr.vercel.app/) GET requests are called from the frontend using SWR (https://swr.vercel.app/)
*/ */
export async function startCourse(course_id: string, org_slug: string) { export async function startCourse(course_uuid: string, org_slug: string) {
const result: any = await fetch(`${getAPIUrl()}trail/org_slug/${org_slug}/add_course/${course_id}`, RequestBody("POST", null, null)) const result: any = await fetch(`${getAPIUrl()}trail/add_course/${course_uuid}`, RequestBody("POST", null, null))
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
export async function removeCourse(course_id: string, org_slug: string) { export async function removeCourse(course_uuid: string, org_slug: string) {
const result: any = await fetch(`${getAPIUrl()}trail/org_slug/${org_slug}/remove_course/${course_id}`, RequestBody("POST", null, null)) const result: any = await fetch(`${getAPIUrl()}trail/remove_course/${course_uuid}`, RequestBody("DELETE", null, null))
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
export async function markActivityAsComplete(org_slug: string, course_id: string, activity_id: string) { export async function markActivityAsComplete(org_slug: string, course_uuid: string, activity_id: string) {
const result: any = await fetch(`${getAPIUrl()}trail/org_slug/${org_slug}/add_activity/course_id/${course_id}/activity_id/${activity_id}`, RequestBody("POST", null, null)) const result: any = await fetch(`${getAPIUrl()}trail/add_activity/${activity_id}`, RequestBody("POST", null, null))
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }

View file

@ -7,16 +7,14 @@ import { RequestBody, RequestBodyWithAuthHeader, errorHandling } from "@services
*/ */
//TODO : depreciate this function //TODO : depreciate this function
export async function getCourseChaptersMetadata(course_id: any, next: any) { export async function getCourseChaptersMetadata(course_uuid: any, next: any) {
const result = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("GET", null, next)); const result = await fetch(`${getAPIUrl()}chapters/meta/course_${course_uuid}`, RequestBody("GET", null, next));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
export async function updateChaptersMetadata(course_uuid: any, data: any) {
const result: any = await fetch(`${getAPIUrl()}chapters/course/course_${course_uuid}/order`, RequestBody("PUT", data, null));
export async function updateChaptersMetadata(course_id: any, data: any) {
const result: any = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("PUT", data, null));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
@ -27,8 +25,8 @@ export async function updateChapter(coursechapter_id: any, data: any) {
return res; return res;
} }
export async function createChapter(data: any, course_id: any) { export async function createChapter(data: any) {
const result: any = await fetch(`${getAPIUrl()}chapters/?course_id=course_${course_id}`, RequestBody("POST", data, null)); const result: any = await fetch(`${getAPIUrl()}chapters/`, RequestBody("POST", data, null));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;

View file

@ -6,8 +6,8 @@ import { RequestBody, RequestBodyWithAuthHeader, errorHandling } from "@services
GET requests are called from the frontend using SWR (https://swr.vercel.app/) GET requests are called from the frontend using SWR (https://swr.vercel.app/)
*/ */
export async function deleteCollection(collection_id: any) { export async function deleteCollection(collection_uuid: any) {
const result: any = await fetch(`${getAPIUrl()}collections/${collection_id}`, RequestBody("DELETE", null, null)); const result: any = await fetch(`${getAPIUrl()}collections/${collection_uuid}`, RequestBody("DELETE", null, null));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
@ -20,14 +20,14 @@ export async function createCollection(collection: any) {
} }
// Get a colletion by id // Get a colletion by id
export async function getCollectionById(collection_id: any) { export async function getCollectionById(collection_uuid: any) {
const result: any = await fetch(`${getAPIUrl()}collections/${collection_id}`, { next: { revalidate: 10 } }); const result: any = await fetch(`${getAPIUrl()}collections/${collection_uuid}`, { next: { revalidate: 10 } });
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
export async function getCollectionByIdWithAuthHeader(collection_id: any, access_token: string, next: any) { export async function getCollectionByIdWithAuthHeader(collection_uuid: any, access_token: string, next: any) {
const result: any = await fetch(`${getAPIUrl()}collections/collection_${collection_id}`, RequestBodyWithAuthHeader("GET", null, next, access_token)); const result: any = await fetch(`${getAPIUrl()}collections/collection_${collection_uuid}`, RequestBodyWithAuthHeader("GET", null, next, access_token));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
@ -41,7 +41,7 @@ export async function getOrgCollections() {
} }
export async function getOrgCollectionsWithAuthHeader(org_id: string, access_token: string, next: any) { export async function getOrgCollectionsWithAuthHeader(org_id: string, access_token: string, next: any) {
const result: any = await fetch(`${getAPIUrl()}collections/org_id/${org_id}/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, next, access_token)); const result: any = await fetch(`${getAPIUrl()}collections/org/${org_id}/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, next, access_token));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }

View file

@ -18,20 +18,20 @@ export async function getOrgCoursesWithAuthHeader(org_id: number, next: any, acc
return res; return res;
} }
export async function getCourseMetadataWithAuthHeader(course_id: any, next: any, access_token: string) { export async function getCourseMetadataWithAuthHeader(course_uuid: any, next: any, access_token: string) {
const result = await fetch(`${getAPIUrl()}courses/meta/course_${course_id}`, RequestBodyWithAuthHeader("GET", null, next, access_token)); const result = await fetch(`${getAPIUrl()}courses/course_${course_uuid}/meta`, RequestBodyWithAuthHeader("GET", null, next, access_token));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
export async function updateCourse(course_id: any, data: any) { export async function updateCourse(course_uuid: any, data: any) {
const result: any = await fetch(`${getAPIUrl()}courses/course_${course_id}`, RequestBody("PUT", data, null)); const result: any = await fetch(`${getAPIUrl()}courses/course_${course_uuid}`, RequestBody("PUT", data, null));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
export async function getCourse(course_id: string, next: any) { export async function getCourse(course_uuid: string, next: any) {
const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("GET", null, next)); const result: any = await fetch(`${getAPIUrl()}courses/${course_uuid}`, RequestBody("GET", null, next));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
@ -39,19 +39,21 @@ export async function getCourse(course_id: string, next: any) {
export async function createNewCourse(org_id: string, course_body: any, thumbnail: any) { export async function createNewCourse(org_id: string, course_body: any, thumbnail: any) {
// Send file thumbnail as form data // Send file thumbnail as form data
const formData = new FormData(); const formData = new FormData();
formData.append("thumbnail", thumbnail);
formData.append("name", course_body.name); formData.append("name", course_body.name);
formData.append("description", course_body.description); formData.append("description", course_body.description);
formData.append("mini_description", "course_body.mini_description"); formData.append("public", course_body.visibility);
formData.append("public", "true"); formData.append("learnings", course_body.tags);
formData.append("tags", course_body.tags);
formData.append("about", course_body.description);
formData.append("thumbnail", thumbnail);
const result = await fetch(`${getAPIUrl()}courses/?org_id=${org_id}`, RequestBodyForm("POST", formData, null)); const result = await fetch(`${getAPIUrl()}courses/?org_id=${org_id}`, RequestBodyForm("POST", formData, null));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }
export async function deleteCourseFromBackend(course_id: any) { export async function deleteCourseFromBackend(course_uuid: any) {
const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("DELETE", null, null)); const result: any = await fetch(`${getAPIUrl()}courses/${course_uuid}`, RequestBody("DELETE", null, null));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }