feat: edit tasks and general improvements

This commit is contained in:
swve 2024-07-14 14:06:25 +02:00
parent acfcea026b
commit 3c41e0ee73
13 changed files with 570 additions and 93 deletions

View file

@ -127,11 +127,6 @@ class AssignmentTaskUpdate(SQLModel):
assignment_type: Optional[AssignmentTaskTypeEnum] assignment_type: Optional[AssignmentTaskTypeEnum]
contents: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) contents: Optional[Dict] = Field(default={}, sa_column=Column(JSON))
max_grade_value: Optional[int] max_grade_value: Optional[int]
assignment_id: Optional[int]
org_id: Optional[int]
course_id: Optional[int]
chapter_id: Optional[int]
activity_id: Optional[int]
class AssignmentTask(AssignmentTaskBase, table=True): class AssignmentTask(AssignmentTaskBase, table=True):

View file

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request, UploadFile
from src.db.courses.assignments import ( from src.db.courses.assignments import (
AssignmentCreate, AssignmentCreate,
AssignmentRead, AssignmentRead,
@ -21,6 +21,7 @@ from src.services.courses.activities.assignments import (
delete_assignment_submission, delete_assignment_submission,
delete_assignment_task, delete_assignment_task,
delete_assignment_task_submission, delete_assignment_task_submission,
put_assignment_task_reference_file,
read_assignment, read_assignment,
read_assignment_from_activity_uuid, read_assignment_from_activity_uuid,
read_assignment_submissions, read_assignment_submissions,
@ -64,6 +65,7 @@ async def api_read_assignment(
""" """
return await read_assignment(request, assignment_uuid, current_user, db_session) return await read_assignment(request, assignment_uuid, current_user, db_session)
@router.get("/activity/{activity_uuid}") @router.get("/activity/{activity_uuid}")
async def api_read_assignment_from_activity( async def api_read_assignment_from_activity(
request: Request, request: Request,
@ -74,7 +76,9 @@ async def api_read_assignment_from_activity(
""" """
Read an assignment Read an assignment
""" """
return await read_assignment_from_activity_uuid(request, activity_uuid, current_user, db_session) return await read_assignment_from_activity_uuid(
request, activity_uuid, current_user, db_session
)
@router.put("/{assignment_uuid}") @router.put("/{assignment_uuid}")
@ -105,6 +109,7 @@ async def api_delete_assignment(
""" """
return await delete_assignment(request, assignment_uuid, current_user, db_session) return await delete_assignment(request, assignment_uuid, current_user, db_session)
@router.delete("/activity/{activity_uuid}") @router.delete("/activity/{activity_uuid}")
async def api_delete_assignment_from_activity( async def api_delete_assignment_from_activity(
request: Request, request: Request,
@ -115,7 +120,9 @@ async def api_delete_assignment_from_activity(
""" """
Delete an assignment Delete an assignment
""" """
return await delete_assignment_from_activity_uuid(request, activity_uuid, current_user, db_session) return await delete_assignment_from_activity_uuid(
request, activity_uuid, current_user, db_session
)
## ASSIGNMENTS Tasks ## ## ASSIGNMENTS Tasks ##
@ -166,7 +173,8 @@ async def api_read_assignment_task(
request, assignment_task_uuid, current_user, db_session request, assignment_task_uuid, current_user, db_session
) )
@router.put("/{assignment_uuid}/tasks/{task_uuid}")
@router.put("/{assignment_uuid}/tasks/{assignment_task_uuid}")
async def api_update_assignment_tasks( async def api_update_assignment_tasks(
request: Request, request: Request,
assignment_task_uuid: str, assignment_task_uuid: str,
@ -182,6 +190,22 @@ async def api_update_assignment_tasks(
) )
@router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/ref_file")
async def api_put_assignment_task_ref_file(
request: Request,
assignment_task_uuid: str,
reference_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_reference_file(
request, db_session, assignment_task_uuid, current_user, reference_file
)
@router.delete("/{assignment_uuid}/tasks/{task_uuid}") @router.delete("/{assignment_uuid}/tasks/{task_uuid}")
async def api_delete_assignment_tasks( async def api_delete_assignment_tasks(
request: Request, request: Request,

View file

@ -5,7 +5,7 @@
from datetime import datetime from datetime import datetime
from typing import Literal from typing import Literal
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request from fastapi import HTTPException, Request, UploadFile
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.courses.activities import Activity from src.db.courses.activities import Activity
@ -26,12 +26,16 @@ from src.db.courses.assignments import (
AssignmentUserSubmissionRead, AssignmentUserSubmissionRead,
) )
from src.db.courses.courses import Course from src.db.courses.courses import Course
from src.db.organizations import Organization
from src.db.users import AnonymousUser, PublicUser from src.db.users import AnonymousUser, PublicUser
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_based_on_roles_and_authorship_and_usergroups,
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.tasks_ref_files import (
upload_reference_file,
)
## > Assignments CRUD ## > Assignments CRUD
@ -104,6 +108,7 @@ async def read_assignment(
# return assignment read # return assignment read
return AssignmentRead.model_validate(assignment) return AssignmentRead.model_validate(assignment)
async def read_assignment_from_activity_uuid( async def read_assignment_from_activity_uuid(
request: Request, request: Request,
activity_uuid: str, activity_uuid: str,
@ -227,6 +232,7 @@ async def delete_assignment(
return {"message": "Assignment deleted"} return {"message": "Assignment deleted"}
async def delete_assignment_from_activity_uuid( async def delete_assignment_from_activity_uuid(
request: Request, request: Request,
activity_uuid: str, activity_uuid: str,
@ -317,7 +323,7 @@ async def create_assignment_task(
assignment_task.org_id = course.org_id assignment_task.org_id = course.org_id
assignment_task.chapter_id = assignment.chapter_id assignment_task.chapter_id = assignment.chapter_id
assignment_task.activity_id = assignment.activity_id assignment_task.activity_id = assignment.activity_id
assignment_task.assignment_id = assignment.id # type: ignore assignment_task.assignment_id = assignment.id # type: ignore
assignment_task.course_id = assignment.course_id assignment_task.course_id = assignment.course_id
# Insert Assignment Task in DB # Insert Assignment Task in DB
@ -369,6 +375,7 @@ async def read_assignment_tasks(
for assignment_task in db_session.exec(statement).all() for assignment_task in db_session.exec(statement).all()
] ]
async def read_assignment_task( async def read_assignment_task(
request: Request, request: Request,
assignment_task_uuid: str, assignment_task_uuid: str,
@ -376,7 +383,9 @@ async def read_assignment_task(
db_session: Session, db_session: Session,
): ):
# Find assignment # Find assignment
statement = select(AssignmentTask).where(AssignmentTask.assignment_task_uuid == assignment_task_uuid) statement = select(AssignmentTask).where(
AssignmentTask.assignment_task_uuid == assignment_task_uuid
)
assignmenttask = db_session.exec(statement).first() assignmenttask = db_session.exec(statement).first()
if not assignmenttask: if not assignmenttask:
@ -412,6 +421,79 @@ async def read_assignment_task(
return AssignmentTaskRead.model_validate(assignmenttask) return AssignmentTaskRead.model_validate(assignmenttask)
async def put_assignment_task_reference_file(
request: Request,
db_session: Session,
assignment_task_uuid: str,
current_user: PublicUser | AnonymousUser,
reference_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, "update", db_session)
# Upload reference file
if reference_file and reference_file.filename and activity and org:
name_in_disk = (
f"{assignment_task_uuid}{uuid4()}.{reference_file.filename.split('.')[-1]}"
)
await upload_reference_file(
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
# Update reference file
assignment_task.reference_file = name_in_disk
assignment_task.update_date = str(datetime.now())
# Insert Assignment Task in DB
db_session.add(assignment_task)
db_session.commit()
db_session.refresh(assignment_task)
# return assignment task read
return AssignmentTaskRead.model_validate(assignment_task)
async def update_assignment_task( async def update_assignment_task(
request: Request, request: Request,
assignment_task_uuid: str, assignment_task_uuid: str,

View file

@ -0,0 +1,24 @@
from uuid import uuid4
from src.services.utils.upload_content import upload_content
async def upload_reference_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}",
"orgs",
org_uuid,
contents,
f"{name_in_disk}",
["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx"],
)

View file

@ -1,24 +1,37 @@
from typing import Literal from typing import Literal, Optional
import boto3 import boto3
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
import os import os
from fastapi import HTTPException
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
async def upload_content( async def upload_content(
directory: str, directory: str,
type_of_dir: Literal["orgs", "users"], type_of_dir: Literal["orgs", "users"],
uuid: str, # org_uuid or user_uuid uuid: str, # org_uuid or user_uuid
file_binary: bytes, file_binary: bytes,
file_and_format: str, file_and_format: str,
allowed_formats: Optional[list[str]] = None,
): ):
# Get Learnhouse Config # Get Learnhouse Config
learnhouse_config = get_learnhouse_config() learnhouse_config = get_learnhouse_config()
file_format = file_and_format.split(".")[-1].strip().lower()
# Get content delivery method # Get content delivery method
content_delivery = learnhouse_config.hosting_config.content_delivery.type content_delivery = learnhouse_config.hosting_config.content_delivery.type
# Check if format file is allowed
if allowed_formats:
if file_format not in allowed_formats:
raise HTTPException(
status_code=400,
detail=f"File format {file_format} not allowed",
)
if content_delivery == "filesystem": if content_delivery == "filesystem":
# create folder for activity # create folder for activity
if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"): if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"):

View file

@ -58,7 +58,7 @@ function NewTaskModal({ closeModal, assignment_uuid }: any) {
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'> <div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
<FileUp size={30} /> <FileUp size={30} />
</div> </div>
<p className='text-xl text-gray-700 font-semibold'>File submissions</p> <p className='text-xl text-gray-700 font-semibold'>File submission</p>
<p className='text-sm text-gray-500 w-40'>Students can submit files for this task</p> <p className='text-sm text-gray-500 w-40'>Students can submit files for this task</p>
</div> </div>
<div <div
@ -67,7 +67,7 @@ function NewTaskModal({ closeModal, assignment_uuid }: any) {
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'> <div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
<AArrowUp size={30} /> <AArrowUp size={30} />
</div> </div>
<p className='text-xl text-gray-700 font-semibold'>Forms</p> <p className='text-xl text-gray-700 font-semibold'>Form</p>
<p className='text-sm text-gray-500 w-40'>Forms for students to fill out</p> <p className='text-sm text-gray-500 w-40'>Forms for students to fill out</p>
</div> </div>
</div> </div>

View file

@ -1,7 +1,17 @@
'use client'; 'use client';
import { useAssignmentsTask } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { Info, TentTree } from 'lucide-react' import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import React, { useEffect } from 'react' import { useLHSession } from '@components/Contexts/LHSessionContext';
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
import * as Form from '@radix-ui/react-form';
import { getActivity } from '@services/courses/activities';
import { updateAssignmentTask, updateReferenceFile } from '@services/courses/assignments';
import { getTaskRefFileDir } from '@services/media/media';
import { useFormik } from 'formik';
import { ArrowBigUpDash, Cloud, File, GalleryVerticalEnd, Info, Loader, TentTree, Upload, UploadCloud } from 'lucide-react'
import Link from 'next/link';
import React, { use, useEffect } from 'react'
import toast from 'react-hot-toast';
function AssignmentTaskEditor({ page }: any) { function AssignmentTaskEditor({ page }: any) {
const [selectedSubPage, setSelectedSubPage] = React.useState(page) const [selectedSubPage, setSelectedSubPage] = React.useState(page)
@ -15,23 +25,41 @@ function AssignmentTaskEditor({ page }: any) {
return ( return (
<div className="flex flex-col font-black text-sm w-full z-20"> <div className="flex flex-col font-black text-sm w-full z-20">
{assignmentTaskState.assignmentTask && Object.keys(assignmentTaskState.assignmentTask).length > 0 && ( {assignmentTaskState.assignmentTask && Object.keys(assignmentTaskState.assignmentTask).length > 0 && (
<div className='flex flex-col bg-white pl-10 pr-10 text-sm tracking-tight z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)] pt-5'> <div className='flex flex-col space-y-3'>
<div className='font-semibold text-lg py-1'> <div className='flex flex-col bg-white pl-10 pr-10 text-sm tracking-tight z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)] pt-5 mb-3 nice-shadow'>
Assignment Test #1 <div className='font-semibold text-lg py-1'>
</div> {assignmentTaskState?.assignmentTask.title}
<div className='flex space-x-2 '> </div>
<div <div className='flex space-x-2 '>
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'overview' <div
? 'border-b-4' onClick={() => setSelectedSubPage('general')}
: 'opacity-50' className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'general'
} cursor-pointer`} ? 'border-b-4'
> : 'opacity-50'
<div className="flex items-center space-x-2.5 mx-2"> } cursor-pointer`}
<Info size={16} /> >
<div>Overview</div> <div className="flex items-center space-x-2.5 mx-2">
<Info size={16} />
<div>General</div>
</div>
</div>
<div
onClick={() => setSelectedSubPage('content')}
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'content'
? 'border-b-4'
: 'opacity-50'
} cursor-pointer`}
>
<div className="flex items-center space-x-2.5 mx-2">
<GalleryVerticalEnd size={16} />
<div>Content</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className='ml-10 mr-10 mt-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5 nice-shadow'>
{selectedSubPage === 'general' && <AssignmentTaskGeneralEdit />}
</div>
</div> </div>
)} )}
{Object.keys(assignmentTaskState.assignmentTask).length == 0 && ( {Object.keys(assignmentTaskState.assignmentTask).length == 0 && (
@ -51,4 +79,255 @@ function AssignmentTaskEditor({ page }: any) {
) )
} }
function AssignmentTaskGeneralEdit() {
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
const validate = (values: any) => {
const errors: any = {};
if (values.max_grade_value < 20 || values.max_grade_value > 100) {
errors.max_grade_value = 'Value should be between 20 and 100';
}
return errors;
};
const formik = useFormik({
initialValues: {
title: assignmentTaskState.assignmentTask.title,
description: assignmentTaskState.assignmentTask.description,
hint: assignmentTaskState.assignmentTask.hint,
max_grade_value: assignmentTaskState.assignmentTask.max_grade_value,
},
validate,
onSubmit: async values => {
const res = await updateAssignmentTask(values, assignmentTaskState.assignmentTask.assignment_task_uuid, assignment.assignment_object.assignment_uuid, access_token)
if (res) {
assignmentTaskStateHook({ type: 'reload' })
}
else {
toast.error('Error updating task, please retry later.')
}
},
enableReinitialize: true,
}) as any;
return (
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="title">
<FormLabelAndMessage label="Title" message={formik.errors.title} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.title}
type="text"
/>
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label="Description" message={formik.errors.description} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.description}
type="text"
/>
</Form.Control>
</FormField>
<FormField name="hint">
<FormLabelAndMessage label="Hint" message={formik.errors.hint} />
<Form.Control asChild>
<Textarea
onChange={formik.handleChange}
value={formik.values.hint}
/>
</Form.Control>
</FormField>
<FormField name="hint">
<div className='flex space-x-3 justify-between items-center'>
<FormLabelAndMessage label="Reference file" message={formik.errors.hint} />
<div className='flex 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>
</div>
<Form.Control asChild>
<UpdateTaskRef />
</Form.Control>
</FormField>
<FormField name="max_grade_value">
<FormLabelAndMessage label="Max Grade Value" message={formik.errors.max_grade_value} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.max_grade_value}
type="number"
/>
</Form.Control>
</FormField>
{/* Submit button */}
<Form.Submit >
<button
type="submit"
className="flex items-center justify-center w-full px-4 py-2 mt-4 font-semibold text-white bg-green-500 rounded-md hover:bg-green-600"
>
Submit
</button>
</Form.Submit>
</FormLayout>
)
}
function UpdateTaskRef() {
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
const [isLoading, setIsLoading] = React.useState(false)
const [error, setError] = React.useState('') as any
const [localRefFile, setLocalRefFile] = React.useState(null) as any
const [activity, setActivity] = React.useState('') as any
const handleFileChange = async (event: any) => {
const file = event.target.files[0]
setLocalRefFile(file)
setIsLoading(true)
const res = await updateReferenceFile(
file,
assignmentTaskState.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('')
}
}
const deleteReferenceFile = async () => {
setIsLoading(true)
const res = await updateReferenceFile(
'',
assignmentTaskState.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 getActivityUI() {
const res = await getActivity(assignment.assignment_object.activity_id, null, access_token)
console.log(res)
setActivity(res.data)
}
useEffect(() => {
getActivityUI()
console.log(assignment.assignment_object.assignment_uuid)
console.log(assignmentTaskState.assignmentTask.assignment_task_uuid)
}
, [assignmentTaskState])
return (
<div className="w-auto bg-gray-50 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>
{assignmentTaskState.assignmentTask.reference_file && (
<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>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{assignmentTaskState.assignmentTask.reference_file.split('.').pop()}
</div>
<div className='flex space-x-2 mt-2'>
<Link
href={''}
//href={getTaskRefFileDir(assignment.assignment_object.assignment_uuid, assignmentTaskState.assignmentTask.reference_file)}
className='bg-blue-500 text-white px-3 py-1 rounded-full text-xs font-semibold'>Download</Link>
{/** <button onClick={() => deleteReferenceFile()}
className='bg-red-500 text-white px-3 py-1 rounded-full text-xs font-semibold'>Delete</button> */}
</div>
</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>Change Reference File</span>
</button>
</div>
)}
</div>
</div>
</div>
)
}
export default AssignmentTaskEditor export default AssignmentTaskEditor

View file

@ -3,10 +3,11 @@ import Modal from '@components/StyledElements/Modal/Modal';
import { FileUp, ListTodo, PanelLeftOpen, Plus } from 'lucide-react'; import { FileUp, ListTodo, PanelLeftOpen, Plus } from 'lucide-react';
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import NewTaskModal from './Modals/NewTaskModal'; import NewTaskModal from './Modals/NewTaskModal';
import { useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext'; import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
function AssignmentTasks({ assignment_uuid }: any) { function AssignmentTasks({ assignment_uuid }: any) {
const assignments = useAssignments() as any; const assignments = useAssignments() as any;
const assignmentTask = useAssignmentsTask() as any;
const assignmentTaskHook = useAssignmentsTaskDispatch() as any; const assignmentTaskHook = useAssignmentsTaskDispatch() as any;
const [isNewTaskModalOpen, setIsNewTaskModalOpen] = React.useState(false) const [isNewTaskModalOpen, setIsNewTaskModalOpen] = React.useState(false)
@ -21,30 +22,7 @@ function AssignmentTasks({ assignment_uuid }: any) {
return ( return (
<div className='flex w-full'> <div className='flex w-full'>
<div className='flex flex-col space-y-3 mx-auto'> <div className='flex flex-col space-y-3 mx-auto'>
{assignments && assignments?.assignment_tasks?.map((task: any) => { {assignments && assignments?.assignment_tasks?.length < 10 && (<Modal
return (
<div
key={task.id}
className='flex flex-col w-[250px] nice-shadow bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.06)] p-3 rounded-md'
onClick={() => setSelectTask(task.assignment_task_uuid)}
>
<div className='flex items-center px-2 justify-between'>
<div className="flex space-x-3 items-center">
<div className='text-gray-500'>
{task.assignment_type === 'QUIZ' && <ListTodo size={15} />}
{task.assignment_type === 'FILE_SUBMISSION' && <FileUp size={15} />}
</div>
<div className='font-semibold text-sm'>{task.title}</div>
</div>
<button className="outline outline-1 outline-gray-200 hover:bg-slate-100/50 rounded-md text-gray-500 font-bold py-2 px-3 focus:bg-slate-100 ease-linear transition-all">
<PanelLeftOpen size={16} />
</button>
</div>
</div>
)
})}
<Modal
isDialogOpen={isNewTaskModalOpen} isDialogOpen={isNewTaskModalOpen}
onOpenChange={setIsNewTaskModalOpen} onOpenChange={setIsNewTaskModalOpen}
minHeight="sm" minHeight="sm"
@ -60,7 +38,31 @@ function AssignmentTasks({ assignment_uuid }: any) {
<p>Add Task</p> <p>Add Task</p>
</div> </div>
} }
/> />)}
{assignments && assignments?.assignment_tasks?.map((task: any) => {
return (
<div
key={task.id}
className='flex flex-col w-[250px] nice-shadow bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.06)] p-3 rounded-md'
onClick={() => setSelectTask(task.assignment_task_uuid)}
>
<div className='flex items-center px-2 justify-between'>
<div className="flex space-x-3 items-center">
<div className='text-gray-500'>
{task.assignment_type === 'QUIZ' && <ListTodo size={15} />}
{task.assignment_type === 'FILE_SUBMISSION' && <FileUp size={15} />}
</div>
<div className='font-semibold text-sm'>{task.title}</div>
</div>
<button className={`outline outline-1 outline-gray-200 ${task.assignment_task_uuid == assignmentTask.selectedAssignmentTaskUUID ? 'bg-slate-100' : ''} hover:bg-slate-100/50 rounded-md text-gray-500 font-bold py-2 px-3 ease-linear transition-all`}>
<PanelLeftOpen size={16} />
</button>
</div>
</div>
)
})}
</div> </div>

View file

@ -40,32 +40,32 @@ function AssignmentEdit() {
slateBlack slateBlack
sideOffset={10} sideOffset={10}
content="Make your Assignment unavailable for students" > content="Make your Assignment unavailable for students" >
<div 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'> <div 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} /> <BookX size={18} />
<p className='text-sm font-bold'>Unpublish</p> <p className='text-sm font-bold'>Unpublish</p>
</div> </div>
</ToolTip> </ToolTip>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<AssignmentsTaskProvider> <AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
<div className='flex w-[400px] flex-col h-full custom-dots-bg'> <AssignmentsTaskProvider>
<div className='flex mx-auto px-3.5 py-1 bg-neutral-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'> <div className='flex w-[400px] flex-col h-full custom-dots-bg'>
<LayoutList size={18} /> <div className='flex mx-auto px-3.5 py-1 bg-neutral-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'>
<p>Tasks</p> <LayoutList size={18} />
</div> <p>Tasks</p>
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}> </div>
<AssignmentTasks assignment_uuid={'assignment_' + params.assignmentuuid} /> <AssignmentTasks assignment_uuid={'assignment_' + params.assignmentuuid} />
</AssignmentProvider> </div>
</div> <div className='flex flex-grow bg-[#fefcfe] nice-shadow h-full w-full'>
<div className='flex flex-grow bg-[#fefcfe] nice-shadow h-full w-full'> <AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}> <AssignmentTaskEditor page='general' />
<AssignmentTaskEditor task_uuid='UUID' page='overview' /> </AssignmentProvider>
</AssignmentProvider> </div>
</div> </AssignmentsTaskProvider>
</AssignmentsTaskProvider> </AssignmentProvider>
</div> </div>
</div> </div>
) )

View file

@ -2,10 +2,14 @@
import React, { createContext, useContext, useEffect, useReducer } from 'react' import React, { createContext, useContext, useEffect, useReducer } from 'react'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { getAssignmentTask } from '@services/courses/assignments' import { getAssignmentTask } from '@services/courses/assignments'
import { useAssignments } from './AssignmentContext';
import { mutate } from 'swr';
import { getAPIUrl } from '@services/config/config';
interface State { interface State {
selectedAssignmentTaskUUID: string | null; selectedAssignmentTaskUUID: string | null;
assignmentTask: Record<string, any>; assignmentTask: Record<string, any>;
reloadTrigger: number;
} }
interface Action { interface Action {
@ -15,7 +19,8 @@ interface Action {
const initialState: State = { const initialState: State = {
selectedAssignmentTaskUUID: null, selectedAssignmentTaskUUID: null,
assignmentTask: {} assignmentTask: {},
reloadTrigger: 0,
}; };
export const AssignmentsTaskContext = createContext<State | undefined>(undefined); export const AssignmentsTaskContext = createContext<State | undefined>(undefined);
@ -24,21 +29,26 @@ export const AssignmentsTaskDispatchContext = createContext<React.Dispatch<Actio
export function AssignmentsTaskProvider({ children }: { children: React.ReactNode }) { export function AssignmentsTaskProvider({ children }: { children: React.ReactNode }) {
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 assignment = useAssignments() as any
const [state, dispatch] = useReducer(assignmentstaskReducer, initialState); const [state, dispatch] = useReducer(assignmentstaskReducer, initialState);
async function fetchAssignmentTask(assignmentTaskUUID: string) { async function fetchAssignmentTask(assignmentTaskUUID: string) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token); const res = await getAssignmentTask(assignmentTaskUUID, access_token);
if (res.success) { if (res.success) {
dispatch({ type: 'setAssignmentTask', payload: res }); dispatch({ type: 'setAssignmentTask', payload: res.data });
} }
} }
useEffect(() => { useEffect(() => {
if (state.selectedAssignmentTaskUUID) { if (state.selectedAssignmentTaskUUID) {
fetchAssignmentTask(state.selectedAssignmentTaskUUID); fetchAssignmentTask(state.selectedAssignmentTaskUUID);
mutate(`${getAPIUrl()}assignments/${assignment.assignment_object?.assignment_uuid}/tasks`);
} }
}, [state.selectedAssignmentTaskUUID]); }, [state.selectedAssignmentTaskUUID, state.reloadTrigger,assignment]);
return ( return (
<AssignmentsTaskContext.Provider value={state}> <AssignmentsTaskContext.Provider value={state}>
@ -72,9 +82,8 @@ function assignmentstaskReducer(state: State, action: Action): State {
case 'setAssignmentTask': case 'setAssignmentTask':
return { ...state, assignmentTask: action.payload }; return { ...state, assignmentTask: action.payload };
case 'reload': case 'reload':
return { ...state }; return { ...state, reloadTrigger: state.reloadTrigger + 1 };
default: default:
return state; return state;
} }
} }

View file

@ -58,7 +58,7 @@ export const inputStyles = {
borderRadius: 4, borderRadius: 4,
fontSize: 15, fontSize: 15,
color: '#7c7c7c', color: '#7c7c7c',
background: '#F9FAFB', background: '#fbfdff',
boxShadow: `0 0 0 1px #edeeef`, boxShadow: `0 0 0 1px #edeeef`,
'&:hover': { boxShadow: `0 0 0 1px #edeeef` }, '&:hover': { boxShadow: `0 0 0 1px #edeeef` },
'&:focus': { boxShadow: `0 0 0 2px #edeeef` }, '&:focus': { boxShadow: `0 0 0 2px #edeeef` },

View file

@ -1,5 +1,6 @@
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { import {
RequestBodyFormWithAuthHeader,
RequestBodyWithAuthHeader, RequestBodyWithAuthHeader,
getResponseMetadata, getResponseMetadata,
} from '@services/utils/ts/requests' } from '@services/utils/ts/requests'
@ -76,3 +77,38 @@ export async function getAssignmentTask(
const res = await getResponseMetadata(result) const res = await getResponseMetadata(result)
return res return res
} }
export async function updateAssignmentTask(
body: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}`,
RequestBodyWithAuthHeader('PUT', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function updateReferenceFile(
file: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
// Send file thumbnail as form data
const formData = new FormData()
if (file) {
formData.append('reference_file', file)
}
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/ref_file`,
RequestBodyFormWithAuthHeader('POST', formData, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}

View file

@ -45,6 +45,19 @@ export function getActivityBlockMediaDirectory(
} }
} }
export function getTaskRefFileDir(
orgUUID: string,
courseId: string,
activityId: string,
assignmentUUID: string,
assignmentTaskUUID: string,
fileID : string
) {
let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/activities/${activityId}/assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/${fileID}`
return uri
}
export function getActivityMediaDirectory( export function getActivityMediaDirectory(
orgUUID: string, orgUUID: string,
courseId: string, courseId: string,