mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #502 from learnhouse/feat/form-assignments
Add Form assignments support
This commit is contained in:
commit
a670336c68
12 changed files with 706 additions and 22 deletions
|
|
@ -4,6 +4,7 @@ import importlib
|
||||||
from config.config import get_learnhouse_config
|
from config.config import get_learnhouse_config
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from sqlmodel import SQLModel, Session, create_engine
|
from sqlmodel import SQLModel, Session, create_engine
|
||||||
|
from sqlalchemy import event
|
||||||
|
|
||||||
def import_all_models():
|
def import_all_models():
|
||||||
base_dir = 'src/db'
|
base_dir = 'src/db'
|
||||||
|
|
@ -48,11 +49,24 @@ else:
|
||||||
learnhouse_config.database_config.sql_connection_string, # type: ignore
|
learnhouse_config.database_config.sql_connection_string, # type: ignore
|
||||||
echo=False,
|
echo=False,
|
||||||
pool_pre_ping=True, # type: ignore
|
pool_pre_ping=True, # type: ignore
|
||||||
pool_size=5,
|
pool_size=20, # Increased from 5 to handle more concurrent requests
|
||||||
max_overflow=0,
|
max_overflow=10, # Allow 10 additional connections beyond pool_size
|
||||||
pool_recycle=300, # Recycle connections after 5 minutes
|
pool_recycle=300, # Recycle connections after 5 minutes
|
||||||
pool_timeout=30
|
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)
|
# Only create tables if not in test mode (tests will handle this themselves)
|
||||||
if not is_testing:
|
if not is_testing:
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
from src.db.courses.assignments import (
|
||||||
AssignmentCreate,
|
AssignmentCreate,
|
||||||
AssignmentRead,
|
AssignmentRead,
|
||||||
|
|
@ -295,9 +295,16 @@ async def api_read_user_assignment_task_submissions_me(
|
||||||
"""
|
"""
|
||||||
Read task submissions for an assignment from a user
|
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
|
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")
|
@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions")
|
||||||
|
|
|
||||||
|
|
@ -764,9 +764,15 @@ async def handle_assignment_task_submission(
|
||||||
# SECURITY: Instructors/admins need update permission to grade
|
# SECURITY: Instructors/admins need update permission to grade
|
||||||
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
await courses_rbac_check_for_assignments(request, course.course_uuid, current_user, "update", db_session)
|
||||||
|
|
||||||
# Try to find existing submission if UUID is provided
|
# Try to find existing submission by user_id and assignment_task_id first (for save progress functionality)
|
||||||
assignment_task_submission = None
|
statement = select(AssignmentTaskSubmission).where(
|
||||||
if assignment_task_submission_uuid:
|
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(
|
statement = select(AssignmentTaskSubmission).where(
|
||||||
AssignmentTaskSubmission.assignment_task_submission_uuid == assignment_task_submission_uuid
|
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,
|
current_user: PublicUser | AnonymousUser,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
):
|
):
|
||||||
return await read_user_assignment_task_submissions(
|
# Check if assignment task exists
|
||||||
request,
|
statement = select(AssignmentTask).where(
|
||||||
assignment_task_uuid,
|
AssignmentTask.assignment_task_uuid == assignment_task_uuid
|
||||||
current_user.id,
|
|
||||||
current_user,
|
|
||||||
db_session,
|
|
||||||
)
|
)
|
||||||
|
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(
|
async def read_assignment_task_submissions(
|
||||||
|
|
|
||||||
|
|
@ -65,13 +65,13 @@ function NewTaskModal({ closeModal, assignment_uuid }: any) {
|
||||||
<p className='text-sm text-gray-500 w-40'>Students can submit files for this task</p>
|
<p className='text-sm text-gray-500 w-40'>Students can submit files for this task</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => toast.error('Forms are not yet supported')}
|
onClick={() => createTask('FORM')}
|
||||||
className='flex flex-col space-y-2 justify-center text-center pt-10 opacity-25'>
|
className='flex flex-col space-y-2 justify-center text-center pt-10'>
|
||||||
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
|
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
|
||||||
<AArrowUp size={30} />
|
<AArrowUp size={30} />
|
||||||
</div>
|
</div>
|
||||||
<p className='text-xl text-gray-700 font-semibold'>Form</p>
|
<p className='text-xl text-gray-700 font-semibold'>Form</p>
|
||||||
<p className='text-sm text-gray-500 w-40'>Forms for students to fill out</p>
|
<p className='text-sm text-gray-500 w-40'>Fill-in-the-blank forms for students</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useLHSession } from '@components/Contexts/LHSessionContext';
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import TaskQuizObject from './TaskTypes/TaskQuizObject';
|
import TaskQuizObject from './TaskTypes/TaskQuizObject';
|
||||||
import TaskFileObject from './TaskTypes/TaskFileObject';
|
import TaskFileObject from './TaskTypes/TaskFileObject';
|
||||||
|
import TaskFormObject from './TaskTypes/TaskFormObject';
|
||||||
|
|
||||||
function AssignmentTaskContentEdit() {
|
function AssignmentTaskContentEdit() {
|
||||||
const session = useLHSession() as any;
|
const session = useLHSession() as any;
|
||||||
|
|
@ -18,6 +19,7 @@ function AssignmentTaskContentEdit() {
|
||||||
<div>
|
<div>
|
||||||
{assignment_task?.assignmentTask.assignment_type === 'QUIZ' && <TaskQuizObject view='teacher' />}
|
{assignment_task?.assignmentTask.assignment_type === 'QUIZ' && <TaskQuizObject view='teacher' />}
|
||||||
{assignment_task?.assignmentTask.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject view='teacher' />}
|
{assignment_task?.assignmentTask.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject view='teacher' />}
|
||||||
|
{assignment_task?.assignmentTask.assignment_type === 'FORM' && <TaskFormObject view='teacher' assignmentTaskUUID={assignment_task?.assignmentTask.assignment_task_uuid} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,9 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the quiz to the server
|
// Save the file submission to the server
|
||||||
const values = {
|
const values = {
|
||||||
assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid,
|
assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid || null,
|
||||||
task_submission: userSubmissions,
|
task_submission: userSubmissions,
|
||||||
grade: 0,
|
grade: 0,
|
||||||
task_submission_grade_feedback: '',
|
task_submission_grade_feedback: '',
|
||||||
|
|
@ -121,6 +121,13 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
|
||||||
});
|
});
|
||||||
toast.success('Task saved successfully');
|
toast.success('Task saved successfully');
|
||||||
setShowSavingDisclaimer(false);
|
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 {
|
} else {
|
||||||
toast.error('Error saving task, please retry later.');
|
toast.error('Error saving task, please retry later.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<FormSchema[]>(
|
||||||
|
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<FormSubmitSchema>({
|
||||||
|
questions: [],
|
||||||
|
submissions: [],
|
||||||
|
});
|
||||||
|
const [initialUserSubmissions, setInitialUserSubmissions] = useState<FormSubmitSchema>({
|
||||||
|
questions: [],
|
||||||
|
submissions: [],
|
||||||
|
});
|
||||||
|
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
|
||||||
|
const [assignmentTaskOutsideProvider, setAssignmentTaskOutsideProvider] = useState<any>(null);
|
||||||
|
const [userSubmissionObject, setUserSubmissionObject] = useState<any>(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 (
|
||||||
|
<AssignmentBoxUI
|
||||||
|
submitFC={submitFC}
|
||||||
|
saveFC={saveFC}
|
||||||
|
gradeFC={gradeFC}
|
||||||
|
view={view}
|
||||||
|
currentPoints={userSubmissionObject?.grade}
|
||||||
|
maxPoints={assignmentTaskOutsideProvider?.max_grade_value}
|
||||||
|
showSavingDisclaimer={showSavingDisclaimer}
|
||||||
|
type="form"
|
||||||
|
>
|
||||||
|
{view === 'grading' && (
|
||||||
|
<div className="mb-6 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg border border-blue-200">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800 mb-2">Submission Summary</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-4 text-sm">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg font-bold text-blue-600">
|
||||||
|
{questions.flatMap(q => q.blanks).length}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600">Total Blanks</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg font-bold text-green-600">
|
||||||
|
{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}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600">Correct</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg font-bold text-red-600">
|
||||||
|
{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}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600">Incorrect</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col space-y-6">
|
||||||
|
{questions && questions.map((question, qIndex) => (
|
||||||
|
<div key={qIndex} className="flex flex-col space-y-1.5">
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
{view === 'teacher' ? (
|
||||||
|
<input
|
||||||
|
value={question.questionText}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p 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}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{view === 'teacher' && (
|
||||||
|
<div
|
||||||
|
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
|
||||||
|
onClick={() => removeQuestion(qIndex)}
|
||||||
|
>
|
||||||
|
<Minus size={12} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Blanks section */}
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
{question.blanks.map((blank, bIndex) => (
|
||||||
|
<div key={bIndex} className="flex">
|
||||||
|
<div className={"blank-item outline-3 outline-white pr-2 shadow-sm w-full flex items-center space-x-2 min-h-[40px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm duration-150 ease-linear nice-shadow " + (view == 'student' ? 'active:scale-105' : '')}>
|
||||||
|
<div className="font-bold text-base flex items-center justify-center h-full w-[40px] rounded-l-md text-slate-800 bg-slate-100/80">
|
||||||
|
<Type size={14} />
|
||||||
|
</div>
|
||||||
|
{view === 'teacher' ? (
|
||||||
|
<div className="flex flex-col space-y-1 w-full py-2">
|
||||||
|
<input
|
||||||
|
value={blank.placeholder}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={blank.correctAnswer}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={blank.hint || ''}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : view === 'grading' ? (
|
||||||
|
<div className="flex flex-col space-y-1 w-full py-2">
|
||||||
|
<div className="flex items-center space-x-2 w-full mx-2">
|
||||||
|
<input
|
||||||
|
value={userSubmissions.submissions.find(
|
||||||
|
(submission) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mx-2 text-xs text-gray-600">
|
||||||
|
<span className="font-semibold">Expected:</span> {blank.correctAnswer}
|
||||||
|
</div>
|
||||||
|
{blank.hint && (
|
||||||
|
<div className="mx-2 text-xs text-blue-600 italic">💡 {blank.hint}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col space-y-1 w-full py-2">
|
||||||
|
<input
|
||||||
|
value={userSubmissions.submissions?.find(
|
||||||
|
(submission) => 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 && (
|
||||||
|
<div className="mx-2 text-xs text-blue-600 italic">💡 {blank.hint}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{view === 'teacher' && (
|
||||||
|
<div
|
||||||
|
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
|
||||||
|
onClick={() => removeBlank(qIndex, bIndex)}
|
||||||
|
>
|
||||||
|
<Minus size={12} className="mx-auto" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{view === 'grading' && (
|
||||||
|
<div className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${
|
||||||
|
userSubmissions.submissions.find(
|
||||||
|
(submission) => 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() ? (
|
||||||
|
<>
|
||||||
|
<Check size={12} className="mx-auto" />
|
||||||
|
<p className="mx-auto font-bold text-xs">Correct</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<X size={12} className="mx-auto" />
|
||||||
|
<p className="mx-auto font-bold text-xs">Incorrect</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{view === 'student' && (
|
||||||
|
<div className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${
|
||||||
|
userSubmissions.submissions.find(
|
||||||
|
(submission) => 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() ? (
|
||||||
|
<Check size={12} className="mx-auto" />
|
||||||
|
) : (
|
||||||
|
<X size={12} className="mx-auto" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{view === 'teacher' && bIndex === question.blanks.length - 1 && question.blanks.length <= 4 && (
|
||||||
|
<div className="flex justify-center mx-auto px-2">
|
||||||
|
<div
|
||||||
|
className="outline-3 outline-white px-2 shadow-sm w-full flex items-center h-[40px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white duration-150 cursor-pointer ease-linear nice-shadow"
|
||||||
|
onClick={() => addBlank(qIndex)}
|
||||||
|
>
|
||||||
|
<Plus size={14} className="inline-block" />
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{view === 'teacher' && questions.length <= 5 && (
|
||||||
|
<div className="flex justify-center mx-auto px-2">
|
||||||
|
<div
|
||||||
|
className="flex w-full my-2 py-2 px-4 bg-white text-slate text-xs rounded-md nice-shadow hover:shadow-xs cursor-pointer space-x-3 items-center transition duration-150 ease-linear"
|
||||||
|
onClick={addQuestion}
|
||||||
|
>
|
||||||
|
<PlusCircle size={14} className="inline-block" />
|
||||||
|
<span>Add Question</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AssignmentBoxUI>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex flex-row space-x-2 text-sm items-center'>
|
||||||
|
<Info size={12} />
|
||||||
|
<p>No questions found</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TaskFormObject;
|
||||||
|
|
@ -221,6 +221,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
|
|
||||||
// Save the quiz to the server
|
// Save the quiz to the server
|
||||||
const values = {
|
const values = {
|
||||||
|
assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid || null,
|
||||||
task_submission: updatedUserSubmissions,
|
task_submission: updatedUserSubmissions,
|
||||||
grade: 0,
|
grade: 0,
|
||||||
task_submission_grade_feedback: '',
|
task_submission_grade_feedback: '',
|
||||||
|
|
@ -234,7 +235,13 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
});
|
});
|
||||||
toast.success('Task saved successfully');
|
toast.success('Task saved successfully');
|
||||||
setShowSavingDisclaimer(false);
|
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 {
|
} else {
|
||||||
toast.error('Error saving task, please retry later.');
|
toast.error('Error saving task, please retry later.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'
|
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'
|
||||||
import Modal from '@components/Objects/StyledElements/Modal/Modal';
|
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 React, { useEffect } from 'react'
|
||||||
import NewTaskModal from './Modals/NewTaskModal';
|
import NewTaskModal from './Modals/NewTaskModal';
|
||||||
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||||
|
|
@ -51,6 +51,7 @@ function AssignmentTasks({ assignment_uuid }: any) {
|
||||||
<div className='text-gray-500'>
|
<div className='text-gray-500'>
|
||||||
{task.assignment_type === 'QUIZ' && <ListTodo size={15} />}
|
{task.assignment_type === 'QUIZ' && <ListTodo size={15} />}
|
||||||
{task.assignment_type === 'FILE_SUBMISSION' && <FileUp size={15} />}
|
{task.assignment_type === 'FILE_SUBMISSION' && <FileUp size={15} />}
|
||||||
|
{task.assignment_type === 'FORM' && <Type size={15} />}
|
||||||
</div>
|
</div>
|
||||||
<div className='font-semibold text-sm'>{task.title}</div>
|
<div className='font-semibold text-sm'>{task.title}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import Link from 'next/link';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import TaskQuizObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskQuizObject';
|
import TaskQuizObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskQuizObject';
|
||||||
import TaskFileObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskFileObject';
|
import TaskFileObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskFileObject';
|
||||||
|
import TaskFormObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskFormObject';
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
import { getTaskRefFileDir } from '@services/media/media';
|
import { getTaskRefFileDir } from '@services/media/media';
|
||||||
import { deleteUserSubmission, markActivityAsDoneForUser, putFinalGrade } from '@services/courses/assignments';
|
import { deleteUserSubmission, markActivityAsDoneForUser, putFinalGrade } from '@services/courses/assignments';
|
||||||
|
|
@ -86,6 +87,7 @@ function EvaluateAssignment({ user_id }: any) {
|
||||||
<div className='min-h-full'>
|
<div className='min-h-full'>
|
||||||
{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='custom-grading' user_id={user_id} 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} />}
|
||||||
|
{task.assignment_type === 'FORM' && <TaskFormObject key={task.assignment_task_uuid} view='grading' user_id={user_id} assignmentTaskUUID={task.assignment_task_uuid} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext'
|
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 React, { useEffect } from 'react'
|
||||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||||
|
|
||||||
type AssignmentBoxProps = {
|
type AssignmentBoxProps = {
|
||||||
type: 'quiz' | 'file'
|
type: 'quiz' | 'file' | 'form'
|
||||||
view?: 'teacher' | 'student' | 'grading' | 'custom-grading'
|
view?: 'teacher' | 'student' | 'grading' | 'custom-grading'
|
||||||
maxPoints?: number
|
maxPoints?: number
|
||||||
currentPoints?: number
|
currentPoints?: number
|
||||||
|
|
@ -44,6 +44,11 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF
|
||||||
<FileUp size={17} />
|
<FileUp size={17} />
|
||||||
<p>File Submission</p>
|
<p>File Submission</p>
|
||||||
</div>}
|
</div>}
|
||||||
|
{type === 'form' &&
|
||||||
|
<div className='flex space-x-1.5 items-center'>
|
||||||
|
<Type size={17} />
|
||||||
|
<p>Form</p>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex items-center space-x-1'>
|
<div className='flex items-center space-x-1'>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
import { getTaskRefFileDir } from '@services/media/media';
|
import { getTaskRefFileDir } from '@services/media/media';
|
||||||
import TaskFileObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject';
|
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 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 { Backpack, Calendar, Download, EllipsisVertical, Info } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
|
@ -99,6 +100,7 @@ function AssignmentStudentActivity() {
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
{task.assignment_type === 'QUIZ' && <TaskQuizObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />}
|
{task.assignment_type === 'QUIZ' && <TaskQuizObject key={task.assignment_task_uuid} view='student' 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='student' assignmentTaskUUID={task.assignment_task_uuid} />}
|
||||||
|
{task.assignment_type === 'FORM' && <TaskFormObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue