mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: enable grading for file subs
This commit is contained in:
parent
d6aa071425
commit
ad3f66057c
7 changed files with 149 additions and 24 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -216,34 +216,47 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
if (assignmentTaskUUID) {
|
if (assignmentTaskUUID) {
|
||||||
// Ensure maxPoints is defined
|
// Ensure maxPoints is defined
|
||||||
const maxPoints = assignmentTaskOutsideProvider?.max_grade_value || 100; // Default to 100 if not defined
|
const maxPoints = assignmentTaskOutsideProvider?.max_grade_value || 100; // Default to 100 if not defined
|
||||||
|
|
||||||
// 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 {
|
||||||
// Calculate grade based on correct questions
|
incorrectQuestions++;
|
||||||
const grade = Math.floor((correctQuestions / totalQuestions) * maxPoints);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate grade with penalties for incorrect answers
|
||||||
|
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 */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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={
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue