From 4f75e6a90ac41788c3760cbc0c47637b041fd12b Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 30 Mar 2024 10:22:38 +0000 Subject: [PATCH] feat: init usergroup linking to a course --- apps/api/src/routers/usergroups.py | 14 ++ apps/api/src/services/users/usergroups.py | 51 ++++- .../course/[courseuuid]/[subpage]/page.tsx | 58 +++-- apps/web/app/orgs/[orgslug]/layout.tsx | 2 + .../EditCourseAccess/EditCourseAccess.tsx | 211 ++++++++++++++++++ .../EditCourseGeneral/EditCourseGeneral.tsx | 24 +- .../EditCourseGeneral/ThumbnailUpdate.tsx | 11 +- .../components/Dashboard/UI/BreadCrumbs.tsx | 1 + .../Dashboard/UI/CourseOverviewTop.tsx | 2 +- .../Dash/EditCourseAccess/LinkToUserGroup.tsx | 71 ++++++ .../components/StyledElements/Form/Form.tsx | 3 +- apps/web/services/usergroups/usergroups.ts | 35 +++ 12 files changed, 428 insertions(+), 55 deletions(-) create mode 100644 apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx create mode 100644 apps/web/components/Objects/Modals/Dash/EditCourseAccess/LinkToUserGroup.tsx create mode 100644 apps/web/services/usergroups/usergroups.ts diff --git a/apps/api/src/routers/usergroups.py b/apps/api/src/routers/usergroups.py index 7f8351c6..74160f43 100644 --- a/apps/api/src/routers/usergroups.py +++ b/apps/api/src/routers/usergroups.py @@ -9,6 +9,7 @@ from src.services.users.usergroups import ( add_users_to_usergroup, create_usergroup, delete_usergroup_by_id, + get_usergroups_by_resource, read_usergroup_by_id, read_usergroups_by_org_id, remove_resources_from_usergroup, @@ -64,6 +65,19 @@ async def api_get_usergroups( """ return await read_usergroups_by_org_id(request, db_session, current_user, org_id) +@router.get("/resource/{resource_uuid}", response_model=list[UserGroupRead], tags=["usergroups"]) +async def api_get_usergroupsby_resource( + *, + request: Request, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), + resource_uuid: str, +) -> list[UserGroupRead]: + """ + Get UserGroups by Org + """ + return await get_usergroups_by_resource(request, db_session, current_user, resource_uuid) + @router.put("/{usergroup_id}", response_model=UserGroupRead, tags=["usergroups"]) async def api_update_usergroup( diff --git a/apps/api/src/services/users/usergroups.py b/apps/api/src/services/users/usergroups.py index c2f28926..f5508d0f 100644 --- a/apps/api/src/services/users/usergroups.py +++ b/apps/api/src/services/users/usergroups.py @@ -119,6 +119,41 @@ async def read_usergroups_by_org_id( return usergroups +async def get_usergroups_by_resource( + request: Request, + db_session: Session, + current_user: PublicUser | AnonymousUser, + resource_uuid: str, +) -> list[UserGroupRead]: + + statement = select(UserGroupResource).where( + UserGroupResource.resource_uuid == resource_uuid + ) + usergroup_resources = db_session.exec(statement).all() + + # RBAC check + await rbac_check( + request, + usergroup_uuid="usergroup_X", + current_user=current_user, + action="read", + db_session=db_session, + ) + + usergroup_ids = [usergroup.usergroup_id for usergroup in usergroup_resources] + + # get usergroups + usergroups = [] + for usergroup_id in usergroup_ids: + statement = select(UserGroup).where(UserGroup.id == usergroup_id) + usergroup = db_session.exec(statement).first() + usergroups.append(usergroup) + + usergroups = [UserGroupRead.from_orm(usergroup) for usergroup in usergroups] + + return usergroups + + async def update_usergroup_by_id( request: Request, db_session: Session, @@ -258,7 +293,6 @@ async def remove_users_from_usergroup( detail="UserGroup not found", ) - # RBAC check # RBAC check await rbac_check( request, @@ -312,8 +346,21 @@ async def add_resources_to_usergroup( resources_uuids_array = resources_uuids.split(",") for resource_uuid in resources_uuids_array: - # TODO : Find a way to check if resource exists + # Check if a link between UserGroup and Resource already exists + statement = select(UserGroupResource).where( + UserGroupResource.usergroup_id == usergroup_id, + UserGroupResource.resource_uuid == resource_uuid, + ) + usergroup_resource = db_session.exec(statement).first() + if usergroup_resource: + raise HTTPException( + status_code=400, + detail=f"Resource {resource_uuid} already exists in UserGroup", + ) + continue + + # TODO : Find a way to check if resource really exists usergroup_obj = UserGroupResource( usergroup_id=usergroup_id, resource_uuid=resource_uuid, diff --git a/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx index 6cfe4c83..dc1155cd 100644 --- a/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx @@ -7,7 +7,8 @@ import Link from 'next/link' import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop' import { motion } from 'framer-motion' import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral' -import { GalleryVerticalEnd, Info } from 'lucide-react' +import { GalleryVerticalEnd, Info, Lock, UserRoundCog } from 'lucide-react' +import EditCourseAccess from '@components/Dashboard/Course/EditCourseAccess/EditCourseAccess' export type CourseOverviewParams = { orgslug: string @@ -24,9 +25,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) { return (
-
+
-
+
@@ -46,6 +46,24 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
+ +
+
+ +
Access
+
+
+
@@ -65,7 +82,9 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
+
+
- {params.subpage == 'content' ? ( - - ) : ( - '' - )} - {params.subpage == 'general' ? ( - - ) : ( - '' - )} + {params.subpage == 'content' ? () : ('')} + {params.subpage == 'general' ? () : ('')} + {params.subpage == 'access' ? () : ('')}
diff --git a/apps/web/app/orgs/[orgslug]/layout.tsx b/apps/web/app/orgs/[orgslug]/layout.tsx index a00e1e77..97ccd9fa 100644 --- a/apps/web/app/orgs/[orgslug]/layout.tsx +++ b/apps/web/app/orgs/[orgslug]/layout.tsx @@ -1,6 +1,7 @@ 'use client' import { OrgProvider } from '@components/Contexts/OrgContext' import SessionProvider from '@components/Contexts/SessionContext' +import Toast from '@components/StyledElements/Toast/Toast' import '@styles/globals.css' export default function RootLayout({ @@ -12,6 +13,7 @@ export default function RootLayout({ }) { return (
+ {children} diff --git a/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx b/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx new file mode 100644 index 00000000..74de3d5d --- /dev/null +++ b/apps/web/components/Dashboard/Course/EditCourseAccess/EditCourseAccess.tsx @@ -0,0 +1,211 @@ +import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext' +import LinkToUserGroup from '@components/Objects/Modals/Dash/EditCourseAccess/LinkToUserGroup' +import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal' +import Modal from '@components/StyledElements/Modal/Modal' +import { getAPIUrl } from '@services/config/config' +import { unLinkResourcesToUserGroup } from '@services/usergroups/usergroups' +import { swrFetcher } from '@services/utils/ts/requests' +import { Globe, Users, UsersRound, X } from 'lucide-react' +import React from 'react' +import toast from 'react-hot-toast' +import useSWR, { mutate } from 'swr' + +type EditCourseAccessProps = { + orgslug: string + course_uuid?: string +} + +function EditCourseAccess(props: EditCourseAccessProps) { + const [error, setError] = React.useState('') + + const course = useCourse() as any + const dispatchCourse = useCourseDispatch() as any + const courseStructure = course.courseStructure + const { data: usergroups } = useSWR( + courseStructure ? `${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}` : null, + swrFetcher + ) + const [isPublic, setIsPublic] = React.useState(courseStructure.public) + + + React.useEffect(() => { + // This code will run whenever form values are updated + if (isPublic !== courseStructure.public) { + dispatchCourse({ type: 'setIsNotSaved' }) + const updatedCourse = { + ...courseStructure, + public: isPublic, + } + dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse }) + } + }, [course, isPublic]) + return ( +
+ {' '} +
+
+
+

Access to the course

+

+ {' '} + Choose if want your course to be publicly available on the internet or only accessible to signed in users{' '} +

+
+
+ + {isPublic ? ( +
+ Active +
+ ) : null} +
+ +
+ Public +
+
+ The Course is publicly available on the internet, it is indexed by search engines and can be accessed by anyone +
+
+ +
+ } + functionToExecute={() => { + setIsPublic(true) + }} + status="info" + > + + {!isPublic ? ( +
+ Active +
+ ) : null} +
+ +
+ Users Only +
+
+ The Course is only accessible to signed in users, additionaly you can choose which UserGroups can access this course +
+
+ +
+ } + functionToExecute={() => { + setIsPublic(false) + }} + status="info" + > +
+ +
+
+ ) +} + + +function UserGroupsSection({ usergroups }: { usergroups: any[] }) { + const course = useCourse() as any + const [userGroupModal, setUserGroupModal] = React.useState(false) + + const removeUserGroupLink = async (usergroup_id: number) => { + const res = await unLinkResourcesToUserGroup(usergroup_id, course.courseStructure.course_uuid) + if (res.status === 200) { + toast.success('Successfully unliked from usergroup') + mutate(`${getAPIUrl()}usergroups/resource/${course.courseStructure.course_uuid}`) + } + else { + toast.error('Error ' + res.status + ': ' + res.data.detail) + } + } + + return ( + <> +
+

UserGroups

+

+ {' '} + Choose which UserGroups can access this course{' '} +

+
+ + + + + + + + <> + + {usergroups?.map((usergroup: any) => ( + + + + + ))} + + +
NameActions
{usergroup.name} + + + Delete link + + } + functionToExecute={() => { + removeUserGroupLink(usergroup.id) + }} + status="warning" + > +
+
+ + setUserGroupModal(!userGroupModal) + } + minHeight="no-min" + dialogContent={ + + + } + dialogTitle="Link Course to a UserGroup" + dialogDescription={ + 'Choose which UserGroups can access this course' + } + dialogTrigger={ + + } + /> + +
+ ) +} + +export default EditCourseAccess \ No newline at end of file diff --git a/apps/web/components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral.tsx b/apps/web/components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral.tsx index 896e6ca6..84129421 100644 --- a/apps/web/components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral.tsx +++ b/apps/web/components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral.tsx @@ -116,10 +116,11 @@ function EditCourseGeneral(props: EditCourseStructureProps) { message={formik.errors.description} /> -