diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py index 91de1cbd..808234ff 100644 --- a/apps/api/src/routers/courses/assignments.py +++ b/apps/api/src/routers/courses/assignments.py @@ -20,7 +20,10 @@ from src.services.courses.activities.assignments import ( delete_assignment_submission, delete_assignment_task, delete_assignment_task_submission, + get_grade_assignment_submission, + grade_assignment_submission, handle_assignment_task_submission, + mark_activity_as_done_for_user, put_assignment_task_reference_file, put_assignment_task_submission_file, read_assignment, @@ -263,7 +266,9 @@ async def api_handle_assignment_task_submissions( ) -@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions/user/{user_id}") +@router.get( + "/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions/user/{user_id}" +) async def api_read_user_assignment_task_submissions( request: Request, assignment_task_uuid: str, @@ -410,7 +415,7 @@ async def api_update_user_assignment_submissions( @router.delete("/{assignment_uuid}/submissions/{user_id}") async def api_delete_user_assignment_submissions( request: Request, - assignment_id: str, + assignment_uuid: str, user_id: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), @@ -419,5 +424,53 @@ async def api_delete_user_assignment_submissions( Delete submissions for an assignment from a user """ return await delete_assignment_submission( - request, assignment_id, user_id, current_user, db_session + request, user_id, assignment_uuid, current_user, db_session + ) +@router.get("/{assignment_uuid}/submissions/{user_id}/grade") +async def api_get_submission_grade( + request: Request, + assignment_uuid: str, + user_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Grade submissions for an assignment from a user + """ + + return await get_grade_assignment_submission( + request, user_id, assignment_uuid, current_user, db_session + ) + +@router.post("/{assignment_uuid}/submissions/{user_id}/grade") +async def api_final_grade_submission( + request: Request, + assignment_uuid: str, + user_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Grade submissions for an assignment from a user + """ + + return await grade_assignment_submission( + request, user_id, assignment_uuid, current_user, db_session + ) + + +@router.post("/{assignment_uuid}/submissions/{user_id}/done") +async def api_submission_mark_as_done( + request: Request, + assignment_uuid: str, + user_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Grade submissions for an assignment from a user + """ + + return await mark_activity_as_done_for_user( + request, user_id, assignment_uuid, current_user, db_session ) diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index c66ffb3e..7b6153e2 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -29,7 +29,9 @@ from src.db.courses.assignments import ( ) from src.db.courses.courses import Course from src.db.organizations import Organization -from src.db.users import AnonymousUser, PublicUser +from src.db.trail_runs import TrailRun +from src.db.trail_steps import TrailStep +from src.db.users import AnonymousUser, PublicUser, User from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_if_element_is_public, @@ -39,6 +41,7 @@ from src.services.courses.activities.uploads.sub_file import upload_submission_f from src.services.courses.activities.uploads.tasks_ref_files import ( upload_reference_file, ) +from src.services.trail.trail import check_trail_presence ## > Assignments CRUD @@ -1097,6 +1100,80 @@ async def create_assignment_submission( db_session.add(assignment_user_submission) db_session.commit() + # User + statement = select(User).where(User.id == current_user.id) + user = db_session.exec(statement).first() + + if not user: + raise HTTPException( + status_code=404, + detail="User not found", + ) + + # Activity + statement = select(Activity).where(Activity.id == assignment.activity_id) + activity = db_session.exec(statement).first() + + if not activity: + raise HTTPException( + status_code=404, + detail="Activity not found", + ) + + # Add TrailStep + trail = await check_trail_presence( + org_id=course.org_id, + user_id=user.id, + request=request, + user=user, + db_session=db_session, + ) + + statement = select(TrailRun).where( + TrailRun.trail_id == trail.id, + TrailRun.course_id == course.id, + TrailRun.user_id == user.id, + ) + trailrun = db_session.exec(statement).first() + + if not trailrun: + trailrun = TrailRun( + trail_id=trail.id if trail.id is not None else 0, + course_id=course.id if course.id is not None else 0, + org_id=course.org_id, + user_id=user.id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + db_session.add(trailrun) + db_session.commit() + db_session.refresh(trailrun) + + statement = select(TrailStep).where( + TrailStep.trailrun_id == trailrun.id, + TrailStep.activity_id == activity.id, + TrailStep.user_id == user.id, + ) + trailstep = db_session.exec(statement).first() + + if not trailstep: + trailstep = TrailStep( + trailrun_id=trailrun.id if trailrun.id is not None else 0, + activity_id=activity.id if activity.id is not None else 0, + course_id=course.id if course.id is not None else 0, + trail_id=trail.id if trail.id is not None else 0, + org_id=course.org_id, + complete=True, + teacher_verified=False, + grade="", + user_id=user.id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + db_session.add(trailstep) + db_session.commit() + db_session.refresh(trailstep) + # return assignment user submission read return AssignmentUserSubmissionRead.model_validate(assignment_user_submission) @@ -1262,14 +1339,24 @@ async def update_assignment_submission( async def delete_assignment_submission( request: Request, user_id: str, - assignment_id: str, + assignment_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): + # Check if assignment exists + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + # Check if assignment user submission exists statement = select(AssignmentUserSubmission).where( AssignmentUserSubmission.user_id == user_id, - AssignmentUserSubmission.assignment_id == assignment_id, + AssignmentUserSubmission.assignment_id == assignment.id, ) assignment_user_submission = db_session.exec(statement).first() @@ -1279,18 +1366,6 @@ async def delete_assignment_submission( detail="Assignment User Submission not found", ) - # Check if assignment exists - statement = select(Assignment).where( - Assignment.id == assignment_user_submission.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() @@ -1311,6 +1386,197 @@ async def delete_assignment_submission( return {"message": "Assignment User Submission deleted"} +## > Assignments Submissions Grading +async def grade_assignment_submission( + request: Request, + user_id: str, + assignment_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + + # Check if assignment exists + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if assignment user submission exists + statement = select(AssignmentUserSubmission).where( + AssignmentUserSubmission.user_id == user_id, + AssignmentUserSubmission.assignment_id == assignment.id, + ) + assignment_user_submission = db_session.exec(statement).first() + + if not assignment_user_submission: + raise HTTPException( + status_code=404, + detail="Assignment User Submission not found", + ) + + # Get all the task submissions for the user + task_subs = select(AssignmentTaskSubmission).where( + AssignmentTaskSubmission.user_id == user_id, + AssignmentTaskSubmission.activity_id == assignment.activity_id, + ) + task_submissions = db_session.exec(task_subs).all() + + # Calculate the grade + grade = 0 + for task_submission in task_submissions: + grade += task_submission.grade + + # Update the assignment user submission + assignment_user_submission.grade = grade + + # Insert Assignment User Submission in DB + db_session.add(assignment_user_submission) + db_session.commit() + db_session.refresh(assignment_user_submission) + + # Change the status of the submission + assignment_user_submission.submission_status = AssignmentUserSubmissionStatus.GRADED + + # Insert Assignment User Submission in DB + db_session.add(assignment_user_submission) + db_session.commit() + db_session.refresh(assignment_user_submission) + + # return OK + return { + "message": "Assignment User Submission graded with the grade of " + str(grade) + } + + +async def get_grade_assignment_submission( + request: Request, + user_id: str, + assignment_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + + # Check if assignment exists + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if assignment user submission exists + statement = select(AssignmentUserSubmission).where( + AssignmentUserSubmission.user_id == user_id, + AssignmentUserSubmission.assignment_id == assignment.id, + ) + assignment_user_submission = db_session.exec(statement).first() + + if not assignment_user_submission: + raise HTTPException( + status_code=404, + detail="Assignment User Submission not found", + ) + + # Get the max grade value from the sum of every assignmenttask + statement = select(AssignmentTask).where( + AssignmentTask.assignment_id == assignment.id + ) + assignment_tasks = db_session.exec(statement).all() + max_grade = 0 + + for task in assignment_tasks: + max_grade += task.max_grade_value + + # Now get the grade from the user submission + statement = select(AssignmentUserSubmission).where( + AssignmentUserSubmission.user_id == user_id, + AssignmentUserSubmission.assignment_id == assignment.id, + ) + assignment_user_submission = db_session.exec(statement).first() + + if not assignment_user_submission: + raise HTTPException( + status_code=404, + detail="Assignment User Submission not found", + ) + + # return the grade + return { + "grade": int(assignment_user_submission.grade), + "max_grade": max_grade, + "grading_type": assignment.grading_type, + } + + +async def mark_activity_as_done_for_user( + request: Request, + user_id: str, + assignment_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Get Assignment + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if activity exists + statement = select(Activity).where(Activity.id == assignment.activity_id) + activity = db_session.exec(statement).first() + + if not activity: + raise HTTPException( + status_code=404, + detail="Activity not found", + ) + + # Check if user exists + statement = select(User).where(User.id == user_id) + user = db_session.exec(statement).first() + + if not user: + raise HTTPException( + status_code=404, + detail="User not found", + ) + + # Check if user is enrolled in the course + trailsteps = select(TrailStep).where( + TrailStep.activity_id == activity.id, + TrailStep.user_id == user_id, + ) + trailstep = db_session.exec(trailsteps).first() + + if not trailstep: + raise HTTPException( + status_code=404, + detail="User not enrolled in the course", + ) + + # Mark activity as done + trailstep.complete = True + trailstep.update_date = str(datetime.now()) + + # Insert TrailStep in DB + db_session.add(trailstep) + db_session.commit() + db_session.refresh(trailstep) + + # return OK + return {"message": "Activity marked as done for user"} + + ## 🔒 RBAC Utils ## diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 3101ceca..7daebdea 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -244,7 +244,7 @@ async def add_activity_to_trail( course_id=course.id if course.id is not None else 0, trail_id=trail.id if trail.id is not None else 0, org_id=course.org_id, - complete=False, + complete=True, teacher_verified=False, grade="", user_id=user.id, diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index d441b31c..45ea5944 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { getAPIUrl, getUriWithOrg } from '@services/config/config' import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva' import VideoActivity from '@components/Objects/Activities/Video/Video' -import { BookOpenCheck, Check, MoreVertical, UserRoundPen } from 'lucide-react' +import { BookOpenCheck, Check, CheckCircle, MoreVertical, UserRoundPen } from 'lucide-react' import { markActivityAsComplete } from '@services/courses/activity' import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf' import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' @@ -17,7 +17,7 @@ import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk' import AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext' import { useLHSession } from '@components/Contexts/LHSessionContext' import React, { useEffect } from 'react' -import { getAssignmentFromActivityUUID, submitAssignmentForGrading } from '@services/courses/assignments' +import { getAssignmentFromActivityUUID, getFinalGrade, submitAssignmentForGrading } from '@services/courses/assignments' import AssignmentStudentActivity from '@components/Objects/Activities/Assignment/AssignmentStudentActivity' import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext' import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext' @@ -79,7 +79,7 @@ function ActivityClient(props: ActivityClientProps) { setBgColor('bg-zinc-950'); } } - , [activity,pathname ]) + , [activity, pathname]) return ( <> @@ -127,37 +127,37 @@ function ActivityClient(props: ActivityClientProps) {
- {activity && activity.published == true && ( - - {activity.activity_type != 'TYPE_ASSIGNMENT' && - <> - - - - - } - {activity.activity_type == 'TYPE_ASSIGNMENT' && - <> - - - + {activity.activity_type != 'TYPE_ASSIGNMENT' && + <> + + + - - - } + + } + {activity.activity_type == 'TYPE_ASSIGNMENT' && + <> + + + + + + } - - )} + + )}
{activity && activity.published == false && ( @@ -240,7 +240,7 @@ export function MarkStatus(props: { ) if (run) { return run.steps.find( - (step: any) => step.activity_id == props.activity.id + (step: any) => (step.activity_id == props.activity.id) && (step.complete == true) ) } } @@ -252,7 +252,7 @@ export function MarkStatus(props: { {' '} - Already completed + Complete ) : (
{ if (props.assignment) { @@ -296,38 +297,91 @@ function AssignmentTools(props: { } } + const getGradingBasedOnMethod = async () => { + const res = await getFinalGrade( + session.data?.user?.id, + props.assignment?.assignment_uuid, + session.data?.tokens?.access_token + ); + + if (res.success) { + const { grade, max_grade, grading_type } = res.data; + let displayGrade; + + switch (grading_type) { + case 'ALPHABET': + displayGrade = convertNumericToAlphabet(grade, max_grade); + break; + case 'NUMERIC': + displayGrade = `${grade}/${max_grade}`; + break; + case 'PERCENTAGE': + const percentage = (grade / max_grade) * 100; + displayGrade = `${percentage.toFixed(2)}%`; + break; + default: + displayGrade = 'Unknown grading type'; + } + + // Use displayGrade here, e.g., update state or display it + setFinalGrade(displayGrade); + } else { + } + }; + + // Helper function to convert numeric grade to alphabet grade + function convertNumericToAlphabet(grade : any, maxGrade : any) { + const percentage = (grade / maxGrade) * 100; + if (percentage >= 90) return 'A'; + if (percentage >= 80) return 'B'; + if (percentage >= 70) return 'C'; + if (percentage >= 60) return 'D'; + return 'F'; + } + useEffect(() => { + getGradingBasedOnMethod(); } , [submission, props.assignment]) - return <> - {submission && submission.length == 0 && ( + if (!submission || submission.length === 0) { + return ( - - - {' '} - Submit for grading -
} - functionToExecute={() => submitForGradingUI()} +
+ + Submit for grading +
+ } + functionToExecute={submitForGradingUI} status="info" /> - )} - {submission && submission.length > 0 && ( -
- - - {' '} - Grading in progress -
) - } - + ) + } + + if (submission[0].submission_status === 'SUBMITTED') { + return ( +
+ + Grading in progress +
+ ) + } + + if (submission[0].submission_status === 'GRADED') { + return ( +
+ + Graded {finalGrade} +
+ ) + } + + // Default return in case none of the conditions are met + return null } export default ActivityClient 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 5a5c8d6d..31360191 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 @@ -195,7 +195,7 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
-

Please download the file and grade it manually, then input the grade below

+

Please download the file and grade it manually, then input the grade above

{userSubmissions.fileUUID && !isLoading && assignmentTaskUUID && ( {assignments && assignments?.assignment_tasks?.sort((a: any, b: any) => a.id - b.id).map((task: any, index: number) => { @@ -53,18 +84,29 @@ function EvaluateAssignment({ user_id }: any) {
-
+
{task.assignment_type === 'QUIZ' && } - {task.assignment_type === 'FILE_SUBMISSION' && } + {task.assignment_type === 'FILE_SUBMISSION' && }
) })} -
- +
+ + + +
) diff --git a/apps/web/services/courses/assignments.ts b/apps/web/services/courses/assignments.ts index 3e558801..e7ea35ce 100644 --- a/apps/web/services/courses/assignments.ts +++ b/apps/web/services/courses/assignments.ts @@ -165,13 +165,12 @@ export async function updateReferenceFile( assignmentUUID: string, access_token: string ) { + // Send file thumbnail as form data + const formData = new FormData() - // Send file thumbnail as form data - const formData = new FormData() - - if (file) { - formData.append('reference_file', file) - } + if (file) { + formData.append('reference_file', file) + } const result: any = await fetch( `${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/ref_file`, RequestBodyFormWithAuthHeader('POST', formData, null, access_token) @@ -180,20 +179,18 @@ export async function updateReferenceFile( return res } - export async function updateSubFile( file: any, assignmentTaskUUID: string, assignmentUUID: string, access_token: string ) { + // Send file thumbnail as form data + const formData = new FormData() - // Send file thumbnail as form data - const formData = new FormData() - - if (file) { - formData.append('sub_file', file) - } + if (file) { + formData.append('sub_file', file) + } const result: any = await fetch( `${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/sub_file`, RequestBodyFormWithAuthHeader('POST', formData, null, access_token) @@ -214,4 +211,70 @@ export async function submitAssignmentForGrading( ) const res = await getResponseMetadata(result) return res -} \ No newline at end of file +} + +export async function deleteUserSubmission( + user_id: string, + assignmentUUID: string, + access_token: string +) { + const result: any = await fetch( + `${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}`, + RequestBodyWithAuthHeader('DELETE', null, null, access_token) + ) + const res = await getResponseMetadata(result) + return res +} + +export async function putUserSubmission( + body: any, + user_id: string, + assignmentUUID: string, + access_token: string +) { + const result: any = await fetch( + `${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}`, + RequestBodyWithAuthHeader('PUT', body, null, access_token) + ) + const res = await getResponseMetadata(result) + return res +} + +export async function putFinalGrade( + user_id: string, + assignmentUUID: string, + access_token: string +) { + const result: any = await fetch( + `${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}/grade`, + RequestBodyWithAuthHeader('POST', null, null, access_token) + ) + const res = await getResponseMetadata(result) + return res +} + +export async function getFinalGrade( + user_id: string, + assignmentUUID: string, + access_token: string +) { + const result: any = await fetch( + `${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}/grade`, + RequestBodyWithAuthHeader('GET', null, null, access_token) + ) + const res = await getResponseMetadata(result) + return res +} + +export async function markActivityAsDoneForUser( + user_id: string, + assignmentUUID: string, + access_token: string +) { + const result: any = await fetch( + `${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}/done`, + RequestBodyWithAuthHeader('POST', null, null, access_token) + ) + const res = await getResponseMetadata(result) + return res +}