mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: edit tasks and general improvements
This commit is contained in:
parent
acfcea026b
commit
3c41e0ee73
13 changed files with 570 additions and 93 deletions
|
|
@ -127,11 +127,6 @@ class AssignmentTaskUpdate(SQLModel):
|
|||
assignment_type: Optional[AssignmentTaskTypeEnum]
|
||||
contents: Optional[Dict] = Field(default={}, sa_column=Column(JSON))
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
||||
from src.db.courses.assignments import (
|
||||
AssignmentCreate,
|
||||
AssignmentRead,
|
||||
|
|
@ -21,6 +21,7 @@ from src.services.courses.activities.assignments import (
|
|||
delete_assignment_submission,
|
||||
delete_assignment_task,
|
||||
delete_assignment_task_submission,
|
||||
put_assignment_task_reference_file,
|
||||
read_assignment,
|
||||
read_assignment_from_activity_uuid,
|
||||
read_assignment_submissions,
|
||||
|
|
@ -64,6 +65,7 @@ async def api_read_assignment(
|
|||
"""
|
||||
return await read_assignment(request, assignment_uuid, current_user, db_session)
|
||||
|
||||
|
||||
@router.get("/activity/{activity_uuid}")
|
||||
async def api_read_assignment_from_activity(
|
||||
request: Request,
|
||||
|
|
@ -74,7 +76,9 @@ async def api_read_assignment_from_activity(
|
|||
"""
|
||||
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}")
|
||||
|
|
@ -105,6 +109,7 @@ async def api_delete_assignment(
|
|||
"""
|
||||
return await delete_assignment(request, assignment_uuid, current_user, db_session)
|
||||
|
||||
|
||||
@router.delete("/activity/{activity_uuid}")
|
||||
async def api_delete_assignment_from_activity(
|
||||
request: Request,
|
||||
|
|
@ -115,7 +120,9 @@ async def api_delete_assignment_from_activity(
|
|||
"""
|
||||
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 ##
|
||||
|
|
@ -166,7 +173,8 @@ async def api_read_assignment_task(
|
|||
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(
|
||||
request: Request,
|
||||
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}")
|
||||
async def api_delete_assignment_tasks(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi import HTTPException, Request, UploadFile
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from src.db.courses.activities import Activity
|
||||
|
|
@ -26,12 +26,16 @@ from src.db.courses.assignments import (
|
|||
AssignmentUserSubmissionRead,
|
||||
)
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.organizations import Organization
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.courses.activities.uploads.tasks_ref_files import (
|
||||
upload_reference_file,
|
||||
)
|
||||
|
||||
## > Assignments CRUD
|
||||
|
||||
|
|
@ -104,6 +108,7 @@ async def read_assignment(
|
|||
# return assignment read
|
||||
return AssignmentRead.model_validate(assignment)
|
||||
|
||||
|
||||
async def read_assignment_from_activity_uuid(
|
||||
request: Request,
|
||||
activity_uuid: str,
|
||||
|
|
@ -119,7 +124,7 @@ async def read_assignment_from_activity_uuid(
|
|||
status_code=404,
|
||||
detail="Activity not found",
|
||||
)
|
||||
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.id == activity.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
|
@ -129,7 +134,7 @@ async def read_assignment_from_activity_uuid(
|
|||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
|
||||
# Check if assignment exists
|
||||
statement = select(Assignment).where(Assignment.activity_id == activity.id)
|
||||
assignment = db_session.exec(statement).first()
|
||||
|
|
@ -227,6 +232,7 @@ async def delete_assignment(
|
|||
|
||||
return {"message": "Assignment deleted"}
|
||||
|
||||
|
||||
async def delete_assignment_from_activity_uuid(
|
||||
request: Request,
|
||||
activity_uuid: str,
|
||||
|
|
@ -243,7 +249,7 @@ async def delete_assignment_from_activity_uuid(
|
|||
status_code=404,
|
||||
detail="Activity not found",
|
||||
)
|
||||
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.id == activity.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
|
@ -253,7 +259,7 @@ async def delete_assignment_from_activity_uuid(
|
|||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
|
||||
# Check if assignment exists
|
||||
statement = select(Assignment).where(Assignment.activity_id == activity.id)
|
||||
assignment = db_session.exec(statement).first()
|
||||
|
|
@ -263,7 +269,7 @@ async def delete_assignment_from_activity_uuid(
|
|||
status_code=404,
|
||||
detail="Assignment not found",
|
||||
)
|
||||
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
|
|
@ -317,7 +323,7 @@ async def create_assignment_task(
|
|||
assignment_task.org_id = course.org_id
|
||||
assignment_task.chapter_id = assignment.chapter_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
|
||||
|
||||
# Insert Assignment Task in DB
|
||||
|
|
@ -369,6 +375,7 @@ async def read_assignment_tasks(
|
|||
for assignment_task in db_session.exec(statement).all()
|
||||
]
|
||||
|
||||
|
||||
async def read_assignment_task(
|
||||
request: Request,
|
||||
assignment_task_uuid: str,
|
||||
|
|
@ -376,7 +383,9 @@ async def read_assignment_task(
|
|||
db_session: Session,
|
||||
):
|
||||
# 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()
|
||||
|
||||
if not assignmenttask:
|
||||
|
|
@ -384,7 +393,7 @@ async def read_assignment_task(
|
|||
status_code=404,
|
||||
detail="Assignment Task not found",
|
||||
)
|
||||
|
||||
|
||||
# Check if assignment exists
|
||||
statement = select(Assignment).where(Assignment.id == assignmenttask.assignment_id)
|
||||
assignment = db_session.exec(statement).first()
|
||||
|
|
@ -394,7 +403,57 @@ async def read_assignment_task(
|
|||
status_code=404,
|
||||
detail="Assignment not found",
|
||||
)
|
||||
|
||||
|
||||
# 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",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# return assignment task read
|
||||
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()
|
||||
|
|
@ -405,11 +464,34 @@ async def read_assignment_task(
|
|||
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)
|
||||
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(assignmenttask)
|
||||
return AssignmentTaskRead.model_validate(assignment_task)
|
||||
|
||||
|
||||
async def update_assignment_task(
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
)
|
||||
|
|
@ -1,24 +1,37 @@
|
|||
from typing import Literal
|
||||
from typing import Literal, Optional
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
async def upload_content(
|
||||
directory: str,
|
||||
type_of_dir: Literal["orgs", "users"],
|
||||
uuid: str, # org_uuid or user_uuid
|
||||
uuid: str, # org_uuid or user_uuid
|
||||
file_binary: bytes,
|
||||
file_and_format: str,
|
||||
allowed_formats: Optional[list[str]] = None,
|
||||
):
|
||||
# Get Learnhouse Config
|
||||
learnhouse_config = get_learnhouse_config()
|
||||
|
||||
file_format = file_and_format.split(".")[-1].strip().lower()
|
||||
|
||||
# Get content delivery method
|
||||
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":
|
||||
# create folder for activity
|
||||
if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"):
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
<FileUp size={30} />
|
||||
</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>
|
||||
</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'>
|
||||
<AArrowUp size={30} />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
'use client';
|
||||
import { useAssignmentsTask } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||
import { Info, TentTree } from 'lucide-react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
||||
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||
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) {
|
||||
const [selectedSubPage, setSelectedSubPage] = React.useState(page)
|
||||
|
|
@ -15,23 +25,41 @@ function AssignmentTaskEditor({ page }: any) {
|
|||
return (
|
||||
<div className="flex flex-col font-black text-sm w-full z-20">
|
||||
{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='font-semibold text-lg py-1'>
|
||||
Assignment Test #1
|
||||
</div>
|
||||
<div className='flex space-x-2 '>
|
||||
<div
|
||||
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'overview'
|
||||
? 'border-b-4'
|
||||
: 'opacity-50'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
<div className="flex items-center space-x-2.5 mx-2">
|
||||
<Info size={16} />
|
||||
<div>Overview</div>
|
||||
<div className='flex flex-col space-y-3'>
|
||||
<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'>
|
||||
<div className='font-semibold text-lg py-1'>
|
||||
{assignmentTaskState?.assignmentTask.title}
|
||||
</div>
|
||||
<div className='flex space-x-2 '>
|
||||
<div
|
||||
onClick={() => setSelectedSubPage('general')}
|
||||
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'general'
|
||||
? 'border-b-4'
|
||||
: 'opacity-50'
|
||||
} cursor-pointer`}
|
||||
>
|
||||
<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 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>
|
||||
)}
|
||||
{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
|
||||
|
|
@ -3,10 +3,11 @@ import Modal from '@components/StyledElements/Modal/Modal';
|
|||
import { FileUp, ListTodo, PanelLeftOpen, Plus } from 'lucide-react';
|
||||
import React, { useEffect } from 'react'
|
||||
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) {
|
||||
const assignments = useAssignments() as any;
|
||||
const assignmentTask = useAssignmentsTask() as any;
|
||||
const assignmentTaskHook = useAssignmentsTaskDispatch() as any;
|
||||
const [isNewTaskModalOpen, setIsNewTaskModalOpen] = React.useState(false)
|
||||
|
||||
|
|
@ -21,30 +22,7 @@ function AssignmentTasks({ assignment_uuid }: any) {
|
|||
return (
|
||||
<div className='flex w-full'>
|
||||
<div className='flex flex-col space-y-3 mx-auto'>
|
||||
{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 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
|
||||
{assignments && assignments?.assignment_tasks?.length < 10 && (<Modal
|
||||
isDialogOpen={isNewTaskModalOpen}
|
||||
onOpenChange={setIsNewTaskModalOpen}
|
||||
minHeight="sm"
|
||||
|
|
@ -60,7 +38,31 @@ function AssignmentTasks({ assignment_uuid }: any) {
|
|||
<p>Add Task</p>
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -40,32 +40,32 @@ function AssignmentEdit() {
|
|||
slateBlack
|
||||
sideOffset={10}
|
||||
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'>
|
||||
<BookX size={18} />
|
||||
<p className='text-sm font-bold'>Unpublish</p>
|
||||
</div>
|
||||
<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} />
|
||||
<p className='text-sm font-bold'>Unpublish</p>
|
||||
</div>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full">
|
||||
<AssignmentsTaskProvider>
|
||||
<div className='flex w-[400px] flex-col h-full custom-dots-bg'>
|
||||
<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'>
|
||||
<LayoutList size={18} />
|
||||
<p>Tasks</p>
|
||||
</div>
|
||||
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
|
||||
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
|
||||
<AssignmentsTaskProvider>
|
||||
<div className='flex w-[400px] flex-col h-full custom-dots-bg'>
|
||||
<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'>
|
||||
<LayoutList size={18} />
|
||||
<p>Tasks</p>
|
||||
</div>
|
||||
<AssignmentTasks assignment_uuid={'assignment_' + params.assignmentuuid} />
|
||||
</AssignmentProvider>
|
||||
</div>
|
||||
<div className='flex flex-grow bg-[#fefcfe] nice-shadow h-full w-full'>
|
||||
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
|
||||
<AssignmentTaskEditor task_uuid='UUID' page='overview' />
|
||||
</AssignmentProvider>
|
||||
</div>
|
||||
</AssignmentsTaskProvider>
|
||||
</div>
|
||||
<div className='flex flex-grow bg-[#fefcfe] nice-shadow h-full w-full'>
|
||||
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
|
||||
<AssignmentTaskEditor page='general' />
|
||||
</AssignmentProvider>
|
||||
</div>
|
||||
</AssignmentsTaskProvider>
|
||||
</AssignmentProvider>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@
|
|||
import React, { createContext, useContext, useEffect, useReducer } from 'react'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import { getAssignmentTask } from '@services/courses/assignments'
|
||||
import { useAssignments } from './AssignmentContext';
|
||||
import { mutate } from 'swr';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
|
||||
interface State {
|
||||
selectedAssignmentTaskUUID: string | null;
|
||||
assignmentTask: Record<string, any>;
|
||||
reloadTrigger: number;
|
||||
}
|
||||
|
||||
interface Action {
|
||||
|
|
@ -15,7 +19,8 @@ interface Action {
|
|||
|
||||
const initialState: State = {
|
||||
selectedAssignmentTaskUUID: null,
|
||||
assignmentTask: {}
|
||||
assignmentTask: {},
|
||||
reloadTrigger: 0,
|
||||
};
|
||||
|
||||
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 }) {
|
||||
const session = useLHSession() as any;
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const assignment = useAssignments() as any
|
||||
|
||||
const [state, dispatch] = useReducer(assignmentstaskReducer, initialState);
|
||||
|
||||
async function fetchAssignmentTask(assignmentTaskUUID: string) {
|
||||
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
|
||||
|
||||
|
||||
if (res.success) {
|
||||
dispatch({ type: 'setAssignmentTask', payload: res });
|
||||
dispatch({ type: 'setAssignmentTask', payload: res.data });
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (state.selectedAssignmentTaskUUID) {
|
||||
fetchAssignmentTask(state.selectedAssignmentTaskUUID);
|
||||
mutate(`${getAPIUrl()}assignments/${assignment.assignment_object?.assignment_uuid}/tasks`);
|
||||
}
|
||||
}, [state.selectedAssignmentTaskUUID]);
|
||||
}, [state.selectedAssignmentTaskUUID, state.reloadTrigger,assignment]);
|
||||
|
||||
return (
|
||||
<AssignmentsTaskContext.Provider value={state}>
|
||||
|
|
@ -72,9 +82,8 @@ function assignmentstaskReducer(state: State, action: Action): State {
|
|||
case 'setAssignmentTask':
|
||||
return { ...state, assignmentTask: action.payload };
|
||||
case 'reload':
|
||||
return { ...state };
|
||||
return { ...state, reloadTrigger: state.reloadTrigger + 1 };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export const inputStyles = {
|
|||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
color: '#7c7c7c',
|
||||
background: '#F9FAFB',
|
||||
background: '#fbfdff',
|
||||
boxShadow: `0 0 0 1px #edeeef`,
|
||||
'&:hover': { boxShadow: `0 0 0 1px #edeeef` },
|
||||
'&:focus': { boxShadow: `0 0 0 2px #edeeef` },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { getAPIUrl } from '@services/config/config'
|
||||
import {
|
||||
RequestBodyFormWithAuthHeader,
|
||||
RequestBodyWithAuthHeader,
|
||||
getResponseMetadata,
|
||||
} from '@services/utils/ts/requests'
|
||||
|
|
@ -76,3 +77,38 @@ export async function getAssignmentTask(
|
|||
const res = await getResponseMetadata(result)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
orgUUID: string,
|
||||
courseId: string,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue