From 6a4e16ec2956c60ebf73e3cbc97ff8d44e20a589 Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 12 Jul 2024 21:28:50 +0200 Subject: [PATCH] feat: init assignments UI and fix bugs --- ...95932_add_reference_for_assignmenttasks.py | 31 +++++ apps/api/src/db/courses/activities.py | 8 +- apps/api/src/db/courses/assignments.py | 3 +- apps/api/src/routers/courses/assignments.py | 13 ++ .../courses/activities/assignments.py | 42 ++++++ .../_components/TaskEditor.tsx | 32 +++++ .../[assignmentuuid]/_components/Tasks.tsx | 35 +++++ .../assignments/[assignmentuuid]/page.tsx | 59 +++++++++ .../orgs/[orgslug]/dash/assignments/page.tsx | 9 ++ .../course/[courseuuid]/[subpage]/page.tsx | 1 - .../Assignments/AssignmentContext.tsx | 40 ++++++ .../DraggableElements/ActivityElement.tsx | 121 ++++++++++++++---- .../components/Dashboard/UI/BreadCrumbs.tsx | 19 ++- apps/web/components/Dashboard/UI/LeftMenu.tsx | 10 +- apps/web/services/courses/assignments.ts | 51 ++++++-- apps/web/styles/globals.css | 9 ++ 16 files changed, 436 insertions(+), 47 deletions(-) create mode 100644 apps/api/migrations/versions/d8bc71595932_add_reference_for_assignmenttasks.py create mode 100644 apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx create mode 100644 apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Tasks.tsx create mode 100644 apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx create mode 100644 apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx create mode 100644 apps/web/components/Contexts/Assignments/AssignmentContext.tsx diff --git a/apps/api/migrations/versions/d8bc71595932_add_reference_for_assignmenttasks.py b/apps/api/migrations/versions/d8bc71595932_add_reference_for_assignmenttasks.py new file mode 100644 index 00000000..10634418 --- /dev/null +++ b/apps/api/migrations/versions/d8bc71595932_add_reference_for_assignmenttasks.py @@ -0,0 +1,31 @@ +"""Add reference for AssignmentTasks + +Revision ID: d8bc71595932 +Revises: 6295e05ff7d0 +Create Date: 2024-07-12 18:59:50.242716 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = 'd8bc71595932' +down_revision: Union[str, None] = '6295e05ff7d0' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('assignmenttask', sa.Column('reference_file', sa.VARCHAR(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('assignmenttask', 'reference_file') + # ### end Alembic commands ### diff --git a/apps/api/src/db/courses/activities.py b/apps/api/src/db/courses/activities.py index 8e40e21a..d6d8f0d8 100644 --- a/apps/api/src/db/courses/activities.py +++ b/apps/api/src/db/courses/activities.py @@ -29,8 +29,8 @@ class ActivitySubTypeEnum(str, Enum): class ActivityBase(SQLModel): name: str - activity_type: ActivityTypeEnum = ActivityTypeEnum.TYPE_CUSTOM - activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.SUBTYPE_CUSTOM + activity_type: ActivityTypeEnum + activity_sub_type: ActivitySubTypeEnum content: dict = Field(default={}, sa_column=Column(JSON)) published: bool = False @@ -51,12 +51,16 @@ class Activity(ActivityBase, table=True): class ActivityCreate(ActivityBase): chapter_id: int + activity_type: ActivityTypeEnum = ActivityTypeEnum.TYPE_CUSTOM + activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.SUBTYPE_CUSTOM pass class ActivityUpdate(ActivityBase): name: Optional[str] content: dict = Field(default={}, sa_column=Column(JSON)) + activity_type: Optional[ActivityTypeEnum] + activity_sub_type: Optional[ActivitySubTypeEnum] published_version: Optional[int] version: Optional[int] diff --git a/apps/api/src/db/courses/assignments.py b/apps/api/src/db/courses/assignments.py index 67b1f872..fc288e36 100644 --- a/apps/api/src/db/courses/assignments.py +++ b/apps/api/src/db/courses/assignments.py @@ -97,6 +97,7 @@ class AssignmentTaskBase(SQLModel): title: str description: str hint: str + reference_file: Optional[str] assignment_type: AssignmentTaskTypeEnum contents: Dict = Field(default={}, sa_column=Column(JSON)) max_grade_value: int = 0 # Value is always between 0-100 @@ -108,7 +109,7 @@ class AssignmentTaskBase(SQLModel): activity_id: int -class AssignmentTaskCreate(AssignmentTaskBase ): +class AssignmentTaskCreate(AssignmentTaskBase): """Model for creating a new assignment task.""" pass # Inherits all fields from AssignmentTaskBase diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py index 1dba87ce..f627d099 100644 --- a/apps/api/src/routers/courses/assignments.py +++ b/apps/api/src/routers/courses/assignments.py @@ -22,6 +22,7 @@ from src.services.courses.activities.assignments import ( delete_assignment_task, delete_assignment_task_submission, read_assignment, + read_assignment_from_activity_uuid, read_assignment_submissions, read_assignment_task_submissions, read_assignment_tasks, @@ -62,6 +63,18 @@ 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, + activity_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +) -> AssignmentRead: + """ + Read an assignment + """ + return await read_assignment_from_activity_uuid(request, activity_uuid, current_user, db_session) + @router.put("/{assignment_uuid}") async def api_update_assignment( diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py index cb133190..46102cf7 100644 --- a/apps/api/src/services/courses/activities/assignments.py +++ b/apps/api/src/services/courses/activities/assignments.py @@ -104,6 +104,48 @@ async def read_assignment( # return assignment read return AssignmentRead.model_validate(assignment) +async def read_assignment_from_activity_uuid( + request: Request, + activity_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if activity exists + statement = select(Activity).where(Activity.activity_uuid == activity_uuid) + activity = db_session.exec(statement).first() + + if not activity: + raise HTTPException( + 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() + + if not course: + raise HTTPException( + 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() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + + # return assignment read + return AssignmentRead.model_validate(assignment) + async def update_assignment( request: Request, 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 new file mode 100644 index 00000000..c76be5e6 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor.tsx @@ -0,0 +1,32 @@ +'use client'; +import { Info, Link } from 'lucide-react' +import React from 'react' + +function AssignmentTaskEditor({ task_uuid, page }: any) { + const [selectedSubPage, setSelectedSubPage] = React.useState(page) + return ( +
+ +
+
+ Assignment Test #1 +
+
+
+
+ +
Overview
+
+
+
+
+
+ ) +} + +export default AssignmentTaskEditor \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Tasks.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Tasks.tsx new file mode 100644 index 00000000..f2933817 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/Tasks.tsx @@ -0,0 +1,35 @@ +import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext' +import { Plus } from 'lucide-react'; +import React, { useEffect } from 'react' + +function AssignmentTasks() { + const assignments = useAssignments() as any; + + useEffect(() => { + console.log(assignments) + }, [assignments]) + + + return ( +
+
+ {assignments && assignments?.assignment_tasks?.map((task: any) => { + return ( +
+
+
{task.title}
+
+
+ ) + })} +
+ +

Add Task

+
+
+ +
+ ) +} + +export default AssignmentTasks \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx new file mode 100644 index 00000000..cf7f609a --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/page.tsx @@ -0,0 +1,59 @@ +'use client'; +import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' +import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement' +import { BookOpen, BookOpenCheck, BookX, Check, Ellipsis, EllipsisVertical, GalleryVerticalEnd, Info, LayoutList, UserRoundCog } from 'lucide-react' +import React from 'react' +import AssignmentTaskEditor from './_components/TaskEditor'; +import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext'; +import AssignmentTasks from './_components/Tasks'; +import { useParams } from 'next/navigation'; + +function AssignmentEdit() { + const params = useParams<{ assignmentuuid: string; }>() + return ( +
+
+
+
+ +
+
Assignment Editor
+
+
+
+
+
Published
+
+
+ +

Publish

+
+
+ +

Unpublish

+
+
+
+
+
+
+
+
+ +

Tasks

+
+ + + +
+
+ + + +
+
+
+ ) +} + +export default AssignmentEdit \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx b/apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx new file mode 100644 index 00000000..c54c9cc9 --- /dev/null +++ b/apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function AssignmentsHome() { + return ( +
AssignmentsHome
+ ) +} + +export default AssignmentsHome \ No newline at end of file diff --git a/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx b/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx index 9de28441..9b44a988 100644 --- a/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page.tsx @@ -82,7 +82,6 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) { - diff --git a/apps/web/components/Contexts/Assignments/AssignmentContext.tsx b/apps/web/components/Contexts/Assignments/AssignmentContext.tsx new file mode 100644 index 00000000..f54844ee --- /dev/null +++ b/apps/web/components/Contexts/Assignments/AssignmentContext.tsx @@ -0,0 +1,40 @@ +'use client' +import { getAPIUrl } from '@services/config/config' +import { swrFetcher } from '@services/utils/ts/requests' +import React, { createContext, useContext, useEffect } from 'react' +import useSWR from 'swr' +import { useLHSession } from '@components/Contexts/LHSessionContext' + +export const AssignmentContext = createContext({}) + +export function AssignmentProvider({ children, assignment_uuid }: { children: React.ReactNode, assignment_uuid: string }) { + const session = useLHSession() as any + const accessToken = session?.data?.tokens?.access_token + const [assignmentsFull, setAssignmentsFull] = React.useState({ assignment_object: null, assignment_tasks: null }) + + const { data: assignment, error: assignmentError } = useSWR( + `${getAPIUrl()}assignments/${assignment_uuid}`, + (url) => swrFetcher(url, accessToken) + ) + + const { data: assignment_tasks, error: assignmentTasksError } = useSWR( + `${getAPIUrl()}assignments/${assignment_uuid}/tasks`, + (url) => swrFetcher(url, accessToken) + ) + + useEffect(() => { + setAssignmentsFull({ assignment_object: assignment, assignment_tasks: assignment_tasks }) + } + , [assignment, assignment_tasks]) + + if (assignmentError || assignmentTasksError) return
+ + if (!assignment || !assignment_tasks) return
+ + + return {children} +} + +export function useAssignments() { + return useContext(AssignmentContext) +} diff --git a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx index a631adcc..daeea79a 100644 --- a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx +++ b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx @@ -3,6 +3,7 @@ import { getAPIUrl, getUriWithOrg } from '@services/config/config' import { deleteActivity, updateActivity } from '@services/courses/activities' import { revalidateTags } from '@services/utils/ts/requests' import { + Backpack, Eye, File, FilePenLine, @@ -16,10 +17,12 @@ import { import { useLHSession } from '@components/Contexts/LHSessionContext' import Link from 'next/link' import { useRouter } from 'next/navigation' -import React from 'react' +import React, { useEffect, useState } from 'react' import { Draggable } from 'react-beautiful-dnd' import { mutate } from 'swr' -import { deleteAssignment, deleteAssignmentUsingActivityUUID } from '@services/courses/assignments' +import { deleteAssignment, deleteAssignmentUsingActivityUUID, getAssignmentFromActivityUUID } from '@services/courses/assignments' +import { useOrg } from '@components/Contexts/OrgContext' +import { useCourse } from '@components/Contexts/CourseContext' type ActivitiyElementProps = { orgslug: string @@ -47,7 +50,7 @@ function ActivityElement(props: ActivitiyElementProps) { async function deleteActivityUI() { // Assignments - if(props.activity.activity_type === 'TYPE_ASSIGNMENT') { + if (props.activity.activity_type === 'TYPE_ASSIGNMENT') { await deleteAssignmentUsingActivityUUID(props.activity.activity_uuid, access_token) } @@ -66,8 +69,6 @@ function ActivityElement(props: ActivitiyElementProps) { let modifiedActivityCopy = { name: modifiedActivity.activityName, description: '', - type: props.activity.type, - content: props.activity.content, } await updateActivity(modifiedActivityCopy, activityUUID, access_token) @@ -135,29 +136,7 @@ function ActivityElement(props: ActivitiyElementProps) { {/* Edit and View Button */}
- {props.activity.activity_type === 'TYPE_DYNAMIC' && ( - <> - -
- Edit Page -
- - - )} + {
)} + {props.activityType === 'TYPE_ASSIGNMENT' && ( + <> +
+
+ {' '} +
+
+ Assignment +
{' '} +
+ + )} {props.activityType === 'TYPE_DYNAMIC' && ( <>
@@ -240,4 +231,78 @@ const ActivityTypeIndicator = (props: { activityType: string }) => {
) } + +const ActivityElementOptions = ({ activity }: any) => { + const [assignmentUUID, setAssignmentUUID] = useState(''); + const org = useOrg() as any; + const course = useCourse() as any; + const session = useLHSession() as any; + const access_token = session?.data?.tokens?.access_token; + + async function getAssignmentUUIDFromActivityUUID(activityUUID: string) { + const activity = await getAssignmentFromActivityUUID(activityUUID, access_token); + if (activity) { + return activity.data.assignment_uuid; + } + } + + const fetchAssignmentUUID = async () => { + if (activity.activity_type === 'TYPE_ASSIGNMENT') { + const assignment_uuid = await getAssignmentUUIDFromActivityUUID(activity.activity_uuid); + setAssignmentUUID(assignment_uuid.replace('assignment_', '')); + } + }; + + useEffect(() => { + + console.log(activity) + + fetchAssignmentUUID(); + }, [activity, course]); + + return ( + <> + {activity.activity_type === 'TYPE_DYNAMIC' && ( + <> + +
+ Edit Page +
+ + + )} + {activity.activity_type === 'TYPE_ASSIGNMENT' && assignmentUUID && ( + <> + +
+ Edit Assignment +
+ + + )} + + ); +}; + export default ActivityElement diff --git a/apps/web/components/Dashboard/UI/BreadCrumbs.tsx b/apps/web/components/Dashboard/UI/BreadCrumbs.tsx index 05396e2b..f242f315 100644 --- a/apps/web/components/Dashboard/UI/BreadCrumbs.tsx +++ b/apps/web/components/Dashboard/UI/BreadCrumbs.tsx @@ -1,15 +1,16 @@ -import { useCourse } from '@components/Contexts/CourseContext' -import { Book, ChevronRight, School, User, Users } from 'lucide-react' +'use client'; +import { useOrg } from '@components/Contexts/OrgContext'; +import { Backpack, Book, ChevronRight, School, User, Users } from 'lucide-react' import Link from 'next/link' import React from 'react' type BreadCrumbsProps = { - type: 'courses' | 'user' | 'users' | 'org' | 'orgusers' + type: 'courses' | 'user' | 'users' | 'org' | 'orgusers' | 'assignments' last_breadcrumb?: string } function BreadCrumbs(props: BreadCrumbsProps) { - const course = useCourse() as any + const org = useOrg() as any return (
@@ -25,6 +26,15 @@ function BreadCrumbs(props: BreadCrumbsProps) { ) : ( '' )} + {props.type == 'assignments' ? ( +
+ {' '} + + Assignments +
+ ) : ( + '' + )} {props.type == 'user' ? (
{' '} @@ -64,7 +74,6 @@ function BreadCrumbs(props: BreadCrumbsProps) {
-
) } diff --git a/apps/web/components/Dashboard/UI/LeftMenu.tsx b/apps/web/components/Dashboard/UI/LeftMenu.tsx index 9f695bbc..bc2eff28 100644 --- a/apps/web/components/Dashboard/UI/LeftMenu.tsx +++ b/apps/web/components/Dashboard/UI/LeftMenu.tsx @@ -3,7 +3,7 @@ import { useOrg } from '@components/Contexts/OrgContext' import { signOut } from 'next-auth/react' import ToolTip from '@components/StyledElements/Tooltip/Tooltip' import LearnHouseDashboardLogo from '@public/dashLogo.png' -import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react' +import { Backpack, BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import React, { useEffect } from 'react' @@ -96,6 +96,14 @@ function LeftMenu() { + + + + +