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_submission,
put_assignment_task_reference_file,
put_assignment_task_submission_file,
read_assignment,
read_assignment_from_activity_uuid,
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
)
@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}")
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_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 (
upload_reference_file,
)
@ -493,6 +494,68 @@ async def put_assignment_task_reference_file(
# return assignment task read
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(
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 AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext'
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 {
activityid: string
@ -32,6 +37,11 @@ function ActivityClient(props: ActivityClientProps) {
const activity = props.activity
const course = props.course
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) {
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
}
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 (
<>
<CourseProvider courseuuid={course?.course_uuid}>
@ -93,24 +123,26 @@ function ActivityClient(props: ActivityClientProps) {
</div>
<div className="flex space-x-1 items-center">
<AuthenticatedClientElement checkMethod="authentication">
<AIActivityAsk activity={activity} />
<MoreVertical size={17} className="text-gray-300 " />
<MarkStatus
activity={activity}
activityid={activityid}
course={course}
orgslug={orgslug}
/>
{activity.activity_type != 'TYPE_ASSIGNMENT' &&
<>
<AIActivityAsk activity={activity} />
<MoreVertical size={17} className="text-gray-300 " />
<MarkStatus
activity={activity}
activityid={activityid}
course={course}
orgslug={orgslug}
/>
</>
}
</AuthenticatedClientElement>
</div>
</div>
{activity ? (
<div
className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC'
? 'bg-white'
: 'bg-zinc-950'
}`}
className={`p-7 drop-shadow-sm rounded-lg ${bgColor}`}
>
<div>
{activity.activity_type == 'TYPE_DYNAMIC' && (
@ -126,6 +158,19 @@ function ActivityClient(props: ActivityClientProps) {
activity={activity}
/>
)}
{activity.activity_type == 'TYPE_ASSIGNMENT' && (
<div>
{assignment ? (
<AssignmentProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentsTaskProvider>
<AssignmentStudentActivity />
</AssignmentsTaskProvider>
</AssignmentProvider>
) : (
<div></div>
)}
</div>
)}
</div>
</div>
) : (

View file

@ -16,7 +16,7 @@ function AssignmentTaskContentEdit() {
return (
<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' />}
</div>
)

View file

@ -1,14 +1,146 @@
import AssignmentBoxUI from '@components/Objects/Assignments/AssignmentBoxUI'
import { Info } from 'lucide-react'
import React from 'react'
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
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 (
<AssignmentBoxUI view={view} type="file">
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Info size={20} />
<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 === 'teacher' && (
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Info size={20} />
<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>
)
}

View file

@ -1,8 +1,8 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import AssignmentBoxUI from '@components/Objects/Assignments/AssignmentBoxUI';
import { updateAssignmentTask } from '@services/courses/assignments';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI';
import { getAssignmentTask, updateAssignmentTask } from '@services/courses/assignments';
import { Check, Minus, Plus, PlusCircle, X } from 'lucide-react';
import React, { useEffect, useState } from 'react';
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 access_token = session?.data?.tokens?.access_token;
const assignmentTaskState = useAssignmentsTask() as any;
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
const assignment = useAssignments() as any;
// Teacher area
/* TEACHER VIEW CODE */
const [questions, setQuestions] = useState<QuizSchema[]>([
{ 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.');
}
};
/* 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(() => {
if (assignmentTaskState.assignmentTask.contents?.questions) {
assignmentTaskStateHook({
setSelectedAssignmentTaskUUID: assignmentTaskUUID,
});
// Teacher area
if (view == 'teacher' && 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 (
<AssignmentBoxUI saveFC={saveFC} view='teacher' type="quiz">
<AssignmentBoxUI saveFC={saveFC} view={view} type="quiz">
<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 className="flex space-x-2 items-center">
<input
value={question.questionText}
onChange={(e) => handleQuestionChange(qIndex, e.target.value)}
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)}
>
<Minus size={12} className="mx-auto" />
</div>
{view === 'teacher' ? (
<input
value={question.questionText}
onChange={(e) => handleQuestionChange(qIndex, e.target.value)}
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"
/>
) : (
<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}
</p>
)}
{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 className="flex flex-col space-y-2">
{question.options.map((option, oIndex) => (
<div className="flex" key={oIndex}>
<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"
>
<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>
</div>
<input
type="text"
value={option.text}
onChange={(e) => handleOptionChange(qIndex, oIndex, e.target.value)}
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`}
onClick={() => toggleCorrectOption(qIndex, oIndex)}
>
{option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
{option.correct ? (
<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">
{view === 'teacher' ? (
<input
type="text"
value={option.text}
onChange={(e) => handleOptionChange(qIndex, oIndex, e.target.value)}
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"
/>
) : (
<p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] text-sm font-bold">
{option.text}
</p>
)}
{view === 'teacher' && (
<>
<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)}
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`}
onClick={() => toggleCorrectOption(qIndex, oIndex)}
>
<Plus size={14} className="inline-block" />
<span></span>
{option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
{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
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>
{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 className="flex justify-center mx-auto px-2">
<div
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>
{view === 'teacher' && (
<div className="flex justify-center mx-auto px-2">
<div
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>
</div>
</div>
</div>
)}
</AssignmentBoxUI>
);
}

View file

@ -1,10 +1,9 @@
'use client';
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 { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import AssignmentTasks from './_components/Tasks';
import { useParams } from 'next/navigation';
import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
import AssignmentTaskEditor from './_components/TaskEditor/TaskEditor';
@ -13,6 +12,8 @@ import { useLHSession } from '@components/Contexts/LHSessionContext';
import { mutate } from 'swr';
import { getAPIUrl } from '@services/config/config';
import toast from 'react-hot-toast';
import Link from 'next/link';
import { useParams } from 'next/navigation';
function AssignmentEdit() {
const params = useParams<{ assignmentuuid: string; }>()
@ -83,7 +84,7 @@ function PublishingState() {
}
useEffect(() => {
console.log('assignment', assignment?.assignment_object?.assignment_uuid)
console.log('assignment', assignment)
}, [assignment])
return (
@ -104,6 +105,19 @@ function PublishingState() {
<p className='text-sm font-bold'>Unpublish</p>
</div>
</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 &&
<ToolTip
side='left'

View file

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

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'
type AssignmentBoxProps = {
type: 'quiz' | 'file'
view?: 'teacher' | 'student'
saveFC?: () => void
submitFC?: () => void
children: React.ReactNode
}
function AssignmentBoxUI({ type, view, saveFC, children }: AssignmentBoxProps) {
function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentBoxProps) {
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 space-x-1 items-center'>
<div className='text-lg font-semibold'>
@ -20,13 +21,13 @@ function AssignmentBoxUI({ type, view, saveFC, children }: AssignmentBoxProps) {
<ListTodo size={17} />
<p>Quiz</p>
</div>}
{type === 'file' &&
{type === 'file' &&
<div className='flex space-x-1.5 items-center'>
<FileUp size={17} />
<p>File Submission</p>
</div>}
</div>
<div className='flex items-center space-x-1'>
<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'>
{/* Save button */}
<div
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 '>
<Save size={14} />
<p className='text-xs font-semibold'>Save</p>
</div>
{view === 'teacher' &&
<div
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 '>
<Save size={14} />
<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>

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)
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
}