feat: assignmentTask creation and switching

This commit is contained in:
swve 2024-07-13 20:03:08 +02:00
parent 6a4e16ec29
commit acfcea026b
9 changed files with 354 additions and 55 deletions

View file

@ -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):

View file

@ -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,

View file

@ -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,

View file

@ -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 (
<div className='flex space-x-6 mx-auto justify-center items-center'>
<div
onClick={() => createTask('QUIZ')}
className='flex flex-col space-y-2 justify-center text-center pt-10'>
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
<ListTodo size={30} />
</div>
<p className='text-xl text-gray-700 font-semibold'>Quiz</p>
<p className='text-sm text-gray-500 w-40'>Questions with multiple choice answers</p>
</div>
<div
onClick={() => createTask('FILE_SUBMISSION')}
className='flex flex-col space-y-2 justify-center text-center pt-10'>
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
<FileUp size={30} />
</div>
<p className='text-xl text-gray-700 font-semibold'>File submissions</p>
<p className='text-sm text-gray-500 w-40'>Students can submit files for this task</p>
</div>
<div
onClick={() => toast.error('Forms are not yet supported')}
className='flex flex-col space-y-2 justify-center text-center pt-10 opacity-25'>
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
<AArrowUp size={30} />
</div>
<p className='text-xl text-gray-700 font-semibold'>Forms</p>
<p className='text-sm text-gray-500 w-40'>Forms for students to fill out</p>
</div>
</div>
)
}
export default NewTaskModal

View file

@ -1,12 +1,20 @@
'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 (
<div className="flex flex-col font-black text-sm w-full z-20">
{assignmentTaskState.assignmentTask && Object.keys(assignmentTaskState.assignmentTask).length > 0 && (
<div className='flex flex-col bg-white pl-10 pr-10 text-sm tracking-tight z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)] pt-5'>
<div className='font-semibold text-lg py-1'>
Assignment Test #1
@ -25,6 +33,20 @@ function AssignmentTaskEditor({ task_uuid, page }: any) {
</div>
</div>
</div>
)}
{Object.keys(assignmentTaskState.assignmentTask).length == 0 && (
<div className='flex flex-col h-full bg-white pl-10 pr-10 text-sm tracking-tight z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)] pt-5'>
<div className='flex justify-center items-center h-full text-gray-300 antialiased'>
<div className='flex flex-col space-y-2 items-center'>
<TentTree size={60} />
<div className='font-semibold text-2xl py-1'>
No Task Selected
</div>
</div>
</div>
</div>
)}
</div>
)
}

View file

@ -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,17 +23,45 @@ function AssignmentTasks() {
<div className='flex flex-col space-y-3 mx-auto'>
{assignments && assignments?.assignment_tasks?.map((task: any) => {
return (
<div key={task.id} className='flex flex-col w-[250px] nice-shadow bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.06)] p-3 rounded-md'>
<div className='flex justify-between px-2'>
<div
key={task.id}
className='flex flex-col w-[250px] nice-shadow bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.06)] p-3 rounded-md'
onClick={() => setSelectTask(task.assignment_task_uuid)}
>
<div className='flex items-center px-2 justify-between'>
<div className="flex space-x-3 items-center">
<div className='text-gray-500'>
{task.assignment_type === 'QUIZ' && <ListTodo size={15} />}
{task.assignment_type === 'FILE_SUBMISSION' && <FileUp size={15} />}
</div>
<div className='font-semibold text-sm'>{task.title}</div>
</div>
<button className="outline outline-1 outline-gray-200 hover:bg-slate-100/50 rounded-md text-gray-500 font-bold py-2 px-3 focus:bg-slate-100 ease-linear transition-all">
<PanelLeftOpen size={16} />
</button>
</div>
</div>
)
})}
<div className='flex space-x-1.5 px-2 py-2 bg-black text-white text-xs rounded-md antialiased items-center font-semibold cursor-pointer'>
<Modal
isDialogOpen={isNewTaskModalOpen}
onOpenChange={setIsNewTaskModalOpen}
minHeight="sm"
minWidth='sm'
dialogContent={
<NewTaskModal assignment_uuid={assignment_uuid} closeModal={setIsNewTaskModalOpen} />
}
dialogTitle="Add an Assignment Task"
dialogDescription="Create a new task for this assignment"
dialogTrigger={
<div className='flex space-x-1.5 px-2 py-2 justify-center bg-black text-white text-xs rounded-md antialiased items-center font-semibold cursor-pointer'>
<Plus size={17} />
<p>Add Task</p>
</div>
}
/>
</div>
</div>

View file

@ -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() {
<div className='flex mx-auto mt-5 items-center space-x-4'>
<div className='flex bg-green-200/60 text-xs rounded-full px-3.5 py-2 mx-auto font-bold outline outline-1 outline-green-300'>Published</div>
<div><EllipsisVertical className='text-gray-500' size={13} /></div>
<ToolTip
side='left'
slateBlack
sideOffset={10}
content="Make your Assignment public and available for students" >
<div className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-green-800 font-medium from-green-400/50 to-lime-200/80 border border-green-600/10 shadow-green-900/10 shadow-lg'>
<BookOpen size={18} />
<p className=' text-sm font-bold'>Publish</p>
</div>
</ToolTip>
<ToolTip
side='left'
slateBlack
sideOffset={10}
content="Make your Assignment unavailable for students" >
<div className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'>
<BookX size={18} />
<p className='text-sm font-bold'>Unpublish</p>
</div>
</ToolTip>
</div>
</div>
</div>
</div>
<div className="flex w-full">
<div className='flex w-[400px] flex-col h-screen custom-dots-bg'>
<div className="flex h-full w-full">
<AssignmentsTaskProvider>
<div className='flex w-[400px] flex-col h-full custom-dots-bg'>
<div className='flex mx-auto px-3.5 py-1 bg-neutral-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'>
<LayoutList size={18} />
<p>Tasks</p>
</div>
<AssignmentProvider assignment_uuid='assignment_a35fdbb9-11bd-40cf-a781-f6bdd5d87165'>
<AssignmentTasks />
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
<AssignmentTasks assignment_uuid={'assignment_' + params.assignmentuuid} />
</AssignmentProvider>
</div>
<div className='flex flex-grow bg-[#fefcfe] nice-shadow h-screen w-full'>
<div className='flex flex-grow bg-[#fefcfe] nice-shadow h-full w-full'>
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
<AssignmentTaskEditor task_uuid='UUID' page='overview' />
</AssignmentProvider>
</div>
</AssignmentsTaskProvider>
</div>
</div>
)

View file

@ -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<string, any>;
}
interface Action {
type: string;
payload?: any;
}
const initialState: State = {
selectedAssignmentTaskUUID: null,
assignmentTask: {}
};
export const AssignmentsTaskContext = createContext<State | undefined>(undefined);
export const AssignmentsTaskDispatchContext = createContext<React.Dispatch<Action> | 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 (
<AssignmentsTaskContext.Provider value={state}>
<AssignmentsTaskDispatchContext.Provider value={dispatch}>
{children}
</AssignmentsTaskDispatchContext.Provider>
</AssignmentsTaskContext.Provider>
);
}
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;
}
}

View file

@ -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
}