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) {
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