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 }) {
+
+
+
@@ -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{' '}
+
+
+
+
+
+ | Name |
+ Actions |
+
+
+ <>
+
+ {usergroups?.map((usergroup: any) => (
+
+ | {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}
/>
-
@@ -177,26 +178,7 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
-
-
-
-
- formik.setFieldValue('public', checked)
- }
- checked={formik.values.public === 'true'}
- >
-
-
-
-
+
)}
diff --git a/apps/web/components/Dashboard/Course/EditCourseGeneral/ThumbnailUpdate.tsx b/apps/web/components/Dashboard/Course/EditCourseGeneral/ThumbnailUpdate.tsx
index 7f0c9568..2a3fce58 100644
--- a/apps/web/components/Dashboard/Course/EditCourseGeneral/ThumbnailUpdate.tsx
+++ b/apps/web/components/Dashboard/Course/EditCourseGeneral/ThumbnailUpdate.tsx
@@ -46,18 +46,17 @@ function ThumbnailUpdate() {
{localThumbnail ? (
) : (
)}
diff --git a/apps/web/components/Dashboard/UI/BreadCrumbs.tsx b/apps/web/components/Dashboard/UI/BreadCrumbs.tsx
index 9afb7097..05396e2b 100644
--- a/apps/web/components/Dashboard/UI/BreadCrumbs.tsx
+++ b/apps/web/components/Dashboard/UI/BreadCrumbs.tsx
@@ -64,6 +64,7 @@ function BreadCrumbs(props: BreadCrumbsProps) {
+
)
}
diff --git a/apps/web/components/Dashboard/UI/CourseOverviewTop.tsx b/apps/web/components/Dashboard/UI/CourseOverviewTop.tsx
index c1784e71..4fafdc3a 100644
--- a/apps/web/components/Dashboard/UI/CourseOverviewTop.tsx
+++ b/apps/web/components/Dashboard/UI/CourseOverviewTop.tsx
@@ -27,7 +27,7 @@ export function CourseOverviewTop({
last_breadcrumb={course.courseStructure.name}
>
-
+
diff --git a/apps/web/components/Objects/Modals/Dash/EditCourseAccess/LinkToUserGroup.tsx b/apps/web/components/Objects/Modals/Dash/EditCourseAccess/LinkToUserGroup.tsx
new file mode 100644
index 00000000..81e1716c
--- /dev/null
+++ b/apps/web/components/Objects/Modals/Dash/EditCourseAccess/LinkToUserGroup.tsx
@@ -0,0 +1,71 @@
+'use client';
+import { useCourse } from '@components/Contexts/CourseContext';
+import { useOrg } from '@components/Contexts/OrgContext';
+import { getAPIUrl } from '@services/config/config';
+import { linkResourcesToUserGroup } from '@services/usergroups/usergroups';
+import { swrFetcher } from '@services/utils/ts/requests';
+import React, { useEffect } from 'react'
+import toast from 'react-hot-toast';
+import useSWR, { mutate } from 'swr'
+
+type LinkToUserGroupProps = {
+ // React function, todo: fix types
+ setUserGroupModal: any
+}
+
+function LinkToUserGroup(props: LinkToUserGroupProps) {
+ const course = useCourse() as any
+ const org = useOrg() as any
+ const courseStructure = course.courseStructure
+
+ const { data: usergroups } = useSWR(
+ courseStructure && org ? `${getAPIUrl()}usergroups/org/${org.id}` : null,
+ swrFetcher
+ )
+ const [selectedUserGroup, setSelectedUserGroup] = React.useState(null) as any
+
+
+ const handleLink = async () => {
+ console.log('selectedUserGroup', selectedUserGroup)
+ const res = await linkResourcesToUserGroup(selectedUserGroup, courseStructure.course_uuid)
+ if (res.status === 200) {
+ props.setUserGroupModal(false)
+ toast.success('Successfully linked to usergroup')
+ mutate(`${getAPIUrl()}usergroups/resource/${courseStructure.course_uuid}`)
+ }
+ else {
+ toast.error('Error ' + res.status + ': ' + res.data.detail)
+ }
+
+ }
+
+ useEffect(() => {
+ if (usergroups && usergroups.length > 0) {
+ setSelectedUserGroup(usergroups[0].id)
+ }
+ }
+ , [usergroups])
+
+ return (
+
+
+
+ UserGroup Name
+
+
+
+
+
+
+ )
+}
+
+export default LinkToUserGroup
\ No newline at end of file
diff --git a/apps/web/components/StyledElements/Form/Form.tsx b/apps/web/components/StyledElements/Form/Form.tsx
index 273a48da..497ff3f9 100644
--- a/apps/web/components/StyledElements/Form/Form.tsx
+++ b/apps/web/components/StyledElements/Form/Form.tsx
@@ -15,7 +15,7 @@ export const FormLabelAndMessage = (props: {
message?: string
}) => (
-
{props.label}
+
{props.label}
{(props.message && (
@@ -35,7 +35,6 @@ export const FormField = styled(Form.Field, {
})
export const FormLabel = styled(Form.Label, {
- fontSize: 15,
fontWeight: 500,
lineHeight: '35px',
color: 'black',
diff --git a/apps/web/services/usergroups/usergroups.ts b/apps/web/services/usergroups/usergroups.ts
new file mode 100644
index 00000000..c1387c76
--- /dev/null
+++ b/apps/web/services/usergroups/usergroups.ts
@@ -0,0 +1,35 @@
+import { getAPIUrl } from '@services/config/config'
+import { RequestBody, getResponseMetadata } from '@services/utils/ts/requests'
+
+export async function getUserGroups(org_id: any) {
+ const result: any = await fetch(
+ `${getAPIUrl()}usergroups/org/${org_id}`,
+ RequestBody('GET', null, null)
+ )
+ const res = await getResponseMetadata(result)
+ return res
+}
+
+export async function linkResourcesToUserGroup(
+ usergroup_id: any,
+ resource_uuids: any
+) {
+ const result: any = await fetch(
+ `${getAPIUrl()}usergroups/${usergroup_id}/add_resources?resource_uuids=${resource_uuids}`,
+ RequestBody('POST', null, null)
+ )
+ const res = await getResponseMetadata(result)
+ return res
+}
+
+export async function unLinkResourcesToUserGroup(
+ usergroup_id: any,
+ resource_uuids: any
+) {
+ const result: any = await fetch(
+ `${getAPIUrl()}usergroups/${usergroup_id}/remove_resources?resource_uuids=${resource_uuids}`,
+ RequestBody('DELETE', null, null)
+ )
+ const res = await getResponseMetadata(result)
+ return res
+}