diff --git a/apps/api/src/db/courses/assignments.py b/apps/api/src/db/courses/assignments.py index fc288e36..a7dbc221 100644 --- a/apps/api/src/db/courses/assignments.py +++ b/apps/api/src/db/courses/assignments.py @@ -102,11 +102,7 @@ class AssignmentTaskBase(SQLModel): contents: Dict = Field(default={}, sa_column=Column(JSON)) max_grade_value: int = 0 # Value is always between 0-100 - assignment_id: int - org_id: int - course_id: int - chapter_id: int - activity_id: int + class AssignmentTaskCreate(AssignmentTaskBase): diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py index f627d099..e238449c 100644 --- a/apps/api/src/routers/courses/assignments.py +++ b/apps/api/src/routers/courses/assignments.py @@ -24,6 +24,7 @@ from src.services.courses.activities.assignments import ( read_assignment, read_assignment_from_activity_uuid, read_assignment_submissions, + read_assignment_task, read_assignment_task_submissions, read_assignment_tasks, read_user_assignment_submissions, @@ -151,6 +152,20 @@ async def api_read_assignment_tasks( ) +@router.get("/task/{assignment_task_uuid}") +async def api_read_assignment_task( + request: Request, + assignment_task_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Read task for an assignment + """ + return await read_assignment_task( + request, assignment_task_uuid, current_user, db_session + ) + @router.put("/{assignment_uuid}/tasks/{task_uuid}") async def api_update_assignment_tasks( request: Request, diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index 46102cf7..6414a222 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -315,6 +315,10 @@ async def create_assignment_task( assignment_task.creation_date = str(datetime.now()) assignment_task.update_date = str(datetime.now()) assignment_task.org_id = course.org_id + assignment_task.chapter_id = assignment.chapter_id + assignment_task.activity_id = assignment.activity_id + assignment_task.assignment_id = assignment.id # type: ignore + assignment_task.course_id = assignment.course_id # Insert Assignment Task in DB db_session.add(assignment_task) @@ -365,6 +369,48 @@ async def read_assignment_tasks( for assignment_task in db_session.exec(statement).all() ] +async def read_assignment_task( + request: Request, + assignment_task_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Find assignment + statement = select(AssignmentTask).where(AssignmentTask.assignment_task_uuid == assignment_task_uuid) + assignmenttask = db_session.exec(statement).first() + + if not assignmenttask: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignmenttask.assignment_id) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + + # return assignment task read + return AssignmentTaskRead.model_validate(assignmenttask) + async def update_assignment_task( request: Request, diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx new file mode 100644 index 00000000..84a7e66f --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx @@ -0,0 +1,77 @@ +import { useLHSession } from '@components/Contexts/LHSessionContext'; +import { getAPIUrl } from '@services/config/config'; +import { createAssignmentTask } from '@services/courses/assignments' +import { AArrowUp, FileUp, ListTodo } from 'lucide-react' +import React from 'react' +import toast from 'react-hot-toast'; +import { mutate } from 'swr'; + +function NewTaskModal({ closeModal, assignment_uuid }: any) { + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + const reminderShownRef = React.useRef(false); + + function showReminderToast() { + // Check if the reminder has already been shown using sessionStorage + if (sessionStorage.getItem("TasksReminderShown") !== "true") { + setTimeout(() => { + toast('When editing/adding your tasks, make sure to Unpublish your Assignment to avoid any issues with students, you can Publish it again when you are ready.', + { icon: '✋', duration: 10000, style: { minWidth: 600 } }); + // Mark the reminder as shown in sessionStorage + sessionStorage.setItem("TasksReminderShown", "true"); + }, 3000); + } + } + + async function createTask(type: string) { + const task_object = { + title: "Untitled Task", + description: "", + hint: "", + reference_file: "", + assignment_type: type, + contents: {}, + max_grade_value: 100, + } + await createAssignmentTask(task_object, assignment_uuid, access_token) + toast.success('Task created successfully') + showReminderToast() + mutate(`${getAPIUrl()}assignments/${assignment_uuid}/tasks`) + closeModal(false) + } + + + return ( +
+
createTask('QUIZ')} + className='flex flex-col space-y-2 justify-center text-center pt-10'> +
+ +
+

Quiz

+

Questions with multiple choice answers

+
+
createTask('FILE_SUBMISSION')} + className='flex flex-col space-y-2 justify-center text-center pt-10'> +
+ +
+

File submissions

+

Students can submit files for this task

+
+
toast.error('Forms are not yet supported')} + className='flex flex-col space-y-2 justify-center text-center pt-10 opacity-25'> +
+ +
+

Forms

+

Forms for students to fill out

+
+
+ ) +} + +export default NewTaskModal \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx index c76be5e6..c9e917b5 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx @@ -1,30 +1,52 @@ 'use client'; -import { Info, Link } from 'lucide-react' -import React from 'react' +import { useAssignmentsTask } from '@components/Contexts/Assignments/AssignmentsTaskContext'; +import { Info, TentTree } from 'lucide-react' +import React, { useEffect } from 'react' -function AssignmentTaskEditor({ task_uuid, page }: any) { +function AssignmentTaskEditor({ page }: any) { const [selectedSubPage, setSelectedSubPage] = React.useState(page) + const assignmentTaskState = useAssignmentsTask() as any + + useEffect(() => { + console.log(assignmentTaskState) + } + , [assignmentTaskState]) + return (
- -
-
- Assignment Test #1 -
-
-
-
- -
Overview
+ {assignmentTaskState.assignmentTask && Object.keys(assignmentTaskState.assignmentTask).length > 0 && ( +
+
+ Assignment Test #1 +
+
+
+
+ +
Overview
+
-
+ )} + {Object.keys(assignmentTaskState.assignmentTask).length == 0 && ( +
+
+
+ +
+ No Task Selected +
+
+
+
+ )} +
) } diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Tasks.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Tasks.tsx index f2933817..1236cd45 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Tasks.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Tasks.tsx @@ -1,12 +1,20 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext' -import { Plus } from 'lucide-react'; +import Modal from '@components/StyledElements/Modal/Modal'; +import { FileUp, ListTodo, PanelLeftOpen, Plus } from 'lucide-react'; import React, { useEffect } from 'react' +import NewTaskModal from './Modals/NewTaskModal'; +import { useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; -function AssignmentTasks() { +function AssignmentTasks({ assignment_uuid }: any) { const assignments = useAssignments() as any; + const assignmentTaskHook = useAssignmentsTaskDispatch() as any; + const [isNewTaskModalOpen, setIsNewTaskModalOpen] = React.useState(false) + + async function setSelectTask(task_uuid: string) { + assignmentTaskHook({ type: 'setSelectedAssignmentTaskUUID', payload: task_uuid }) + } useEffect(() => { - console.log(assignments) }, [assignments]) @@ -15,19 +23,47 @@ function AssignmentTasks() {
{assignments && assignments?.assignment_tasks?.map((task: any) => { return ( -
-
-
{task.title}
+
setSelectTask(task.assignment_task_uuid)} + > +
+
+
+ {task.assignment_type === 'QUIZ' && } + {task.assignment_type === 'FILE_SUBMISSION' && } +
+
{task.title}
+
+
) })} -
- -

Add Task

-
+ + + } + dialogTitle="Add an Assignment Task" + dialogDescription="Create a new task for this assignment" + dialogTrigger={ +
+ +

Add Task

+
+ } + /> +
- +
) } diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx index cf7f609a..47f9793e 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx @@ -1,12 +1,13 @@ 'use client'; import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' -import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement' -import { BookOpen, BookOpenCheck, BookX, Check, Ellipsis, EllipsisVertical, GalleryVerticalEnd, Info, LayoutList, UserRoundCog } from 'lucide-react' +import { BookOpen, BookX, EllipsisVertical, LayoutList } from 'lucide-react' import React from 'react' import AssignmentTaskEditor from './_components/TaskEditor'; import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext'; import AssignmentTasks from './_components/Tasks'; import { useParams } from 'next/navigation'; +import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext'; +import ToolTip from '@components/StyledElements/Tooltip/Tooltip'; function AssignmentEdit() { const params = useParams<{ assignmentuuid: string; }>() @@ -24,33 +25,47 @@ function AssignmentEdit() {
Published
-
- -

Publish

-
+ +
+ +

Publish

+
+
+

Unpublish

+
-
-
-
- -

Tasks

+
+ +
+
+ +

Tasks

+
+ + +
- - - -
-
- - - -
+
+ + + +
+
) diff --git a/apps/web/components/Contexts/Assignments/AssignmentsTaskContext.tsx b/apps/web/components/Contexts/Assignments/AssignmentsTaskContext.tsx new file mode 100644 index 00000000..64910a86 --- /dev/null +++ b/apps/web/components/Contexts/Assignments/AssignmentsTaskContext.tsx @@ -0,0 +1,80 @@ +'use client' +import React, { createContext, useContext, useEffect, useReducer } from 'react' +import { useLHSession } from '@components/Contexts/LHSessionContext' +import { getAssignmentTask } from '@services/courses/assignments' + +interface State { + selectedAssignmentTaskUUID: string | null; + assignmentTask: Record; +} + +interface Action { + type: string; + payload?: any; +} + +const initialState: State = { + selectedAssignmentTaskUUID: null, + assignmentTask: {} +}; + +export const AssignmentsTaskContext = createContext(undefined); +export const AssignmentsTaskDispatchContext = createContext | undefined>(undefined); + +export function AssignmentsTaskProvider({ children }: { children: React.ReactNode }) { + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + + const [state, dispatch] = useReducer(assignmentstaskReducer, initialState); + + async function fetchAssignmentTask(assignmentTaskUUID: string) { + const res = await getAssignmentTask(assignmentTaskUUID, access_token); + if (res.success) { + dispatch({ type: 'setAssignmentTask', payload: res }); + } + } + + useEffect(() => { + if (state.selectedAssignmentTaskUUID) { + fetchAssignmentTask(state.selectedAssignmentTaskUUID); + } + }, [state.selectedAssignmentTaskUUID]); + + return ( + + + {children} + + + ); +} + +export function useAssignmentsTask() { + const context = useContext(AssignmentsTaskContext); + if (context === undefined) { + throw new Error('useAssignmentsTask must be used within an AssignmentsTaskProvider'); + } + return context; +} + +export function useAssignmentsTaskDispatch() { + const context = useContext(AssignmentsTaskDispatchContext); + if (context === undefined) { + throw new Error('useAssignmentsTaskDispatch must be used within an AssignmentsTaskProvider'); + } + return context; +} + +function assignmentstaskReducer(state: State, action: Action): State { + switch (action.type) { + case 'setSelectedAssignmentTaskUUID': + return { ...state, selectedAssignmentTaskUUID: action.payload }; + case 'setAssignmentTask': + return { ...state, assignmentTask: action.payload }; + case 'reload': + return { ...state }; + default: + return state; + } +} + diff --git a/apps/web/services/courses/assignments.ts b/apps/web/services/courses/assignments.ts index f73f57ab..db88b95d 100644 --- a/apps/web/services/courses/assignments.ts +++ b/apps/web/services/courses/assignments.ts @@ -64,3 +64,15 @@ export async function createAssignmentTask( const res = await getResponseMetadata(result) return res } + +export async function getAssignmentTask( + assignmentTaskUUID: string, + access_token: string +) { + const result: any = await fetch( + `${getAPIUrl()}assignments/task/${assignmentTaskUUID}`, + RequestBodyWithAuthHeader('GET', null, null, access_token) + ) + const res = await getResponseMetadata(result) + return res +}