feat: enable grading for file subs

This commit is contained in:
swve 2024-08-06 18:55:32 +02:00
parent d6aa071425
commit ad3f66057c
7 changed files with 149 additions and 24 deletions

View file

@ -1,9 +1,12 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext'; import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI' import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
import { getAssignmentTask, getAssignmentTaskSubmissionsMe, handleAssignmentTaskSubmission, updateSubFile } from '@services/courses/assignments'; import { getAssignmentTask, getAssignmentTaskSubmissionsMe, getAssignmentTaskSubmissionsUser, handleAssignmentTaskSubmission, updateSubFile } from '@services/courses/assignments';
import { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react' import { getTaskFileSubmissionDir } from '@services/media/media';
import { Cloud, Download, File, Info, Loader, UploadCloud } from 'lucide-react'
import Link from 'next/link';
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@ -12,12 +15,14 @@ type FileSchema = {
}; };
type TaskFileObjectProps = { type TaskFileObjectProps = {
view: 'teacher' | 'student'; view: 'teacher' | 'student' | 'grading' | 'custom-grading';
assignmentTaskUUID?: string; assignmentTaskUUID?: string;
user_id?: string;
}; };
export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObjectProps) { export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: TaskFileObjectProps) {
const session = useLHSession() as any; const session = useLHSession() as any;
const org = useOrg() as any;
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [localUploadFile, setLocalUploadFile] = React.useState<File | null>(null); const [localUploadFile, setLocalUploadFile] = React.useState<File | null>(null);
@ -104,6 +109,7 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
const res = await getAssignmentTask(assignmentTaskUUID, access_token); const res = await getAssignmentTask(assignmentTaskUUID, access_token);
if (res.success) { if (res.success) {
setAssignmentTask(res.data); setAssignmentTask(res.data);
setAssignmentTaskOutsideProvider(res.data);
} }
} }
@ -120,22 +126,98 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
/* STUDENT VIEW CODE */ /* STUDENT VIEW CODE */
/* GRADING VIEW CODE */
const [userSubmissionObject, setUserSubmissionObject] = useState<any>(null);
async function getAssignmentTaskSubmissionFromIdentifiedUserUI() {
if (assignmentTaskUUID && user_id) {
const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
setUserSubmissions(res.data.task_submission);
setUserSubmissionObject(res.data);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
async function gradeCustomFC(grade: number) {
if (assignmentTaskUUID) {
if (grade > assignmentTaskOutsideProvider.max_grade_value) {
toast.error(`Grade cannot be more than ${assignmentTaskOutsideProvider.max_grade_value} points`);
return;
}
// Save the grade to the server
const values = {
task_submission: userSubmissions,
grade: grade,
task_submission_grade_feedback: 'Graded by teacher : @' + session.data.user.username,
};
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
getAssignmentTaskSubmissionFromIdentifiedUserUI();
toast.success(`Task graded successfully with ${grade} points`);
} else {
toast.error('Error grading task, please retry later.');
}
}
}
/* GRADING VIEW CODE */
const [assignmentTaskOutsideProvider, setAssignmentTaskOutsideProvider] = useState<any>(null);
useEffect(() => { useEffect(() => {
// Student area
if (view === 'student') { if (view === 'student') {
getAssignmentTaskUI() getAssignmentTaskUI()
getAssignmentTaskSubmissionFromUserUI() getAssignmentTaskSubmissionFromUserUI()
} }
// Grading area
else if (view == 'custom-grading') {
getAssignmentTaskUI();
//setQuestions(assignmentTaskState.assignmentTask.contents.questions);
getAssignmentTaskSubmissionFromIdentifiedUserUI();
}
} }
, [assignmentTaskUUID]) , [assignmentTaskUUID])
return ( return (
<AssignmentBoxUI submitFC={submitFC} showSavingDisclaimer={showSavingDisclaimer} view={view} type="file"> <AssignmentBoxUI submitFC={submitFC} showSavingDisclaimer={showSavingDisclaimer} view={view} gradeCustomFC={gradeCustomFC} currentPoints={userSubmissionObject?.grade} maxPoints={assignmentTaskOutsideProvider?.max_grade_value} type="file">
{view === 'teacher' && ( {view === 'teacher' && (
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'> <div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Info size={20} /> <Info size={20} />
<p>User will be able to submit a file for this task, you'll be able to review it in the Submissions Tab</p> <p>User will be able to submit a file for this task, you'll be able to review it in the Submissions Tab</p>
</div> </div>
)} )}
{view === 'custom-grading' && (
<div className='flex flex-col space-y-1'>
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Download size={20} />
<p>Please download the file and grade it manually, then input the grade below</p>
</div>
{userSubmissions.fileUUID && !isLoading && assignmentTaskUUID && (
<Link
href={getTaskFileSubmissionDir(org?.org_uuid, assignment.course_object.course_uuid, assignment.activity_object.activity_uuid, assignment.assignment_object.assignment_uuid, assignmentTaskUUID, userSubmissions.fileUUID)}
target='_blank'
className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
</div>
<div
className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
</div>
</div>
</Link>
)}
</div>
)}
{view === 'student' && ( {view === 'student' && (
<> <>
<div className="w-auto bg-white rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow"> <div className="w-auto bg-white rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow">
@ -213,7 +295,6 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
</button> </button>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>

View file

@ -219,32 +219,45 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
// Ensure userSubmissions.questions are set // Ensure userSubmissions.questions are set
const totalQuestions = questions.length; const totalQuestions = questions.length;
const correctQuestions = userSubmissions.submissions.filter((submission) => { let correctQuestions = 0;
let incorrectQuestions = 0;
userSubmissions.submissions.forEach((submission) => {
const question = questions.find((q) => q.questionUUID === submission.questionUUID); const question = questions.find((q) => q.questionUUID === submission.questionUUID);
const option = question?.options.find((o) => o.optionUUID === submission.optionUUID); const option = question?.options.find((o) => o.optionUUID === submission.optionUUID);
return option?.correct; if (option?.correct) {
}).length; correctQuestions++;
} else {
incorrectQuestions++;
}
});
// Calculate grade based on correct questions // Calculate grade with penalties for incorrect answers
const grade = Math.floor((correctQuestions / totalQuestions) * maxPoints); const pointsPerQuestion = maxPoints / totalQuestions;
const rawGrade = (correctQuestions - incorrectQuestions) * pointsPerQuestion;
// Ensure the grade is within the valid range
const finalGrade = Math.max(0, Math.min(rawGrade, maxPoints));
// Save the grade to the server // Save the grade to the server
const values = { const values = {
task_submission: userSubmissions, task_submission: userSubmissions,
grade, grade: finalGrade,
task_submission_grade_feedback: 'Auto graded by system', task_submission_grade_feedback: 'Auto graded by system',
}; };
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token); const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) { if (res) {
getAssignmentTaskSubmissionFromIdentifiedUserUI(); getAssignmentTaskSubmissionFromIdentifiedUserUI();
toast.success(`Task graded successfully with ${grade} points`); toast.success(`Task graded successfully with ${finalGrade} points`);
} else { } else {
toast.error('Error grading task, please retry later.'); toast.error('Error grading task, please retry later.');
} }
} }
} }
/* GRADING VIEW CODE */ /* GRADING VIEW CODE */
useEffect(() => { useEffect(() => {

View file

@ -27,7 +27,7 @@ function EvaluateAssignment({ user_id }: any) {
onClick={() => alert(task.hint)} onClick={() => alert(task.hint)}
className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'> className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'>
<Info size={13} /> <Info size={13} />
<p className='text-xs font-semibold'>View Hint</p> <p className='text-xs font-semibold'>Hint</p>
</div> </div>
<Link <Link
href={getTaskRefFileDir( href={getTaskRefFileDir(
@ -55,7 +55,7 @@ function EvaluateAssignment({ user_id }: any) {
</div> </div>
<div> <div>
{task.assignment_type === 'QUIZ' && <TaskQuizObject key={task.assignment_task_uuid} view='grading' user_id={user_id} assignmentTaskUUID={task.assignment_task_uuid} />} {task.assignment_type === 'QUIZ' && <TaskQuizObject key={task.assignment_task_uuid} view='grading' user_id={user_id} assignmentTaskUUID={task.assignment_task_uuid} />}
{task.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />} {task.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject key={task.assignment_task_uuid} view='custom-grading' user_id={user_id} assignmentTaskUUID={task.assignment_task_uuid} />}
</div> </div>
</div> </div>
) )

View file

@ -168,7 +168,7 @@ function ActivityElement(props: ActivitiyElementProps) {
) : ( ) : (
<Lock strokeWidth={2} size={12} className="text-gray-600" /> <Lock strokeWidth={2} size={12} className="text-gray-600" />
)} )}
<span>{!props.activity.published ? 'Publish' : 'UnPublish'}</span> <span>{!props.activity.published ? 'Publish' : 'Unpublish'}</span>
</div> </div>
<Link <Link
href={ href={

View file

@ -4,22 +4,25 @@ import React, { useEffect } from 'react'
type AssignmentBoxProps = { type AssignmentBoxProps = {
type: 'quiz' | 'file' type: 'quiz' | 'file'
view?: 'teacher' | 'student' | 'grading' view?: 'teacher' | 'student' | 'grading' | 'custom-grading'
maxPoints?: number maxPoints?: number
currentPoints?: number currentPoints?: number
saveFC?: () => void saveFC?: () => void
submitFC?: () => void submitFC?: () => void
gradeFC?: () => void gradeFC?: () => void
gradeCustomFC?: (grade: number) => void
showSavingDisclaimer?: boolean showSavingDisclaimer?: boolean
children: React.ReactNode children: React.ReactNode
} }
function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitFC, gradeFC, showSavingDisclaimer, children }: AssignmentBoxProps) { function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitFC, gradeFC, gradeCustomFC, showSavingDisclaimer, children }: AssignmentBoxProps) {
const [customGrade, setCustomGrade] = React.useState<number>(0)
const submission = useAssignmentSubmission() as any const submission = useAssignmentSubmission() as any
useEffect(() => { useEffect(() => {
} }
, [submission]) , [submission])
return ( return (
<div className='flex flex-col px-6 py-4 nice-shadow rounded-md bg-slate-100/30'> <div className='flex flex-col px-6 py-4 nice-shadow rounded-md bg-slate-100/30'>
<div className='flex justify-between space-x-2 pb-2 text-slate-400 items-center'> <div className='flex justify-between space-x-2 pb-2 text-slate-400 items-center'>
@ -89,7 +92,23 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF
className='flex px-0.5 py-0.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'> className='flex px-0.5 py-0.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'>
<p className='font-semibold px-2 text-xs text-orange-700'>Current points : {currentPoints}</p> <p className='font-semibold px-2 text-xs text-orange-700'>Current points : {currentPoints}</p>
<div className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'> <div className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'>
<BookPlus size={14} /> <BookPlus size={14} />
<p className='text-xs font-semibold'>Grade</p>
</div>
</div>
}
{/* CustomGrading button */}
{view === 'custom-grading' && maxPoints &&
<div
onClick={() => gradeCustomFC && gradeCustomFC(customGrade)}
className='flex px-0.5 py-0.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'>
<p className='font-semibold px-2 text-xs text-orange-700'>Current points : {currentPoints}</p>
<input
onChange={(e) => setCustomGrade(parseInt(e.target.value))}
placeholder={maxPoints.toString()} className='w-[100px] light-shadow text-sm py-0.5 outline outline-gray-200 rounded-lg px-2' type="number" />
<div className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'>
<BookPlus size={14} />
<p className='text-xs font-semibold'>Grade</p> <p className='text-xs font-semibold'>Grade</p>
</div> </div>
</div> </div>

View file

@ -55,7 +55,7 @@ function AssignmentStudentActivity() {
onClick={() => alert(task.hint)} onClick={() => alert(task.hint)}
className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'> className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'>
<Info size={13} /> <Info size={13} />
<p className='text-xs font-semibold'>View Hint</p> <p className='text-xs font-semibold'>Hint</p>
</div> </div>
<Link <Link
href={getTaskRefFileDir( href={getTaskRefFileDir(

View file

@ -58,6 +58,18 @@ export function getTaskRefFileDir(
return uri return uri
} }
export function getTaskFileSubmissionDir(
orgUUID: string,
courseUUID: string,
activityUUID: string,
assignmentUUID: string,
assignmentTaskUUID: string,
fileSubID : string
) {
let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseUUID}/activities/${activityUUID}/assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/subs/${fileSubID}`
return uri
}
export function getActivityMediaDirectory( export function getActivityMediaDirectory(
orgUUID: string, orgUUID: string,
courseUUID: string, courseUUID: string,