feat: enable assignments page

This commit is contained in:
swve 2024-08-07 23:47:18 +02:00
parent 40ef2d0cec
commit 364c24e15d
6 changed files with 232 additions and 3 deletions

View file

@ -20,6 +20,7 @@ from src.services.courses.activities.assignments import (
delete_assignment_submission,
delete_assignment_task,
delete_assignment_task_submission,
get_assignments_from_course,
get_grade_assignment_submission,
grade_assignment_submission,
handle_assignment_task_submission,
@ -426,6 +427,8 @@ async def api_delete_user_assignment_submissions(
return await delete_assignment_submission(
request, user_id, assignment_uuid, current_user, db_session
)
@router.get("/{assignment_uuid}/submissions/{user_id}/grade")
async def api_get_submission_grade(
request: Request,
@ -442,6 +445,7 @@ async def api_get_submission_grade(
request, user_id, assignment_uuid, current_user, db_session
)
@router.post("/{assignment_uuid}/submissions/{user_id}/grade")
async def api_final_grade_submission(
request: Request,
@ -474,3 +478,18 @@ async def api_submission_mark_as_done(
return await mark_activity_as_done_for_user(
request, user_id, assignment_uuid, current_user, db_session
)
@router.get("/course/{course_uuid}")
async def api_get_assignments(
request: Request,
course_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Get assignments for a course
"""
return await get_assignments_from_course(
request, course_uuid, current_user, db_session
)

View file

@ -1576,6 +1576,40 @@ async def mark_activity_as_done_for_user(
# return OK
return {"message": "Activity marked as done for user"}
async def get_assignments_from_course(
request: Request,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
# Find course
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=404,
detail="Course not found",
)
# Get Activities
statement = select(Activity).where(Activity.course_id == course.id)
activities = db_session.exec(statement).all()
# Get Assignments
assignments = []
for activity in activities:
statement = select(Assignment).where(Assignment.activity_id == activity.id)
assignment = db_session.exec(statement).first()
if assignment:
assignments.append(assignment)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# return assignments read
return [AssignmentRead.model_validate(assignment) for assignment in assignments]
## 🔒 RBAC Utils ##

View file

@ -10,7 +10,7 @@ import { mutate } from 'swr';
import { getAPIUrl } from '@services/config/config';
import toast from 'react-hot-toast';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useParams, useSearchParams } from 'next/navigation';
import { updateActivity } from '@services/courses/activities';
// Lazy Loading
import dynamic from 'next/dynamic';
@ -19,7 +19,8 @@ const AssignmentSubmissionsSubPage = dynamic(() => import('./subpages/Assignment
function AssignmentEdit() {
const params = useParams<{ assignmentuuid: string; }>()
const [selectedSubPage, setSelectedSubPage] = React.useState('editor')
const searchParams = useSearchParams()
const [selectedSubPage, setSelectedSubPage] = React.useState( searchParams.get('subpage') || 'editor')
return (
<div className='flex w-full flex-col'>
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>

View file

@ -1,9 +1,167 @@
'use client';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { getAssignmentsFromACourse } from '@services/courses/assignments';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import { swrFetcher } from '@services/utils/ts/requests';
import { Book, EllipsisVertical, GalleryVertical, GalleryVerticalEnd, Layers2, PenBox, UserRoundPen } from 'lucide-react';
import Link from 'next/link';
import React from 'react'
import useSWR from 'swr';
function AssignmentsHome() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const org = useOrg() as any;
const [courseAssignments, setCourseAssignments] = React.useState<any[]>([])
const { data: courses } = useSWR(`${getAPIUrl()}courses/org_slug/${org?.slug}/page/1/limit/50`, (url) => swrFetcher(url, access_token))
async function getAvailableAssignmentsForCourse(course_uuid: string) {
const res = await getAssignmentsFromACourse(course_uuid, access_token)
return res.data
}
function removeAssignmentPrefix(assignment_uuid: string) {
return assignment_uuid.replace('assignment_', '')
}
function removeCoursePrefix(course_uuid: string) {
return course_uuid.replace('course_', '')
}
React.useEffect(() => {
if (courses) {
const course_uuids = courses.map((course: any) => course.course_uuid)
const courseAssignmentsPromises = course_uuids.map((course_uuid: string) => getAvailableAssignmentsForCourse(course_uuid))
Promise.all(courseAssignmentsPromises).then((results) => {
setCourseAssignments(results)
})
}
}, [courses])
return (
<div>AssignmentsHome</div>
<div className='flex w-full'>
<div className='pl-10 mr-10 tracking-tighter flex flex-col space-y-5 w-full'>
<div className='flex flex-col space-y-2'>
<BreadCrumbs type="assignments" />
<h1 className="pt-3 flex font-bold text-4xl">Assignments</h1>
</div>
<div className='flex flex-col space-y-3 w-full'>
{courseAssignments.map((assignments: any, index: number) => (
<div key={index} className='flex flex-col space-y-2 bg-white nice-shadow p-4 rounded-xl w-full'>
<div>
<div className='flex space-x-2 items-center justify-between w-full'>
<div className='flex space-x-2 items-center'>
<MiniThumbnail course={courses[index]} />
<div className='flex flex-col font-bold text-lg '>
<p className='bg-gray-200 text-gray-700 px-2 text-xs py-0.5 rounded-full'>Course</p>
<p>{courses[index].name}</p>
</div>
</div>
<Link
href={{
pathname: getUriWithOrg(org.slug, `/dash/courses/course/${removeCoursePrefix(courses[index].course_uuid)}/content`),
query: { subpage: 'editor' }
}}
prefetch
className='bg-black font-semibold text-sm text-zinc-100 rounded-md flex space-x-1.5 nice-shadow items-center px-3 py-1'>
<GalleryVerticalEnd size={15} />
<p>Course Editor</p>
</Link>
</div>
{assignments && assignments.map((assignment: any) => (
<div key={assignment.assignment_uuid} className='flex mt-3 p-3 rounded flex-row space-x-2 w-full light-shadow justify-between bg-gray-50 items-center'>
<div className='flex flex-row items-center space-x-2 '>
<div className='flex text-xs font-bold bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full h-fit'>
<p>Assignment</p>
</div>
<div className='flex font-semibold text-lg'>{assignment.title}</div>
<div className='flex font-semibold text'>{assignment.description}</div>
</div>
<div className='flex space-x-2 font-bold text-sm items-center'>
<EllipsisVertical className='text-gray-500' size={17} />
<Link
href={{
pathname: getUriWithOrg(org.slug, `/dash/assignments/${removeAssignmentPrefix(assignment.assignment_uuid)}`),
query: { subpage: 'editor' }
}}
prefetch
className='bg-white rounded-full flex space-x-2 nice-shadow items-center px-3 py-0.5'>
<Layers2 size={15} />
<p>Editor</p>
</Link>
<Link
href={{
pathname: getUriWithOrg(org.slug, `/dash/assignments/${removeAssignmentPrefix(assignment.assignment_uuid)}`),
query: { subpage: 'submissions' }
}}
prefetch
className='bg-white rounded-full flex space-x-2 nice-shadow items-center px-3 py-0.5'>
<UserRoundPen size={15} />
<p>Submissions</p>
</Link>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}
const MiniThumbnail = (props: { course: any }) => {
const org = useOrg() as any
// function to remove "course_" from the course_uuid
function removeCoursePrefix(course_uuid: string) {
return course_uuid.replace('course_', '')
}
return (
<Link
href={getUriWithOrg(
org.orgslug,
'/course/' + removeCoursePrefix(props.course.course_uuid)
)}
>
{props.course.thumbnail_image ? (
<div
className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl w-[70px] h-[40px] bg-cover"
style={{
backgroundImage: `url(${getCourseThumbnailMediaDirectory(
org?.org_uuid,
props.course.course_uuid,
props.course.thumbnail_image
)})`,
}}
/>
) : (
<div
className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl w-[70px] h-[40px] bg-cover"
style={{
backgroundImage: `url('../empty_thumbnail.png')`,
backgroundSize: 'contain',
}}
/>
)}
</Link>
)
}
export default AssignmentsHome

View file

@ -278,3 +278,15 @@ export async function markActivityAsDoneForUser(
const res = await getResponseMetadata(result)
return res
}
export async function getAssignmentsFromACourse(
courseUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/course/${courseUUID}`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}

View file

@ -9,6 +9,11 @@
@apply shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40
}
.light-shadow {
@apply shadow-lg shadow-gray-300/15 outline outline-1 outline-neutral-200/30
}
.custom-dots-bg {
@apply bg-fixed;
background-image: radial-gradient(#4744446b 1px, transparent 1px),