diff --git a/apps/api/src/db/courses/assignments.py b/apps/api/src/db/courses/assignments.py index e5d38b9e..dc7722a5 100644 --- a/apps/api/src/db/courses/assignments.py +++ b/apps/api/src/db/courses/assignments.py @@ -1,5 +1,5 @@ from typing import Optional, Dict -from sqlalchemy import JSON, Column, ForeignKey +from sqlalchemy import JSON, Column, ForeignKey, null from sqlmodel import Field, SQLModel from enum import Enum @@ -125,7 +125,7 @@ class AssignmentTaskUpdate(SQLModel): description: Optional[str] hint: Optional[str] 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] @@ -194,17 +194,12 @@ class AssignmentTaskSubmissionRead(AssignmentTaskSubmissionBase): class AssignmentTaskSubmissionUpdate(SQLModel): """Model for updating an assignment task submission.""" - + assignment_task_id: Optional[int] 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] task_submission_grade_feedback: Optional[str] 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): diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py index 6f4c4aa8..3365ca94 100644 --- a/apps/api/src/routers/courses/assignments.py +++ b/apps/api/src/routers/courses/assignments.py @@ -4,6 +4,7 @@ from src.db.courses.assignments import ( AssignmentRead, AssignmentTaskCreate, AssignmentTaskSubmissionCreate, + AssignmentTaskSubmissionUpdate, AssignmentTaskUpdate, AssignmentUpdate, AssignmentUserSubmissionCreate, @@ -15,12 +16,12 @@ from src.services.courses.activities.assignments import ( create_assignment, create_assignment_submission, create_assignment_task, - create_assignment_task_submission, delete_assignment, delete_assignment_from_activity_uuid, delete_assignment_submission, delete_assignment_task, delete_assignment_task_submission, + handle_assignment_task_submission, put_assignment_task_reference_file, put_assignment_task_submission_file, read_assignment, @@ -31,6 +32,7 @@ from src.services.courses.activities.assignments import ( read_assignment_tasks, read_user_assignment_submissions, read_user_assignment_task_submissions, + read_user_assignment_task_submissions_me, update_assignment, update_assignment_submission, update_assignment_task, @@ -240,10 +242,10 @@ async def api_delete_assignment_tasks( ## ASSIGNMENTS Tasks Submissions ## -@router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions") -async def api_create_assignment_task_submissions( +@router.put("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions") +async def api_handle_assignment_task_submissions( request: Request, - assignment_task_submission_object: AssignmentTaskSubmissionCreate, + assignment_task_submission_object: AssignmentTaskSubmissionUpdate, assignment_task_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), @@ -251,7 +253,7 @@ async def api_create_assignment_task_submissions( """ Create new task submissions for an assignment """ - return await create_assignment_task_submission( + return await handle_assignment_task_submission( request, assignment_task_uuid, 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 ) +@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") async def api_read_assignment_task_submissions( diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index f8f196d6..ee96e6aa 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -19,6 +19,7 @@ from src.db.courses.assignments import ( AssignmentTaskSubmission, AssignmentTaskSubmissionCreate, AssignmentTaskSubmissionRead, + AssignmentTaskSubmissionUpdate, AssignmentTaskUpdate, AssignmentUpdate, AssignmentUserSubmission, @@ -464,7 +465,7 @@ async def put_assignment_task_reference_file( status_code=404, detail="Course not found", ) - + # Get org uuid org_statement = select(Organization).where(Organization.id == course.org_id) org = db_session.exec(org_statement).first() @@ -478,7 +479,13 @@ async def put_assignment_task_reference_file( f"{assignment_task_uuid}{uuid4()}.{reference_file.filename.split('.')[-1]}" ) 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 # Update reference file @@ -494,6 +501,7 @@ async def put_assignment_task_reference_file( # return assignment task read return AssignmentTaskRead.model_validate(assignment_task) + async def put_assignment_task_submission_file( request: Request, db_session: Session, @@ -536,7 +544,7 @@ async def put_assignment_task_submission_file( status_code=404, detail="Course not found", ) - + # Get org uuid org_statement = select(Organization).where(Organization.id == course.org_id) org = db_session.exec(org_statement).first() @@ -546,15 +554,18 @@ async def put_assignment_task_submission_file( # Upload reference file if sub_file and sub_file.filename and activity and org: - name_in_disk = ( - f"{assignment_task_uuid}_sub_{current_user.email}_{uuid4()}.{sub_file.filename.split('.')[-1]}" - ) + name_in_disk = f"{assignment_task_uuid}_sub_{current_user.email}_{uuid4()}.{sub_file.filename.split('.')[-1]}" 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( @@ -665,13 +676,14 @@ async def delete_assignment_task( ## > Assignments Tasks Submissions CRUD -async def create_assignment_task_submission( +async def handle_assignment_task_submission( request: Request, assignment_task_uuid: str, - assignment_task_submission_object: AssignmentTaskSubmissionCreate, + assignment_task_submission_object: AssignmentTaskSubmissionUpdate, current_user: PublicUser | AnonymousUser, db_session: Session, ): + # TODO: Improve terrible implementation of this function # Check if assignment task exists statement = select(AssignmentTask).where( AssignmentTask.assignment_task_uuid == assignment_task_uuid @@ -694,51 +706,82 @@ async def create_assignment_task_submission( detail="Assignment not found", ) - # Check if course exists - statement = select(Course).where(Course.id == assignment.course_id) - course = db_session.exec(statement).first() + # Check if user already submitted the assignment + 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 course: - raise HTTPException( - status_code=404, - detail="Course not found", + # Update Task submission if it exists + if assignment_task_submission: + # Update only the fields that were passed in + 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 - await rbac_check(request, course.course_uuid, current_user, "create", db_session) + # Insert Assignment Task Submission in DB + db_session.add(assignment_task_submission) + db_session.commit() - # Create Assignment Task Submission - assignment_task_submission = AssignmentTaskSubmission( - **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) + # return assignment task submission read + return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission) async def read_user_assignment_task_submissions( request: Request, - assignment_task_submission_uuid: str, + assignment_task_uuid: str, user_id: int, current_user: PublicUser | AnonymousUser, 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 statement = select(AssignmentTaskSubmission).where( - AssignmentTaskSubmission.assignment_task_submission_uuid - == assignment_task_submission_uuid, + AssignmentTaskSubmission.assignment_task_id == assignment_task.id, AssignmentTaskSubmission.user_id == user_id, ) 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", ) - # 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 statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) assignment = db_session.exec(statement).first() @@ -788,6 +819,21 @@ async def read_user_assignment_task_submissions( 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( request: Request, assignment_task_submission_uuid: str, 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 4aafcee3..c5d34976 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 @@ -2,13 +2,13 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentConte import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import { useLHSession } from '@components/Contexts/LHSessionContext'; 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 React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import toast from 'react-hot-toast'; type FileSchema = { - fileID: string; + fileUUID: string; }; type TaskFileObjectProps = { @@ -26,6 +26,18 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any; const assignment = useAssignments() as any; + /* TEACHER VIEW CODE */ + /* TEACHER VIEW CODE */ + + /* STUDENT VIEW CODE */ + const [showSavingDisclaimer, setShowSavingDisclaimer] = useState(false); + const [userSubmissions, setUserSubmissions] = useState({ + fileUUID: '', + }); + const [initialUserSubmissions, setInitialUserSubmissions] = useState({ + fileUUID: '', + }); + const handleFileChange = async (event: any) => { const file = event.target.files[0] @@ -37,19 +49,56 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj assignment.assignment_object.assignment_uuid, access_token ) - assignmentTaskStateHook({ type: 'reload' }) + // wait for 1 second to show loading animation await new Promise((r) => setTimeout(r, 1500)) if (res.success === false) { + setError(res.data.detail) setIsLoading(false) } else { + assignmentTaskStateHook({ type: 'reload' }) + setUserSubmissions({ + fileUUID: res.data.file_uuid, + }) setIsLoading(false) 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() { if (assignmentTaskUUID) { 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(() => { - getAssignmentTaskUI() + if (userSubmissions.fileUUID !== initialUserSubmissions.fileUUID) { + setShowSavingDisclaimer(true); + } else { + setShowSavingDisclaimer(false); + } + }, [userSubmissions]); + + /* STUDENT VIEW CODE */ + + useEffect(() => { + if (view === 'student') { + getAssignmentTaskUI() + getAssignmentTaskSubmissionFromUserUI() + } } , [assignmentTaskUUID]) return ( - + {view === 'teacher' && (
@@ -91,20 +154,35 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
- +
- -
- {localUploadFile.name} + +
+ {localUploadFile.name} +
+
+ )} + {userSubmissions.fileUUID && !isLoading && !localUploadFile && ( +
+
+ +
+ +
+ + +
+ {`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`} +
)}
- -

Allowed formats : pdf, docx, mp4, jpg, jpeg, pptx

-
+ +

Allowed formats : pdf, docx, mp4, jpg, jpeg, pptx

+
{isLoading ? (