From cf7285b6f995db823fbb3ebd66da27e305043053 Mon Sep 17 00:00:00 2001 From: swve Date: Sun, 22 Jan 2023 15:43:42 +0100 Subject: [PATCH] feat: init activity starting from course --- .../[courseid]/lecture/[lectureid]/page.tsx | 55 +++++++ .../[orgslug]/course/[courseid]/page.tsx | 136 +++++++++++++----- front/services/courses/activity.ts | 24 ++++ front/services/utils/requests.ts | 13 ++ src/routers/activity.py | 20 +-- src/services/activity.py | 35 +++-- src/services/courses/chapters.py | 2 +- src/services/courses/courses.py | 32 +++-- 8 files changed, 247 insertions(+), 70 deletions(-) create mode 100644 front/services/courses/activity.ts create mode 100644 front/services/utils/requests.ts diff --git a/front/app/_orgs/[orgslug]/course/[courseid]/lecture/[lectureid]/page.tsx b/front/app/_orgs/[orgslug]/course/[courseid]/lecture/[lectureid]/page.tsx index 3d59b43a..8deb297b 100644 --- a/front/app/_orgs/[orgslug]/course/[courseid]/lecture/[lectureid]/page.tsx +++ b/front/app/_orgs/[orgslug]/course/[courseid]/lecture/[lectureid]/page.tsx @@ -9,6 +9,7 @@ import Canva from "../../../../../../../components/LectureViews/DynamicCanva/Dyn import styled from "styled-components"; import { getCourse, getCourseMetadata } from "../../../../../../../services/courses/courses"; import VideoLecture from "@components/LectureViews/Video/Video"; +import { Check } from "lucide-react"; function LecturePage(params: any) { const router = useRouter(); @@ -82,7 +83,12 @@ function LecturePage(params: any) { {lecture.type == "dynamic" && } {/* todo : use apis & streams instead of this */} {lecture.type == "video" && } + + + + + )} @@ -151,7 +157,56 @@ const LectureTopWrapper = styled.div` const CourseContent = styled.div` display: flex; + flex-direction: column; background-color: white; min-height: 600px; `; + +const ActivityMarkerWrapper = styled.div` + display: block; + width: 1300px; + justify-content: flex-end; + margin: 0 auto; + align-items: center; + + + button{ + background-color: #151515; + border: none; + padding: 18px; + border-radius: 15px; + margin: 15px; + margin-left: 20px; + margin-top: 20px; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + margin: auto; + color: white; + font-weight: 700; + font-family: "DM Sans"; + font-size: 16px; + letter-spacing: -0.05em; + box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42); + + + i{ + margin-right: 5px; + + // center the icon + display: flex; + align-items: center; + justify-content: center; + + + } + + &:hover{ + background-color: #000000; + } + } +`; + export default LecturePage; diff --git a/front/app/_orgs/[orgslug]/course/[courseid]/page.tsx b/front/app/_orgs/[orgslug]/course/[courseid]/page.tsx index b10b9087..a4031e94 100644 --- a/front/app/_orgs/[orgslug]/course/[courseid]/page.tsx +++ b/front/app/_orgs/[orgslug]/course/[courseid]/page.tsx @@ -1,5 +1,6 @@ "use client"; import { EyeOpenIcon, Pencil2Icon } from "@radix-ui/react-icons"; +import { closeActivity, createActivity } from "@services/courses/activity"; import Link from "next/link"; import { useRouter } from "next/navigation"; import React from "react"; @@ -18,12 +19,26 @@ const CourseIdPage = (params: any) => { async function fetchCourseInfo() { const course = await getCourseMetadata("course_" + courseid); - setCourseInfo(course); - setIsLoading(false); } + async function startActivity() { + const activity = await createActivity("course_" + courseid); + fetchCourseInfo(); + } + + async function quitActivity() { + let activity_id = courseInfo.activity.activity_id; + let org_id = courseInfo.activity.org_id; + console.log("activity", activity_id); + + let activity = await closeActivity(activity_id, org_id); + console.log(activity); + + fetchCourseInfo(); + } + React.useEffect(() => { if (courseid && orgslug) { fetchCourseInfo(); @@ -69,41 +84,52 @@ const CourseIdPage = (params: any) => { -

Description

+ + +

Description

- -

{courseInfo.course.description}

-
+ +

{courseInfo.course.description}

+
-

What you will learn

- -

{courseInfo.course.learnings == ![] ? "no data" : courseInfo.course.learnings}

-
+

What you will learn

+ +

{courseInfo.course.learnings == ![] ? "no data" : courseInfo.course.learnings}

+
-

Course Lessons

+

Course Lessons

- - {courseInfo.chapters.map((chapter: any) => { - return ( - <> -

Chapter : {chapter.name}

- {chapter.lectures.map((lecture: any) => { - return ( - <> -

- Lecture {lecture.name} - - - {" "} -

- - ); - })} -      - - ); - })} -
+ + {courseInfo.chapters.map((chapter: any) => { + return ( + <> +

Chapter : {chapter.name}

+ {chapter.lectures.map((lecture: any) => { + return ( + <> +

+ Lecture {lecture.name} + + + {" "} +

+ + ); + })} +      + + ); + })} +
+
+ + {courseInfo.activity.status == "ongoing" ? ( + + ) : ( + + )} + +
)} @@ -139,8 +165,6 @@ const CoursePageLayout = styled.div` letter-spacing: -0.05em; margin-bottom: 10px; } - - `; const ChaptersWrapper = styled.div` @@ -175,7 +199,6 @@ const BoxWrapper = styled.div` padding-top: 7px; padding-left: 30px; - p { font-family: "DM Sans"; font-style: normal; @@ -187,4 +210,45 @@ const BoxWrapper = styled.div` } `; +const CourseMetaWrapper = styled.div` + display: flex; + justify-content: space-between; +`; + +const CourseMetaLeft = styled.div` + width: 80%; +`; + +const CourseMetaRight = styled.div` + background: #ffffff; + box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.03); + border-radius: 7px; + padding: 20px; + width: 30%; + display: flex; + height: 100%; + justify-content: center; + margin-left: 50px; + margin-top: 20px; + button { + width: 100%; + height: 50px; + background: #151515; + border-radius: 15px; + border: none; + color: white; + font-weight: 700; + font-family: "DM Sans"; + font-size: 16px; + letter-spacing: -0.05em; + transition: all 0.2s ease; + box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.42); + + &:hover { + cursor: pointer; + background: #000000; + } + } +`; + export default CourseIdPage; diff --git a/front/services/courses/activity.ts b/front/services/courses/activity.ts new file mode 100644 index 00000000..d1da007f --- /dev/null +++ b/front/services/courses/activity.ts @@ -0,0 +1,24 @@ +import { RequestBody } from "@services/utils/requests"; +import { getAPIUrl } from "../config"; + +/* + This file includes only POST, PUT, DELETE requests + GET requests are called from the frontend using SWR (https://swr.vercel.app/) +*/ + +export async function createActivity(course_id: string) { + let data = { + course_id: course_id, + }; + const result: any = await fetch(`${getAPIUrl()}activity/start`, RequestBody("POST", data)) + .then((result) => result.json()) + .catch((error) => console.log("error", error)); + return result; +} + +export async function closeActivity(org_id: string, activity_id: string) { + const result: any = await fetch(`${getAPIUrl()}activity/${org_id}/close_activity/${activity_id}"`, RequestBody("PATCH", null)) + .then((result) => result.json()) + .catch((error) => console.log("error", error)); + return result; +} diff --git a/front/services/utils/requests.ts b/front/services/utils/requests.ts new file mode 100644 index 00000000..a13fc876 --- /dev/null +++ b/front/services/utils/requests.ts @@ -0,0 +1,13 @@ +export const RequestBody = (method: string, data: any) => { + let HeadersConfig = new Headers({ "Content-Type": "application/json" }); + let options: any = { + method: method, + headers: HeadersConfig, + redirect: "follow", + credentials: "include", + }; + if (data) { + options.body = JSON.stringify(data); + } + return options; +}; diff --git a/src/routers/activity.py b/src/routers/activity.py index 2ccc2362..dd3b8b84 100644 --- a/src/routers/activity.py +++ b/src/routers/activity.py @@ -1,20 +1,22 @@ from fastapi import APIRouter, Depends, Request from src.dependencies.auth import get_current_user -from src.services.activity import Activity, add_chapter_to_activity, close_activity, create_activity, get_user_activities +from src.services.activity import Activity, add_lecture_to_activity, close_activity, create_activity, get_user_activities router = APIRouter() -@router.post("/") +@router.post("/start") async def api_start_activity(request: Request, activity_object: Activity, user=Depends(get_current_user)): """ - Start activity + Start activity """ return await create_activity(request, user, activity_object) +# TODO : get activity by user_is and org_id and course_id -@router.get("{org_id}/activities") + +@router.get("/{org_id}/activities") async def api_get_activity_by_userid(request: Request, org_id: str, user=Depends(get_current_user)): """ Get a user activities @@ -22,15 +24,15 @@ async def api_get_activity_by_userid(request: Request, org_id: str, user=Depends return await get_user_activities(request, user, org_id) -@router.post("/{org_id}/add_chapter/{course_id}/{chapter_id}") -async def api_add_chapter_to_activity(request: Request, org_id: str, course_id: str, chapter_id: str, user=Depends(get_current_user)): +@router.post("/{org_id}/add_lecture/{course_id}/{lecture_id}") +async def api_add_lecture_to_activity(request: Request, org_id: str, course_id: str, lecture_id: str, user=Depends(get_current_user)): """ - Add chapter to activity + Add lecture to activity """ - return await add_chapter_to_activity(request, user, org_id, course_id, chapter_id) + return await add_lecture_to_activity(request, user, org_id, course_id, lecture_id) -@router.patch("{org_id}/close_activity/{activity_id}") +@router.patch("/{org_id}/close_activity/{activity_id}") async def api_close_activity(request: Request, org_id: str, activity_id: str, user=Depends(get_current_user)): """ Close activity diff --git a/src/services/activity.py b/src/services/activity.py index 926b443e..6b621740 100644 --- a/src/services/activity.py +++ b/src/services/activity.py @@ -14,8 +14,8 @@ class Activity(BaseModel): course_id: str status: Optional[Literal['ongoing', 'done', 'closed']] = 'ongoing' masked: Optional[bool] = False - chapters_marked_complete: Optional[List[str]] - chapters_data: Optional[List[dict]] + lectures_marked_complete: Optional[List[str]] + lectures_data: Optional[List[dict]] class ActivityInDB(Activity): @@ -33,12 +33,18 @@ async def create_activity(request: Request, user: PublicUser, activity_object: A activities = request.app.db["activities"] # find if the user has already started the course - isActivityAlreadyStarted = activities.find_one( + isActivityAlreadCreated = activities.find_one( {"course_id": activity_object.course_id, "user_id": user.user_id}) - if isActivityAlreadyStarted: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Activity already started") + if isActivityAlreadCreated: + if isActivityAlreadCreated['status'] == 'closed': + activity_object.status = 'ongoing' + activities.update_one( + {"activity_id": isActivityAlreadCreated['activity_id']}, {"$set": activity_object.dict()}) + return activity_object + else: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Activity already created") # create activity activity = ActivityInDB(**activity_object.dict(), @@ -62,7 +68,7 @@ async def get_user_activities(request: Request, user: PublicUser, org_id: str): return [json.loads(json.dumps(activity, default=str)) for activity in user_activities] -async def add_chapter_to_activity(request: Request, user: PublicUser, org_id: str, course_id: str, chapter_id: str): +async def add_lecture_to_activity(request: Request, user: PublicUser, org_id: str, course_id: str, lecture_id: str): activities = request.app.db["activities"] activity = activities.find_one( @@ -75,24 +81,25 @@ async def add_chapter_to_activity(request: Request, user: PublicUser, org_id: st raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Activity not found") - if chapter_id in activity['chapters_marked_complete']: + if lecture_id in activity['lectures_marked_complete']: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Chapter already marked complete") + status_code=status.HTTP_409_CONFLICT, detail="Lecture already marked complete") - activity['chapters_marked_complete'].append(chapter_id) + activity['lectures_marked_complete'].append(lecture_id) activities.update_one( {"activity_id": activity['activity_id']}, {"$set": activity}) # send 200 custom message - return {"message": "Chapter added to activity"} + return {"message": "Lecture added to activity"} -async def close_activity(request: Request, user: PublicUser, org_id: str, activity_id: str): +async def close_activity(request: Request, user: PublicUser, activity_id: str, org_id: str,): activities = request.app.db["activities"] - + print(activity_id) + print(org_id) activity = activities.find_one( - {"activity_id": activity_id, "user_id": user.user_id, "org_id": org_id}) + {"activity_id": activity_id, "user_id": user.user_id}) if not activity: raise HTTPException( diff --git a/src/services/courses/chapters.py b/src/services/courses/chapters.py index 17391764..268b2ba6 100644 --- a/src/services/courses/chapters.py +++ b/src/services/courses/chapters.py @@ -206,7 +206,7 @@ async def update_coursechapters_meta(request: Request,course_id: str, coursechap "$set": {"chapters": coursechapters_metadata.chapterOrder}}) # update lectures in coursechapters - # TODO : performance/optimization improvement + # TODO : performance/optimization improvement, this does not work anyway. for coursechapter in coursechapters_metadata.chapters.__dict__.items(): coursechapters.update_one({"coursechapter_id": coursechapter}, { "$set": {"lectures": coursechapters_metadata.chapters[coursechapter]["lectureIds"]}}) # type: ignore diff --git a/src/services/courses/courses.py b/src/services/courses/courses.py index c27ef657..e5c95de4 100644 --- a/src/services/courses/courses.py +++ b/src/services/courses/courses.py @@ -53,13 +53,13 @@ class CourseChapterInDB(CourseChapter): # CRUD #################################################### -async def get_course(request: Request,course_id: str, current_user: PublicUser): +async def get_course(request: Request, course_id: str, current_user: PublicUser): courses = request.app.db["courses"] course = courses.find_one({"course_id": course_id}) # verify course rights - await verify_rights(request,course_id, current_user, "read") + await verify_rights(request, course_id, current_user, "read") if not course: raise HTTPException( @@ -69,14 +69,16 @@ async def get_course(request: Request,course_id: str, current_user: PublicUser): return course -async def get_course_meta(request: Request,course_id: str, current_user: PublicUser): +async def get_course_meta(request: Request, course_id: str, current_user: PublicUser): courses = request.app.db["courses"] coursechapters = request.app.db["coursechapters"] + activities = request.app.db["activities"] + course = courses.find_one({"course_id": course_id}) lectures = request.app.db["lectures"] # verify course rights - await verify_rights(request,course_id, current_user, "read") + await verify_rights(request, course_id, current_user, "read") if not course: raise HTTPException( @@ -115,13 +117,23 @@ async def get_course_meta(request: Request,course_id: str, current_user: PublicU chapters_list_with_lectures.append( {"id": chapters[chapter]["id"], "name": chapters[chapter]["name"], "lectures": [lectures_list[lecture] for lecture in chapters[chapter]["lectureIds"]]}) course = Course(**course) + + # Get activity by user + activity = activities.find_one( + {"course_id": course_id, "user_id": current_user.user_id}) + if activity: + activity = json.loads(json.dumps(activity, default=str)) + else: + activity = "" + return { "course": course, "chapters": chapters_list_with_lectures, + "activity": activity } -async def create_course(request: Request,course_object: Course, org_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): +async def create_course(request: Request, course_object: Course, org_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): courses = request.app.db["courses"] # generate course_id with uuid4 @@ -152,7 +164,7 @@ async def create_course(request: Request,course_object: Course, org_id: str, cur return course.dict() -async def update_course_thumbnail(request: Request,course_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): +async def update_course_thumbnail(request: Request, course_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): # verify course rights await verify_rights(request, course_id, current_user, "update") @@ -182,7 +194,7 @@ async def update_course_thumbnail(request: Request,course_id: str, current_user: status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") -async def update_course(request: Request,course_object: Course, course_id: str, current_user: PublicUser): +async def update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser): # verify course rights await verify_rights(request, course_id, current_user, "update") @@ -211,7 +223,7 @@ async def update_course(request: Request,course_object: Course, course_id: str, status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") -async def delete_course(request: Request,course_id: str, current_user: PublicUser): +async def delete_course(request: Request, course_id: str, current_user: PublicUser): # verify course rights await verify_rights(request, course_id, current_user, "delete") @@ -237,7 +249,7 @@ async def delete_course(request: Request,course_id: str, current_user: PublicUse #################################################### -async def get_courses(request: Request,page: int = 1, limit: int = 10, org_id: str | None = None): +async def get_courses(request: Request, page: int = 1, limit: int = 10, org_id: str | None = None): courses = request.app.db["courses"] # TODO : Get only courses that user is admin/has roles of # get all courses from database @@ -250,7 +262,7 @@ async def get_courses(request: Request,page: int = 1, limit: int = 10, org_id: s #### Security #################################################### -async def verify_rights(request: Request,course_id: str, current_user: PublicUser, action: str): +async def verify_rights(request: Request, course_id: str, current_user: PublicUser, action: str): courses = request.app.db["courses"] course = courses.find_one({"course_id": course_id})