diff --git a/apps/api/src/db/courses/assignments.py b/apps/api/src/db/courses/assignments.py index dc7722a5..a2884114 100644 --- a/apps/api/src/db/courses/assignments.py +++ b/apps/api/src/db/courses/assignments.py @@ -4,7 +4,6 @@ from sqlmodel import Field, SQLModel from enum import Enum - ## Assignment ## class GradingTypeEnum(str, Enum): ALPHABET = "ALPHABET" @@ -87,7 +86,7 @@ class Assignment(AssignmentBase, table=True): class AssignmentTaskTypeEnum(str, Enum): FILE_SUBMISSION = "FILE_SUBMISSION" QUIZ = "QUIZ" - FORM = "FORM" # soon to be implemented + FORM = "FORM" # soon to be implemented OTHER = "OTHER" @@ -102,8 +101,6 @@ class AssignmentTaskBase(SQLModel): contents: Dict = Field(default={}, sa_column=Column(JSON)) max_grade_value: int = 0 # Value is always between 0-100 - - class AssignmentTaskCreate(AssignmentTaskBase): """Model for creating a new assignment task.""" @@ -194,6 +191,7 @@ 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=None, sa_column=Column(JSON)) @@ -233,10 +231,12 @@ class AssignmentTaskSubmission(AssignmentTaskSubmissionBase, table=True): creation_date: str update_date: str + ## AssignmentTaskSubmission ## ## AssignmentUserSubmission ## + class AssignmentUserSubmissionStatus(str, Enum): PENDING = "PENDING" SUBMITTED = "SUBMITTED" @@ -248,11 +248,10 @@ class AssignmentUserSubmissionStatus(str, Enum): class AssignmentUserSubmissionBase(SQLModel): """Represents the submission status of an assignment for a user.""" - submission_status: AssignmentUserSubmissionStatus = ( AssignmentUserSubmissionStatus.PENDING ) - grade: str + grade: int user_id: int = Field( sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) ) @@ -262,11 +261,14 @@ class AssignmentUserSubmissionBase(SQLModel): ) ) -class AssignmentUserSubmissionCreate(AssignmentUserSubmissionBase): + +class AssignmentUserSubmissionCreate(SQLModel): """Model for creating a new assignment user submission.""" + assignment_id: int pass # Inherits all fields from AssignmentUserSubmissionBase + class AssignmentUserSubmissionRead(AssignmentUserSubmissionBase): """Model for reading an assignment user submission.""" @@ -274,6 +276,7 @@ class AssignmentUserSubmissionRead(AssignmentUserSubmissionBase): creation_date: str update_date: str + class AssignmentUserSubmissionUpdate(SQLModel): """Model for updating an assignment user submission.""" @@ -282,17 +285,19 @@ class AssignmentUserSubmissionUpdate(SQLModel): user_id: Optional[int] assignment_id: Optional[int] + class AssignmentUserSubmission(AssignmentUserSubmissionBase, table=True): """Represents the submission status of an assignment for a user.""" id: Optional[int] = Field(default=None, primary_key=True) creation_date: str update_date: str + assignmentusersubmission_uuid: str submission_status: AssignmentUserSubmissionStatus = ( AssignmentUserSubmissionStatus.PENDING ) - grade: str + grade: int user_id: int = Field( sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) ) @@ -301,4 +306,3 @@ class AssignmentUserSubmission(AssignmentUserSubmissionBase, table=True): "assignment_id", ForeignKey("assignment.id", ondelete="CASCADE") ) ) - diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py index 3365ca94..8211b776 100644 --- a/apps/api/src/routers/courses/assignments.py +++ b/apps/api/src/routers/courses/assignments.py @@ -31,6 +31,7 @@ from src.services.courses.activities.assignments import ( read_assignment_task_submissions, read_assignment_tasks, read_user_assignment_submissions, + read_user_assignment_submissions_me, read_user_assignment_task_submissions, read_user_assignment_task_submissions_me, update_assignment, @@ -208,6 +209,7 @@ async def api_put_assignment_task_ref_file( request, db_session, assignment_task_uuid, current_user, reference_file ) + @router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/sub_file") async def api_put_assignment_task_sub_file( request: Request, @@ -277,6 +279,7 @@ 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, @@ -331,7 +334,6 @@ async def api_delete_assignment_task_submissions( async def api_create_assignment_submissions( request: Request, assignment_uuid: str, - assignment_submission: AssignmentUserSubmissionCreate, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ): @@ -339,7 +341,7 @@ async def api_create_assignment_submissions( Create new submissions for an assignment """ return await create_assignment_submission( - request, assignment_uuid, assignment_submission, current_user, db_session + request, assignment_uuid, current_user, db_session ) @@ -358,6 +360,21 @@ async def api_read_assignment_submissions( ) +@router.get("/{assignment_uuid}/submissions/me") +async def api_read_user_assignment_submission_me( + request: Request, + assignment_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Read submissions for an assignment from the current user + """ + return await read_user_assignment_submissions_me( + request, assignment_uuid, current_user, db_session + ) + + @router.get("/{assignment_uuid}/submissions/{user_id}") async def api_read_user_assignment_submissions( request: Request, diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index c29e84f3..447ec12b 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -7,6 +7,7 @@ from typing import Literal from uuid import uuid4 from fastapi import HTTPException, Request, UploadFile from sqlmodel import Session, select +from sympy import Sum from src.db.courses.activities import Activity from src.db.courses.assignments import ( @@ -25,6 +26,7 @@ from src.db.courses.assignments import ( AssignmentUserSubmission, AssignmentUserSubmissionCreate, AssignmentUserSubmissionRead, + AssignmentUserSubmissionStatus, ) from src.db.courses.courses import Course from src.db.organizations import Organization @@ -1028,7 +1030,6 @@ async def delete_assignment_task_submission( async def create_assignment_submission( request: Request, assignment_uuid: str, - assignment_user_submission_object: AssignmentUserSubmissionCreate, current_user: PublicUser | AnonymousUser, db_session: Session, ): @@ -1045,7 +1046,7 @@ async def create_assignment_submission( # Check if the submission has already been made statement = select(AssignmentUserSubmission).where( AssignmentUserSubmission.assignment_id == assignment.id, - AssignmentUserSubmission.user_id == assignment_user_submission_object.user_id, + AssignmentUserSubmission.user_id == current_user.id, ) assignment_user_submission = db_session.exec(statement).first() @@ -1066,21 +1067,33 @@ async def create_assignment_submission( detail="Course not found", ) + # Check if User already submitted the assignment + statement = select(AssignmentUserSubmission).where( + AssignmentUserSubmission.assignment_id == assignment.id, + AssignmentUserSubmission.user_id == current_user.id, + ) + assignment_user_submission = db_session.exec(statement).first() + + if assignment_user_submission: + raise HTTPException( + status_code=400, + detail="Assignment User Submission already exists", + ) + # RBAC check await rbac_check(request, course.course_uuid, current_user, "create", db_session) # Create Assignment User Submission assignment_user_submission = AssignmentUserSubmission( - **assignment_user_submission_object.model_dump() + user_id=current_user.id, + assignment_id=assignment.id, # type: ignore + grade=0, + assignmentusersubmission_uuid=str(f"assignmentusersubmission_{uuid4()}"), + submission_status=AssignmentUserSubmissionStatus.PENDING, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) - assignment_user_submission.assignment_user_submission_uuid = str( - f"assignmentusersubmission_{uuid4()}" - ) - assignment_user_submission.creation_date = str(datetime.now()) - assignment_user_submission.update_date = str(datetime.now()) - assignment_user_submission.org_id = course.org_id - # Insert Assignment User Submission in DB db_session.add(assignment_user_submission) db_session.commit() @@ -1173,6 +1186,21 @@ async def read_user_assignment_submissions( ] +async def read_user_assignment_submissions_me( + request: Request, + assignment_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + return await read_user_assignment_submissions( + request, + assignment_uuid, + current_user.id, + current_user, + db_session, + ) + + async def update_assignment_submission( request: Request, user_id: str, 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 d579d760..fe12ea9d 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 @@ -1,9 +1,9 @@ 'use client' import Link from 'next/link' -import { getUriWithOrg } from '@services/config/config' +import { getAPIUrl, getUriWithOrg } from '@services/config/config' import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva' import VideoActivity from '@components/Objects/Activities/Video/Video' -import { Check, MoreVertical } from 'lucide-react' +import { BookOpenCheck, Check, 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,10 +17,14 @@ 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 } from '@services/courses/assignments' +import { getAssignmentFromActivityUUID, 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' +import AssignmentSubmissionProvider, { AssignmentSubmissionContext, useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext' +import toast from 'react-hot-toast' +import { mutate } from 'swr' +import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal' interface ActivityClientProps { activityid: string @@ -135,6 +139,21 @@ function ActivityClient(props: ActivityClientProps) { /> } + {activity.activity_type == 'TYPE_ASSIGNMENT' && + <> + + + + + + + } @@ -172,7 +191,9 @@ function ActivityClient(props: ActivityClientProps) { {assignment ? ( - + + + ) : ( @@ -225,7 +246,7 @@ export function MarkStatus(props: { return ( <> {isActivityCompleted() ? ( -
+
{' '} @@ -233,7 +254,7 @@ export function MarkStatus(props: {
) : (
{' '} @@ -247,4 +268,64 @@ export function MarkStatus(props: { ) } +function AssignmentTools(props: { + activity: any + activityid: string + course: any + orgslug: string + assignment: any +}) { + const submission = useAssignmentSubmission() as any + const session = useLHSession() as any; + + const submitForGradingUI = async () => { + if (props.assignment) { + const res = await submitAssignmentForGrading( + props.assignment?.assignment_uuid, + session.data?.tokens?.access_token + ) + if (res.success) { + toast.success('Assignment submitted for grading') + mutate(`${getAPIUrl()}assignments/${props.assignment?.assignment_uuid}/submissions/me`,) + } + else { + toast.error('Failed to submit assignment for grading') + } + } + } + + useEffect(() => { + } + , [submission, props.assignment]) + + return <> + {submission && submission.length == 0 && ( + + + + {' '} + Submit for grading +
} + functionToExecute={() => submitForGradingUI()} + status="info" + /> + )} + {submission && submission.length > 0 && ( +
+ + + {' '} + Grading in progress +
) + } + +} + export default ActivityClient diff --git a/apps/web/components/Contexts/Assignments/AssignmentSubmissionContext.tsx b/apps/web/components/Contexts/Assignments/AssignmentSubmissionContext.tsx new file mode 100644 index 00000000..0fc36739 --- /dev/null +++ b/apps/web/components/Contexts/Assignments/AssignmentSubmissionContext.tsx @@ -0,0 +1,28 @@ +'use client' +import React from 'react' +import { useLHSession } from '../LHSessionContext' +import { getAPIUrl } from '@services/config/config' +import { swrFetcher } from '@services/utils/ts/requests' +import useSWR from 'swr' + +export const AssignmentSubmissionContext = React.createContext({}) + +function AssignmentSubmissionProvider({ children, assignment_uuid }: { children: React.ReactNode, assignment_uuid: string }) { + const session = useLHSession() as any + const accessToken = session?.data?.tokens?.access_token + + const { data: assignmentSubmission, error: assignmentError } = useSWR( + `${getAPIUrl()}assignments/${assignment_uuid}/submissions/me`, + (url) => swrFetcher(url, accessToken) + ) + + return ( + {children} + ) +} + +export function useAssignmentSubmission() { + return React.useContext(AssignmentSubmissionContext) +} + +export default AssignmentSubmissionProvider \ No newline at end of file diff --git a/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx b/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx index 85f6d92c..4767a802 100644 --- a/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx +++ b/apps/web/components/Objects/Activities/Assignment/AssignmentBoxUI.tsx @@ -1,5 +1,6 @@ +import { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext' import { BookUser, EllipsisVertical, FileUp, Forward, Info, InfoIcon, ListTodo, Save } from 'lucide-react' -import React from 'react' +import React, { use, useEffect } from 'react' type AssignmentBoxProps = { type: 'quiz' | 'file' @@ -12,6 +13,10 @@ type AssignmentBoxProps = { } function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, children }: AssignmentBoxProps) { + const submission = useAssignmentSubmission() as any + useEffect(() => { + } + , [submission]) return (
@@ -47,6 +52,7 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, c

Don't forget to save your progress

} + {/* Save button */} {view === 'teacher' &&
Save

} - {view === 'student' && + {view === 'student' && submission.length <= 0 &&
submitFC && submitFC()} 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'> diff --git a/apps/web/services/courses/assignments.ts b/apps/web/services/courses/assignments.ts index b79b124f..bddf86fa 100644 --- a/apps/web/services/courses/assignments.ts +++ b/apps/web/services/courses/assignments.ts @@ -187,3 +187,17 @@ export async function updateSubFile( const res = await getResponseMetadata(result) return res } + +// submissions + +export async function submitAssignmentForGrading( + assignmentUUID: string, + access_token: string +) { + const result: any = await fetch( + `${getAPIUrl()}assignments/${assignmentUUID}/submissions`, + RequestBodyWithAuthHeader('POST', null, null, access_token) + ) + const res = await getResponseMetadata(result) + return res +} \ No newline at end of file