diff --git a/front/app/_orgs/[orgslug]/activity/page.tsx b/front/app/_orgs/[orgslug]/activity/page.tsx new file mode 100644 index 00000000..5228795c --- /dev/null +++ b/front/app/_orgs/[orgslug]/activity/page.tsx @@ -0,0 +1,124 @@ +"use client"; +import { getBackendUrl } from "@services/config"; +import { getActivities } from "@services/courses/activity"; +import { getOrganizationContextInfo } from "@services/orgs"; +import { RequestBody } from "@services/utils/requests"; +import React from "react"; +import { styled } from "styled-components"; +import useSWR from "swr"; + +function Activity(params: any) { + let orgslug = params.params.orgslug; + const [isLoading, setIsLoading] = React.useState(true); + const [activities, setActivities] = React.useState([]); + + async function fetchActivities() { + setIsLoading(true); + const org = await getOrganizationContextInfo(orgslug); + const activities = await getActivities(org.org_id); + console.log(activities); + + setActivities(activities); + setIsLoading(false); + } + React.useEffect(() => { + fetchActivities(); + }, []); + + return ( + +

Activity

+
+ {isLoading ? ( +
Loading...
+ ) : ( +
+ {activities.map((activity: any) => ( + + + + + + + +

Course

+

{activity.course.name}

+
+
+ +
+ ))} +
+ )} +
+ ); +} + +export default Activity; + +const ActivityLayout = styled.div` + display: flex; + margin: 0 auto; + width: 1300px; + height: 100%; + flex-direction: column; +`; + +const ActivityMetadata = styled.div` + display: flex; + flex-direction: row; + width: 100%; + height: 100%; +`; +const ActivityBox = styled.div` + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + margin-top: 20px; + margin-bottom: 20px; + padding: 15px; + border-radius: 7px; + box-shadow: 0px 13px 33px -13px rgba(0, 0, 0, 0.206); + background: #ffffff; +`; + +const ActivityThumbnail = styled.div` + padding-right: 30px; + height: 100%; + border-radius: 7px 0px 0px 7px; + + img { + width: 60px; + border-radius: 7px; + } +`; + +const ActivityInfo = styled.div` + width: 100%; + height: 100%; + background: #ffffff; + border-radius: 0px 7px 7px 0px; + + h2 { + font-size: 12px; + color: #2b2b2b; + padding: 0; + margin: 0; + } + + h3 { + font-size: 23px; + color: #2b2b2b; + padding: 0; + margin: 0; + } +`; + +const ActivityProgress = styled.div` + margin-top: 10px; + border-radius: 20px; + height: 10px; + width: ${(props: any) => props.progress + "%"}; + background: #06a487; +`; 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..1148b6da 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,8 @@ 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"; +import { maskLectureAsComplete } from "@services/courses/activity"; function LecturePage(params: any) { const router = useRouter(); @@ -31,6 +33,11 @@ function LecturePage(params: any) { setIsLoading(false); } + async function markLectureAsCompleteFront() { + const activity = await maskLectureAsComplete("" + lectureid, courseid, lecture.lecture_id.replace("lecture_", "")); + fetchCourseData(); + } + React.useEffect(() => { if (lectureid) { fetchLectureData(); @@ -82,6 +89,25 @@ function LecturePage(params: any) { {lecture.type == "dynamic" && } {/* todo : use apis & streams instead of this */} {lecture.type == "video" && } + + + {course.activity.lectures_marked_complete.includes("lecture_"+lectureid) && course.activity.status == "ongoing" ? ( + + ) : ( + + )} + )} @@ -151,7 +177,52 @@ 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/components/UI/Elements/Menu.tsx b/front/components/UI/Elements/Menu.tsx index 44b15afe..facbe583 100644 --- a/front/components/UI/Elements/Menu.tsx +++ b/front/components/UI/Elements/Menu.tsx @@ -7,10 +7,9 @@ import learnhouseLogo from "public/learnhouse_logo.png"; import Link from "next/link"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; -import { headers } from 'next/headers'; - -export const Menu = ({orgslug } : any) => { +import { headers } from "next/headers"; +export const Menu = ({ orgslug }: any) => { return ( @@ -30,12 +29,15 @@ export const Menu = ({orgslug } : any) => {
  • - Courses + Courses
  • - Collections + Collections +
  • +
  • + {" "} + Activity
  • -
  • Activity
  • More
diff --git a/front/package-lock.json b/front/package-lock.json index 8a2578e7..57229587 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -24,6 +24,7 @@ "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "styled-components": "^6.0.0-beta.9", + "swr": "^2.0.1", "y-indexeddb": "^9.0.9", "y-webrtc": "^10.2.3", "yjs": "^13.5.42" @@ -6731,6 +6732,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swr": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.0.1.tgz", + "integrity": "sha512-6z4FpS9dKAay7axedlStsPahEw25nuMlVh4GHkuPpGptbmEEP8v/+kr0GkAE/7ErUs25U2VFOnZQz3AWfkmXdw==", + "dependencies": { + "use-sync-external-store": "^1.2.0" + }, + "engines": { + "pnpm": "7" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/synckit": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.4.tgz", @@ -7014,6 +7029,14 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11817,6 +11840,14 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, + "swr": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.0.1.tgz", + "integrity": "sha512-6z4FpS9dKAay7axedlStsPahEw25nuMlVh4GHkuPpGptbmEEP8v/+kr0GkAE/7ErUs25U2VFOnZQz3AWfkmXdw==", + "requires": { + "use-sync-external-store": "^1.2.0" + } + }, "synckit": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.4.tgz", @@ -12008,6 +12039,12 @@ "tslib": "^2.0.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/front/package.json b/front/package.json index f9ef7153..a310c591 100644 --- a/front/package.json +++ b/front/package.json @@ -25,6 +25,7 @@ "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "styled-components": "^6.0.0-beta.9", + "swr": "^2.0.1", "y-indexeddb": "^9.0.9", "y-webrtc": "^10.2.3", "yjs": "^13.5.42" diff --git a/front/services/courses/activity.ts b/front/services/courses/activity.ts new file mode 100644 index 00000000..e6fa9ebf --- /dev/null +++ b/front/services/courses/activity.ts @@ -0,0 +1,40 @@ +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; +} + +export async function maskLectureAsComplete(org_id: string, course_id: string, lecture_id: string) { + const result: any = await fetch(`${getAPIUrl()}activity/${org_id}/add_lecture/${course_id}/${lecture_id}`, RequestBody("POST", null)) + .then((result) => result.json()) + .catch((error) => console.log("error", error)); + return result; +} + +// get all activities +export async function getActivities(org_id: string) { + const result: any = await fetch(`${getAPIUrl()}activity/${org_id}/activities`, RequestBody("GET", 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..df32b5e3 --- /dev/null +++ b/front/services/utils/requests.ts @@ -0,0 +1,14 @@ +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/main.py b/src/main.py index 4d189a38..6a41e764 100644 --- a/src/main.py +++ b/src/main.py @@ -1,12 +1,12 @@ from fastapi import APIRouter -from src.routers import users, auth, houses, orgs, roles, files +from src.routers import activity, users, auth, houses, orgs, roles, files from src.routers.courses import chapters, collections, courses,lectures global_router = APIRouter(prefix="/api") -# API Routes +# API Routes global_router.include_router(users.router, prefix="/users", tags=["users"]) global_router.include_router(auth.router, prefix="/auth", tags=["auth"]) global_router.include_router(houses.router, prefix="/houses", tags=["houses"]) @@ -17,4 +17,6 @@ global_router.include_router(courses.router, prefix="/courses", tags=["courses"] global_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"]) global_router.include_router(lectures.router, prefix="/lectures", tags=["lectures"]) global_router.include_router(collections.router, prefix="/collections", tags=["collections"]) +global_router.include_router(activity.router, prefix="/activity", tags=["activity"]) + diff --git a/src/routers/activity.py b/src/routers/activity.py new file mode 100644 index 00000000..edc5e000 --- /dev/null +++ b/src/routers/activity.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Depends, Request +from src.dependencies.auth import get_current_user +from src.services.activity import Activity, add_lecture_to_activity, close_activity, create_activity, get_user_activities + + +router = APIRouter() + + +@router.post("/start") +async def api_start_activity(request: Request, activity_object: Activity, user=Depends(get_current_user)): + """ + 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") +async def api_get_activity_by_userid(request: Request, org_id: str, user=Depends(get_current_user)): + """ + Get a user activities + """ + return await get_user_activities(request, user, org_id=org_id) + + +@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 lecture to activity + """ + return await add_lecture_to_activity(request, user, org_id, course_id, lecture_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 + """ + return await close_activity(request, user, org_id, activity_id) diff --git a/src/services/activity.py b/src/services/activity.py new file mode 100644 index 00000000..afaf0d56 --- /dev/null +++ b/src/services/activity.py @@ -0,0 +1,134 @@ +from cmath import log +from datetime import datetime +import json +from typing import List, Literal, Optional +from uuid import uuid4 +from fastapi import HTTPException, Request, status +from pydantic import BaseModel +from src.services.courses.chapters import get_coursechapters_meta + +from src.services.users import PublicUser + +#### Classes #################################################### + + +class Activity(BaseModel): + course_id: str + status: Optional[Literal['ongoing', 'done', 'closed']] = 'ongoing' + masked: Optional[bool] = False + lectures_marked_complete: Optional[List[str]] = [] + lectures_data: Optional[List[dict]] = [] + + +class ActivityInDB(Activity): + activity_id: str + user_id: str + org_id: str + creationDate: str = datetime.now().isoformat() + updateDate: str = datetime.now().isoformat() + + +#### Classes #################################################### + + +async def create_activity(request: Request, user: PublicUser, activity_object: Activity): + activities = request.app.db["activities"] + + # find if the user has already started the course + isActivityAlreadCreated = activities.find_one( + {"course_id": activity_object.course_id, "user_id": user.user_id}) + + 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 id + activity_id = f"activity_{uuid4()}" + + # create activity + activity = ActivityInDB(**activity_object.dict(),activity_id=activity_id, + user_id=user.user_id, org_id=activity_object.course_id) + + activities.insert_one(activity.dict()) + + return activity + + +async def get_user_activities(request: Request, user: PublicUser, org_id: str): + activities = request.app.db["activities"] + courses = request.app.db["courses"] + coursechapters = request.app.db["coursechapters"] + + activities_metadata = [] + + user_activities = activities.find( + {"user_id": user.user_id}, {'_id': 0}) + + if not user_activities: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="No activities found") + + for activity in user_activities: + # get number of lectures in the course + coursechapters = await get_coursechapters_meta(request, activity['course_id'], user) + + # calculate progression using the number of lectures marked complete and the total number of lectures + progression = round( + len(activity['lectures_marked_complete']) / len(coursechapters['lectures']) * 100, 2) + + course = courses.find_one({"course_id": activity['course_id']}, {'_id': 0}) + + # add progression to the activity + one_activity = {"course": course, "activitydata": activity, "progression": progression} + activities_metadata.append(one_activity) + + return activities_metadata + + +async def add_lecture_to_activity(request: Request, user: PublicUser, org_id: str, course_id: str, lecture_id: str): + activities = request.app.db["activities"] + course_id = f"course_{course_id}" + lecture_id = f"lecture_{lecture_id}" + + activity = activities.find_one( + {"course_id": course_id, + "user_id": user.user_id + }, {'_id': 0}) + + if lecture_id not in activity['lectures_marked_complete']: + activity['lectures_marked_complete'].append(str(lecture_id)) + activities.update_one( + {"activity_id": activity['activity_id']}, {"$set": activity}) + return activity + + if not activity: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Activity not found") + + else: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Lecture already marked complete") + + +async def close_activity(request: Request, user: PublicUser, activity_id: str, org_id: str,): + activities = request.app.db["activities"] + activity = activities.find_one( + {"activity_id": activity_id, "user_id": user.user_id}) + + if not activity: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Activity not found") + + activity['status'] = 'closed' + + activities.update_one( + {"activity_id": activity['activity_id']}, {"$set": activity}) + + activity = ActivityInDB(**activity) + + return activity diff --git a/src/services/courses/chapters.py b/src/services/courses/chapters.py index 17391764..651bc58e 100644 --- a/src/services/courses/chapters.py +++ b/src/services/courses/chapters.py @@ -152,7 +152,7 @@ async def get_coursechapters(request: Request,course_id: str, page: int = 1, lim return [json.loads(json.dumps(coursechapter, default=str)) for coursechapter in all_coursechapters] -async def get_coursechapters_meta(request: Request,course_id: str, current_user: PublicUser): +async def get_coursechapters_meta(request: Request, course_id: str, current_user: PublicUser): coursechapters = request.app.db["coursechapters"] courses = request.app.db["courses"] lectures = request.app.db["lectures"] @@ -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}) diff --git a/src/services/orgs.py b/src/services/orgs.py index 4c16e15a..fe07f291 100644 --- a/src/services/orgs.py +++ b/src/services/orgs.py @@ -135,7 +135,6 @@ async def delete_org(request: Request, org_id: str, current_user: PublicUser): async def get_orgs_by_user(request: Request, user_id: str, page: int = 1, limit: int = 10): orgs = request.app.db["organizations"] - print(user_id) # find all orgs where user_id is in owners or admins arrays all_orgs = orgs.find({"$or": [{"owners": user_id}, {"admins": user_id}]}).sort( "name", 1).skip(10 * (page - 1)).limit(limit) @@ -155,7 +154,7 @@ async def verify_org_rights(request: Request, org_id: str, current_user: Public status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist") isOwner = current_user.user_id in org["owners"] - hasRoleRights = await verify_user_rights_with_roles(action, current_user.user_id, org_id) + hasRoleRights = await verify_user_rights_with_roles(request, action, current_user.user_id, org_id) if not hasRoleRights and not isOwner: raise HTTPException(