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 && (
+
+ )}
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+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' &&
+ }
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' && }
)