mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: enable Tasks submissions
This commit is contained in:
parent
6d7e521bba
commit
bfb977ac5d
9 changed files with 330 additions and 110 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict
|
||||||
from sqlalchemy import JSON, Column, ForeignKey
|
from sqlalchemy import JSON, Column, ForeignKey, null
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
@ -125,7 +125,7 @@ class AssignmentTaskUpdate(SQLModel):
|
||||||
description: Optional[str]
|
description: Optional[str]
|
||||||
hint: Optional[str]
|
hint: Optional[str]
|
||||||
assignment_type: Optional[AssignmentTaskTypeEnum]
|
assignment_type: Optional[AssignmentTaskTypeEnum]
|
||||||
contents: Optional[Dict] = Field(default={}, sa_column=Column(JSON))
|
contents: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
|
||||||
max_grade_value: Optional[int]
|
max_grade_value: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -194,17 +194,12 @@ 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_submission_uuid: Optional[str]
|
assignment_task_submission_uuid: Optional[str]
|
||||||
task_submission: Optional[Dict] = Field(default={}, sa_column=Column(JSON))
|
task_submission: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
|
||||||
grade: Optional[int]
|
grade: Optional[int]
|
||||||
task_submission_grade_feedback: Optional[str]
|
task_submission_grade_feedback: Optional[str]
|
||||||
assignment_type: Optional[AssignmentTaskTypeEnum]
|
assignment_type: Optional[AssignmentTaskTypeEnum]
|
||||||
user_id: Optional[int]
|
|
||||||
activity_id: Optional[int]
|
|
||||||
course_id: Optional[int]
|
|
||||||
chapter_id: Optional[int]
|
|
||||||
assignment_task_id: Optional[int]
|
|
||||||
|
|
||||||
|
|
||||||
class AssignmentTaskSubmission(AssignmentTaskSubmissionBase, table=True):
|
class AssignmentTaskSubmission(AssignmentTaskSubmissionBase, table=True):
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from src.db.courses.assignments import (
|
||||||
AssignmentRead,
|
AssignmentRead,
|
||||||
AssignmentTaskCreate,
|
AssignmentTaskCreate,
|
||||||
AssignmentTaskSubmissionCreate,
|
AssignmentTaskSubmissionCreate,
|
||||||
|
AssignmentTaskSubmissionUpdate,
|
||||||
AssignmentTaskUpdate,
|
AssignmentTaskUpdate,
|
||||||
AssignmentUpdate,
|
AssignmentUpdate,
|
||||||
AssignmentUserSubmissionCreate,
|
AssignmentUserSubmissionCreate,
|
||||||
|
|
@ -15,12 +16,12 @@ from src.services.courses.activities.assignments import (
|
||||||
create_assignment,
|
create_assignment,
|
||||||
create_assignment_submission,
|
create_assignment_submission,
|
||||||
create_assignment_task,
|
create_assignment_task,
|
||||||
create_assignment_task_submission,
|
|
||||||
delete_assignment,
|
delete_assignment,
|
||||||
delete_assignment_from_activity_uuid,
|
delete_assignment_from_activity_uuid,
|
||||||
delete_assignment_submission,
|
delete_assignment_submission,
|
||||||
delete_assignment_task,
|
delete_assignment_task,
|
||||||
delete_assignment_task_submission,
|
delete_assignment_task_submission,
|
||||||
|
handle_assignment_task_submission,
|
||||||
put_assignment_task_reference_file,
|
put_assignment_task_reference_file,
|
||||||
put_assignment_task_submission_file,
|
put_assignment_task_submission_file,
|
||||||
read_assignment,
|
read_assignment,
|
||||||
|
|
@ -31,6 +32,7 @@ from src.services.courses.activities.assignments import (
|
||||||
read_assignment_tasks,
|
read_assignment_tasks,
|
||||||
read_user_assignment_submissions,
|
read_user_assignment_submissions,
|
||||||
read_user_assignment_task_submissions,
|
read_user_assignment_task_submissions,
|
||||||
|
read_user_assignment_task_submissions_me,
|
||||||
update_assignment,
|
update_assignment,
|
||||||
update_assignment_submission,
|
update_assignment_submission,
|
||||||
update_assignment_task,
|
update_assignment_task,
|
||||||
|
|
@ -240,10 +242,10 @@ async def api_delete_assignment_tasks(
|
||||||
## ASSIGNMENTS Tasks Submissions ##
|
## ASSIGNMENTS Tasks Submissions ##
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions")
|
@router.put("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions")
|
||||||
async def api_create_assignment_task_submissions(
|
async def api_handle_assignment_task_submissions(
|
||||||
request: Request,
|
request: Request,
|
||||||
assignment_task_submission_object: AssignmentTaskSubmissionCreate,
|
assignment_task_submission_object: AssignmentTaskSubmissionUpdate,
|
||||||
assignment_task_uuid: str,
|
assignment_task_uuid: str,
|
||||||
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),
|
||||||
|
|
@ -251,7 +253,7 @@ async def api_create_assignment_task_submissions(
|
||||||
"""
|
"""
|
||||||
Create new task submissions for an assignment
|
Create new task submissions for an assignment
|
||||||
"""
|
"""
|
||||||
return await create_assignment_task_submission(
|
return await handle_assignment_task_submission(
|
||||||
request,
|
request,
|
||||||
assignment_task_uuid,
|
assignment_task_uuid,
|
||||||
assignment_task_submission_object,
|
assignment_task_submission_object,
|
||||||
|
|
@ -275,6 +277,20 @@ 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")
|
||||||
|
async def api_read_user_assignment_task_submissions_me(
|
||||||
|
request: Request,
|
||||||
|
assignment_task_uuid: str,
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Read task submissions for an assignment from a user
|
||||||
|
"""
|
||||||
|
return await read_user_assignment_task_submissions_me(
|
||||||
|
request, assignment_task_uuid, current_user, db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions")
|
@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions")
|
||||||
async def api_read_assignment_task_submissions(
|
async def api_read_assignment_task_submissions(
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from src.db.courses.assignments import (
|
||||||
AssignmentTaskSubmission,
|
AssignmentTaskSubmission,
|
||||||
AssignmentTaskSubmissionCreate,
|
AssignmentTaskSubmissionCreate,
|
||||||
AssignmentTaskSubmissionRead,
|
AssignmentTaskSubmissionRead,
|
||||||
|
AssignmentTaskSubmissionUpdate,
|
||||||
AssignmentTaskUpdate,
|
AssignmentTaskUpdate,
|
||||||
AssignmentUpdate,
|
AssignmentUpdate,
|
||||||
AssignmentUserSubmission,
|
AssignmentUserSubmission,
|
||||||
|
|
@ -478,7 +479,13 @@ async def put_assignment_task_reference_file(
|
||||||
f"{assignment_task_uuid}{uuid4()}.{reference_file.filename.split('.')[-1]}"
|
f"{assignment_task_uuid}{uuid4()}.{reference_file.filename.split('.')[-1]}"
|
||||||
)
|
)
|
||||||
await upload_reference_file(
|
await upload_reference_file(
|
||||||
reference_file, name_in_disk, activity.activity_uuid, org.org_uuid, course.course_uuid, assignment.assignment_uuid, assignment_task_uuid
|
reference_file,
|
||||||
|
name_in_disk,
|
||||||
|
activity.activity_uuid,
|
||||||
|
org.org_uuid,
|
||||||
|
course.course_uuid,
|
||||||
|
assignment.assignment_uuid,
|
||||||
|
assignment_task_uuid,
|
||||||
)
|
)
|
||||||
course.thumbnail_image = name_in_disk
|
course.thumbnail_image = name_in_disk
|
||||||
# Update reference file
|
# Update reference file
|
||||||
|
|
@ -494,6 +501,7 @@ async def put_assignment_task_reference_file(
|
||||||
# return assignment task read
|
# return assignment task read
|
||||||
return AssignmentTaskRead.model_validate(assignment_task)
|
return AssignmentTaskRead.model_validate(assignment_task)
|
||||||
|
|
||||||
|
|
||||||
async def put_assignment_task_submission_file(
|
async def put_assignment_task_submission_file(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
|
@ -546,15 +554,18 @@ async def put_assignment_task_submission_file(
|
||||||
|
|
||||||
# Upload reference file
|
# Upload reference 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 = (
|
name_in_disk = f"{assignment_task_uuid}_sub_{current_user.email}_{uuid4()}.{sub_file.filename.split('.')[-1]}"
|
||||||
f"{assignment_task_uuid}_sub_{current_user.email}_{uuid4()}.{sub_file.filename.split('.')[-1]}"
|
|
||||||
)
|
|
||||||
await upload_submission_file(
|
await upload_submission_file(
|
||||||
sub_file, name_in_disk, activity.activity_uuid, org.org_uuid, course.course_uuid, assignment.assignment_uuid, assignment_task_uuid
|
sub_file,
|
||||||
|
name_in_disk,
|
||||||
|
activity.activity_uuid,
|
||||||
|
org.org_uuid,
|
||||||
|
course.course_uuid,
|
||||||
|
assignment.assignment_uuid,
|
||||||
|
assignment_task_uuid,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"message": "Assignment Task Submission File uploaded"}
|
return {"file_uuid": name_in_disk}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def update_assignment_task(
|
async def update_assignment_task(
|
||||||
|
|
@ -665,13 +676,14 @@ async def delete_assignment_task(
|
||||||
## > Assignments Tasks Submissions CRUD
|
## > Assignments Tasks Submissions CRUD
|
||||||
|
|
||||||
|
|
||||||
async def create_assignment_task_submission(
|
async def handle_assignment_task_submission(
|
||||||
request: Request,
|
request: Request,
|
||||||
assignment_task_uuid: str,
|
assignment_task_uuid: str,
|
||||||
assignment_task_submission_object: AssignmentTaskSubmissionCreate,
|
assignment_task_submission_object: AssignmentTaskSubmissionUpdate,
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
):
|
):
|
||||||
|
# TODO: Improve terrible implementation of this function
|
||||||
# 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
|
||||||
|
|
@ -694,30 +706,20 @@ async def create_assignment_task_submission(
|
||||||
detail="Assignment not found",
|
detail="Assignment not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if course exists
|
# Check if user already submitted the assignment
|
||||||
statement = select(Course).where(Course.id == assignment.course_id)
|
statement = select(AssignmentTaskSubmission).where(
|
||||||
course = db_session.exec(statement).first()
|
AssignmentTaskSubmission.assignment_task_id == assignment_task.id,
|
||||||
|
AssignmentTaskSubmission.user_id == current_user.id,
|
||||||
if not course:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=404,
|
|
||||||
detail="Course not found",
|
|
||||||
)
|
)
|
||||||
|
assignment_task_submission = db_session.exec(statement).first()
|
||||||
|
|
||||||
# RBAC check
|
# Update Task submission if it exists
|
||||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
if assignment_task_submission:
|
||||||
|
# Update only the fields that were passed in
|
||||||
# Create Assignment Task Submission
|
for var, value in vars(assignment_task_submission_object).items():
|
||||||
assignment_task_submission = AssignmentTaskSubmission(
|
if value is not None:
|
||||||
**assignment_task_submission_object.model_dump()
|
setattr(assignment_task_submission, var, value)
|
||||||
)
|
|
||||||
|
|
||||||
assignment_task_submission.assignment_task_submission_uuid = str(
|
|
||||||
f"assignmenttasksubmission_{uuid4()}"
|
|
||||||
)
|
|
||||||
assignment_task_submission.creation_date = str(datetime.now())
|
|
||||||
assignment_task_submission.update_date = str(datetime.now())
|
assignment_task_submission.update_date = str(datetime.now())
|
||||||
assignment_task_submission.org_id = course.org_id
|
|
||||||
|
|
||||||
# Insert Assignment Task Submission in DB
|
# Insert Assignment Task Submission in DB
|
||||||
db_session.add(assignment_task_submission)
|
db_session.add(assignment_task_submission)
|
||||||
|
|
@ -727,18 +729,59 @@ async def create_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)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Create new Task submission
|
||||||
|
current_time = str(datetime.now())
|
||||||
|
|
||||||
|
# Assuming model_dump() returns a dictionary
|
||||||
|
model_data = assignment_task_submission_object.model_dump()
|
||||||
|
|
||||||
|
assignment_task_submission = AssignmentTaskSubmission(
|
||||||
|
assignment_task_submission_uuid=f"assignmenttasksubmission_{uuid4()}",
|
||||||
|
task_submission=model_data["task_submission"],
|
||||||
|
grade=model_data["grade"],
|
||||||
|
task_submission_grade_feedback=model_data["task_submission_grade_feedback"],
|
||||||
|
assignment_task_id=int(assignment_task.id), # type: ignore
|
||||||
|
assignment_type=assignment_task.assignment_type,
|
||||||
|
activity_id=assignment.activity_id,
|
||||||
|
course_id=assignment.course_id,
|
||||||
|
chapter_id=assignment.chapter_id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
creation_date=current_time,
|
||||||
|
update_date=current_time,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert Assignment Task Submission in DB
|
||||||
|
db_session.add(assignment_task_submission)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# return assignment task submission read
|
||||||
|
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
|
||||||
|
|
||||||
|
|
||||||
async def read_user_assignment_task_submissions(
|
async def read_user_assignment_task_submissions(
|
||||||
request: Request,
|
request: Request,
|
||||||
assignment_task_submission_uuid: str,
|
assignment_task_uuid: str,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
):
|
):
|
||||||
|
|
||||||
|
# Check if assignment task exists
|
||||||
|
statement = select(AssignmentTask).where(
|
||||||
|
AssignmentTask.assignment_task_uuid == assignment_task_uuid
|
||||||
|
)
|
||||||
|
assignment_task = db_session.exec(statement).first()
|
||||||
|
|
||||||
|
if not assignment_task:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Assignment Task not found",
|
||||||
|
)
|
||||||
|
|
||||||
# Check if assignment task submission exists
|
# Check if assignment task submission exists
|
||||||
statement = select(AssignmentTaskSubmission).where(
|
statement = select(AssignmentTaskSubmission).where(
|
||||||
AssignmentTaskSubmission.assignment_task_submission_uuid
|
AssignmentTaskSubmission.assignment_task_id == assignment_task.id,
|
||||||
== assignment_task_submission_uuid,
|
|
||||||
AssignmentTaskSubmission.user_id == user_id,
|
AssignmentTaskSubmission.user_id == user_id,
|
||||||
)
|
)
|
||||||
assignment_task_submission = db_session.exec(statement).first()
|
assignment_task_submission = db_session.exec(statement).first()
|
||||||
|
|
@ -749,18 +792,6 @@ async def read_user_assignment_task_submissions(
|
||||||
detail="Assignment Task Submission not found",
|
detail="Assignment Task Submission not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if assignment task exists
|
|
||||||
statement = select(AssignmentTask).where(
|
|
||||||
AssignmentTask.id == assignment_task_submission.assignment_task_id
|
|
||||||
)
|
|
||||||
assignment_task = db_session.exec(statement).first()
|
|
||||||
|
|
||||||
if not assignment_task:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=404,
|
|
||||||
detail="Assignment Task not found",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if assignment exists
|
# Check if assignment exists
|
||||||
statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id)
|
statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id)
|
||||||
assignment = db_session.exec(statement).first()
|
assignment = db_session.exec(statement).first()
|
||||||
|
|
@ -788,6 +819,21 @@ async def read_user_assignment_task_submissions(
|
||||||
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
|
return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission)
|
||||||
|
|
||||||
|
|
||||||
|
async def read_user_assignment_task_submissions_me(
|
||||||
|
request: Request,
|
||||||
|
assignment_task_uuid: str,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
db_session: Session,
|
||||||
|
):
|
||||||
|
return await read_user_assignment_task_submissions(
|
||||||
|
request,
|
||||||
|
assignment_task_uuid,
|
||||||
|
current_user.id,
|
||||||
|
current_user,
|
||||||
|
db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def read_assignment_task_submissions(
|
async def read_assignment_task_submissions(
|
||||||
request: Request,
|
request: Request,
|
||||||
assignment_task_submission_uuid: str,
|
assignment_task_submission_uuid: str,
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentConte
|
||||||
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||||
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
||||||
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
|
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
|
||||||
import { getAssignmentTask, updateSubFile } from '@services/courses/assignments';
|
import { getAssignmentTask, getAssignmentTaskSubmissionsMe, handleAssignmentTaskSubmission, updateSubFile } from '@services/courses/assignments';
|
||||||
import { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react'
|
import { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
type FileSchema = {
|
type FileSchema = {
|
||||||
fileID: string;
|
fileUUID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TaskFileObjectProps = {
|
type TaskFileObjectProps = {
|
||||||
|
|
@ -26,6 +26,18 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
|
||||||
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
|
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
|
||||||
const assignment = useAssignments() as any;
|
const assignment = useAssignments() as any;
|
||||||
|
|
||||||
|
/* TEACHER VIEW CODE */
|
||||||
|
/* TEACHER VIEW CODE */
|
||||||
|
|
||||||
|
/* STUDENT VIEW CODE */
|
||||||
|
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
|
||||||
|
const [userSubmissions, setUserSubmissions] = useState<FileSchema>({
|
||||||
|
fileUUID: '',
|
||||||
|
});
|
||||||
|
const [initialUserSubmissions, setInitialUserSubmissions] = useState<FileSchema>({
|
||||||
|
fileUUID: '',
|
||||||
|
});
|
||||||
|
|
||||||
const handleFileChange = async (event: any) => {
|
const handleFileChange = async (event: any) => {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0]
|
||||||
|
|
||||||
|
|
@ -37,19 +49,56 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
|
||||||
assignment.assignment_object.assignment_uuid,
|
assignment.assignment_object.assignment_uuid,
|
||||||
access_token
|
access_token
|
||||||
)
|
)
|
||||||
assignmentTaskStateHook({ type: 'reload' })
|
|
||||||
// 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' })
|
||||||
|
setUserSubmissions({
|
||||||
|
fileUUID: res.data.file_uuid,
|
||||||
|
})
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setError('')
|
setError('')
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAssignmentTaskSubmissionFromUserUI() {
|
||||||
|
if (assignmentTaskUUID) {
|
||||||
|
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
|
||||||
|
if (res.success) {
|
||||||
|
setUserSubmissions(res.data.task_submission);
|
||||||
|
setInitialUserSubmissions(res.data.task_submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitFC = async () => {
|
||||||
|
// Save the quiz to the server
|
||||||
|
const values = {
|
||||||
|
task_submission: userSubmissions,
|
||||||
|
grade: 0,
|
||||||
|
task_submission_grade_feedback: '',
|
||||||
|
};
|
||||||
|
if (assignmentTaskUUID) {
|
||||||
|
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
|
||||||
|
if (res) {
|
||||||
|
assignmentTaskStateHook({
|
||||||
|
type: 'reload',
|
||||||
|
});
|
||||||
|
toast.success('Task saved successfully');
|
||||||
|
setShowSavingDisclaimer(false);
|
||||||
|
} else {
|
||||||
|
toast.error('Error saving task, please retry later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function getAssignmentTaskUI() {
|
async function getAssignmentTaskUI() {
|
||||||
if (assignmentTaskUUID) {
|
if (assignmentTaskUUID) {
|
||||||
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
|
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
|
||||||
|
|
@ -60,13 +109,27 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect changes between initial and current submissions
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (userSubmissions.fileUUID !== initialUserSubmissions.fileUUID) {
|
||||||
|
setShowSavingDisclaimer(true);
|
||||||
|
} else {
|
||||||
|
setShowSavingDisclaimer(false);
|
||||||
|
}
|
||||||
|
}, [userSubmissions]);
|
||||||
|
|
||||||
|
/* STUDENT VIEW CODE */
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (view === 'student') {
|
||||||
getAssignmentTaskUI()
|
getAssignmentTaskUI()
|
||||||
|
getAssignmentTaskSubmissionFromUserUI()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
, [assignmentTaskUUID])
|
, [assignmentTaskUUID])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AssignmentBoxUI view={view} type="file">
|
<AssignmentBoxUI submitFC={submitFC} showSavingDisclaimer={showSavingDisclaimer} view={view} type="file">
|
||||||
{view === 'teacher' && (
|
{view === 'teacher' && (
|
||||||
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
|
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
|
||||||
<Info size={20} />
|
<Info size={20} />
|
||||||
|
|
@ -101,6 +164,21 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{userSubmissions.fileUUID && !isLoading && !localUploadFile && (
|
||||||
|
<div className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
|
||||||
|
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
|
||||||
|
<Cloud size={15} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex space-x-2 mt-2'>
|
||||||
|
|
||||||
|
<File size={20} className='' />
|
||||||
|
<div className='font-semibold text-sm uppercase'>
|
||||||
|
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className='flex pt-4 font-semibold space-x-1.5 text-xs items-center text-gray-500 '>
|
<div className='flex pt-4 font-semibold space-x-1.5 text-xs items-center text-gray-500 '>
|
||||||
<Info size={16} />
|
<Info size={16} />
|
||||||
<p>Allowed formats : pdf, docx, mp4, jpg, jpeg, pptx</p>
|
<p>Allowed formats : pdf, docx, mp4, jpg, jpeg, pptx</p>
|
||||||
|
|
@ -122,13 +200,13 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-center items-center">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="fileInput"
|
id={"fileInput_" + assignmentTaskUUID}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
|
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
|
||||||
onClick={() => document.getElementById('fileInput')?.click()}
|
onClick={() => document.getElementById("fileInput_" + assignmentTaskUUID)?.click()}
|
||||||
>
|
>
|
||||||
<UploadCloud size={16} className="mr-2" />
|
<UploadCloud size={16} className="mr-2" />
|
||||||
<span>Submit File</span>
|
<span>Submit File</span>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentConte
|
||||||
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||||
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
||||||
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI';
|
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI';
|
||||||
import { getAssignmentTask, updateAssignmentTask } from '@services/courses/assignments';
|
import { getAssignmentTask, getAssignmentTaskSubmissionsMe, handleAssignmentTaskSubmission, updateAssignmentTask } from '@services/courses/assignments';
|
||||||
import { Check, Minus, Plus, PlusCircle, X } from 'lucide-react';
|
import { Check, Minus, Plus, PlusCircle, X } from 'lucide-react';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
@ -113,6 +113,11 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
|
||||||
questions: [],
|
questions: [],
|
||||||
submissions: [],
|
submissions: [],
|
||||||
});
|
});
|
||||||
|
const [initialUserSubmissions, setInitialUserSubmissions] = useState<QuizSubmitSchema>({
|
||||||
|
questions: [],
|
||||||
|
submissions: [],
|
||||||
|
});
|
||||||
|
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
|
||||||
|
|
||||||
async function chooseOption(qIndex: number, oIndex: number) {
|
async function chooseOption(qIndex: number, oIndex: number) {
|
||||||
const updatedSubmissions = [...userSubmissions.submissions];
|
const updatedSubmissions = [...userSubmissions.submissions];
|
||||||
|
|
@ -136,11 +141,8 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
|
||||||
...userSubmissions,
|
...userSubmissions,
|
||||||
submissions: updatedSubmissions,
|
submissions: updatedSubmissions,
|
||||||
});
|
});
|
||||||
console.log(userSubmissions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function getAssignmentTaskUI() {
|
async function getAssignmentTaskUI() {
|
||||||
if (assignmentTaskUUID) {
|
if (assignmentTaskUUID) {
|
||||||
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
|
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
|
||||||
|
|
@ -151,6 +153,48 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getAssignmentTaskSubmissionFromUserUI() {
|
||||||
|
if (assignmentTaskUUID) {
|
||||||
|
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
|
||||||
|
if (res.success) {
|
||||||
|
setUserSubmissions(res.data.task_submission);
|
||||||
|
setInitialUserSubmissions(res.data.task_submission);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect changes between initial and current submissions
|
||||||
|
useEffect(() => {
|
||||||
|
const hasChanges = JSON.stringify(initialUserSubmissions.submissions) !== JSON.stringify(userSubmissions.submissions);
|
||||||
|
setShowSavingDisclaimer(hasChanges);
|
||||||
|
}, [userSubmissions, initialUserSubmissions.submissions]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const submitFC = async () => {
|
||||||
|
// Save the quiz to the server
|
||||||
|
const values = {
|
||||||
|
task_submission: userSubmissions,
|
||||||
|
grade: 0,
|
||||||
|
task_submission_grade_feedback: '',
|
||||||
|
};
|
||||||
|
if (assignmentTaskUUID) {
|
||||||
|
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
|
||||||
|
if (res) {
|
||||||
|
assignmentTaskStateHook({
|
||||||
|
type: 'reload',
|
||||||
|
});
|
||||||
|
toast.success('Task saved successfully');
|
||||||
|
setShowSavingDisclaimer(false);
|
||||||
|
} else {
|
||||||
|
toast.error('Error saving task, please retry later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* STUDENT VIEW CODE */
|
/* STUDENT VIEW CODE */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -164,12 +208,13 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
|
||||||
// Student area
|
// Student area
|
||||||
else if (view == 'student') {
|
else if (view == 'student') {
|
||||||
getAssignmentTaskUI();
|
getAssignmentTaskUI();
|
||||||
|
getAssignmentTaskSubmissionFromUserUI();
|
||||||
}
|
}
|
||||||
}, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token]);
|
}, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AssignmentBoxUI saveFC={saveFC} view={view} type="quiz">
|
<AssignmentBoxUI submitFC={submitFC} saveFC={saveFC} view={view} showSavingDisclaimer={showSavingDisclaimer} type="quiz">
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
{questions && questions.map((question, qIndex) => (
|
{questions && questions.map((question, qIndex) => (
|
||||||
<div key={qIndex} className="flex flex-col space-y-1.5">
|
<div key={qIndex} className="flex flex-col space-y-1.5">
|
||||||
|
|
|
||||||
|
|
@ -93,18 +93,7 @@ function PublishingState() {
|
||||||
{assignment?.assignment_object?.published ? 'Published' : 'Unpublished'}
|
{assignment?.assignment_object?.published ? 'Published' : 'Unpublished'}
|
||||||
</div>
|
</div>
|
||||||
<div><EllipsisVertical className='text-gray-500' size={13} /></div>
|
<div><EllipsisVertical className='text-gray-500' size={13} /></div>
|
||||||
{assignment?.assignment_object?.published && <ToolTip
|
|
||||||
side='left'
|
|
||||||
slateBlack
|
|
||||||
sideOffset={10}
|
|
||||||
content="Make your Assignment unavailable for students" >
|
|
||||||
<div
|
|
||||||
onClick={() => updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)}
|
|
||||||
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'>
|
|
||||||
<BookX size={18} />
|
|
||||||
<p className='text-sm font-bold'>Unpublish</p>
|
|
||||||
</div>
|
|
||||||
</ToolTip>}
|
|
||||||
<ToolTip
|
<ToolTip
|
||||||
side='left'
|
side='left'
|
||||||
slateBlack
|
slateBlack
|
||||||
|
|
@ -118,6 +107,18 @@ function PublishingState() {
|
||||||
<p className=' text-sm font-bold'>Preview</p>
|
<p className=' text-sm font-bold'>Preview</p>
|
||||||
</Link>
|
</Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
|
{assignment?.assignment_object?.published && <ToolTip
|
||||||
|
side='left'
|
||||||
|
slateBlack
|
||||||
|
sideOffset={10}
|
||||||
|
content="Make your Assignment unavailable for students" >
|
||||||
|
<div
|
||||||
|
onClick={() => updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)}
|
||||||
|
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'>
|
||||||
|
<BookX size={18} />
|
||||||
|
<p className='text-sm font-bold'>Unpublish</p>
|
||||||
|
</div>
|
||||||
|
</ToolTip>}
|
||||||
{!assignment?.assignment_object?.published &&
|
{!assignment?.assignment_object?.published &&
|
||||||
<ToolTip
|
<ToolTip
|
||||||
side='left'
|
side='left'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { BookUser, EllipsisVertical, FileUp, Forward, ListTodo, Save } from 'lucide-react'
|
import { BookUser, EllipsisVertical, FileUp, Forward, Info, InfoIcon, ListTodo, Save } from 'lucide-react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type AssignmentBoxProps = {
|
type AssignmentBoxProps = {
|
||||||
|
|
@ -6,11 +6,12 @@ type AssignmentBoxProps = {
|
||||||
view?: 'teacher' | 'student'
|
view?: 'teacher' | 'student'
|
||||||
saveFC?: () => void
|
saveFC?: () => void
|
||||||
submitFC?: () => void
|
submitFC?: () => void
|
||||||
|
showSavingDisclaimer?: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentBoxProps) {
|
function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, children }: AssignmentBoxProps) {
|
||||||
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'>
|
||||||
|
|
@ -40,12 +41,17 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentB
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex px-1 py-1 rounded-md items-center'>
|
<div className='flex px-1 py-1 rounded-md items-center'>
|
||||||
|
{showSavingDisclaimer &&
|
||||||
|
<div className='flex space-x-2 items-center font-semibold px-3 py-1 outline-dashed outline-red-200 text-red-400 mr-5 rounded-full'>
|
||||||
|
<InfoIcon size={14} />
|
||||||
|
<p className='text-xs'>Don't forget to save your progress</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{/* Save button */}
|
{/* Save button */}
|
||||||
{view === 'teacher' &&
|
{view === 'teacher' &&
|
||||||
<div
|
<div
|
||||||
onClick={() => saveFC && saveFC()}
|
onClick={() => saveFC && saveFC()}
|
||||||
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-slate-500 bg-white/60 hover:bg-white/80 linear transition-all nice-shadow '>
|
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'>
|
||||||
<Save size={14} />
|
<Save size={14} />
|
||||||
<p className='text-xs font-semibold'>Save</p>
|
<p className='text-xs font-semibold'>Save</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -53,9 +59,9 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentB
|
||||||
{view === 'student' &&
|
{view === 'student' &&
|
||||||
<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-slate-500 bg-white/60 hover:bg-white/80 linear transition-all nice-shadow '>
|
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'>
|
||||||
<Forward size={14} />
|
<Forward size={14} />
|
||||||
<p className='text-xs font-semibold'>Save</p>
|
<p className='text-xs font-semibold'>Save your progress</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ function AssignmentStudentActivity() {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(assignments)
|
|
||||||
}, [assignments, org])
|
}, [assignments, org])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,7 +70,14 @@ function AssignmentStudentActivity() {
|
||||||
download={true}
|
download={true}
|
||||||
className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-2 cursor-pointer'>
|
className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-2 cursor-pointer'>
|
||||||
<Download size={13} />
|
<Download size={13} />
|
||||||
<p className='text-xs font-semibold'>Reference file</p>
|
<div className='flex items-center space-x-2'>
|
||||||
|
{task.reference_file && (
|
||||||
|
<span className='relative'>
|
||||||
|
<span className='absolute right-0 top-0 block h-2 w-2 rounded-full ring-2 ring-white bg-green-400'></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<p className='text-xs font-semibold'>Reference Document</p>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,33 @@ export async function getAssignmentTask(
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAssignmentTaskSubmissionsMe(
|
||||||
|
assignmentTaskUUID: string,
|
||||||
|
assignmentUUID: string,
|
||||||
|
access_token: string
|
||||||
|
) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions/user/me`,
|
||||||
|
RequestBodyWithAuthHeader('GET', null, null, access_token)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleAssignmentTaskSubmission(
|
||||||
|
body: any,
|
||||||
|
assignmentTaskUUID: string,
|
||||||
|
assignmentUUID: string,
|
||||||
|
access_token: string
|
||||||
|
) {
|
||||||
|
const result: any = await fetch(
|
||||||
|
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions`,
|
||||||
|
RequestBodyWithAuthHeader('PUT', body, null, access_token)
|
||||||
|
)
|
||||||
|
const res = await getResponseMetadata(result)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateAssignmentTask(
|
export async function updateAssignmentTask(
|
||||||
body: any,
|
body: any,
|
||||||
assignmentTaskUUID: string,
|
assignmentTaskUUID: string,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue