Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Student endpoints #137

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions clients/core/src/managementConsole/ManagementConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,71 @@ import DarkModeProvider from '@/contexts/DarkModeProvider'
import { useParams } from 'react-router-dom'
import CourseNotFound from './shared/components/CourseNotFound'
import { Breadcrumbs } from './layout/Breadcrumbs/Breadcrumbs'
import { getOwnCourseIDs } from '@core/network/queries/ownCourseIDs'

export const ManagementRoot = ({ children }: { children?: React.ReactNode }): JSX.Element => {
const { keycloak, logout } = useKeycloak()
const { user, permissions } = useAuthStore()
const courseId = useParams<{ courseId: string }>()
const hasChildren = React.Children.count(children) > 0

const { setCourses } = useCourseStore()
const { setCourses, setOwnCourseIDs } = useCourseStore()

// getting the courses
const {
data: fetchedCourses,
error,
isPending,
isError,
refetch,
isError: isCourseError,
refetch: refetchCourses,
} = useQuery<Course[]>({
queryKey: ['courses'],
queryFn: () => getAllCourses(),
})

const isLoading = !(keycloak && user) || isPending
// getting the course ids of the course a user is enrolled in
const {
data: fetchedOwnCourseIDs,
isPending: isOwnCourseIdPending,
isError: isOwnCourseIdError,
refetch: refetchOwnCourseIds,
} = useQuery<string[]>({
queryKey: ['own_courses'],
queryFn: () => getOwnCourseIDs(),
})

const isLoading = !(keycloak && user) || isPending || isOwnCourseIdPending
const isError = isCourseError || isOwnCourseIdError
const refetch = () => {
refetchOwnCourseIds()
refetchCourses()
}

useEffect(() => {
if (fetchedCourses) {
setCourses(fetchedCourses)
}
}, [fetchedCourses, setCourses])

useEffect(() => {
if (fetchedOwnCourseIDs) {
setOwnCourseIDs(fetchedOwnCourseIDs)
}
}, [fetchedOwnCourseIDs, setOwnCourseIDs])

if (isLoading) {
return <LoadingPage />
}

if (isError) {
console.error(error)
if (isCourseError && error.message.includes('401')) {
return <UnauthorizedPage />
}
return <ErrorPage onRetry={() => refetch()} onLogout={() => logout()} />
}

// Check if the user has at least some Prompt rights
if (permissions.length === 0) {
if (permissions.length === 0 && fetchedCourses && fetchedCourses.length === 0) {
return <UnauthorizedPage />
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ import { FileUser } from 'lucide-react'
import { ExternalSidebarComponent } from './ExternalSidebar'
import { SidebarMenuItemProps } from '@/interfaces/sidebar'

export const ApplicationSidebar = ({ rootPath, title }: { rootPath: string; title: string }) => {
export const ApplicationSidebar = ({
rootPath,
title,
coursePhaseID,
}: {
rootPath: string
title: string
coursePhaseID: string
}) => {
const applicationSidebarItems: SidebarMenuItemProps = {
title: 'Application',
icon: <FileUser />,
goToPath: '',
requiredPermissions: [Role.PROMPT_ADMIN, Role.COURSE_LECTURER],
subitems: [
{
title: 'Applications',
Expand All @@ -31,6 +40,7 @@ export const ApplicationSidebar = ({ rootPath, title }: { rootPath: string; titl
title={title}
rootPath={rootPath}
sidebarElement={applicationSidebarItems}
coursePhaseID={coursePhaseID}
/>
)
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,79 @@
import { SidebarMenuItemProps } from '@/interfaces/sidebar'
import { useAuthStore, useCourseStore } from '@tumaet/prompt-shared-state'
import { Role, useAuthStore, useCourseStore } from '@tumaet/prompt-shared-state'
import { InsideSidebarMenuItem } from '../../layout/Sidebar/InsideSidebar/components/InsideSidebarMenuItem'
import { getPermissionString } from '@tumaet/prompt-shared-state'
import { useParams } from 'react-router-dom'
import { CourseParticipation } from '@core/managementConsole/shared/interfaces/CourseParticipation'
import { getCourseParticipation } from '@core/network/queries/courseParticipation'
import { useQuery } from '@tanstack/react-query'
import { ErrorPage } from '@/components/ErrorPage'

interface ExternalSidebarProps {
rootPath: string
title?: string
sidebarElement: SidebarMenuItemProps
coursePhaseID?: string
}

export const ExternalSidebarComponent: React.FC<ExternalSidebarProps> = ({
title,
rootPath,
sidebarElement,
coursePhaseID,
}: ExternalSidebarProps) => {
// Example of using a custom hook
const { permissions } = useAuthStore() // Example of calling your custom hook
const { courses } = useCourseStore()
const { courses, isStudentOfCourse } = useCourseStore()
const courseId = useParams<{ courseId: string }>().courseId

const course = courses.find((c) => c.id === courseId)

// get the current progression if the user is a student
const {
data: fetchedCourseParticipation,
isError: isCourseParticipationError,
refetch: refetchCourseParitcipation,
} = useQuery<CourseParticipation>({
queryKey: ['course_participation', courseId],
queryFn: () => getCourseParticipation(courseId ?? ''),
})

let hasComponentPermission = false
if (sidebarElement.requiredPermissions && sidebarElement.requiredPermissions.length > 0) {
// checks if user has access through keycloak roles
hasComponentPermission = sidebarElement.requiredPermissions.some((role) => {
return permissions.includes(getPermissionString(role, course?.name, course?.semesterTag))
})

// case that user is only student
if (
!hasComponentPermission &&
coursePhaseID && // some sidebar items (i.e. Mailing are not connected to a phase)
sidebarElement.requiredPermissions.includes(Role.COURSE_STUDENT) &&
isStudentOfCourse(courseId ?? '') &&
fetchedCourseParticipation
) {
hasComponentPermission = fetchedCourseParticipation.activeCoursePhases.some(
(phaseID) => phaseID === coursePhaseID,
)
}
} else {
// no permissions required
hasComponentPermission = true
}

// we ignore this error if the user has access anyway
if (isCourseParticipationError && !hasComponentPermission) {
return (
<>
<ErrorPage
message='Failed to get the course participation data'
onRetry={refetchCourseParitcipation}
/>
</>
)
}

return (
<>
{hasComponentPermission && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ import { ExternalSidebarComponent } from './ExternalSidebar'
interface InterviewSidebarProps {
rootPath: string
title?: string
coursePhaseID: string
}

export const InterviewSidebar = React.lazy(() =>
import('interview_component/sidebar')
.then((module): { default: React.FC<InterviewSidebarProps> } => ({
default: ({ title, rootPath }) => {
default: ({ title, rootPath, coursePhaseID }) => {
const sidebarElement: SidebarMenuItemProps = module.default || {}
return (
<ExternalSidebarComponent
title={title}
rootPath={rootPath}
sidebarElement={sidebarElement}
coursePhaseID={coursePhaseID}
/>
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ import { ExternalSidebarComponent } from './ExternalSidebar'
interface MatchingSidebarProps {
rootPath: string
title?: string
coursePhaseID: string
}

export const MatchingSidebar = React.lazy(() =>
import('matching_component/sidebar')
.then((module): { default: React.FC<MatchingSidebarProps> } => ({
default: ({ title, rootPath }) => {
default: ({ title, rootPath, coursePhaseID }) => {
const sidebarElement: SidebarMenuItemProps = module.default || {}
return (
<ExternalSidebarComponent
title={title}
rootPath={rootPath}
sidebarElement={sidebarElement}
coursePhaseID={coursePhaseID}
/>
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ import { ExternalSidebarComponent } from './ExternalSidebar'
interface TemplateSidebarProps {
rootPath: string
title?: string
coursePhaseID: string
}

export const TemplateSidebar = React.lazy(() =>
import('template_component/sidebar')
.then((module): { default: React.FC<TemplateSidebarProps> } => ({
default: ({ title, rootPath }) => {
default: ({ title, rootPath, coursePhaseID }) => {
const sidebarElement: SidebarMenuItemProps = module.default || {}
return (
<ExternalSidebarComponent
title={title}
rootPath={rootPath}
sidebarElement={sidebarElement}
coursePhaseID={coursePhaseID}
/>
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { InterviewSidebar } from './ExternalSidebars/InterviewSidebar'
import { ApplicationSidebar } from './ExternalSidebars/ApplicationSidebar'
import { MatchingSidebar } from './ExternalSidebars/MatchingSidebar'

export const PhaseSidebarMapping: { [key: string]: React.FC<{ rootPath: string; title: string }> } =
{
template_component: TemplateSidebar,
Application: ApplicationSidebar,
Interview: InterviewSidebar,
Matching: MatchingSidebar,
}
export const PhaseSidebarMapping: {
[key: string]: React.FC<{ rootPath: string; title: string; coursePhaseID: string }>
} = {
template_component: TemplateSidebar,
Application: ApplicationSidebar,
Interview: InterviewSidebar,
Matching: MatchingSidebar,
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export const InsideCourseSidebar = (): JSX.Element => {
key={phase.id}
fallback={<DisabledSidebarMenuItem key={phase.id} title={'Loading...'} />}
>
<PhaseComponent rootPath={rootPath + '/' + phase.id} title={phase.name} />
<PhaseComponent
rootPath={rootPath + '/' + phase.id}
title={phase.name}
coursePhaseID={phase.id}
/>
</Suspense>
)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const PermissionRestriction = ({
children,
}: PermissionRestrictionProps): JSX.Element => {
const { permissions } = useAuthStore()
const { courses } = useCourseStore()
const { courses, isStudentOfCourse } = useCourseStore()
const courseId = useParams<{ courseId: string }>().courseId

// This means something /general
Expand All @@ -33,6 +33,12 @@ export const PermissionRestriction = ({
hasPermission = requiredPermissions.some((role) => {
return permissions.includes(getPermissionString(role, course?.name, course?.semesterTag))
})

// We need to compare student role with ownCourseIDs -> otherwise we could not hide pages from i.e. instructors
// set hasPermission to true if the user is a student in the course and the page is accessible for students
if (requiredPermissions.includes(Role.COURSE_STUDENT) && isStudentOfCourse(courseId)) {
hasPermission = true
}
}

return <>{hasPermission ? children : <UnauthorizedPage />}</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface CourseParticipation {
id: string
courseID: string
studentID: string
activeCoursePhases: string[]
}
11 changes: 11 additions & 0 deletions clients/core/src/network/queries/courseParticipation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { axiosInstance } from '@/network/configService'
import { CourseParticipation } from '@core/managementConsole/shared/interfaces/CourseParticipation'

export const getCourseParticipation = async (courseId: string): Promise<CourseParticipation> => {
try {
return (await axiosInstance.get(`/api/courses/${courseId}/participations/self`)).data
} catch (err) {
console.error(err)
throw err
}
}
10 changes: 10 additions & 0 deletions clients/core/src/network/queries/ownCourseIDs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { axiosInstance } from '@/network/configService'

export const getOwnCourseIDs = async (): Promise<string[]> => {
try {
return (await axiosInstance.get(`/api/courses/self`)).data
} catch (err) {
console.error(err)
throw err
}
}
10 changes: 8 additions & 2 deletions clients/core/src/validations/course.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import * as z from 'zod'

export const courseFormSchema = z.object({
name: z.string().min(1, 'Course name is required'),
name: z
.string()
.min(1, 'Course name is required')
.refine((val) => !val.includes('-'), 'Course name cannot contain a "-" character'),
dateRange: z.object({
from: z.date(),
to: z.date(),
}),
courseType: z.string().min(1, 'Course type is required'),
ects: z.number().min(0, 'ECTS must be a positive number'),
semesterTag: z.string().min(1, 'Semester tag is required'),
semesterTag: z
.string()
.min(1, 'Semester tag is required')
.refine((val) => !val.includes('-'), 'Semester tag cannot contain a "-" character'),
})

export type CourseFormValues = z.infer<typeof courseFormSchema>
2 changes: 1 addition & 1 deletion clients/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@tiptap/pm": "^2.10.4",
"@tiptap/react": "^2.10.4",
"@tiptap/starter-kit": "^2.10.4",
"@tumaet/prompt-shared-state": "^0.0.12",
"@tumaet/prompt-shared-state": "^0.0.13",
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"class-variance-authority": "^0.7.0",
Expand Down
10 changes: 5 additions & 5 deletions clients/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3710,13 +3710,13 @@ __metadata:
languageName: node
linkType: hard

"@tumaet/prompt-shared-state@npm:^0.0.12":
version: 0.0.12
resolution: "@tumaet/prompt-shared-state@npm:0.0.12"
"@tumaet/prompt-shared-state@npm:^0.0.13":
version: 0.0.13
resolution: "@tumaet/prompt-shared-state@npm:0.0.13"
dependencies:
typescript: "npm:^5.7.3"
zustand: "npm:^5.0.3"
checksum: 10c0/42b65ea4ea24fe3b8e58db92f09af47717b50b70f38e5eb4f2e59c2a3a94189857bed2f71aa4252dec4f6c72cd4cbf9210b07dc9adef8beb8b3f7cbdf6cf4114
checksum: 10c0/1862bbf98819d6dae2a9243434c4a3705b59420a6989f831cb06a7e1e5fe4ff675e9204b29968712419901c53377f67783609326838c6501e38f4e2b19ecf233
languageName: node
linkType: hard

Expand Down Expand Up @@ -12789,7 +12789,7 @@ __metadata:
"@tiptap/pm": "npm:^2.10.4"
"@tiptap/react": "npm:^2.10.4"
"@tiptap/starter-kit": "npm:^2.10.4"
"@tumaet/prompt-shared-state": "npm:^0.0.12"
"@tumaet/prompt-shared-state": "npm:^0.0.13"
"@types/copy-webpack-plugin": "npm:^10.1.0"
"@types/dotenv-webpack": "npm:^7.0.7"
"@types/eslint": "npm:^8.56.10"
Expand Down
Loading
Loading