feat: add assignment submission from the activity page

This commit is contained in:
swve 2024-07-20 20:10:43 +02:00
parent c13a7b4538
commit e9caa2ceb4
7 changed files with 207 additions and 29 deletions

View file

@ -4,7 +4,6 @@ from sqlmodel import Field, SQLModel
from enum import Enum from enum import Enum
## Assignment ## ## Assignment ##
class GradingTypeEnum(str, Enum): class GradingTypeEnum(str, Enum):
ALPHABET = "ALPHABET" ALPHABET = "ALPHABET"
@ -87,7 +86,7 @@ class Assignment(AssignmentBase, table=True):
class AssignmentTaskTypeEnum(str, Enum): class AssignmentTaskTypeEnum(str, Enum):
FILE_SUBMISSION = "FILE_SUBMISSION" FILE_SUBMISSION = "FILE_SUBMISSION"
QUIZ = "QUIZ" QUIZ = "QUIZ"
FORM = "FORM" # soon to be implemented FORM = "FORM" # soon to be implemented
OTHER = "OTHER" OTHER = "OTHER"
@ -102,8 +101,6 @@ class AssignmentTaskBase(SQLModel):
contents: Dict = Field(default={}, sa_column=Column(JSON)) contents: Dict = Field(default={}, sa_column=Column(JSON))
max_grade_value: int = 0 # Value is always between 0-100 max_grade_value: int = 0 # Value is always between 0-100
class AssignmentTaskCreate(AssignmentTaskBase): class AssignmentTaskCreate(AssignmentTaskBase):
"""Model for creating a new assignment task.""" """Model for creating a new assignment task."""
@ -194,6 +191,7 @@ class AssignmentTaskSubmissionRead(AssignmentTaskSubmissionBase):
class AssignmentTaskSubmissionUpdate(SQLModel): class AssignmentTaskSubmissionUpdate(SQLModel):
"""Model for updating an assignment task submission.""" """Model for updating an assignment task submission."""
assignment_task_id: Optional[int] assignment_task_id: Optional[int]
assignment_task_submission_uuid: Optional[str] assignment_task_submission_uuid: Optional[str]
task_submission: Optional[Dict] = Field(default=None, sa_column=Column(JSON)) task_submission: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
@ -233,10 +231,12 @@ class AssignmentTaskSubmission(AssignmentTaskSubmissionBase, table=True):
creation_date: str creation_date: str
update_date: str update_date: str
## AssignmentTaskSubmission ## ## AssignmentTaskSubmission ##
## AssignmentUserSubmission ## ## AssignmentUserSubmission ##
class AssignmentUserSubmissionStatus(str, Enum): class AssignmentUserSubmissionStatus(str, Enum):
PENDING = "PENDING" PENDING = "PENDING"
SUBMITTED = "SUBMITTED" SUBMITTED = "SUBMITTED"
@ -248,11 +248,10 @@ class AssignmentUserSubmissionStatus(str, Enum):
class AssignmentUserSubmissionBase(SQLModel): class AssignmentUserSubmissionBase(SQLModel):
"""Represents the submission status of an assignment for a user.""" """Represents the submission status of an assignment for a user."""
submission_status: AssignmentUserSubmissionStatus = ( submission_status: AssignmentUserSubmissionStatus = (
AssignmentUserSubmissionStatus.PENDING AssignmentUserSubmissionStatus.PENDING
) )
grade: str grade: int
user_id: int = Field( user_id: int = Field(
sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) 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.""" """Model for creating a new assignment user submission."""
assignment_id: int
pass # Inherits all fields from AssignmentUserSubmissionBase pass # Inherits all fields from AssignmentUserSubmissionBase
class AssignmentUserSubmissionRead(AssignmentUserSubmissionBase): class AssignmentUserSubmissionRead(AssignmentUserSubmissionBase):
"""Model for reading an assignment user submission.""" """Model for reading an assignment user submission."""
@ -274,6 +276,7 @@ class AssignmentUserSubmissionRead(AssignmentUserSubmissionBase):
creation_date: str creation_date: str
update_date: str update_date: str
class AssignmentUserSubmissionUpdate(SQLModel): class AssignmentUserSubmissionUpdate(SQLModel):
"""Model for updating an assignment user submission.""" """Model for updating an assignment user submission."""
@ -282,17 +285,19 @@ class AssignmentUserSubmissionUpdate(SQLModel):
user_id: Optional[int] user_id: Optional[int]
assignment_id: Optional[int] assignment_id: Optional[int]
class AssignmentUserSubmission(AssignmentUserSubmissionBase, table=True): class AssignmentUserSubmission(AssignmentUserSubmissionBase, table=True):
"""Represents the submission status of an assignment for a user.""" """Represents the submission status of an assignment for a user."""
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
creation_date: str creation_date: str
update_date: str update_date: str
assignmentusersubmission_uuid: str
submission_status: AssignmentUserSubmissionStatus = ( submission_status: AssignmentUserSubmissionStatus = (
AssignmentUserSubmissionStatus.PENDING AssignmentUserSubmissionStatus.PENDING
) )
grade: str grade: int
user_id: int = Field( user_id: int = Field(
sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) 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") "assignment_id", ForeignKey("assignment.id", ondelete="CASCADE")
) )
) )

View file

@ -31,6 +31,7 @@ from src.services.courses.activities.assignments import (
read_assignment_task_submissions, read_assignment_task_submissions,
read_assignment_tasks, read_assignment_tasks,
read_user_assignment_submissions, read_user_assignment_submissions,
read_user_assignment_submissions_me,
read_user_assignment_task_submissions, read_user_assignment_task_submissions,
read_user_assignment_task_submissions_me, read_user_assignment_task_submissions_me,
update_assignment, update_assignment,
@ -208,6 +209,7 @@ async def api_put_assignment_task_ref_file(
request, db_session, assignment_task_uuid, current_user, reference_file request, db_session, assignment_task_uuid, current_user, reference_file
) )
@router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/sub_file") @router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/sub_file")
async def api_put_assignment_task_sub_file( async def api_put_assignment_task_sub_file(
request: Request, request: Request,
@ -277,6 +279,7 @@ async def api_read_user_assignment_task_submissions(
request, assignment_task_uuid, user_id, current_user, db_session request, assignment_task_uuid, user_id, current_user, db_session
) )
@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions/user/me") @router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions/user/me")
async def api_read_user_assignment_task_submissions_me( async def api_read_user_assignment_task_submissions_me(
request: Request, request: Request,
@ -331,7 +334,6 @@ async def api_delete_assignment_task_submissions(
async def api_create_assignment_submissions( async def api_create_assignment_submissions(
request: Request, request: Request,
assignment_uuid: str, assignment_uuid: str,
assignment_submission: AssignmentUserSubmissionCreate,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
): ):
@ -339,7 +341,7 @@ async def api_create_assignment_submissions(
Create new submissions for an assignment Create new submissions for an assignment
""" """
return await create_assignment_submission( 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}") @router.get("/{assignment_uuid}/submissions/{user_id}")
async def api_read_user_assignment_submissions( async def api_read_user_assignment_submissions(
request: Request, request: Request,

View file

@ -7,6 +7,7 @@ from typing import Literal
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request, UploadFile from fastapi import HTTPException, Request, UploadFile
from sqlmodel import Session, select from sqlmodel import Session, select
from sympy import Sum
from src.db.courses.activities import Activity from src.db.courses.activities import Activity
from src.db.courses.assignments import ( from src.db.courses.assignments import (
@ -25,6 +26,7 @@ from src.db.courses.assignments import (
AssignmentUserSubmission, AssignmentUserSubmission,
AssignmentUserSubmissionCreate, AssignmentUserSubmissionCreate,
AssignmentUserSubmissionRead, AssignmentUserSubmissionRead,
AssignmentUserSubmissionStatus,
) )
from src.db.courses.courses import Course from src.db.courses.courses import Course
from src.db.organizations import Organization from src.db.organizations import Organization
@ -1028,7 +1030,6 @@ async def delete_assignment_task_submission(
async def create_assignment_submission( async def create_assignment_submission(
request: Request, request: Request,
assignment_uuid: str, assignment_uuid: str,
assignment_user_submission_object: AssignmentUserSubmissionCreate,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
): ):
@ -1045,7 +1046,7 @@ async def create_assignment_submission(
# Check if the submission has already been made # Check if the submission has already been made
statement = select(AssignmentUserSubmission).where( statement = select(AssignmentUserSubmission).where(
AssignmentUserSubmission.assignment_id == assignment.id, 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() assignment_user_submission = db_session.exec(statement).first()
@ -1066,21 +1067,33 @@ async def create_assignment_submission(
detail="Course not found", 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 # RBAC check
await rbac_check(request, course.course_uuid, current_user, "create", db_session) await rbac_check(request, course.course_uuid, current_user, "create", db_session)
# Create Assignment User Submission # Create Assignment User Submission
assignment_user_submission = AssignmentUserSubmission( 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 # Insert Assignment User Submission in DB
db_session.add(assignment_user_submission) db_session.add(assignment_user_submission)
db_session.commit() 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( async def update_assignment_submission(
request: Request, request: Request,
user_id: str, user_id: str,

View file

@ -1,9 +1,9 @@
'use client' 'use client'
import Link from 'next/link' 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 Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva'
import VideoActivity from '@components/Objects/Activities/Video/Video' 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 { markActivityAsComplete } from '@services/courses/activity'
import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf' import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf'
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' 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 AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import React, { useEffect } from 'react' 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 AssignmentStudentActivity from '@components/Objects/Activities/Assignment/AssignmentStudentActivity'
import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext' import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext'
import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext' 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 { interface ActivityClientProps {
activityid: string activityid: string
@ -135,6 +139,21 @@ function ActivityClient(props: ActivityClientProps) {
/> />
</> </>
} }
{activity.activity_type == 'TYPE_ASSIGNMENT' &&
<>
<MoreVertical size={17} className="text-gray-300 " />
<AssignmentSubmissionProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentTools
assignment={assignment}
activity={activity}
activityid={activityid}
course={course}
orgslug={orgslug}
/>
</AssignmentSubmissionProvider>
</>
}
</AuthenticatedClientElement> </AuthenticatedClientElement>
</div> </div>
@ -172,7 +191,9 @@ function ActivityClient(props: ActivityClientProps) {
{assignment ? ( {assignment ? (
<AssignmentProvider assignment_uuid={assignment?.assignment_uuid}> <AssignmentProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentsTaskProvider> <AssignmentsTaskProvider>
<AssignmentStudentActivity /> <AssignmentSubmissionProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentStudentActivity />
</AssignmentSubmissionProvider>
</AssignmentsTaskProvider> </AssignmentsTaskProvider>
</AssignmentProvider> </AssignmentProvider>
) : ( ) : (
@ -225,7 +246,7 @@ export function MarkStatus(props: {
return ( return (
<> <>
{isActivityCompleted() ? ( {isActivityCompleted() ? (
<div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out"> <div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out">
<i> <i>
<Check size={17}></Check> <Check size={17}></Check>
</i>{' '} </i>{' '}
@ -233,7 +254,7 @@ export function MarkStatus(props: {
</div> </div>
) : ( ) : (
<div <div
className="bg-gray-800 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" className="bg-gray-800 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out"
onClick={markActivityAsCompleteFront} onClick={markActivityAsCompleteFront}
> >
{' '} {' '}
@ -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 && (
<ConfirmationModal
confirmationButtonText="Submit Assignment"
confirmationMessage="Are you sure you want to submit your assignment for grading?, once submitted you will not be able to make any changes"
dialogTitle="Submit your assignment for grading"
dialogTrigger={
<div
className="bg-cyan-800 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out">
<i>
<BookOpenCheck size={17}></BookOpenCheck>
</i>{' '}
<i className="not-italic text-xs font-bold">Submit for grading</i>
</div>}
functionToExecute={() => submitForGradingUI()}
status="info"
/>
)}
{submission && submission.length > 0 && (
<div
className="bg-amber-800 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white transition delay-150 duration-300 ease-in-out">
<i>
<UserRoundPen size={17}></UserRoundPen>
</i>{' '}
<i className="not-italic text-xs font-bold">Grading in progress</i>
</div>)
}
</>
}
export default ActivityClient export default ActivityClient

View file

@ -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 (
<AssignmentSubmissionContext.Provider value={assignmentSubmission} >{children}</AssignmentSubmissionContext.Provider>
)
}
export function useAssignmentSubmission() {
return React.useContext(AssignmentSubmissionContext)
}
export default AssignmentSubmissionProvider

View file

@ -1,5 +1,6 @@
import { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext'
import { BookUser, EllipsisVertical, FileUp, Forward, Info, InfoIcon, ListTodo, Save } from 'lucide-react' 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 AssignmentBoxProps = {
type: 'quiz' | 'file' type: 'quiz' | 'file'
@ -12,6 +13,10 @@ type AssignmentBoxProps = {
} }
function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, children }: AssignmentBoxProps) { function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, children }: AssignmentBoxProps) {
const submission = useAssignmentSubmission() as any
useEffect(() => {
}
, [submission])
return ( return (
<div className='flex flex-col px-6 py-4 nice-shadow rounded-md bg-slate-100/30'> <div className='flex flex-col px-6 py-4 nice-shadow rounded-md bg-slate-100/30'>
<div className='flex justify-between space-x-2 pb-2 text-slate-400 items-center'> <div className='flex justify-between space-x-2 pb-2 text-slate-400 items-center'>
@ -47,6 +52,7 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, c
<p className='text-xs'>Don't forget to save your progress</p> <p className='text-xs'>Don't forget to save your progress</p>
</div> </div>
} }
{/* Save button */} {/* Save button */}
{view === 'teacher' && {view === 'teacher' &&
<div <div
@ -56,7 +62,7 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, c
<p className='text-xs font-semibold'>Save</p> <p className='text-xs font-semibold'>Save</p>
</div> </div>
} }
{view === 'student' && {view === 'student' && submission.length <= 0 &&
<div <div
onClick={() => submitFC && submitFC()} onClick={() => 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'> 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'>

View file

@ -187,3 +187,17 @@ export async function updateSubFile(
const res = await getResponseMetadata(result) const res = await getResponseMetadata(result)
return res 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
}