fix: misc assignments bugs

This commit is contained in:
swve 2025-03-16 10:59:53 +01:00
parent d244ff2076
commit a6b5db29ca
6 changed files with 121 additions and 37 deletions

View file

@ -162,7 +162,7 @@ class AssignmentTask(AssignmentTaskBase, table=True):
class AssignmentTaskSubmissionBase(SQLModel): class AssignmentTaskSubmissionBase(SQLModel):
"""Represents the common fields for an assignment task submission.""" """Represents the common fields for an assignment task submission."""
assignment_task_submission_uuid: str
task_submission: Dict = Field(default={}, sa_column=Column(JSON)) task_submission: Dict = Field(default={}, sa_column=Column(JSON))
grade: int = 0 # Value is always between 0-100 grade: int = 0 # Value is always between 0-100
task_submission_grade_feedback: str task_submission_grade_feedback: str

View file

@ -37,6 +37,7 @@ from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship, authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_element_is_public, authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, 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.sub_file import upload_submission_file
from src.services.courses.activities.uploads.tasks_ref_files import ( 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_statement = select(Organization).where(Organization.id == course.org_id)
org = db_session.exec(org_statement).first() org = db_session.exec(org_statement).first()
# RBAC check # RBAC check - only need read permission to submit files
await rbac_check(request, course.course_uuid, current_user, "update", db_session) 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: 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]}" name_in_disk = f"{assignment_task_uuid}_sub_{current_user.email}_{uuid4()}.{sub_file.filename.split('.')[-1]}"
await upload_submission_file( await upload_submission_file(
@ -699,7 +707,7 @@ async def handle_assignment_task_submission(
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, 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 # Check if assignment task exists
statement = select(AssignmentTask).where( statement = select(AssignmentTask).where(
AssignmentTask.assignment_task_uuid == assignment_task_uuid AssignmentTask.assignment_task_uuid == assignment_task_uuid
@ -722,15 +730,59 @@ async def handle_assignment_task_submission(
detail="Assignment not found", detail="Assignment not found",
) )
# Check if user already submitted the assignment # Check if course exists
statement = select(AssignmentTaskSubmission).where( statement = select(Course).where(Course.id == assignment.course_id)
AssignmentTaskSubmission.assignment_task_id == assignment_task.id, course = db_session.exec(statement).first()
AssignmentTaskSubmission.user_id == current_user.id,
)
assignment_task_submission = 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: 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 # Update only the fields that were passed in
for var, value in vars(assignment_task_submission_object).items(): for var, value in vars(assignment_task_submission_object).items():
if value is not None: if value is not None:
@ -742,9 +794,6 @@ async def handle_assignment_task_submission(
db_session.commit() db_session.commit()
db_session.refresh(assignment_task_submission) db_session.refresh(assignment_task_submission)
# return assignment task submission read
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
else: else:
# Create new Task submission # Create new Task submission
current_time = str(datetime.now()) current_time = str(datetime.now())
@ -753,10 +802,10 @@ async def handle_assignment_task_submission(
model_data = assignment_task_submission_object.model_dump() model_data = assignment_task_submission_object.model_dump()
assignment_task_submission = AssignmentTaskSubmission( 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"], task_submission=model_data["task_submission"],
grade=model_data["grade"], grade=0, # Always start with 0 for new submissions
task_submission_grade_feedback=model_data["task_submission_grade_feedback"], task_submission_grade_feedback="", # Start with empty feedback
assignment_task_id=int(assignment_task.id), # type: ignore assignment_task_id=int(assignment_task.id), # type: ignore
assignment_type=assignment_task.assignment_type, assignment_type=assignment_task.assignment_type,
activity_id=assignment.activity_id, activity_id=assignment.activity_id,
@ -770,9 +819,10 @@ async def handle_assignment_task_submission(
# Insert Assignment Task Submission in DB # Insert Assignment Task Submission in DB
db_session.add(assignment_task_submission) db_session.add(assignment_task_submission)
db_session.commit() db_session.commit()
db_session.refresh(assignment_task_submission)
# return assignment task submission read # return assignment task submission read
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission) return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
async def read_user_assignment_task_submissions( async def read_user_assignment_task_submissions(
@ -1096,7 +1146,7 @@ async def create_assignment_submission(
) )
# RBAC check # 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 # Create Assignment User Submission
assignment_user_submission = AssignmentUserSubmission( assignment_user_submission = AssignmentUserSubmission(

View file

@ -12,6 +12,7 @@ import toast from 'react-hot-toast';
type FileSchema = { type FileSchema = {
fileUUID: string; fileUUID: string;
assignment_task_submission_uuid?: string;
}; };
type TaskFileObjectProps = { type TaskFileObjectProps = {
@ -64,13 +65,13 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
// wait for 1 second to show loading animation // wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500)) await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) { if (res.success === false) {
setError(res.data.detail) setError(res.data.detail)
setIsLoading(false) setIsLoading(false)
} else { } else {
assignmentTaskStateHook({ type: 'reload' }) assignmentTaskStateHook({ type: 'reload' })
setUserSubmissions({ setUserSubmissions({
fileUUID: res.data.file_uuid, fileUUID: res.data.file_uuid,
assignment_task_submission_uuid: res.data.assignment_task_submission_uuid
}) })
setIsLoading(false) setIsLoading(false)
setError('') setError('')
@ -86,8 +87,14 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
if (assignmentTaskUUID) { if (assignmentTaskUUID) {
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token); const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) { if (res.success) {
setUserSubmissions(res.data.task_submission); setUserSubmissions({
setInitialUserSubmissions(res.data.task_submission); ...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 // Save the quiz to the server
const values = { const values = {
assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid,
task_submission: userSubmissions, task_submission: userSubmissions,
grade: 0, grade: 0,
task_submission_grade_feedback: '', task_submission_grade_feedback: '',
@ -156,9 +164,15 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
if (assignmentTaskUUID && user_id) { if (assignmentTaskUUID && user_id) {
const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token); const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) { 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); 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 // Save the grade to the server
const values = { const values = {
assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid,
task_submission: userSubmissions, task_submission: userSubmissions,
grade: grade, grade: grade,
task_submission_grade_feedback: 'Graded by teacher : @' + session.data.user.username, task_submission_grade_feedback: 'Graded by teacher : @' + session.data.user.username,

View file

@ -27,6 +27,7 @@ type QuizSubmitSchema = {
optionUUID: string; optionUUID: string;
answer: boolean answer: boolean
}[]; }[];
assignment_task_submission_uuid?: string;
}; };
type TaskQuizObjectProps = { type TaskQuizObjectProps = {
@ -175,8 +176,14 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
if (assignmentTaskUUID) { if (assignmentTaskUUID) {
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token); const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) { if (res.success) {
setUserSubmissions(res.data.task_submission); setUserSubmissions({
setInitialUserSubmissions(res.data.task_submission); ...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) { if (assignmentTaskUUID && user_id) {
const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token); const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) { 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); 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 // Save the grade to the server
const values = { const values = {
assignment_task_submission_uuid: userSubmissions.assignment_task_submission_uuid,
task_submission: userSubmissions, task_submission: userSubmissions,
grade: finalGrade, grade: finalGrade,
task_submission_grade_feedback: 'Auto graded by system', task_submission_grade_feedback: 'Auto graded by system',

View file

@ -41,21 +41,27 @@ function AssignmentSubmissionsSubPage({ assignment_uuid }: { assignment_uuid: st
<X size={18} /> <X size={18} />
<h3>Late</h3> <h3>Late</h3>
</div> </div>
{renderSubmissions('LATE')} <div className='flex flex-col gap-4'>
{renderSubmissions('LATE')}
</div>
</div> </div>
<div className='flex-1'> <div className='flex-1'>
<div className='flex w-fit mx-auto px-3.5 py-1 bg-amber-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'> <div className='flex w-fit mx-auto px-3.5 py-1 bg-amber-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'>
<SendHorizonal size={18} /> <SendHorizonal size={18} />
<h3>Submitted</h3> <h3>Submitted</h3>
</div> </div>
{renderSubmissions('SUBMITTED')} <div className='flex flex-col gap-4'>
{renderSubmissions('SUBMITTED')}
</div>
</div> </div>
<div className='flex-1'> <div className='flex-1'>
<div className='flex w-fit mx-auto px-3.5 py-1 bg-emerald-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'> <div className='flex w-fit mx-auto px-3.5 py-1 bg-emerald-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'>
<UserCheck size={18} /> <UserCheck size={18} />
<h3>Graded</h3> <h3>Graded</h3>
</div> </div>
{renderSubmissions('GRADED')} <div className='flex flex-col gap-4'>
{renderSubmissions('GRADED')}
</div>
</div> </div>
</div> </div>

View file

@ -15,7 +15,6 @@ function EvaluateAssignment({ user_id }: any) {
const assignments = useAssignments() as any; const assignments = useAssignments() as any;
const session = useLHSession() as any; const session = useLHSession() as any;
const org = useOrg() as any; const org = useOrg() as any;
const router = useRouter();
async function gradeAssignment() { async function gradeAssignment() {
const res = await putFinalGrade(user_id, assignments?.assignment_object.assignment_uuid, session.data?.tokens?.access_token); 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) {
) )
})} })}
<div className='flex space-x-4 font-semibold items-center justify-between'> <div className='flex space-x-4 font-semibold items-center justify-between'>
<button onClick={rejectAssignment} className='flex space-x-2 px-4 py-2 text-sm bg-rose-600/80 text-white rounded-lg nice-shadow items-center'> <button onClick={rejectAssignment} className='flex space-x-2 px-4 py-2 text-sm bg-rose-600/80 text-white rounded-lg nice-shadow items-center cursor-pointer'>
<X size={18} /> <X size={18} />
<span>Reject Assignment</span> <span>Reject Assignment</span>
</button> </button>
<div className='flex space-x-3 items-center'> <div className='flex space-x-3 items-center'>
<button onClick={gradeAssignment} className='flex space-x-2 px-4 py-2 text-sm bg-violet-600/80 text-white rounded-lg nice-shadow items-center'> <button onClick={gradeAssignment} className='flex space-x-2 px-4 py-2 text-sm bg-violet-600/80 text-white rounded-lg nice-shadow items-center cursor-pointer'>
<BookOpenCheck size={18} /> <BookOpenCheck size={18} />
<span>Set final grade</span> <span>Set final grade</span>
</button> </button>
<MoveRight className='text-gray-400' size={18} /> <MoveRight className='text-gray-400' size={18} />
<button onClick={markActivityAsDone} className='flex space-x-2 px-4 py-2 text-sm bg-teal-600/80 text-white rounded-lg nice-shadow items-center'> <button onClick={markActivityAsDone} className='flex space-x-2 px-4 py-2 text-sm bg-teal-600/80 text-white rounded-lg nice-shadow items-center cursor-pointer'>
<Check size={18} /> <Check size={18} />
<span>Mark Activity as Done for User</span> <span>Mark Activity as Done for User</span>
</button> </button>