From 46e06201fb84247705e0f03e79c143197793f0a1 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 17 Apr 2025 15:57:57 +0200 Subject: [PATCH] feat: unpublished activities are now hidden by default --- apps/api/src/routers/courses/courses.py | 3 +- .../services/courses/activities/activities.py | 12 +++-- apps/api/src/services/courses/chapters.py | 3 +- apps/api/src/services/courses/courses.py | 3 +- .../course/[courseuuid]/[subpage]/page.tsx | 2 +- .../web/components/Contexts/CourseContext.tsx | 7 +-- .../components/Dashboard/Misc/SaveState.tsx | 53 +++++++++++++------ .../EditCourseGeneral/ThumbnailUpdate.tsx | 3 +- .../Buttons/NewActivityButton.tsx | 7 +-- .../DraggableElements/ActivityElement.tsx | 8 +-- .../DraggableElements/ChapterElement.tsx | 7 ++- .../EditCourseStructure.tsx | 4 +- .../Pages/CourseEdit/Draggables/Activity.tsx | 7 ++- .../Pages/CourseEdit/Draggables/Chapter.tsx | 6 ++- 14 files changed, 83 insertions(+), 42 deletions(-) diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index 19042524..f2271983 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -126,6 +126,7 @@ async def api_get_course_by_id( async def api_get_course_meta( request: Request, course_uuid: str, + with_unpublished_activities: bool = False, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), ) -> FullCourseReadWithTrail: @@ -133,7 +134,7 @@ async def api_get_course_meta( Get single Course Metadata (chapters, activities) by course_uuid """ return await get_course_meta( - request, course_uuid, current_user=current_user, db_session=db_session + request, course_uuid, with_unpublished_activities, current_user=current_user, db_session=db_session ) diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index f20f51fb..42ad1f05 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -260,15 +260,21 @@ async def get_activities( current_user: PublicUser | AnonymousUser, db_session: Session, ) -> list[ActivityRead]: - statement = select(ChapterActivity).where( - ChapterActivity.chapter_id == coursechapter_id + # Get activities that are published and belong to the chapter + statement = ( + select(Activity) + .join(ChapterActivity) + .where( + ChapterActivity.chapter_id == coursechapter_id, + Activity.published == True + ) ) activities = db_session.exec(statement).all() if not activities: raise HTTPException( status_code=404, - detail="No activities found", + detail="No published activities found", ) # RBAC check diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 97afcf92..4a30bb3d 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -214,6 +214,7 @@ async def get_course_chapters( course_id: int, db_session: Session, current_user: PublicUser | AnonymousUser, + with_unpublished_activities: bool, page: int = 1, limit: int = 10, ) -> List[ChapterRead]: @@ -249,7 +250,7 @@ async def get_course_chapters( for chapter_activity in chapter_activities: statement = ( select(Activity) - .where(Activity.id == chapter_activity.activity_id) + .where(Activity.id == chapter_activity.activity_id, with_unpublished_activities or Activity.published == True) .distinct(Activity.id) ) activity = db_session.exec(statement).first() diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 53d11fd2..444a8e4e 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -126,6 +126,7 @@ async def get_course_by_id( async def get_course_meta( request: Request, course_uuid: str, + with_unpublished_activities: bool, current_user: PublicUser | AnonymousUser, db_session: Session, ) -> FullCourseReadWithTrail: @@ -165,7 +166,7 @@ async def get_course_meta( # Ensure course.id is not None if course.id is None: return [] - return await get_course_chapters(request, course.id, db_session, current_user) + return await get_course_chapters(request, course.id, db_session, current_user, with_unpublished_activities) # Task 3: Get user trail (only for authenticated users) async def get_trail(): 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 a095945a..00eb734a 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 @@ -25,7 +25,7 @@ function CourseOverviewPage(props: { params: Promise }) { return (
- +
diff --git a/apps/web/components/Contexts/CourseContext.tsx b/apps/web/components/Contexts/CourseContext.tsx index 5bfaea91..238157d2 100644 --- a/apps/web/components/Contexts/CourseContext.tsx +++ b/apps/web/components/Contexts/CourseContext.tsx @@ -8,11 +8,11 @@ import { useLHSession } from '@components/Contexts/LHSessionContext' export const CourseContext = createContext(null) export const CourseDispatchContext = createContext(null) -export function CourseProvider({ children, courseuuid }: any) { +export function CourseProvider({ children, courseuuid, withUnpublishedActivities = false }: any) { const session = useLHSession() as any; const access_token = session?.data?.tokens?.access_token; - const { data: courseStructureData, error } = useSWR(`${getAPIUrl()}courses/${courseuuid}/meta`, + const { data: courseStructureData, error } = useSWR(`${getAPIUrl()}courses/${courseuuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`, url => swrFetcher(url, access_token) ); @@ -22,7 +22,8 @@ export function CourseProvider({ children, courseuuid }: any) { }, courseOrder: {}, isSaved: true, - isLoading: true + isLoading: true, + withUnpublishedActivities: withUnpublishedActivities }; const [state, dispatch] = useReducer(courseReducer, initialState) as any; diff --git a/apps/web/components/Dashboard/Misc/SaveState.tsx b/apps/web/components/Dashboard/Misc/SaveState.tsx index a773e74b..672fd36c 100644 --- a/apps/web/components/Dashboard/Misc/SaveState.tsx +++ b/apps/web/components/Dashboard/Misc/SaveState.tsx @@ -6,37 +6,43 @@ import { useCourse, useCourseDispatch, } from '@components/Contexts/CourseContext' -import { Check, SaveAllIcon, Timer } from 'lucide-react' +import { Check, SaveAllIcon, Timer, Loader2 } from 'lucide-react' import { useRouter } from 'next/navigation' -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import { mutate } from 'swr' import { updateCourse } from '@services/courses/courses' import { useLHSession } from '@components/Contexts/LHSessionContext' function SaveState(props: { orgslug: string }) { + const [isLoading, setIsLoading] = useState(false) const course = useCourse() as any const session = useLHSession() as any; const router = useRouter() const saved = course ? course.isSaved : true const dispatchCourse = useCourseDispatch() as any const course_structure = course.courseStructure - + const withUnpublishedActivities = course ? course.withUnpublishedActivities : false const saveCourseState = async () => { - // Course order - if (saved) return - await changeOrderBackend() - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) - // Course metadata - await changeMetadataBackend() - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) - await revalidateTags(['courses'], props.orgslug) - dispatchCourse({ type: 'setIsSaved' }) + if (saved || isLoading) return + setIsLoading(true) + try { + // Course order + await changeOrderBackend() + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) + // Course metadata + await changeMetadataBackend() + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) + await revalidateTags(['courses'], props.orgslug) + dispatchCourse({ type: 'setIsSaved' }) + } finally { + setIsLoading(false) + } } // // Course Order const changeOrderBackend = async () => { - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) await updateCourseOrderStructure( course.courseStructure.course_uuid, course.courseOrder, @@ -49,7 +55,7 @@ function SaveState(props: { orgslug: string }) { // Course metadata const changeMetadataBackend = async () => { - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) await updateCourse( course.courseStructure.course_uuid, course.courseStructure, @@ -117,12 +123,25 @@ function SaveState(props: { orgslug: string }) { `px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` + (saved ? 'bg-gray-600 text-white' - : 'bg-black text-white border hover:bg-gray-900 ') + : 'bg-black text-white border hover:bg-gray-900 ') + + (isLoading ? 'opacity-50 cursor-not-allowed' : '') } onClick={saveCourseState} > - {saved ? : } - {saved ?
Saved
:
Save
} + {isLoading ? ( + + ) : saved ? ( + + ) : ( + + )} + {isLoading ? ( +
Saving...
+ ) : saved ? ( +
Saved
+ ) : ( +
Save
+ )}
) diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseGeneral/ThumbnailUpdate.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseGeneral/ThumbnailUpdate.tsx index 3b7a77ef..461508d5 100644 --- a/apps/web/components/Dashboard/Pages/Course/EditCourseGeneral/ThumbnailUpdate.tsx +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseGeneral/ThumbnailUpdate.tsx @@ -17,6 +17,7 @@ function ThumbnailUpdate() { const [isLoading, setIsLoading] = React.useState(false) as any const [error, setError] = React.useState('') as any const [showUnsplashPicker, setShowUnsplashPicker] = useState(false) + const withUnpublishedActivities = course ? course.withUnpublishedActivities : false const handleFileChange = async (event: any) => { const file = event.target.files[0] @@ -40,7 +41,7 @@ function ThumbnailUpdate() { file, session.data?.tokens?.access_token ) - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) // wait for 1 second to show loading animation await new Promise((r) => setTimeout(r, 1500)) if (res.success === false) { diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/Buttons/NewActivityButton.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/Buttons/NewActivityButton.tsx index e955c0cc..b970075c 100644 --- a/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/Buttons/NewActivityButton.tsx +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/Buttons/NewActivityButton.tsx @@ -27,6 +27,7 @@ function NewActivityButton(props: NewActivityButtonProps) { const course = useCourse() as any const session = useLHSession() as any; const access_token = session?.data?.tokens?.access_token; + const withUnpublishedActivities = course ? course.withUnpublishedActivities : false const openNewActivityModal = async (chapterId: any) => { setNewActivityModal(true) @@ -44,7 +45,7 @@ function NewActivityButton(props: NewActivityButtonProps) { ) const toast_loading = toast.loading('Creating activity...') await createActivity(activity, props.chapterId, org.org_id, access_token) - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) toast.dismiss(toast_loading) toast.success('Activity created successfully') setNewActivityModal(false) @@ -61,7 +62,7 @@ function NewActivityButton(props: NewActivityButtonProps) { ) => { toast.loading('Uploading file and creating activity...') await createFileActivity(file, type, activity, chapterId, access_token) - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) setNewActivityModal(false) toast.dismiss() toast.success('File uploaded successfully') @@ -82,7 +83,7 @@ function NewActivityButton(props: NewActivityButtonProps) { activity, props.chapterId, access_token ) - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) setNewActivityModal(false) toast.dismiss(toast_loading) toast.success('Activity created successfully') diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx index de2b844e..88ce81dc 100644 --- a/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx @@ -56,6 +56,8 @@ function ActivityElement(props: ActivitiyElementProps) { const [isUpdatingName, setIsUpdatingName] = React.useState(false) const activityUUID = props.activity.activity_uuid const isMobile = useMediaQuery('(max-width: 767px)') + const course = useCourse() as any; + const withUnpublishedActivities = course ? course.withUnpublishedActivities : false async function deleteActivityUI() { const toast_loading = toast.loading('Deleting activity...') @@ -65,7 +67,7 @@ function ActivityElement(props: ActivitiyElementProps) { } await deleteActivity(props.activity.activity_uuid, access_token) - mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) await revalidateTags(['courses'], props.orgslug) toast.dismiss(toast_loading) toast.success('Activity deleted successfully') @@ -82,7 +84,7 @@ function ActivityElement(props: ActivitiyElementProps) { props.activity.activity_uuid, access_token ) - mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) toast.dismiss(toast_loading) toast.success('The activity has been updated successfully') await revalidateTags(['courses'], props.orgslug) @@ -103,7 +105,7 @@ function ActivityElement(props: ActivitiyElementProps) { try { await updateActivity(modifiedActivityCopy, activityUUID, access_token) - mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) await revalidateTags(['courses'], props.orgslug) toast.success('Activity name updated successfully') router.refresh() diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/DraggableElements/ChapterElement.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/DraggableElements/ChapterElement.tsx index 4cbe84e6..a6235893 100644 --- a/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/DraggableElements/ChapterElement.tsx +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/DraggableElements/ChapterElement.tsx @@ -18,6 +18,7 @@ import { useRouter } from 'next/navigation' import { getAPIUrl } from '@services/config/config' import { mutate } from 'swr' import { useLHSession } from '@components/Contexts/LHSessionContext' +import { useCourse } from '@components/Contexts/CourseContext' type ChapterElementProps = { chapter: any @@ -41,12 +42,14 @@ function ChapterElement(props: ChapterElementProps) { const [selectedChapter, setSelectedChapter] = React.useState< string | undefined >(undefined) + const course = useCourse() as any; + const withUnpublishedActivities = course ? course.withUnpublishedActivities : false const router = useRouter() const deleteChapterUI = async () => { await deleteChapter(props.chapter.id, access_token) - mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) await revalidateTags(['courses'], props.orgslug) router.refresh() } @@ -57,7 +60,7 @@ function ChapterElement(props: ChapterElementProps) { name: modifiedChapter.chapterName, } await updateChapter(chapterId, modifiedChapterCopy, access_token) - mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) await revalidateTags(['courses'], props.orgslug) router.refresh() } diff --git a/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/EditCourseStructure.tsx b/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/EditCourseStructure.tsx index ed222c12..f65e11f0 100644 --- a/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/EditCourseStructure.tsx +++ b/apps/web/components/Dashboard/Pages/Course/EditCourseStructure/EditCourseStructure.tsx @@ -50,7 +50,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => { const course = useCourse() as any const course_structure = course ? course.courseStructure : {} const course_uuid = course ? course.courseStructure.course_uuid : '' - + const withUnpublishedActivities = course ? course.withUnpublishedActivities : false // New Chapter creation const [newChapterModal, setNewChapterModal] = useState(false) @@ -61,7 +61,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => { // Submit new chapter const submitChapter = async (chapter: any) => { await createChapter(chapter,access_token) - mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) + mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) await revalidateTags(['courses'], props.orgslug) router.refresh() setNewChapterModal(false) diff --git a/apps/web/components/Pages/CourseEdit/Draggables/Activity.tsx b/apps/web/components/Pages/CourseEdit/Draggables/Activity.tsx index 6e5df6bb..55c87bb3 100644 --- a/apps/web/components/Pages/CourseEdit/Draggables/Activity.tsx +++ b/apps/web/components/Pages/CourseEdit/Draggables/Activity.tsx @@ -18,6 +18,7 @@ import { useRouter } from 'next/navigation' import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal' import { deleteActivity, updateActivity } from '@services/courses/activities' import { useLHSession } from '@components/Contexts/LHSessionContext' +import { useCourse } from '@components/Contexts/CourseContext' interface ModifiedActivityInterface { activityId: string @@ -33,10 +34,12 @@ function Activity(props: any) { const [selectedActivity, setSelectedActivity] = React.useState< string | undefined >(undefined) + const course = useCourse() as any; + const withUnpublishedActivities = course ? course.withUnpublishedActivities : false async function removeActivity() { await deleteActivity(props.activity.id, session.data?.tokens?.access_token) - mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}`) + mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}?with_unpublished_activities=${withUnpublishedActivities}`) await revalidateTags(['courses'], props.orgslug) router.refresh() } @@ -52,7 +55,7 @@ function Activity(props: any) { } await updateActivity(modifiedActivityCopy, activityId, session.data?.tokens?.access_token) - await mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}`) + await mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}?with_unpublished_activities=${withUnpublishedActivities}`) await revalidateTags(['courses'], props.orgslug) router.refresh() } diff --git a/apps/web/components/Pages/CourseEdit/Draggables/Chapter.tsx b/apps/web/components/Pages/CourseEdit/Draggables/Chapter.tsx index 19e1452b..b93cac10 100644 --- a/apps/web/components/Pages/CourseEdit/Draggables/Chapter.tsx +++ b/apps/web/components/Pages/CourseEdit/Draggables/Chapter.tsx @@ -10,7 +10,7 @@ import { mutate } from 'swr' import { getAPIUrl } from '@services/config/config' import { revalidateTags } from '@services/utils/ts/requests' import { useLHSession } from '@components/Contexts/LHSessionContext' - +import { useCourse } from '@components/Contexts/CourseContext' interface ModifiedChapterInterface { chapterId: string chapterName: string @@ -25,6 +25,8 @@ function Chapter(props: any) { const [selectedChapter, setSelectedChapter] = React.useState< string | undefined >(undefined) + const course = useCourse() as any; + const withUnpublishedActivities = course ? course.withUnpublishedActivities : false async function updateChapterName(chapterId: string) { if (modifiedChapter?.chapterId === chapterId) { @@ -32,7 +34,7 @@ function Chapter(props: any) { name: modifiedChapter.chapterName, } await updateChapter(chapterId, modifiedChapterCopy, session.data?.tokens?.access_token) - await mutate(`${getAPIUrl()}chapters/course/${props.course_uuid}/meta`) + await mutate(`${getAPIUrl()}chapters/course/${props.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) await revalidateTags(['courses'], props.orgslug) router.refresh() }