feat: assignments activity page init

This commit is contained in:
swve 2024-07-18 21:05:32 +02:00
parent 182cd73001
commit 29600d9b6c
12 changed files with 564 additions and 105 deletions

View file

@ -22,6 +22,7 @@ from src.services.courses.activities.assignments import (
delete_assignment_task, delete_assignment_task,
delete_assignment_task_submission, delete_assignment_task_submission,
put_assignment_task_reference_file, put_assignment_task_reference_file,
put_assignment_task_submission_file,
read_assignment, read_assignment,
read_assignment_from_activity_uuid, read_assignment_from_activity_uuid,
read_assignment_submissions, read_assignment_submissions,
@ -205,6 +206,21 @@ 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")
async def api_put_assignment_task_sub_file(
request: Request,
assignment_task_uuid: str,
sub_file: UploadFile | None = None,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Update tasks for an assignment
"""
return await put_assignment_task_submission_file(
request, db_session, assignment_task_uuid, current_user, sub_file
)
@router.delete("/{assignment_uuid}/tasks/{assignment_task_uuid}") @router.delete("/{assignment_uuid}/tasks/{assignment_task_uuid}")
async def api_delete_assignment_tasks( async def api_delete_assignment_tasks(

View file

@ -33,6 +33,7 @@ from src.security.rbac.rbac import (
authorization_verify_if_element_is_public, authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
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 (
upload_reference_file, upload_reference_file,
) )
@ -493,6 +494,68 @@ 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(
request: Request,
db_session: Session,
assignment_task_uuid: str,
current_user: PublicUser | AnonymousUser,
sub_file: UploadFile | None = None,
):
# 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 exists
statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id)
assignment = db_session.exec(statement).first()
if not assignment:
raise HTTPException(
status_code=404,
detail="Assignment not found",
)
# Check for activity
statement = select(Activity).where(Activity.id == assignment.activity_id)
activity = db_session.exec(statement).first()
# Check if course exists
statement = select(Course).where(Course.id == assignment.course_id)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=404,
detail="Course not found",
)
# Get org uuid
org_statement = select(Organization).where(Organization.id == course.org_id)
org = db_session.exec(org_statement).first()
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Upload reference file
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]}"
)
await upload_submission_file(
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"}
async def update_assignment_task( async def update_assignment_task(
request: Request, request: Request,

View file

@ -0,0 +1,24 @@
from uuid import uuid4
from src.services.utils.upload_content import upload_content
async def upload_submission_file(
file,
name_in_disk,
activity_uuid,
org_uuid,
course_uuid,
assignment_uuid,
assignment_task_uuid,
):
contents = file.file.read()
file_format = file.filename.split(".")[-1]
await upload_content(
f"courses/{course_uuid}/activities/{activity_uuid}/assignments/{assignment_uuid}/tasks/{assignment_task_uuid}/subs",
"orgs",
org_uuid,
contents,
f"{name_in_disk}",
["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx"],
)

View file

@ -16,6 +16,11 @@ import { CourseProvider } from '@components/Contexts/CourseContext'
import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk' 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 { getAssignmentFromActivityUUID } 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'
interface ActivityClientProps { interface ActivityClientProps {
activityid: string activityid: string
@ -32,6 +37,11 @@ function ActivityClient(props: ActivityClientProps) {
const activity = props.activity const activity = props.activity
const course = props.course const course = props.course
const org = useOrg() as any const org = useOrg() as any
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const [bgColor, setBgColor] = React.useState('bg-white')
const [assignment, setAssignment] = React.useState(null) as any;
const [markStatusButtonActive, setMarkStatusButtonActive] = React.useState(false);
function getChapterNameByActivityId(course: any, activity_id: any) { function getChapterNameByActivityId(course: any, activity_id: any) {
for (let i = 0; i < course.chapters.length; i++) { for (let i = 0; i < course.chapters.length; i++) {
@ -46,6 +56,26 @@ function ActivityClient(props: ActivityClientProps) {
return null // return null if no matching activity is found return null // return null if no matching activity is found
} }
async function getAssignmentUI() {
const assignment = await getAssignmentFromActivityUUID(activity.activity_uuid, access_token)
setAssignment(assignment.data)
}
useEffect(() => {
if (activity.activity_type == 'TYPE_DYNAMIC') {
setBgColor('bg-white nice-shadow');
}
else if (activity.activity_type == 'TYPE_ASSIGNMENT') {
setMarkStatusButtonActive(false);
setBgColor('bg-white nice-shadow');
getAssignmentUI();
}
else {
setBgColor('bg-zinc-950');
}
}
, [activity])
return ( return (
<> <>
<CourseProvider courseuuid={course?.course_uuid}> <CourseProvider courseuuid={course?.course_uuid}>
@ -93,24 +123,26 @@ function ActivityClient(props: ActivityClientProps) {
</div> </div>
<div className="flex space-x-1 items-center"> <div className="flex space-x-1 items-center">
<AuthenticatedClientElement checkMethod="authentication"> <AuthenticatedClientElement checkMethod="authentication">
<AIActivityAsk activity={activity} /> {activity.activity_type != 'TYPE_ASSIGNMENT' &&
<MoreVertical size={17} className="text-gray-300 " /> <>
<MarkStatus <AIActivityAsk activity={activity} />
activity={activity} <MoreVertical size={17} className="text-gray-300 " />
activityid={activityid} <MarkStatus
course={course} activity={activity}
orgslug={orgslug} activityid={activityid}
/> course={course}
orgslug={orgslug}
/>
</>
}
</AuthenticatedClientElement> </AuthenticatedClientElement>
</div> </div>
</div> </div>
{activity ? ( {activity ? (
<div <div
className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC' className={`p-7 drop-shadow-sm rounded-lg ${bgColor}`}
? 'bg-white'
: 'bg-zinc-950'
}`}
> >
<div> <div>
{activity.activity_type == 'TYPE_DYNAMIC' && ( {activity.activity_type == 'TYPE_DYNAMIC' && (
@ -126,6 +158,19 @@ function ActivityClient(props: ActivityClientProps) {
activity={activity} activity={activity}
/> />
)} )}
{activity.activity_type == 'TYPE_ASSIGNMENT' && (
<div>
{assignment ? (
<AssignmentProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentsTaskProvider>
<AssignmentStudentActivity />
</AssignmentsTaskProvider>
</AssignmentProvider>
) : (
<div></div>
)}
</div>
)}
</div> </div>
</div> </div>
) : ( ) : (

View file

@ -16,7 +16,7 @@ function AssignmentTaskContentEdit() {
return ( return (
<div> <div>
{assignment_task?.assignmentTask.assignment_type === 'QUIZ' && <TaskQuizObject />} {assignment_task?.assignmentTask.assignment_type === 'QUIZ' && <TaskQuizObject view='teacher' />}
{assignment_task?.assignmentTask.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject view='teacher' />} {assignment_task?.assignmentTask.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject view='teacher' />}
</div> </div>
) )

View file

@ -1,14 +1,146 @@
import AssignmentBoxUI from '@components/Objects/Assignments/AssignmentBoxUI' import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { Info } from 'lucide-react' import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import React from 'react' import { useLHSession } from '@components/Contexts/LHSessionContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
import { getAssignmentTask, updateSubFile } from '@services/courses/assignments';
import { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react'
import React, { useEffect } from 'react'
import toast from 'react-hot-toast';
type FileSchema = {
fileID: string;
};
type TaskFileObjectProps = {
view: 'teacher' | 'student';
assignmentTaskUUID?: string;
};
export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObjectProps) {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const [isLoading, setIsLoading] = React.useState(false);
const [localUploadFile, setLocalUploadFile] = React.useState<File | null>(null);
const [error, setError] = React.useState<string | null>(null);
const [assignmentTask, setAssignmentTask] = React.useState<any>(null);
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
const assignment = useAssignments() as any;
const handleFileChange = async (event: any) => {
const file = event.target.files[0]
setLocalUploadFile(file)
setIsLoading(true)
const res = await updateSubFile(
file,
assignmentTask.assignment_task_uuid,
assignment.assignment_object.assignment_uuid,
access_token
)
assignmentTaskStateHook({ type: 'reload' })
// wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) {
setError(res.data.detail)
setIsLoading(false)
} else {
setIsLoading(false)
setError('')
}
}
async function getAssignmentTaskUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
if (res.success) {
setAssignmentTask(res.data);
}
}
}
useEffect(() => {
getAssignmentTaskUI()
}
, [assignmentTaskUUID])
export default function TaskFileObject({ view }: any) {
return ( return (
<AssignmentBoxUI view={view} type="file"> <AssignmentBoxUI view={view} type="file">
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'> {view === 'teacher' && (
<Info size={20} /> <div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<p>User will be able to submit a file for this task, you'll be able to review it in the Submissions Tab</p> <Info size={20} />
</div> <p>User will be able to submit a file for this task, you'll be able to review it in the Submissions Tab</p>
</div>
)}
{view === 'student' && (
<>
<div className="w-auto bg-white rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow">
<div className="flex flex-col justify-center items-center h-full">
<div className="flex flex-col justify-center items-center">
<div className="flex flex-col justify-center items-center">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
<div className="text-sm font-semibold">{error}</div>
</div>
)}
</div>
{localUploadFile && !isLoading && (
<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'>
{localUploadFile.name}
</div>
</div>
</div>
)}
<div className='flex pt-4 font-semibold space-x-1.5 text-xs items-center text-gray-500 '>
<Info size={16} />
<p>Allowed formats : pdf, docx, mp4, jpg, jpeg, pptx</p>
</div>
{isLoading ? (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<div className="font-bold animate-pulse antialiased items-center bg-slate-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
<Loader size={16} className="mr-2" />
<span>Loading</span>
</div>
</div>
) : (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
onClick={() => document.getElementById('fileInput')?.click()}
>
<UploadCloud size={16} className="mr-2" />
<span>Submit File</span>
</button>
</div>
)}
</div>
</div>
</div>
</>
)}
</AssignmentBoxUI> </AssignmentBoxUI>
) )
} }

View file

@ -1,8 +1,8 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
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/Assignments/AssignmentBoxUI'; import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI';
import { updateAssignmentTask } from '@services/courses/assignments'; import { getAssignmentTask, 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';
@ -20,14 +20,19 @@ type QuizSchema = {
}[]; }[];
}; };
function TaskQuizObject() { type TaskQuizObjectProps = {
view: 'teacher' | 'student';
assignmentTaskUUID?: string;
};
function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
const session = useLHSession() as any; const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;
const assignmentTaskState = useAssignmentsTask() as any; const assignmentTaskState = useAssignmentsTask() as any;
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any; const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
const assignment = useAssignments() as any; const assignment = useAssignments() as any;
// Teacher area /* TEACHER VIEW CODE */
const [questions, setQuestions] = useState<QuizSchema[]>([ const [questions, setQuestions] = useState<QuizSchema[]>([
{ questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] }, { questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] },
]); ]);
@ -92,99 +97,136 @@ function TaskQuizObject() {
toast.error('Error saving task, please retry later.'); toast.error('Error saving task, please retry later.');
} }
}; };
/* TEACHER VIEW CODE */
/* STUDENT VIEW CODE */
async function getAssignmentTaskUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
if (res.success) {
setQuestions(res.data.contents.questions);
}
}
}
/* STUDENT VIEW CODE */
useEffect(() => { useEffect(() => {
if (assignmentTaskState.assignmentTask.contents?.questions) { assignmentTaskStateHook({
setSelectedAssignmentTaskUUID: assignmentTaskUUID,
});
// Teacher area
if (view == 'teacher' && assignmentTaskState.assignmentTask.contents?.questions) {
setQuestions(assignmentTaskState.assignmentTask.contents.questions); setQuestions(assignmentTaskState.assignmentTask.contents.questions);
} }
}, [assignmentTaskState,assignment,assignmentTaskStateHook,access_token]); // Student area
else if (view == 'student') {
getAssignmentTaskUI();
}
}, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token]);
// Teacher area end
return ( return (
<AssignmentBoxUI saveFC={saveFC} view='teacher' type="quiz"> <AssignmentBoxUI saveFC={saveFC} view={view} type="quiz">
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
{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">
<div className="flex space-x-2 items-center"> <div className="flex space-x-2 items-center">
<input {view === 'teacher' ? (
value={question.questionText} <input
onChange={(e) => handleQuestionChange(qIndex, e.target.value)} value={question.questionText}
placeholder="Question" onChange={(e) => handleQuestionChange(qIndex, e.target.value)}
className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold" placeholder="Question"
/> className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"
<div />
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer" ) : (
onClick={() => removeQuestion(qIndex)} <p className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold">
> {question.questionText}
<Minus size={12} className="mx-auto" /> </p>
</div> )}
{view === 'teacher' && (
<div
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
onClick={() => removeQuestion(qIndex)}
>
<Minus size={12} className="mx-auto" />
</div>
)}
</div> </div>
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
{question.options.map((option, oIndex) => ( {question.options.map((option, oIndex) => (
<div className="flex" key={oIndex}> <div className="flex" key={oIndex}>
<div <div
key={oIndex}
className="answer outline outline-3 outline-white pr-2 shadow w-full flex items-center space-x-2 h-[30px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm duration-150 cursor-pointer ease-linear nice-shadow" className="answer outline outline-3 outline-white pr-2 shadow w-full flex items-center space-x-2 h-[30px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm duration-150 cursor-pointer ease-linear nice-shadow"
> >
<div className="font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800 bg-slate-100/80"> <div className="font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800 bg-slate-100/80">
<p className="mx-auto font-bold text-sm">{String.fromCharCode(65 + oIndex)}</p> <p className="mx-auto font-bold text-sm">{String.fromCharCode(65 + oIndex)}</p>
</div> </div>
<input {view === 'teacher' ? (
type="text" <input
value={option.text} type="text"
onChange={(e) => handleOptionChange(qIndex, oIndex, e.target.value)} value={option.text}
placeholder="Option" onChange={(e) => handleOptionChange(qIndex, oIndex, e.target.value)}
className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold" placeholder="Option"
/> className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"
<div />
className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.correct ? 'bg-lime-200 text-lime-600' : 'bg-rose-200/60 text-rose-500' ) : (
} hover:bg-lime-300 text-sm transition-all ease-linear cursor-pointer`} <p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] text-sm font-bold">
onClick={() => toggleCorrectOption(qIndex, oIndex)} {option.text}
> </p>
{option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />} )}
{option.correct ? ( {view === 'teacher' && (
<p className="mx-auto font-bold text-xs">Correct</p> <>
) : (
<p className="mx-auto font-bold text-xs">Incorrect</p>
)}
</div>
<div
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
onClick={() => removeOption(qIndex, oIndex)}
>
<Minus size={12} className="mx-auto" />
</div>
</div>
<div>
{/* Show this at the last option */}
{oIndex === question.options.length - 1 && (
<div className="flex justify-center mx-auto px-2">
<div <div
className="outline text-xs outline-3 outline-white px-2 shadow w-full flex items-center h-[30px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white duration-150 cursor-pointer ease-linear nice-shadow" className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.correct ? 'bg-lime-200 text-lime-600' : 'bg-rose-200/60 text-rose-500'
onClick={() => addOption(qIndex)} } hover:bg-lime-300 text-sm transition-all ease-linear cursor-pointer`}
onClick={() => toggleCorrectOption(qIndex, oIndex)}
> >
<Plus size={14} className="inline-block" /> {option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
<span></span> {option.correct ? (
<p className="mx-auto font-bold text-xs">Correct</p>
) : (
<p className="mx-auto font-bold text-xs">Incorrect</p>
)}
</div> </div>
</div> <div
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
onClick={() => removeOption(qIndex, oIndex)}
>
<Minus size={12} className="mx-auto" />
</div>
</>
)} )}
</div> </div>
{view === 'teacher' && oIndex === question.options.length - 1 && (
<div className="flex justify-center mx-auto px-2">
<div
className="outline text-xs outline-3 outline-white px-2 shadow w-full flex items-center h-[30px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white duration-150 cursor-pointer ease-linear nice-shadow"
onClick={() => addOption(qIndex)}
>
<Plus size={14} className="inline-block" />
<span></span>
</div>
</div>
)}
</div> </div>
))} ))}
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="flex justify-center mx-auto px-2"> {view === 'teacher' && (
<div <div className="flex justify-center mx-auto px-2">
className="flex w-full my-2 py-2 px-4 bg-white text-slate text-xs rounded-md nice-shadow hover:shadow-sm cursor-pointer space-x-3 items-center transition duration-150 ease-linear" <div
onClick={addQuestion} className="flex w-full my-2 py-2 px-4 bg-white text-slate text-xs rounded-md nice-shadow hover:shadow-sm cursor-pointer space-x-3 items-center transition duration-150 ease-linear"
> onClick={addQuestion}
<PlusCircle size={14} className="inline-block" /> >
<span>Add Question</span> <PlusCircle size={14} className="inline-block" />
<span>Add Question</span>
</div>
</div> </div>
</div> )}
</AssignmentBoxUI> </AssignmentBoxUI>
); );
} }

View file

@ -1,10 +1,9 @@
'use client'; 'use client';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { BookOpen, BookX, EllipsisVertical, LayoutList } from 'lucide-react' import { BookOpen, BookX, EllipsisVertical, Eye, LayoutList } from 'lucide-react'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext'; import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import AssignmentTasks from './_components/Tasks'; import AssignmentTasks from './_components/Tasks';
import { useParams } from 'next/navigation';
import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'; import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
import AssignmentTaskEditor from './_components/TaskEditor/TaskEditor'; import AssignmentTaskEditor from './_components/TaskEditor/TaskEditor';
@ -13,6 +12,8 @@ import { useLHSession } from '@components/Contexts/LHSessionContext';
import { mutate } from 'swr'; import { mutate } from 'swr';
import { getAPIUrl } from '@services/config/config'; import { getAPIUrl } from '@services/config/config';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import Link from 'next/link';
import { useParams } from 'next/navigation';
function AssignmentEdit() { function AssignmentEdit() {
const params = useParams<{ assignmentuuid: string; }>() const params = useParams<{ assignmentuuid: string; }>()
@ -83,7 +84,7 @@ function PublishingState() {
} }
useEffect(() => { useEffect(() => {
console.log('assignment', assignment?.assignment_object?.assignment_uuid) console.log('assignment', assignment)
}, [assignment]) }, [assignment])
return ( return (
@ -104,6 +105,19 @@ function PublishingState() {
<p className='text-sm font-bold'>Unpublish</p> <p className='text-sm font-bold'>Unpublish</p>
</div> </div>
</ToolTip>} </ToolTip>}
<ToolTip
side='left'
slateBlack
sideOffset={10}
content="Preview the Assignment as a student" >
<Link
target='_blank'
href={`/course/${assignment?.course_object?.course_uuid.replace('course_', '')}/activity/${assignment?.activity_object?.activity_uuid.replace('activity_', '')}`}
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-cyan-800 font-medium from-sky-400/50 to-cyan-200/80 border border-cyan-600/10 shadow-cyan-900/10 shadow-lg'>
<Eye size={18} />
<p className=' text-sm font-bold'>Preview</p>
</Link>
</ToolTip>
{!assignment?.assignment_object?.published && {!assignment?.assignment_object?.published &&
<ToolTip <ToolTip
side='left' side='left'

View file

@ -22,7 +22,6 @@ export function AssignmentProvider({ children, assignment_uuid }: { children: Re
(url) => swrFetcher(url, accessToken) (url) => swrFetcher(url, accessToken)
) )
// Define a key for the course object based on assignment data
const course_id = assignment?.course_id const course_id = assignment?.course_id
const { data: course_object, error: courseObjectError } = useSWR( const { data: course_object, error: courseObjectError } = useSWR(
@ -38,12 +37,14 @@ export function AssignmentProvider({ children, assignment_uuid }: { children: Re
) )
useEffect(() => { useEffect(() => {
setAssignmentsFull({ assignment_object: assignment, assignment_tasks: assignment_tasks, course_object: course_object, activity_object: activity_object }) if (assignment && assignment_tasks && (!course_id || course_object) && (!activity_id || activity_object)) {
}, [assignment, assignment_tasks, course_object, activity_object]) setAssignmentsFull({ assignment_object: assignment, assignment_tasks: assignment_tasks, course_object: course_object, activity_object: activity_object })
}
}, [assignment, assignment_tasks, course_object, activity_object, course_id, activity_id])
if (assignmentError || assignmentTasksError || courseObjectError) return <div></div> if (assignmentError || assignmentTasksError || courseObjectError || activityObjectError) return <div></div>
if (!assignment || !assignment_tasks || (course_id && !course_object)) return <div></div> if (!assignment || !assignment_tasks || (course_id && !course_object) || (activity_id && !activity_object)) return <div></div>
return <AssignmentContext.Provider value={assignmentsFull}>{children}</AssignmentContext.Provider> return <AssignmentContext.Provider value={assignmentsFull}>{children}</AssignmentContext.Provider>
} }

View file

@ -1,17 +1,18 @@
import { BookUser, EllipsisVertical, FileUp, ListTodo, Save } from 'lucide-react' import { BookUser, EllipsisVertical, FileUp, Forward, ListTodo, Save } from 'lucide-react'
import React from 'react' import React from 'react'
type AssignmentBoxProps = { type AssignmentBoxProps = {
type: 'quiz' | 'file' type: 'quiz' | 'file'
view?: 'teacher' | 'student' view?: 'teacher' | 'student'
saveFC?: () => void saveFC?: () => void
submitFC?: () => void
children: React.ReactNode children: React.ReactNode
} }
function AssignmentBoxUI({ type, view, saveFC, children }: AssignmentBoxProps) { function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentBoxProps) {
return ( return (
<div className='flex flex-col px-4 py-2 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'>
<div className='flex space-x-1 items-center'> <div className='flex space-x-1 items-center'>
<div className='text-lg font-semibold'> <div className='text-lg font-semibold'>
@ -20,13 +21,13 @@ function AssignmentBoxUI({ type, view, saveFC, children }: AssignmentBoxProps) {
<ListTodo size={17} /> <ListTodo size={17} />
<p>Quiz</p> <p>Quiz</p>
</div>} </div>}
{type === 'file' && {type === 'file' &&
<div className='flex space-x-1.5 items-center'> <div className='flex space-x-1.5 items-center'>
<FileUp size={17} /> <FileUp size={17} />
<p>File Submission</p> <p>File Submission</p>
</div>} </div>}
</div> </div>
<div className='flex items-center space-x-1'> <div className='flex items-center space-x-1'>
<EllipsisVertical size={15} /> <EllipsisVertical size={15} />
@ -41,12 +42,22 @@ function AssignmentBoxUI({ type, view, saveFC, children }: AssignmentBoxProps) {
<div className='flex px-1 py-1 rounded-md items-center'> <div className='flex px-1 py-1 rounded-md items-center'>
{/* Save button */} {/* Save button */}
<div {view === 'teacher' &&
onClick={() => saveFC && saveFC()} <div
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 '> onClick={() => saveFC && saveFC()}
<Save size={14} /> 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 '>
<p className='text-xs font-semibold'>Save</p> <Save size={14} />
</div> <p className='text-xs font-semibold'>Save</p>
</div>
}
{view === 'student' &&
<div
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 '>
<Forward size={14} />
<p className='text-xs font-semibold'>Save</p>
</div>
}
</div> </div>
</div> </div>

View file

@ -0,0 +1,89 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useCourse } from '@components/Contexts/CourseContext';
import { useOrg } from '@components/Contexts/OrgContext';
import { getTaskRefFileDir } from '@services/media/media';
import TaskFileObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject';
import TaskQuizObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject'
import { Backpack, Calendar, Download, EllipsisVertical, Info } from 'lucide-react';
import Link from 'next/link';
import React, { useEffect } from 'react'
function AssignmentStudentActivity() {
const assignments = useAssignments() as any;
const course = useCourse() as any;
const org = useOrg() as any;
useEffect(() => {
console.log(assignments)
}, [assignments, org])
return (
<div className='flex flex-col space-y-6'>
<div className='flex flex-row justify-center space-x-3 items-center '>
<div className='text-xs h-fit flex space-x-3 items-center '>
<div className='flex space-x-2 py-2 px-5 h-fit text-sm text-slate-700 bg-slate-100/5 rounded-full nice-shadow'>
<Backpack size={18} />
<p className='font-semibold'>Assignment</p>
</div>
</div>
<div>
<div className='flex space-x-2 items-center'>
<EllipsisVertical className='text-slate-400' size={18} />
<div className='flex space-x-2 items-center'>
<div className='flex space-x-2 text-xs items-center text-slate-400'>
<Calendar size={14} />
<p className=' font-semibold'>Due Date</p>
<p className=' font-semibold'>{assignments?.assignment_object?.due_date}</p>
</div>
</div>
</div>
</div>
</div>
<div className='w-full rounded-full bg-slate-500/5 nice-shadow h-[2px]'></div>
{assignments && assignments?.assignment_tasks?.sort((a: any, b: any) => a.id - b.id).map((task: any, index: number) => {
return (
<div className='flex flex-col space-y-2' key={task.assignment_task_uuid}>
<div className='flex justify-between py-2'>
<div className='flex space-x-2 font-semibold text-slate-800'>
<p>Task {index + 1} : </p>
<p className='text-slate-500'>{task.description}</p>
</div>
<div className='flex space-x-2'>
<div
onClick={() => alert(task.hint)}
className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'>
<Info size={13} />
<p className='text-xs font-semibold'>View Hint</p>
</div>
<Link
href={getTaskRefFileDir(
org?.org_uuid,
assignments?.course_object.course_uuid,
assignments?.activity_object.activity_uuid,
assignments?.assignment_object.assignment_uuid,
task.assignment_task_uuid,
task.reference_file
)}
target='_blank'
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'>
<Download size={13} />
<p className='text-xs font-semibold'>Reference file</p>
</Link>
</div>
</div>
<div>
{task.assignment_type === 'QUIZ' && <TaskQuizObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />}
{task.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />}
</div>
</div>
)
})}
</div>
)
}
export default AssignmentStudentActivity

View file

@ -138,3 +138,25 @@ export async function updateReferenceFile(
const res = await getResponseMetadata(result) const res = await getResponseMetadata(result)
return res return res
} }
export async function updateSubFile(
file: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
// Send file thumbnail as form data
const formData = new FormData()
if (file) {
formData.append('sub_file', file)
}
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/sub_file`,
RequestBodyFormWithAuthHeader('POST', formData, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}