feat: unpublished activities are now hidden by default

This commit is contained in:
swve 2025-04-17 15:57:57 +02:00
parent e6d7e881e3
commit 46e06201fb
14 changed files with 83 additions and 42 deletions

View file

@ -126,6 +126,7 @@ async def api_get_course_by_id(
async def api_get_course_meta( async def api_get_course_meta(
request: Request, request: Request,
course_uuid: str, course_uuid: str,
with_unpublished_activities: bool = False,
db_session: Session = Depends(get_db_session), db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
) -> FullCourseReadWithTrail: ) -> FullCourseReadWithTrail:
@ -133,7 +134,7 @@ async def api_get_course_meta(
Get single Course Metadata (chapters, activities) by course_uuid Get single Course Metadata (chapters, activities) by course_uuid
""" """
return await get_course_meta( 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
) )

View file

@ -260,15 +260,21 @@ async def get_activities(
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
) -> list[ActivityRead]: ) -> list[ActivityRead]:
statement = select(ChapterActivity).where( # Get activities that are published and belong to the chapter
ChapterActivity.chapter_id == coursechapter_id statement = (
select(Activity)
.join(ChapterActivity)
.where(
ChapterActivity.chapter_id == coursechapter_id,
Activity.published == True
)
) )
activities = db_session.exec(statement).all() activities = db_session.exec(statement).all()
if not activities: if not activities:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="No activities found", detail="No published activities found",
) )
# RBAC check # RBAC check

View file

@ -214,6 +214,7 @@ async def get_course_chapters(
course_id: int, course_id: int,
db_session: Session, db_session: Session,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
with_unpublished_activities: bool,
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
) -> List[ChapterRead]: ) -> List[ChapterRead]:
@ -249,7 +250,7 @@ async def get_course_chapters(
for chapter_activity in chapter_activities: for chapter_activity in chapter_activities:
statement = ( statement = (
select(Activity) 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) .distinct(Activity.id)
) )
activity = db_session.exec(statement).first() activity = db_session.exec(statement).first()

View file

@ -126,6 +126,7 @@ async def get_course_by_id(
async def get_course_meta( async def get_course_meta(
request: Request, request: Request,
course_uuid: str, course_uuid: str,
with_unpublished_activities: bool,
current_user: PublicUser | AnonymousUser, current_user: PublicUser | AnonymousUser,
db_session: Session, db_session: Session,
) -> FullCourseReadWithTrail: ) -> FullCourseReadWithTrail:
@ -165,7 +166,7 @@ async def get_course_meta(
# Ensure course.id is not None # Ensure course.id is not None
if course.id is None: if course.id is None:
return [] 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) # Task 3: Get user trail (only for authenticated users)
async def get_trail(): async def get_trail():

View file

@ -25,7 +25,7 @@ function CourseOverviewPage(props: { params: Promise<CourseOverviewParams> }) {
return ( return (
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto_1fr]"> <div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto_1fr]">
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}> <CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)} withUnpublishedActivities={true}>
<div className="pl-10 pr-10 text-sm tracking-tight bg-[#fcfbfc] z-10 nice-shadow"> <div className="pl-10 pr-10 text-sm tracking-tight bg-[#fcfbfc] z-10 nice-shadow">
<CourseOverviewTop params={params} /> <CourseOverviewTop params={params} />
<div className="flex space-x-3 font-black text-sm"> <div className="flex space-x-3 font-black text-sm">

View file

@ -8,11 +8,11 @@ import { useLHSession } from '@components/Contexts/LHSessionContext'
export const CourseContext = createContext(null) export const CourseContext = createContext(null)
export const CourseDispatchContext = 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 session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token; 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) url => swrFetcher(url, access_token)
); );
@ -22,7 +22,8 @@ export function CourseProvider({ children, courseuuid }: any) {
}, },
courseOrder: {}, courseOrder: {},
isSaved: true, isSaved: true,
isLoading: true isLoading: true,
withUnpublishedActivities: withUnpublishedActivities
}; };
const [state, dispatch] = useReducer(courseReducer, initialState) as any; const [state, dispatch] = useReducer(courseReducer, initialState) as any;

View file

@ -6,37 +6,43 @@ import {
useCourse, useCourse,
useCourseDispatch, useCourseDispatch,
} from '@components/Contexts/CourseContext' } 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 { useRouter } from 'next/navigation'
import React, { useEffect } from 'react' import React, { useEffect, useState } from 'react'
import { mutate } from 'swr' import { mutate } from 'swr'
import { updateCourse } from '@services/courses/courses' import { updateCourse } from '@services/courses/courses'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
function SaveState(props: { orgslug: string }) { function SaveState(props: { orgslug: string }) {
const [isLoading, setIsLoading] = useState(false)
const course = useCourse() as any const course = useCourse() as any
const session = useLHSession() as any; const session = useLHSession() as any;
const router = useRouter() const router = useRouter()
const saved = course ? course.isSaved : true const saved = course ? course.isSaved : true
const dispatchCourse = useCourseDispatch() as any const dispatchCourse = useCourseDispatch() as any
const course_structure = course.courseStructure const course_structure = course.courseStructure
const withUnpublishedActivities = course ? course.withUnpublishedActivities : false
const saveCourseState = async () => { const saveCourseState = async () => {
if (saved || isLoading) return
setIsLoading(true)
try {
// Course order // Course order
if (saved) return
await changeOrderBackend() await changeOrderBackend()
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`)
// Course metadata // Course metadata
await changeMetadataBackend() await changeMetadataBackend()
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) await revalidateTags(['courses'], props.orgslug)
dispatchCourse({ type: 'setIsSaved' }) dispatchCourse({ type: 'setIsSaved' })
} finally {
setIsLoading(false)
}
} }
// //
// Course Order // Course Order
const changeOrderBackend = async () => { 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( await updateCourseOrderStructure(
course.courseStructure.course_uuid, course.courseStructure.course_uuid,
course.courseOrder, course.courseOrder,
@ -49,7 +55,7 @@ function SaveState(props: { orgslug: string }) {
// Course metadata // Course metadata
const changeMetadataBackend = async () => { 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( await updateCourse(
course.courseStructure.course_uuid, course.courseStructure.course_uuid,
course.courseStructure, 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 ` + `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 (saved
? 'bg-gray-600 text-white' ? '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} onClick={saveCourseState}
> >
{saved ? <Check size={20} /> : <SaveAllIcon size={20} />} {isLoading ? (
{saved ? <div className="">Saved</div> : <div className="">Save</div>} <Loader2 size={20} className="animate-spin" />
) : saved ? (
<Check size={20} />
) : (
<SaveAllIcon size={20} />
)}
{isLoading ? (
<div className="">Saving...</div>
) : saved ? (
<div className="">Saved</div>
) : (
<div className="">Save</div>
)}
</div> </div>
</div> </div>
) )

View file

@ -17,6 +17,7 @@ function ThumbnailUpdate() {
const [isLoading, setIsLoading] = React.useState(false) as any const [isLoading, setIsLoading] = React.useState(false) as any
const [error, setError] = React.useState('') as any const [error, setError] = React.useState('') as any
const [showUnsplashPicker, setShowUnsplashPicker] = useState(false) const [showUnsplashPicker, setShowUnsplashPicker] = useState(false)
const withUnpublishedActivities = course ? course.withUnpublishedActivities : false
const handleFileChange = async (event: any) => { const handleFileChange = async (event: any) => {
const file = event.target.files[0] const file = event.target.files[0]
@ -40,7 +41,7 @@ function ThumbnailUpdate() {
file, file,
session.data?.tokens?.access_token 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 // wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500)) await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) { if (res.success === false) {

View file

@ -27,6 +27,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
const course = useCourse() as any const course = useCourse() as any
const session = useLHSession() as any; const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token; const access_token = session?.data?.tokens?.access_token;
const withUnpublishedActivities = course ? course.withUnpublishedActivities : false
const openNewActivityModal = async (chapterId: any) => { const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true) setNewActivityModal(true)
@ -44,7 +45,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
) )
const toast_loading = toast.loading('Creating activity...') const toast_loading = toast.loading('Creating activity...')
await createActivity(activity, props.chapterId, org.org_id, access_token) 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.dismiss(toast_loading)
toast.success('Activity created successfully') toast.success('Activity created successfully')
setNewActivityModal(false) setNewActivityModal(false)
@ -61,7 +62,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
) => { ) => {
toast.loading('Uploading file and creating activity...') toast.loading('Uploading file and creating activity...')
await createFileActivity(file, type, activity, chapterId, access_token) 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) setNewActivityModal(false)
toast.dismiss() toast.dismiss()
toast.success('File uploaded successfully') toast.success('File uploaded successfully')
@ -82,7 +83,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
activity, activity,
props.chapterId, access_token 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) setNewActivityModal(false)
toast.dismiss(toast_loading) toast.dismiss(toast_loading)
toast.success('Activity created successfully') toast.success('Activity created successfully')

View file

@ -56,6 +56,8 @@ function ActivityElement(props: ActivitiyElementProps) {
const [isUpdatingName, setIsUpdatingName] = React.useState<boolean>(false) const [isUpdatingName, setIsUpdatingName] = React.useState<boolean>(false)
const activityUUID = props.activity.activity_uuid const activityUUID = props.activity.activity_uuid
const isMobile = useMediaQuery('(max-width: 767px)') const isMobile = useMediaQuery('(max-width: 767px)')
const course = useCourse() as any;
const withUnpublishedActivities = course ? course.withUnpublishedActivities : false
async function deleteActivityUI() { async function deleteActivityUI() {
const toast_loading = toast.loading('Deleting activity...') const toast_loading = toast.loading('Deleting activity...')
@ -65,7 +67,7 @@ function ActivityElement(props: ActivitiyElementProps) {
} }
await deleteActivity(props.activity.activity_uuid, access_token) 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) await revalidateTags(['courses'], props.orgslug)
toast.dismiss(toast_loading) toast.dismiss(toast_loading)
toast.success('Activity deleted successfully') toast.success('Activity deleted successfully')
@ -82,7 +84,7 @@ function ActivityElement(props: ActivitiyElementProps) {
props.activity.activity_uuid, props.activity.activity_uuid,
access_token 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.dismiss(toast_loading)
toast.success('The activity has been updated successfully') toast.success('The activity has been updated successfully')
await revalidateTags(['courses'], props.orgslug) await revalidateTags(['courses'], props.orgslug)
@ -103,7 +105,7 @@ function ActivityElement(props: ActivitiyElementProps) {
try { try {
await updateActivity(modifiedActivityCopy, activityUUID, access_token) 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) await revalidateTags(['courses'], props.orgslug)
toast.success('Activity name updated successfully') toast.success('Activity name updated successfully')
router.refresh() router.refresh()

View file

@ -18,6 +18,7 @@ import { useRouter } from 'next/navigation'
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { mutate } from 'swr' import { mutate } from 'swr'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useCourse } from '@components/Contexts/CourseContext'
type ChapterElementProps = { type ChapterElementProps = {
chapter: any chapter: any
@ -41,12 +42,14 @@ function ChapterElement(props: ChapterElementProps) {
const [selectedChapter, setSelectedChapter] = React.useState< const [selectedChapter, setSelectedChapter] = React.useState<
string | undefined string | undefined
>(undefined) >(undefined)
const course = useCourse() as any;
const withUnpublishedActivities = course ? course.withUnpublishedActivities : false
const router = useRouter() const router = useRouter()
const deleteChapterUI = async () => { const deleteChapterUI = async () => {
await deleteChapter(props.chapter.id, access_token) 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) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
} }
@ -57,7 +60,7 @@ function ChapterElement(props: ChapterElementProps) {
name: modifiedChapter.chapterName, name: modifiedChapter.chapterName,
} }
await updateChapter(chapterId, modifiedChapterCopy, access_token) 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) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
} }

View file

@ -50,7 +50,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
const course = useCourse() as any const course = useCourse() as any
const course_structure = course ? course.courseStructure : {} const course_structure = course ? course.courseStructure : {}
const course_uuid = course ? course.courseStructure.course_uuid : '' const course_uuid = course ? course.courseStructure.course_uuid : ''
const withUnpublishedActivities = course ? course.withUnpublishedActivities : false
// New Chapter creation // New Chapter creation
const [newChapterModal, setNewChapterModal] = useState(false) const [newChapterModal, setNewChapterModal] = useState(false)
@ -61,7 +61,7 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
// Submit new chapter // Submit new chapter
const submitChapter = async (chapter: any) => { const submitChapter = async (chapter: any) => {
await createChapter(chapter,access_token) 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) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
setNewChapterModal(false) setNewChapterModal(false)

View file

@ -18,6 +18,7 @@ import { useRouter } from 'next/navigation'
import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal' import ConfirmationModal from '@components/Objects/StyledElements/ConfirmationModal/ConfirmationModal'
import { deleteActivity, updateActivity } from '@services/courses/activities' import { deleteActivity, updateActivity } from '@services/courses/activities'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useCourse } from '@components/Contexts/CourseContext'
interface ModifiedActivityInterface { interface ModifiedActivityInterface {
activityId: string activityId: string
@ -33,10 +34,12 @@ function Activity(props: any) {
const [selectedActivity, setSelectedActivity] = React.useState< const [selectedActivity, setSelectedActivity] = React.useState<
string | undefined string | undefined
>(undefined) >(undefined)
const course = useCourse() as any;
const withUnpublishedActivities = course ? course.withUnpublishedActivities : false
async function removeActivity() { async function removeActivity() {
await deleteActivity(props.activity.id, session.data?.tokens?.access_token) 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) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
} }
@ -52,7 +55,7 @@ function Activity(props: any) {
} }
await updateActivity(modifiedActivityCopy, activityId, session.data?.tokens?.access_token) 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) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
} }

View file

@ -10,7 +10,7 @@ import { mutate } from 'swr'
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'
import { revalidateTags } from '@services/utils/ts/requests' import { revalidateTags } from '@services/utils/ts/requests'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useCourse } from '@components/Contexts/CourseContext'
interface ModifiedChapterInterface { interface ModifiedChapterInterface {
chapterId: string chapterId: string
chapterName: string chapterName: string
@ -25,6 +25,8 @@ function Chapter(props: any) {
const [selectedChapter, setSelectedChapter] = React.useState< const [selectedChapter, setSelectedChapter] = React.useState<
string | undefined string | undefined
>(undefined) >(undefined)
const course = useCourse() as any;
const withUnpublishedActivities = course ? course.withUnpublishedActivities : false
async function updateChapterName(chapterId: string) { async function updateChapterName(chapterId: string) {
if (modifiedChapter?.chapterId === chapterId) { if (modifiedChapter?.chapterId === chapterId) {
@ -32,7 +34,7 @@ function Chapter(props: any) {
name: modifiedChapter.chapterName, name: modifiedChapter.chapterName,
} }
await updateChapter(chapterId, modifiedChapterCopy, session.data?.tokens?.access_token) 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) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
} }