diff --git a/apps/api/src/db/courses/assignments.py b/apps/api/src/db/courses/assignments.py index a7dbc221..e5d38b9e 100644 --- a/apps/api/src/db/courses/assignments.py +++ b/apps/api/src/db/courses/assignments.py @@ -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): diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py index e238449c..c9a9509f 100644 --- a/apps/api/src/routers/courses/assignments.py +++ b/apps/api/src/routers/courses/assignments.py @@ -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, diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index 6414a222..e5e3ef85 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -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( diff --git a/apps/api/src/services/courses/activities/uploads/tasks_ref_files.py b/apps/api/src/services/courses/activities/uploads/tasks_ref_files.py new file mode 100644 index 00000000..d9f7c128 --- /dev/null +++ b/apps/api/src/services/courses/activities/uploads/tasks_ref_files.py @@ -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"], + ) diff --git a/apps/api/src/services/utils/upload_content.py b/apps/api/src/services/utils/upload_content.py index 04448346..d32a787b 100644 --- a/apps/api/src/services/utils/upload_content.py +++ b/apps/api/src/services/utils/upload_content.py @@ -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}"): diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx index 84a7e66f..fea5b9f3 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Modals/NewTaskModal.tsx @@ -58,7 +58,7 @@ function NewTaskModal({ closeModal, assignment_uuid }: any) {
-

File submissions

+

File submission

Students can submit files for this task

-

Forms

+

Form

Forms for students to fill out

diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx index c9e917b5..818bffd0 100644 --- a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx @@ -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 (
{assignmentTaskState.assignmentTask && Object.keys(assignmentTaskState.assignmentTask).length > 0 && ( -
-
- Assignment Test #1 -
-
-
-
- -
Overview
+
+
+
+ {assignmentTaskState?.assignmentTask.title} +
+
+
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`} + > +
+ +
General
+
+
+
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`} + > +
+ +
Content
+
+
+ {selectedSubPage === 'general' && } +
)} {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 ( + + + + + + + + + + + + + + + + + + +