diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index 3be3eac9..6e49e422 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -4,6 +4,7 @@ import importlib from config.config import get_learnhouse_config from fastapi import FastAPI from sqlmodel import SQLModel, Session, create_engine +from sqlalchemy import event def import_all_models(): base_dir = 'src/db' @@ -48,11 +49,24 @@ else: learnhouse_config.database_config.sql_connection_string, # type: ignore echo=False, pool_pre_ping=True, # type: ignore - pool_size=5, - max_overflow=0, + pool_size=20, # Increased from 5 to handle more concurrent requests + max_overflow=10, # Allow 10 additional connections beyond pool_size pool_recycle=300, # Recycle connections after 5 minutes pool_timeout=30 ) + + # Add connection pool monitoring for debugging + @event.listens_for(engine, "connect") + def receive_connect(dbapi_connection, connection_record): + logging.debug("Database connection established") + + @event.listens_for(engine, "checkout") + def receive_checkout(dbapi_connection, connection_record, connection_proxy): + logging.debug("Connection checked out from pool") + + @event.listens_for(engine, "checkin") + def receive_checkin(dbapi_connection, connection_record): + logging.debug("Connection returned to pool") # Only create tables if not in test mode (tests will handle this themselves) if not is_testing: diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py index 277ee134..953f13a0 100644 --- a/apps/api/src/routers/courses/assignments.py +++ b/apps/api/src/routers/courses/assignments.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, Request, UploadFile +from fastapi import APIRouter, Depends, Request, UploadFile, HTTPException from src.db.courses.assignments import ( AssignmentCreate, AssignmentRead, @@ -295,9 +295,16 @@ async def api_read_user_assignment_task_submissions_me( """ Read task submissions for an assignment from a user """ - return await read_user_assignment_task_submissions_me( + result = await read_user_assignment_task_submissions_me( request, assignment_task_uuid, current_user, db_session ) + if result is None: + # Return 404 if no submission exists (maintains current frontend behavior) + raise HTTPException( + status_code=404, + detail="Assignment Task Submission not found", + ) + return result @router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions") diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index b18a6d18..c1182d00 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -764,9 +764,15 @@ async def handle_assignment_task_submission( # SECURITY: Instructors/admins need update permission to grade await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session) - # Try to find existing submission if UUID is provided - assignment_task_submission = None - if assignment_task_submission_uuid: + # Try to find existing submission by user_id and assignment_task_id first (for save progress functionality) + statement = select(AssignmentTaskSubmission).where( + AssignmentTaskSubmission.assignment_task_id == assignment_task.id, + AssignmentTaskSubmission.user_id == current_user.id, + ) + assignment_task_submission = db_session.exec(statement).first() + + # If no submission found by user+task, try to find by UUID if provided (for specific submission updates) + if not assignment_task_submission and assignment_task_submission_uuid: statement = select(AssignmentTaskSubmission).where( AssignmentTaskSubmission.assignment_task_submission_uuid == assignment_task_submission_uuid ) @@ -889,13 +895,54 @@ async def read_user_assignment_task_submissions_me( current_user: PublicUser | AnonymousUser, db_session: Session, ): - return await read_user_assignment_task_submissions( - request, - assignment_task_uuid, - current_user.id, - current_user, - db_session, + # Check if assignment task exists + statement = select(AssignmentTask).where( + AssignmentTask.assignment_task_uuid == assignment_task_uuid ) + assignment_task = db_session.exec(statement).first() + + if not assignment_task: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment task submission exists + statement = select(AssignmentTaskSubmission).where( + AssignmentTaskSubmission.assignment_task_id == assignment_task.id, + AssignmentTaskSubmission.user_id == current_user.id, + ) + assignment_task_submission = db_session.exec(statement).first() + + if not assignment_task_submission: + # Return None instead of raising an error for cases where no submission exists yet + return None + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignment_task.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 courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "read", db_session) + + # return assignment task submission read + return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission) async def read_assignment_task_submissions( 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 index 73a74beb..65aab421 100644 --- 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 @@ -65,13 +65,13 @@ function NewTaskModal({ closeModal, assignment_uuid }: any) {

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'> + onClick={() => createTask('FORM')} + className='flex flex-col space-y-2 justify-center text-center pt-10'>

Form

-

Forms for students to fill out

+

Fill-in-the-blank forms for students

) diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskContentEdit.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskContentEdit.tsx index afddc310..1dec180e 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskContentEdit.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/AssignmentTaskContentEdit.tsx @@ -3,6 +3,7 @@ import { useLHSession } from '@components/Contexts/LHSessionContext'; import React, { useEffect } from 'react' import TaskQuizObject from './TaskTypes/TaskQuizObject'; import TaskFileObject from './TaskTypes/TaskFileObject'; +import TaskFormObject from './TaskTypes/TaskFormObject'; function AssignmentTaskContentEdit() { const session = useLHSession() as any; @@ -18,6 +19,7 @@ function AssignmentTaskContentEdit() {
{assignment_task?.assignmentTask.assignment_type === 'QUIZ' && } {assignment_task?.assignmentTask.assignment_type === 'FILE_SUBMISSION' && } + {assignment_task?.assignmentTask.assignment_type === 'FORM' && }
) } diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx index c3434c92..a5cc6858 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject.tsx @@ -106,9 +106,9 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta return; } - // Save the quiz to the server + // Save the file submission to the server const values = { - assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid, + assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid || null, task_submission: userSubmissions, grade: 0, task_submission_grade_feedback: '', @@ -121,6 +121,13 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta }); toast.success('Task saved successfully'); setShowSavingDisclaimer(false); + // Update userSubmissions with the returned UUID for future updates + const updatedUserSubmissions = { + ...userSubmissions, + assignment_task_submission_uuid: res.data?.assignment_task_submission_uuid || userSubmissions.assignment_task_submission_uuid + }; + setUserSubmissions(updatedUserSubmissions); + setInitialUserSubmissions(updatedUserSubmissions); } else { toast.error('Error saving task, please retry later.'); } diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFormObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFormObject.tsx new file mode 100644 index 00000000..bf842604 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFormObject.tsx @@ -0,0 +1,590 @@ +import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; +import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; +import { useLHSession } from '@components/Contexts/LHSessionContext'; +import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'; +import { getAssignmentTask, getAssignmentTaskSubmissionsMe, getAssignmentTaskSubmissionsUser, handleAssignmentTaskSubmission, updateAssignmentTask } from '@services/courses/assignments'; +import { Check, Info, Minus, Plus, PlusCircle, X, Type } from 'lucide-react'; +import React, { useEffect, useState } from 'react'; +import toast from 'react-hot-toast'; +import { v4 as uuidv4 } from 'uuid'; + +type FormSchema = { + questionText: string; + questionUUID?: string; + blanks: { + blankUUID?: string; + placeholder: string; + correctAnswer: string; + hint?: string; + }[]; +}; + +type FormSubmitSchema = { + questions: FormSchema[]; + submissions: { + questionUUID: string; + blankUUID: string; + answer: string; + }[]; + assignment_task_submission_uuid?: string; +}; + +type TaskFormObjectProps = { + view: 'teacher' | 'student' | 'grading'; + assignmentTaskUUID: string; + user_id?: string; +}; + +function TaskFormObject({ view, assignmentTaskUUID, user_id }: TaskFormObjectProps) { + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + const assignmentTaskState = useAssignmentsTask() as any; + const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any; + const assignment = useAssignments() as any; + + /* TEACHER VIEW CODE */ + const [questions, setQuestions] = useState( + view === 'teacher' ? [ + { + questionText: '', + questionUUID: 'question_' + uuidv4(), + blanks: [{ + placeholder: 'Enter the correct answer', + correctAnswer: '', + hint: '', + blankUUID: 'blank_' + uuidv4() + }] + }, + ] : [] + ); + + const handleQuestionChange = (index: number, value: string) => { + const updatedQuestions = [...questions]; + updatedQuestions[index].questionText = value; + setQuestions(updatedQuestions); + }; + + const handleBlankChange = (qIndex: number, bIndex: number, field: 'placeholder' | 'correctAnswer' | 'hint', value: string) => { + const updatedQuestions = [...questions]; + updatedQuestions[qIndex].blanks[bIndex][field] = value; + setQuestions(updatedQuestions); + }; + + const addBlank = (qIndex: number) => { + const updatedQuestions = [...questions]; + updatedQuestions[qIndex].blanks.push({ + placeholder: 'Enter the correct answer', + correctAnswer: '', + hint: '', + blankUUID: 'blank_' + uuidv4() + }); + setQuestions(updatedQuestions); + }; + + const removeBlank = (qIndex: number, bIndex: number) => { + const updatedQuestions = [...questions]; + if (updatedQuestions[qIndex].blanks.length > 1) { + updatedQuestions[qIndex].blanks.splice(bIndex, 1); + setQuestions(updatedQuestions); + } else { + toast.error('Cannot delete the last blank. At least one blank is required.'); + } + }; + + const addQuestion = () => { + setQuestions([...questions, { + questionText: '', + questionUUID: 'question_' + uuidv4(), + blanks: [{ + placeholder: 'Enter the correct answer', + correctAnswer: '', + hint: '', + blankUUID: 'blank_' + uuidv4() + }] + }]); + }; + + const removeQuestion = (qIndex: number) => { + const updatedQuestions = [...questions]; + updatedQuestions.splice(qIndex, 1); + setQuestions(updatedQuestions); + }; + + const saveFC = async () => { + // Save the form to the server + const values = { + contents: { + questions, + }, + }; + const res = await updateAssignmentTask(values, assignmentTaskState.assignmentTask.assignment_task_uuid, assignment.assignment_object.assignment_uuid, access_token); + if (res) { + assignmentTaskStateHook({ + type: 'reload', + }); + toast.success('Task saved successfully'); + } else { + console.error('Save error:', res); + toast.error('Error saving task, please retry later.'); + } + }; + + /* STUDENT VIEW CODE */ + const [userSubmissions, setUserSubmissions] = useState({ + questions: [], + submissions: [], + }); + const [initialUserSubmissions, setInitialUserSubmissions] = useState({ + questions: [], + submissions: [], + }); + const [showSavingDisclaimer, setShowSavingDisclaimer] = useState(false); + const [assignmentTaskOutsideProvider, setAssignmentTaskOutsideProvider] = useState(null); + const [userSubmissionObject, setUserSubmissionObject] = useState(null); + + const handleUserAnswerChange = (questionUUID: string, blankUUID: string, answer: string) => { + const updatedSubmissions = [...userSubmissions.submissions]; + const existingIndex = updatedSubmissions.findIndex( + (submission) => submission.questionUUID === questionUUID && submission.blankUUID === blankUUID + ); + + if (existingIndex !== -1) { + updatedSubmissions[existingIndex].answer = answer; + } else { + updatedSubmissions.push({ + questionUUID, + blankUUID, + answer, + }); + } + + setUserSubmissions({ + ...userSubmissions, + submissions: updatedSubmissions, + }); + }; + + const handleUserAnswerBlur = (questionUUID: string, blankUUID: string, answer: string) => { + // Auto-focus next blank only when user leaves the current input and it has content + if (answer.trim() && view === 'student') { + const allBlanks = questions.flatMap(q => q.blanks.map(b => ({ questionUUID: q.questionUUID, blankUUID: b.blankUUID }))); + const currentIndex = allBlanks.findIndex(b => b.questionUUID === questionUUID && b.blankUUID === blankUUID); + const nextBlank = allBlanks[currentIndex + 1]; + + if (nextBlank) { + setTimeout(() => { + const nextInput = document.querySelector(`[data-blank-id="${nextBlank.blankUUID}"]`) as HTMLInputElement; + if (nextInput && !nextInput.value.trim()) { + nextInput.focus(); + } + }, 100); + } + } + }; + + const submitFC = async () => { + if (userSubmissions.submissions.length === 0) { + toast.error('Please fill in at least one blank before submitting.'); + return; + } + + const values = { + assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid || null, + task_submission: userSubmissions, + grade: 0, + task_submission_grade_feedback: '', + }; + + const res = await handleAssignmentTaskSubmission( + values, + assignmentTaskUUID, + assignment.assignment_object.assignment_uuid, + access_token + ); + + if (res) { + toast.success('Form submitted successfully!'); + // Update userSubmissions with the returned UUID for future updates + const updatedUserSubmissions = { + ...userSubmissions, + assignment_task_submission_uuid: res.data?.assignment_task_submission_uuid || userSubmissions.assignment_task_submission_uuid + }; + setUserSubmissions(updatedUserSubmissions); + setInitialUserSubmissions(updatedUserSubmissions); + setShowSavingDisclaimer(false); + } else { + console.error('Submission error:', res); + toast.error('Error submitting form, please retry later.'); + } + }; + + const gradeFC = async () => { + if (!user_id) { + toast.error('User ID is required for grading.'); + return; + } + + // Calculate grade based on correct answers + let correctAnswers = 0; + let totalBlanks = 0; + + questions.forEach((question) => { + question.blanks.forEach((blank) => { + totalBlanks++; + const userAnswer = userSubmissions.submissions.find( + (submission) => submission.questionUUID === question.questionUUID && submission.blankUUID === blank.blankUUID + ); + if (userAnswer && userAnswer.answer.toLowerCase().trim() === blank.correctAnswer.toLowerCase().trim()) { + correctAnswers++; + } + }); + }); + + const maxPoints = assignmentTaskOutsideProvider?.max_grade_value || 100; + const finalGrade = totalBlanks > 0 ? Math.round((correctAnswers / totalBlanks) * maxPoints) : 0; + + // Save the grade to the server + const values = { + assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid, + task_submission: userSubmissions, + grade: finalGrade, + task_submission_grade_feedback: 'Auto graded by system', + }; + + const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token); + if (res) { + getAssignmentTaskSubmissionFromIdentifiedUserUI(); + toast.success(`Task graded successfully with ${finalGrade} points (${correctAnswers}/${totalBlanks} correct)`); + } else { + toast.error('Error grading task, please retry later.'); + } + }; + + async function getAssignmentTaskSubmissionFromIdentifiedUserUI() { + if (!access_token || !user_id) { + return; + } + + if (assignmentTaskUUID) { + const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token); + if (res.success) { + setUserSubmissions({ + ...res.data.task_submission, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid + }); + setInitialUserSubmissions({ + ...res.data.task_submission, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid + }); + setUserSubmissionObject(res.data); + } + } + } + + useEffect(() => { + const loadAssignmentTask = async () => { + if (assignmentTaskUUID) { + const res = await getAssignmentTask(assignmentTaskUUID, access_token); + if (res.success) { + setAssignmentTaskOutsideProvider(res.data); + // Only set questions if they exist and we're not in teacher view, or if we're in teacher view and there are existing questions + if (res.data.contents?.questions && res.data.contents.questions.length > 0) { + setQuestions(res.data.contents.questions); + } else if (view !== 'teacher') { + // For non-teacher views, set empty array if no questions exist + setQuestions([]); + } + // For teacher view, keep the initial state if no questions exist + } + } + }; + + const loadUserSubmissions = async () => { + if (view === 'student' && assignmentTaskUUID) { + const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token); + if (res.success) { + setUserSubmissions({ + ...res.data.task_submission, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid + }); + setInitialUserSubmissions({ + ...res.data.task_submission, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid + }); + } + } + }; + + // Set assignment task UUID in context + assignmentTaskStateHook({ + setSelectedAssignmentTaskUUID: assignmentTaskUUID, + }); + + // Teacher area - Load from context first, then from API if needed + if (view === 'teacher') { + if (assignmentTaskState.assignmentTask.contents?.questions) { + setQuestions(assignmentTaskState.assignmentTask.contents.questions); + } else { + loadAssignmentTask(); + } + } + // Student area + else if (view === 'student') { + loadAssignmentTask(); + loadUserSubmissions(); + } + // Grading area + else if (view === 'grading') { + loadAssignmentTask(); + getAssignmentTaskSubmissionFromIdentifiedUserUI(); + } + }, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token, assignmentTaskUUID, view]); + + useEffect(() => { + if (JSON.stringify(userSubmissions) !== JSON.stringify(initialUserSubmissions)) { + setShowSavingDisclaimer(true); + } else { + setShowSavingDisclaimer(false); + } + }, [userSubmissions, initialUserSubmissions]); + + // Ensure questions is always an array for teacher view + if (view === 'teacher' && (!questions || questions.length === 0)) { + setQuestions([ + { + questionText: '', + questionUUID: 'question_' + uuidv4(), + blanks: [{ + placeholder: 'Enter the correct answer', + correctAnswer: '', + hint: '', + blankUUID: 'blank_' + uuidv4() + }] + }, + ]); + return null; // Return null to prevent rendering while state updates + } + + if (view === 'teacher' || (questions && questions.length > 0)) { + return ( + + {view === 'grading' && ( +
+

Submission Summary

+
+
+
+ {questions.flatMap(q => q.blanks).length} +
+
Total Blanks
+
+
+
+ {questions.flatMap(q => q.blanks).filter(blank => { + const userAnswer = userSubmissions.submissions.find(s => s.blankUUID === blank.blankUUID); + return userAnswer && userAnswer.answer.toLowerCase().trim() === blank.correctAnswer.toLowerCase().trim(); + }).length} +
+
Correct
+
+
+
+ {questions.flatMap(q => q.blanks).length - questions.flatMap(q => q.blanks).filter(blank => { + const userAnswer = userSubmissions.submissions.find(s => s.blankUUID === blank.blankUUID); + return userAnswer && userAnswer.answer.toLowerCase().trim() === blank.correctAnswer.toLowerCase().trim(); + }).length} +
+
Incorrect
+
+
+
+ )} +
+ {questions && questions.map((question, qIndex) => ( +
+
+ {view === 'teacher' ? ( + handleQuestionChange(qIndex, e.target.value)} + placeholder="Enter your question with blanks (use ___ for blanks)" + className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold" + /> + ) : ( +

+ {question.questionText} +

+ )} + {view === 'teacher' && ( +
removeQuestion(qIndex)} + > + +
+ )} +
+ + {/* Blanks section */} +
+ {question.blanks.map((blank, bIndex) => ( +
+
+
+ +
+ {view === 'teacher' ? ( +
+ handleBlankChange(qIndex, bIndex, 'placeholder', e.target.value)} + placeholder="Placeholder text for the blank" + className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold" + /> + handleBlankChange(qIndex, bIndex, 'correctAnswer', e.target.value)} + placeholder="Correct answer" + className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-lime-50 border-2 border-lime-200 rounded-md border-dotted text-sm font-bold" + /> + handleBlankChange(qIndex, bIndex, 'hint', e.target.value)} + placeholder="Hint (optional)" + className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-blue-50 border-2 border-blue-200 rounded-md border-dotted text-xs" + /> +
+ ) : view === 'grading' ? ( +
+
+ submission.questionUUID === question.questionUUID && submission.blankUUID === blank.blankUUID + )?.answer || ''} + readOnly + className="flex-1 px-3 pr-6 text-neutral-600 bg-gray-50 border-2 border-gray-200 rounded-md text-sm font-bold" + /> +
+
+ Expected: {blank.correctAnswer} +
+ {blank.hint && ( +
💡 {blank.hint}
+ )} +
+ ) : ( +
+ submission.questionUUID === question.questionUUID && submission.blankUUID === blank.blankUUID + )?.answer || ''} + onChange={(e) => handleUserAnswerChange(question.questionUUID!, blank.blankUUID!, e.target.value)} + onBlur={(e) => handleUserAnswerBlur(question.questionUUID!, blank.blankUUID!, e.target.value)} + placeholder={blank.placeholder} + data-blank-id={blank.blankUUID} + className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md focus:border-blue-400 focus:ring-2 focus:ring-blue-200 text-sm font-bold transition-all" + /> + {blank.hint && ( +
💡 {blank.hint}
+ )} +
+ )} + {view === 'teacher' && ( +
removeBlank(qIndex, bIndex)} + > + +
+ )} + {view === 'grading' && ( +
submission.questionUUID === question.questionUUID && submission.blankUUID === blank.blankUUID + )?.answer?.toLowerCase().trim() === blank.correctAnswer.toLowerCase().trim() + ? 'bg-lime-200 text-lime-600' + : 'bg-rose-200/60 text-rose-500' + } text-sm`}> + {userSubmissions.submissions.find( + (submission) => submission.questionUUID === question.questionUUID && submission.blankUUID === blank.blankUUID + )?.answer?.toLowerCase().trim() === blank.correctAnswer.toLowerCase().trim() ? ( + <> + +

Correct

+ + ) : ( + <> + +

Incorrect

+ + )} +
+ )} + {view === 'student' && ( +
submission.questionUUID === question.questionUUID && submission.blankUUID === blank.blankUUID + )?.answer?.trim() + ? "bg-green-200/60 text-green-500" + : "bg-slate-200/60 text-slate-500" + } text-sm transition-all ease-linear`}> + {userSubmissions.submissions.find( + (submission) => submission.questionUUID === question.questionUUID && submission.blankUUID === blank.blankUUID + )?.answer?.trim() ? ( + + ) : ( + + )} +
+ )} +
+ {view === 'teacher' && bIndex === question.blanks.length - 1 && question.blanks.length <= 4 && ( +
+
addBlank(qIndex)} + > + + +
+
+ )} +
+ ))} +
+
+ ))} +
+ {view === 'teacher' && questions.length <= 5 && ( +
+
+ + Add Question +
+
+ )} +
+ ); + } + + return ( +
+ +

No questions found

+
+ ); +} + +export default TaskFormObject; \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx index 7a8d3294..5c8472b5 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx @@ -221,6 +221,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro // Save the quiz to the server const values = { + assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid || null, task_submission: updatedUserSubmissions, grade: 0, task_submission_grade_feedback: '', @@ -234,7 +235,13 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro }); toast.success('Task saved successfully'); setShowSavingDisclaimer(false); - setUserSubmissions(updatedUserSubmissions); + // Update userSubmissions with the returned UUID for future updates + const updatedUserSubmissionsWithUUID = { + ...updatedUserSubmissions, + assignment_task_submission_uuid: res.data?.assignment_task_submission_uuid || userSubmissions.assignment_task_submission_uuid + }; + setUserSubmissions(updatedUserSubmissionsWithUUID); + setInitialUserSubmissions(updatedUserSubmissionsWithUUID); } else { toast.error('Error saving task, please retry later.'); } 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 5a816ab2..14d0a6c3 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,6 +1,6 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext' import Modal from '@components/Objects/StyledElements/Modal/Modal'; -import { FileUp, ListTodo, PanelLeftOpen, Plus } from 'lucide-react'; +import { FileUp, ListTodo, PanelLeftOpen, Plus, Type } from 'lucide-react'; import React, { useEffect } from 'react' import NewTaskModal from './Modals/NewTaskModal'; import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; @@ -51,6 +51,7 @@ function AssignmentTasks({ assignment_uuid }: any) {
{task.assignment_type === 'QUIZ' && } {task.assignment_type === 'FILE_SUBMISSION' && } + {task.assignment_type === 'FORM' && }
{task.title}
diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/Modals/EvaluateAssignment.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/Modals/EvaluateAssignment.tsx index 364cfe3a..0f8d7291 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/Modals/EvaluateAssignment.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/Modals/EvaluateAssignment.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import React from 'react' import TaskQuizObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskQuizObject'; import TaskFileObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskFileObject'; +import TaskFormObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskFormObject'; import { useOrg } from '@components/Contexts/OrgContext'; import { getTaskRefFileDir } from '@services/media/media'; import { deleteUserSubmission, markActivityAsDoneForUser, putFinalGrade } from '@services/courses/assignments'; @@ -86,6 +87,7 @@ function EvaluateAssignment({ user_id }: any) {
{task.assignment_type === 'QUIZ' && } {task.assignment_type === 'FILE_SUBMISSION' && } + {task.assignment_type === 'FORM' && }
) diff --git a/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx b/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx index 088185e4..5ac4c536 100644 --- a/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx +++ b/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx @@ -1,10 +1,10 @@ import { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext' -import { BookPlus, BookUser, EllipsisVertical, FileUp, Forward, InfoIcon, ListTodo, Save } from 'lucide-react' +import { BookPlus, BookUser, EllipsisVertical, FileUp, Forward, InfoIcon, ListTodo, Save, Type } from 'lucide-react' import React, { useEffect } from 'react' import { useLHSession } from '@components/Contexts/LHSessionContext' type AssignmentBoxProps = { - type: 'quiz' | 'file' + type: 'quiz' | 'file' | 'form' view?: 'teacher' | 'student' | 'grading' | 'custom-grading' maxPoints?: number currentPoints?: number @@ -44,6 +44,11 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF

File Submission

} + {type === 'form' && +
+ +

Form

+
}
diff --git a/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx b/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx index a5420330..364fa739 100644 --- a/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx +++ b/apps/web/components/Objects/Activities/Assignment/AssignmentStudentActivity.tsx @@ -4,6 +4,7 @@ import { useOrg } from '@components/Contexts/OrgContext'; import { getTaskRefFileDir } from '@services/media/media'; import TaskFileObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject'; import TaskQuizObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject' +import TaskFormObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFormObject' import { Backpack, Calendar, Download, EllipsisVertical, Info } from 'lucide-react'; import Link from 'next/link'; import React, { useEffect } from 'react' @@ -99,6 +100,7 @@ function AssignmentStudentActivity() {
{task.assignment_type === 'QUIZ' && } {task.assignment_type === 'FILE_SUBMISSION' && } + {task.assignment_type === 'FORM' && }
)