feat: enable Tasks submissions

This commit is contained in:
swve 2024-07-19 19:56:49 +02:00
parent 6d7e521bba
commit bfb977ac5d
9 changed files with 330 additions and 110 deletions

View file

@ -1,5 +1,5 @@
from typing import Optional, Dict from typing import Optional, Dict
from sqlalchemy import JSON, Column, ForeignKey from sqlalchemy import JSON, Column, ForeignKey, null
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from enum import Enum from enum import Enum
@ -125,7 +125,7 @@ class AssignmentTaskUpdate(SQLModel):
description: Optional[str] description: Optional[str]
hint: Optional[str] hint: Optional[str]
assignment_type: Optional[AssignmentTaskTypeEnum] assignment_type: Optional[AssignmentTaskTypeEnum]
contents: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) contents: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
max_grade_value: Optional[int] max_grade_value: Optional[int]
@ -194,17 +194,12 @@ class AssignmentTaskSubmissionRead(AssignmentTaskSubmissionBase):
class AssignmentTaskSubmissionUpdate(SQLModel): class AssignmentTaskSubmissionUpdate(SQLModel):
"""Model for updating an assignment task submission.""" """Model for updating an assignment task submission."""
assignment_task_id: Optional[int]
assignment_task_submission_uuid: Optional[str] assignment_task_submission_uuid: Optional[str]
task_submission: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) task_submission: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
grade: Optional[int] grade: Optional[int]
task_submission_grade_feedback: Optional[str] task_submission_grade_feedback: Optional[str]
assignment_type: Optional[AssignmentTaskTypeEnum] assignment_type: Optional[AssignmentTaskTypeEnum]
user_id: Optional[int]
activity_id: Optional[int]
course_id: Optional[int]
chapter_id: Optional[int]
assignment_task_id: Optional[int]
class AssignmentTaskSubmission(AssignmentTaskSubmissionBase, table=True): class AssignmentTaskSubmission(AssignmentTaskSubmissionBase, table=True):

View file

@ -4,6 +4,7 @@ from src.db.courses.assignments import (
AssignmentRead, AssignmentRead,
AssignmentTaskCreate, AssignmentTaskCreate,
AssignmentTaskSubmissionCreate, AssignmentTaskSubmissionCreate,
AssignmentTaskSubmissionUpdate,
AssignmentTaskUpdate, AssignmentTaskUpdate,
AssignmentUpdate, AssignmentUpdate,
AssignmentUserSubmissionCreate, AssignmentUserSubmissionCreate,
@ -15,12 +16,12 @@ from src.services.courses.activities.assignments import (
create_assignment, create_assignment,
create_assignment_submission, create_assignment_submission,
create_assignment_task, create_assignment_task,
create_assignment_task_submission,
delete_assignment, delete_assignment,
delete_assignment_from_activity_uuid, delete_assignment_from_activity_uuid,
delete_assignment_submission, delete_assignment_submission,
delete_assignment_task, delete_assignment_task,
delete_assignment_task_submission, delete_assignment_task_submission,
handle_assignment_task_submission,
put_assignment_task_reference_file, put_assignment_task_reference_file,
put_assignment_task_submission_file, put_assignment_task_submission_file,
read_assignment, read_assignment,
@ -31,6 +32,7 @@ from src.services.courses.activities.assignments import (
read_assignment_tasks, read_assignment_tasks,
read_user_assignment_submissions, read_user_assignment_submissions,
read_user_assignment_task_submissions, read_user_assignment_task_submissions,
read_user_assignment_task_submissions_me,
update_assignment, update_assignment,
update_assignment_submission, update_assignment_submission,
update_assignment_task, update_assignment_task,
@ -240,10 +242,10 @@ async def api_delete_assignment_tasks(
## ASSIGNMENTS Tasks Submissions ## ## ASSIGNMENTS Tasks Submissions ##
@router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions") @router.put("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions")
async def api_create_assignment_task_submissions( async def api_handle_assignment_task_submissions(
request: Request, request: Request,
assignment_task_submission_object: AssignmentTaskSubmissionCreate, assignment_task_submission_object: AssignmentTaskSubmissionUpdate,
assignment_task_uuid: str, assignment_task_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
@ -251,7 +253,7 @@ async def api_create_assignment_task_submissions(
""" """
Create new task submissions for an assignment Create new task submissions for an assignment
""" """
return await create_assignment_task_submission( return await handle_assignment_task_submission(
request, request,
assignment_task_uuid, assignment_task_uuid,
assignment_task_submission_object, assignment_task_submission_object,
@ -275,6 +277,20 @@ async def api_read_user_assignment_task_submissions(
request, assignment_task_uuid, user_id, current_user, db_session request, assignment_task_uuid, user_id, current_user, db_session
) )
@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions/user/me")
async def api_read_user_assignment_task_submissions_me(
request: Request,
assignment_task_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Read task submissions for an assignment from a user
"""
return await read_user_assignment_task_submissions_me(
request, assignment_task_uuid, current_user, db_session
)
@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions") @router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions")
async def api_read_assignment_task_submissions( async def api_read_assignment_task_submissions(

View file

@ -19,6 +19,7 @@ from src.db.courses.assignments import (
AssignmentTaskSubmission, AssignmentTaskSubmission,
AssignmentTaskSubmissionCreate, AssignmentTaskSubmissionCreate,
AssignmentTaskSubmissionRead, AssignmentTaskSubmissionRead,
AssignmentTaskSubmissionUpdate,
AssignmentTaskUpdate, AssignmentTaskUpdate,
AssignmentUpdate, AssignmentUpdate,
AssignmentUserSubmission, AssignmentUserSubmission,
@ -478,7 +479,13 @@ async def put_assignment_task_reference_file(
f"{assignment_task_uuid}{uuid4()}.{reference_file.filename.split('.')[-1]}" f"{assignment_task_uuid}{uuid4()}.{reference_file.filename.split('.')[-1]}"
) )
await upload_reference_file( await upload_reference_file(
reference_file, name_in_disk, activity.activity_uuid, org.org_uuid, course.course_uuid, assignment.assignment_uuid, assignment_task_uuid reference_file,
name_in_disk,
activity.activity_uuid,
org.org_uuid,
course.course_uuid,
assignment.assignment_uuid,
assignment_task_uuid,
) )
course.thumbnail_image = name_in_disk course.thumbnail_image = name_in_disk
# Update reference file # Update reference file
@ -494,6 +501,7 @@ async def put_assignment_task_reference_file(
# return assignment task read # return assignment task read
return AssignmentTaskRead.model_validate(assignment_task) return AssignmentTaskRead.model_validate(assignment_task)
async def put_assignment_task_submission_file( async def put_assignment_task_submission_file(
request: Request, request: Request,
db_session: Session, db_session: Session,
@ -546,15 +554,18 @@ async def put_assignment_task_submission_file(
# Upload reference file # Upload reference file
if sub_file and sub_file.filename and activity and org: if sub_file and sub_file.filename and activity and org:
name_in_disk = ( name_in_disk = f"{assignment_task_uuid}_sub_{current_user.email}_{uuid4()}.{sub_file.filename.split('.')[-1]}"
f"{assignment_task_uuid}_sub_{current_user.email}_{uuid4()}.{sub_file.filename.split('.')[-1]}"
)
await upload_submission_file( await upload_submission_file(
sub_file, name_in_disk, activity.activity_uuid, org.org_uuid, course.course_uuid, assignment.assignment_uuid, assignment_task_uuid sub_file,
name_in_disk,
activity.activity_uuid,
org.org_uuid,
course.course_uuid,
assignment.assignment_uuid,
assignment_task_uuid,
) )
return {"message": "Assignment Task Submission File uploaded"} return {"file_uuid": name_in_disk}
async def update_assignment_task( async def update_assignment_task(
@ -665,13 +676,14 @@ async def delete_assignment_task(
## > Assignments Tasks Submissions CRUD ## > Assignments Tasks Submissions CRUD
async def create_assignment_task_submission( async def handle_assignment_task_submission(
request: Request, request: Request,
assignment_task_uuid: str, assignment_task_uuid: str,
assignment_task_submission_object: AssignmentTaskSubmissionCreate, assignment_task_submission_object: AssignmentTaskSubmissionUpdate,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
): ):
# TODO: Improve terrible implementation of this function
# Check if assignment task exists # Check if assignment task exists
statement = select(AssignmentTask).where( statement = select(AssignmentTask).where(
AssignmentTask.assignment_task_uuid == assignment_task_uuid AssignmentTask.assignment_task_uuid == assignment_task_uuid
@ -694,51 +706,82 @@ async def create_assignment_task_submission(
detail="Assignment not found", detail="Assignment not found",
) )
# Check if course exists # Check if user already submitted the assignment
statement = select(Course).where(Course.id == assignment.course_id) statement = select(AssignmentTaskSubmission).where(
course = db_session.exec(statement).first() AssignmentTaskSubmission.assignment_task_id == assignment_task.id,
AssignmentTaskSubmission.user_id == current_user.id,
)
assignment_task_submission = db_session.exec(statement).first()
if not course: # Update Task submission if it exists
raise HTTPException( if assignment_task_submission:
status_code=404, # Update only the fields that were passed in
detail="Course not found", for var, value in vars(assignment_task_submission_object).items():
if value is not None:
setattr(assignment_task_submission, var, value)
assignment_task_submission.update_date = str(datetime.now())
# Insert Assignment Task Submission in DB
db_session.add(assignment_task_submission)
db_session.commit()
db_session.refresh(assignment_task_submission)
# return assignment task submission read
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
else:
# Create new Task submission
current_time = str(datetime.now())
# Assuming model_dump() returns a dictionary
model_data = assignment_task_submission_object.model_dump()
assignment_task_submission = AssignmentTaskSubmission(
assignment_task_submission_uuid=f"assignmenttasksubmission_{uuid4()}",
task_submission=model_data["task_submission"],
grade=model_data["grade"],
task_submission_grade_feedback=model_data["task_submission_grade_feedback"],
assignment_task_id=int(assignment_task.id), # type: ignore
assignment_type=assignment_task.assignment_type,
activity_id=assignment.activity_id,
course_id=assignment.course_id,
chapter_id=assignment.chapter_id,
user_id=current_user.id,
creation_date=current_time,
update_date=current_time,
) )
# RBAC check # Insert Assignment Task Submission in DB
await rbac_check(request, course.course_uuid, current_user, "create", db_session) db_session.add(assignment_task_submission)
db_session.commit()
# Create Assignment Task Submission # return assignment task submission read
assignment_task_submission = AssignmentTaskSubmission( return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
**assignment_task_submission_object.model_dump()
)
assignment_task_submission.assignment_task_submission_uuid = str(
f"assignmenttasksubmission_{uuid4()}"
)
assignment_task_submission.creation_date = str(datetime.now())
assignment_task_submission.update_date = str(datetime.now())
assignment_task_submission.org_id = course.org_id
# Insert Assignment Task Submission in DB
db_session.add(assignment_task_submission)
db_session.commit()
db_session.refresh(assignment_task_submission)
# return assignment task submission read
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
async def read_user_assignment_task_submissions( async def read_user_assignment_task_submissions(
request: Request, request: Request,
assignment_task_submission_uuid: str, assignment_task_uuid: str,
user_id: int, user_id: int,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: 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 # Check if assignment task submission exists
statement = select(AssignmentTaskSubmission).where( statement = select(AssignmentTaskSubmission).where(
AssignmentTaskSubmission.assignment_task_submission_uuid AssignmentTaskSubmission.assignment_task_id == assignment_task.id,
== assignment_task_submission_uuid,
AssignmentTaskSubmission.user_id == user_id, AssignmentTaskSubmission.user_id == user_id,
) )
assignment_task_submission = db_session.exec(statement).first() assignment_task_submission = db_session.exec(statement).first()
@ -749,18 +792,6 @@ async def read_user_assignment_task_submissions(
detail="Assignment Task Submission not found", detail="Assignment Task Submission not found",
) )
# Check if assignment task exists
statement = select(AssignmentTask).where(
AssignmentTask.id == assignment_task_submission.assignment_task_id
)
assignment_task = db_session.exec(statement).first()
if not assignment_task:
raise HTTPException(
status_code=404,
detail="Assignment Task not found",
)
# Check if assignment exists # Check if assignment exists
statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id)
assignment = db_session.exec(statement).first() assignment = db_session.exec(statement).first()
@ -788,6 +819,21 @@ async def read_user_assignment_task_submissions(
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission) return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
async def read_user_assignment_task_submissions_me(
request: Request,
assignment_task_uuid: str,
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,
)
async def read_assignment_task_submissions( async def read_assignment_task_submissions(
request: Request, request: Request,
assignment_task_submission_uuid: str, assignment_task_submission_uuid: str,

View file

@ -2,13 +2,13 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentConte
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext'; import { useLHSession } from '@components/Contexts/LHSessionContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI' import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
import { getAssignmentTask, updateSubFile } from '@services/courses/assignments'; import { getAssignmentTask, getAssignmentTaskSubmissionsMe, handleAssignmentTaskSubmission, updateSubFile } from '@services/courses/assignments';
import { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react' import { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react'
import React, { useEffect } from 'react' import React, { useEffect, useState } from 'react'
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
type FileSchema = { type FileSchema = {
fileID: string; fileUUID: string;
}; };
type TaskFileObjectProps = { type TaskFileObjectProps = {
@ -26,6 +26,18 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any; const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
const assignment = useAssignments() as any; const assignment = useAssignments() as any;
/* TEACHER VIEW CODE */
/* TEACHER VIEW CODE */
/* STUDENT VIEW CODE */
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
const [userSubmissions, setUserSubmissions] = useState<FileSchema>({
fileUUID: '',
});
const [initialUserSubmissions, setInitialUserSubmissions] = useState<FileSchema>({
fileUUID: '',
});
const handleFileChange = async (event: any) => { const handleFileChange = async (event: any) => {
const file = event.target.files[0] const file = event.target.files[0]
@ -37,19 +49,56 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
assignment.assignment_object.assignment_uuid, assignment.assignment_object.assignment_uuid,
access_token access_token
) )
assignmentTaskStateHook({ type: 'reload' })
// wait for 1 second to show loading animation // wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500)) await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) { if (res.success === false) {
setError(res.data.detail) setError(res.data.detail)
setIsLoading(false) setIsLoading(false)
} else { } else {
assignmentTaskStateHook({ type: 'reload' })
setUserSubmissions({
fileUUID: res.data.file_uuid,
})
setIsLoading(false) setIsLoading(false)
setError('') setError('')
} }
} }
async function getAssignmentTaskSubmissionFromUserUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
setUserSubmissions(res.data.task_submission);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
const submitFC = async () => {
// Save the quiz to the server
const values = {
task_submission: userSubmissions,
grade: 0,
task_submission_grade_feedback: '',
};
if (assignmentTaskUUID) {
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
assignmentTaskStateHook({
type: 'reload',
});
toast.success('Task saved successfully');
setShowSavingDisclaimer(false);
} else {
toast.error('Error saving task, please retry later.');
}
}
};
async function getAssignmentTaskUI() { async function getAssignmentTaskUI() {
if (assignmentTaskUUID) { if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token); const res = await getAssignmentTask(assignmentTaskUUID, access_token);
@ -60,13 +109,27 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
} }
} }
// Detect changes between initial and current submissions
useEffect(() => { useEffect(() => {
getAssignmentTaskUI() if (userSubmissions.fileUUID !== initialUserSubmissions.fileUUID) {
setShowSavingDisclaimer(true);
} else {
setShowSavingDisclaimer(false);
}
}, [userSubmissions]);
/* STUDENT VIEW CODE */
useEffect(() => {
if (view === 'student') {
getAssignmentTaskUI()
getAssignmentTaskSubmissionFromUserUI()
}
} }
, [assignmentTaskUUID]) , [assignmentTaskUUID])
return ( return (
<AssignmentBoxUI view={view} type="file"> <AssignmentBoxUI submitFC={submitFC} showSavingDisclaimer={showSavingDisclaimer} view={view} type="file">
{view === 'teacher' && ( {view === 'teacher' && (
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'> <div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Info size={20} /> <Info size={20} />
@ -94,17 +157,32 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
<div className='flex space-x-2 mt-2'> <div className='flex space-x-2 mt-2'>
<File size={20} className='' /> <File size={20} className='' />
<div className='font-semibold text-sm uppercase'> <div className='font-semibold text-sm uppercase'>
{localUploadFile.name} {localUploadFile.name}
</div>
</div> </div>
</div>
)}
{userSubmissions.fileUUID && !isLoading && !localUploadFile && (
<div className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
</div>
<div className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
</div>
</div> </div>
</div> </div>
)} )}
<div className='flex pt-4 font-semibold space-x-1.5 text-xs items-center text-gray-500 '> <div className='flex pt-4 font-semibold space-x-1.5 text-xs items-center text-gray-500 '>
<Info size={16} /> <Info size={16} />
<p>Allowed formats : pdf, docx, mp4, jpg, jpeg, pptx</p> <p>Allowed formats : pdf, docx, mp4, jpg, jpeg, pptx</p>
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<input <input
@ -122,13 +200,13 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<input <input
type="file" type="file"
id="fileInput" id={"fileInput_" + assignmentTaskUUID}
style={{ display: 'none' }} style={{ display: 'none' }}
onChange={handleFileChange} onChange={handleFileChange}
/> />
<button <button
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex" className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
onClick={() => document.getElementById('fileInput')?.click()} onClick={() => document.getElementById("fileInput_" + assignmentTaskUUID)?.click()}
> >
<UploadCloud size={16} className="mr-2" /> <UploadCloud size={16} className="mr-2" />
<span>Submit File</span> <span>Submit File</span>

View file

@ -2,7 +2,7 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentConte
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext'; import { useLHSession } from '@components/Contexts/LHSessionContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'; import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI';
import { getAssignmentTask, updateAssignmentTask } from '@services/courses/assignments'; import { getAssignmentTask, getAssignmentTaskSubmissionsMe, handleAssignmentTaskSubmission, updateAssignmentTask } from '@services/courses/assignments';
import { Check, Minus, Plus, PlusCircle, X } from 'lucide-react'; import { Check, Minus, Plus, PlusCircle, X } from 'lucide-react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
@ -113,6 +113,11 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
questions: [], questions: [],
submissions: [], submissions: [],
}); });
const [initialUserSubmissions, setInitialUserSubmissions] = useState<QuizSubmitSchema>({
questions: [],
submissions: [],
});
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
async function chooseOption(qIndex: number, oIndex: number) { async function chooseOption(qIndex: number, oIndex: number) {
const updatedSubmissions = [...userSubmissions.submissions]; const updatedSubmissions = [...userSubmissions.submissions];
@ -136,11 +141,8 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
...userSubmissions, ...userSubmissions,
submissions: updatedSubmissions, submissions: updatedSubmissions,
}); });
console.log(userSubmissions);
} }
async function getAssignmentTaskUI() { async function getAssignmentTaskUI() {
if (assignmentTaskUUID) { if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token); const res = await getAssignmentTask(assignmentTaskUUID, access_token);
@ -151,6 +153,48 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
} }
} }
async function getAssignmentTaskSubmissionFromUserUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
setUserSubmissions(res.data.task_submission);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
// Detect changes between initial and current submissions
useEffect(() => {
const hasChanges = JSON.stringify(initialUserSubmissions.submissions) !== JSON.stringify(userSubmissions.submissions);
setShowSavingDisclaimer(hasChanges);
}, [userSubmissions, initialUserSubmissions.submissions]);
const submitFC = async () => {
// Save the quiz to the server
const values = {
task_submission: userSubmissions,
grade: 0,
task_submission_grade_feedback: '',
};
if (assignmentTaskUUID) {
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
assignmentTaskStateHook({
type: 'reload',
});
toast.success('Task saved successfully');
setShowSavingDisclaimer(false);
} else {
toast.error('Error saving task, please retry later.');
}
}
};
/* STUDENT VIEW CODE */ /* STUDENT VIEW CODE */
useEffect(() => { useEffect(() => {
@ -164,12 +208,13 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
// Student area // Student area
else if (view == 'student') { else if (view == 'student') {
getAssignmentTaskUI(); getAssignmentTaskUI();
getAssignmentTaskSubmissionFromUserUI();
} }
}, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token]); }, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token]);
return ( return (
<AssignmentBoxUI saveFC={saveFC} view={view} type="quiz"> <AssignmentBoxUI submitFC={submitFC} saveFC={saveFC} view={view} showSavingDisclaimer={showSavingDisclaimer} type="quiz">
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
{questions && questions.map((question, qIndex) => ( {questions && questions.map((question, qIndex) => (
<div key={qIndex} className="flex flex-col space-y-1.5"> <div key={qIndex} className="flex flex-col space-y-1.5">
@ -245,8 +290,8 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
(submission) => (submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
) )
? "bg-green-200/60 text-green-500 hover:bg-green-300" // Selected state colors ? "bg-green-200/60 text-green-500 hover:bg-green-300" // Selected state colors
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300" // Default state colors : "bg-slate-200/60 text-slate-500 hover:bg-slate-300" // Default state colors
} text-sm transition-all ease-linear cursor-pointer`}> } text-sm transition-all ease-linear cursor-pointer`}>
{userSubmissions.submissions.find( {userSubmissions.submissions.find(
(submission) => (submission) =>

View file

@ -93,18 +93,7 @@ function PublishingState() {
{assignment?.assignment_object?.published ? 'Published' : 'Unpublished'} {assignment?.assignment_object?.published ? 'Published' : 'Unpublished'}
</div> </div>
<div><EllipsisVertical className='text-gray-500' size={13} /></div> <div><EllipsisVertical className='text-gray-500' size={13} /></div>
{assignment?.assignment_object?.published && <ToolTip
side='left'
slateBlack
sideOffset={10}
content="Make your Assignment unavailable for students" >
<div
onClick={() => updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)}
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'>
<BookX size={18} />
<p className='text-sm font-bold'>Unpublish</p>
</div>
</ToolTip>}
<ToolTip <ToolTip
side='left' side='left'
slateBlack slateBlack
@ -118,6 +107,18 @@ function PublishingState() {
<p className=' text-sm font-bold'>Preview</p> <p className=' text-sm font-bold'>Preview</p>
</Link> </Link>
</ToolTip> </ToolTip>
{assignment?.assignment_object?.published && <ToolTip
side='left'
slateBlack
sideOffset={10}
content="Make your Assignment unavailable for students" >
<div
onClick={() => updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)}
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'>
<BookX size={18} />
<p className='text-sm font-bold'>Unpublish</p>
</div>
</ToolTip>}
{!assignment?.assignment_object?.published && {!assignment?.assignment_object?.published &&
<ToolTip <ToolTip
side='left' side='left'

View file

@ -1,4 +1,4 @@
import { BookUser, EllipsisVertical, FileUp, Forward, ListTodo, Save } from 'lucide-react' import { BookUser, EllipsisVertical, FileUp, Forward, Info, InfoIcon, ListTodo, Save } from 'lucide-react'
import React from 'react' import React from 'react'
type AssignmentBoxProps = { type AssignmentBoxProps = {
@ -6,11 +6,12 @@ type AssignmentBoxProps = {
view?: 'teacher' | 'student' view?: 'teacher' | 'student'
saveFC?: () => void saveFC?: () => void
submitFC?: () => void submitFC?: () => void
showSavingDisclaimer?: boolean
children: React.ReactNode children: React.ReactNode
} }
function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentBoxProps) { function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, children }: AssignmentBoxProps) {
return ( return (
<div className='flex flex-col px-6 py-4 nice-shadow rounded-md bg-slate-100/30'> <div className='flex flex-col px-6 py-4 nice-shadow rounded-md bg-slate-100/30'>
<div className='flex justify-between space-x-2 pb-2 text-slate-400 items-center'> <div className='flex justify-between space-x-2 pb-2 text-slate-400 items-center'>
@ -40,12 +41,17 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentB
} }
</div> </div>
<div className='flex px-1 py-1 rounded-md items-center'> <div className='flex px-1 py-1 rounded-md items-center'>
{showSavingDisclaimer &&
<div className='flex space-x-2 items-center font-semibold px-3 py-1 outline-dashed outline-red-200 text-red-400 mr-5 rounded-full'>
<InfoIcon size={14} />
<p className='text-xs'>Don't forget to save your progress</p>
</div>
}
{/* Save button */} {/* Save button */}
{view === 'teacher' && {view === 'teacher' &&
<div <div
onClick={() => saveFC && saveFC()} onClick={() => saveFC && saveFC()}
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-slate-500 bg-white/60 hover:bg-white/80 linear transition-all nice-shadow '> className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'>
<Save size={14} /> <Save size={14} />
<p className='text-xs font-semibold'>Save</p> <p className='text-xs font-semibold'>Save</p>
</div> </div>
@ -53,9 +59,9 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentB
{view === 'student' && {view === 'student' &&
<div <div
onClick={() => submitFC && submitFC()} onClick={() => submitFC && submitFC()}
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-slate-500 bg-white/60 hover:bg-white/80 linear transition-all nice-shadow '> className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'>
<Forward size={14} /> <Forward size={14} />
<p className='text-xs font-semibold'>Save</p> <p className='text-xs font-semibold'>Save your progress</p>
</div> </div>
} }

View file

@ -15,7 +15,6 @@ function AssignmentStudentActivity() {
const org = useOrg() as any; const org = useOrg() as any;
useEffect(() => { useEffect(() => {
console.log(assignments)
}, [assignments, org]) }, [assignments, org])
@ -71,7 +70,14 @@ function AssignmentStudentActivity() {
download={true} download={true}
className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-2 cursor-pointer'> className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-2 cursor-pointer'>
<Download size={13} /> <Download size={13} />
<p className='text-xs font-semibold'>Reference file</p> <div className='flex items-center space-x-2'>
{task.reference_file && (
<span className='relative'>
<span className='absolute right-0 top-0 block h-2 w-2 rounded-full ring-2 ring-white bg-green-400'></span>
</span>
)}
<p className='text-xs font-semibold'>Reference Document</p>
</div>
</Link> </Link>
</div> </div>
</div> </div>

View file

@ -91,6 +91,33 @@ export async function getAssignmentTask(
return res return res
} }
export async function getAssignmentTaskSubmissionsMe(
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions/user/me`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function handleAssignmentTaskSubmission(
body: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions`,
RequestBodyWithAuthHeader('PUT', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function updateAssignmentTask( export async function updateAssignmentTask(
body: any, body: any,
assignmentTaskUUID: string, assignmentTaskUUID: string,