diff --git a/apps/api/src/db/courses/assignments.py b/apps/api/src/db/courses/assignments.py index aefd1ff9..e03544fd 100644 --- a/apps/api/src/db/courses/assignments.py +++ b/apps/api/src/db/courses/assignments.py @@ -162,7 +162,7 @@ class AssignmentTask(AssignmentTaskBase, table=True): class AssignmentTaskSubmissionBase(SQLModel): """Represents the common fields for an assignment task submission.""" - + assignment_task_submission_uuid: str task_submission: Dict = Field(default={}, sa_column=Column(JSON)) grade: int = 0 # Value is always between 0-100 task_submission_grade_feedback: str diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index ae563e08..21c0619a 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -37,6 +37,7 @@ from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship, authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, + authorization_verify_based_on_roles, ) from src.services.courses.activities.uploads.sub_file import upload_submission_file from src.services.courses.activities.uploads.tasks_ref_files import ( @@ -565,10 +566,17 @@ async def put_assignment_task_submission_file( org_statement = select(Organization).where(Organization.id == course.org_id) org = db_session.exec(org_statement).first() - # RBAC check - await rbac_check(request, course.course_uuid, current_user, "update", db_session) + # RBAC check - only need read permission to submit files + await rbac_check(request, course.course_uuid, current_user, "read", db_session) - # Upload reference file + # Check if user is enrolled in the course + if not await authorization_verify_based_on_roles(request, current_user.id, "read", course.course_uuid, db_session): + raise HTTPException( + status_code=403, + detail="You must be enrolled in this course to submit files" + ) + + # Upload submission 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]}" await upload_submission_file( @@ -699,7 +707,7 @@ async def handle_assignment_task_submission( current_user: PublicUser | AnonymousUser, db_session: Session, ): - # TODO: Improve terrible implementation of this function + assignment_task_submission_uuid = assignment_task_submission_object.assignment_task_submission_uuid # Check if assignment task exists statement = select(AssignmentTask).where( AssignmentTask.assignment_task_uuid == assignment_task_uuid @@ -722,15 +730,59 @@ async def handle_assignment_task_submission( detail="Assignment not found", ) - # 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() + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() - # Update Task submission if it exists + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # Check if user has instructor/admin permissions + is_instructor = await authorization_verify_based_on_roles(request, current_user.id, "update", course.course_uuid, db_session) + + # For regular users, ensure they can only submit their own work + if not is_instructor: + # Check if user is enrolled in the course + if not await authorization_verify_based_on_roles(request, current_user.id, "read", course.course_uuid, db_session): + raise HTTPException( + status_code=403, + detail="You must be enrolled in this course to submit assignments" + ) + + # Regular users cannot update grades - only check if actual values are being set + if (assignment_task_submission_object.grade is not None and assignment_task_submission_object.grade != 0) or \ + (assignment_task_submission_object.task_submission_grade_feedback is not None and assignment_task_submission_object.task_submission_grade_feedback != ""): + raise HTTPException( + status_code=403, + detail="You do not have permission to update grades" + ) + + # Only need read permission for submissions + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + else: + # Instructors/admins need update permission to grade + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + + # Try to find existing submission if UUID is provided + assignment_task_submission = None + if assignment_task_submission_uuid: + statement = select(AssignmentTaskSubmission).where( + AssignmentTaskSubmission.assignment_task_submission_uuid == assignment_task_submission_uuid + ) + assignment_task_submission = db_session.exec(statement).first() + + # If submission exists, update it if assignment_task_submission: + # For regular users, ensure they can only update their own submissions + if not is_instructor and assignment_task_submission.user_id != current_user.id: + raise HTTPException( + status_code=403, + detail="You can only update your own submissions" + ) + # Update only the fields that were passed in for var, value in vars(assignment_task_submission_object).items(): if value is not None: @@ -742,9 +794,6 @@ async def handle_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()) @@ -753,10 +802,10 @@ async def handle_assignment_task_submission( model_data = assignment_task_submission_object.model_dump() assignment_task_submission = AssignmentTaskSubmission( - assignment_task_submission_uuid=f"assignmenttasksubmission_{uuid4()}", + assignment_task_submission_uuid=assignment_task_submission_uuid or f"assignmenttasksubmission_{uuid4()}", task_submission=model_data["task_submission"], - grade=model_data["grade"], - task_submission_grade_feedback=model_data["task_submission_grade_feedback"], + grade=0, # Always start with 0 for new submissions + task_submission_grade_feedback="", # Start with empty feedback assignment_task_id=int(assignment_task.id), # type: ignore assignment_type=assignment_task.assignment_type, activity_id=assignment.activity_id, @@ -770,9 +819,10 @@ async def handle_assignment_task_submission( # 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( @@ -1096,7 +1146,7 @@ async def create_assignment_submission( ) # RBAC check - await rbac_check(request, course.course_uuid, current_user, "update", db_session) + await rbac_check(request, course.course_uuid, current_user, "read", db_session) # Create Assignment User Submission assignment_user_submission = AssignmentUserSubmission( 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 1d60652b..c3434c92 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 @@ -12,6 +12,7 @@ import toast from 'react-hot-toast'; type FileSchema = { fileUUID: string; + assignment_task_submission_uuid?: string; }; type TaskFileObjectProps = { @@ -64,13 +65,13 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta // 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, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid }) setIsLoading(false) setError('') @@ -86,8 +87,14 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta 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); + 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 + }); } } } @@ -101,6 +108,7 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta // Save the quiz to the server const values = { + assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid, task_submission: userSubmissions, grade: 0, task_submission_grade_feedback: '', @@ -156,9 +164,15 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta if (assignmentTaskUUID && user_id) { const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token); if (res.success) { - setUserSubmissions(res.data.task_submission); + setUserSubmissions({ + ...res.data.task_submission, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid + }); setUserSubmissionObject(res.data); - setInitialUserSubmissions(res.data.task_submission); + setInitialUserSubmissions({ + ...res.data.task_submission, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid + }); } } } @@ -173,6 +187,7 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta // Save the grade to the server const values = { + assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid, task_submission: userSubmissions, grade: grade, task_submission_grade_feedback: 'Graded by teacher : @' + session.data.user.username, diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx index ea901cb0..7a8d3294 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject.tsx @@ -27,6 +27,7 @@ type QuizSubmitSchema = { optionUUID: string; answer: boolean }[]; + assignment_task_submission_uuid?: string; }; type TaskQuizObjectProps = { @@ -175,8 +176,14 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro 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); + 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 + }); } } @@ -242,9 +249,15 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro if (assignmentTaskUUID && user_id) { const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token); if (res.success) { - setUserSubmissions(res.data.task_submission); + setUserSubmissions({ + ...res.data.task_submission, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid + }); setUserSubmissionObject(res.data); - setInitialUserSubmissions(res.data.task_submission); + setInitialUserSubmissions({ + ...res.data.task_submission, + assignment_task_submission_uuid: res.data.assignment_task_submission_uuid + }); } } @@ -271,6 +284,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro // 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', diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/AssignmentSubmissionsSubPage.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/AssignmentSubmissionsSubPage.tsx index 4cc32f02..5ba2d44a 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/AssignmentSubmissionsSubPage.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/AssignmentSubmissionsSubPage.tsx @@ -41,21 +41,27 @@ function AssignmentSubmissionsSubPage({ assignment_uuid }: { assignment_uuid: st

Late

- {renderSubmissions('LATE')} +
+ {renderSubmissions('LATE')} +

Submitted

- {renderSubmissions('SUBMITTED')} +
+ {renderSubmissions('SUBMITTED')} +

Graded

- {renderSubmissions('GRADED')} +
+ {renderSubmissions('GRADED')} +
diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/Modals/EvaluateAssignment.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/Modals/EvaluateAssignment.tsx index 0926440d..364cfe3a 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/Modals/EvaluateAssignment.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/subpages/Modals/EvaluateAssignment.tsx @@ -15,7 +15,6 @@ function EvaluateAssignment({ user_id }: any) { const assignments = useAssignments() as any; const session = useLHSession() as any; const org = useOrg() as any; - const router = useRouter(); async function gradeAssignment() { const res = await putFinalGrade(user_id, assignments?.assignment_object.assignment_uuid, session.data?.tokens?.access_token); @@ -92,17 +91,17 @@ function EvaluateAssignment({ user_id }: any) { ) })}
-
- -