fix: update details handling in video activities to use JSON strings

This commit is contained in:
swve 2025-04-23 17:47:26 +02:00
parent 31b5104dd5
commit 260bd60c7a
4 changed files with 108 additions and 99 deletions

View file

@ -1,4 +1,4 @@
from typing import List, Optional from typing import List
from fastapi import APIRouter, Depends, UploadFile, Form, Request from fastapi import APIRouter, Depends, UploadFile, Form, Request
from src.db.courses.activities import ActivityCreate, ActivityRead, ActivityUpdate from src.db.courses.activities import ActivityCreate, ActivityRead, ActivityUpdate
from src.db.users import PublicUser from src.db.users import PublicUser
@ -113,7 +113,7 @@ async def api_create_video_activity(
request: Request, request: Request,
name: str = Form(), name: str = Form(),
chapter_id: str = Form(), chapter_id: str = Form(),
details: Optional[dict] = Form(default=None), details: str = Form(default="{}"),
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
video_file: UploadFile | None = None, video_file: UploadFile | None = None,
db_session=Depends(get_db_session), db_session=Depends(get_db_session),

View file

@ -1,4 +1,5 @@
from typing import Literal, Optional from typing import Literal
import json
from src.db.courses.courses import Course from src.db.courses.courses import Course
from src.db.organizations import Organization from src.db.organizations import Organization
@ -31,7 +32,7 @@ async def create_video_activity(
current_user: PublicUser, current_user: PublicUser,
db_session: Session, db_session: Session,
video_file: UploadFile | None = None, video_file: UploadFile | None = None,
details: Optional[dict] = None, details: str = "{}",
): ):
# RBAC check # RBAC check
await rbac_check(request, "activity_x", current_user, "create", db_session) await rbac_check(request, "activity_x", current_user, "create", db_session)
@ -40,6 +41,9 @@ async def create_video_activity(
statement = select(Chapter).where(Chapter.id == chapter_id) statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first() chapter = db_session.exec(statement).first()
# convert details to dict
details = json.loads(details)
if not chapter: if not chapter:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
@ -146,7 +150,7 @@ class ExternalVideo(BaseModel):
uri: str uri: str
type: Literal["youtube", "vimeo"] type: Literal["youtube", "vimeo"]
chapter_id: str chapter_id: str
details: Optional[dict] = None details: str = "{}"
class ExternalVideoInDB(BaseModel): class ExternalVideoInDB(BaseModel):
@ -184,6 +188,9 @@ async def create_external_video_activity(
# generate activity_uuid # generate activity_uuid
activity_uuid = str(f"activity_{uuid4()}") activity_uuid = str(f"activity_{uuid4()}")
# convert details to dict
details = json.loads(data.details)
activity_object = Activity( activity_object = Activity(
name=data.name, name=data.name,
activity_type=ActivityTypeEnum.TYPE_VIDEO, activity_type=ActivityTypeEnum.TYPE_VIDEO,
@ -197,7 +204,7 @@ async def create_external_video_activity(
"type": data.type, "type": data.type,
"activity_uuid": activity_uuid, "activity_uuid": activity_uuid,
}, },
details=data.details, details=details,
version=1, version=1,
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),

View file

@ -1,11 +1,10 @@
import FormLayout, { import {
ButtonBlack, Button,
Flex, } from "@components/ui/button"
FormField, import {
FormLabel, Input
FormMessage, } from "@components/ui/input"
Input, import { Label } from "@components/ui/label"
} from '@components/Objects/StyledElements/Form/Form'
import React, { useState } from 'react' import React, { useState } from 'react'
import * as Form from '@radix-ui/react-form' import * as Form from '@radix-ui/react-form'
import BarLoader from 'react-spinners/BarLoader' import BarLoader from 'react-spinners/BarLoader'
@ -100,37 +99,35 @@ function VideoModal({
<div className="space-y-4 mt-4 p-4 bg-gray-50 rounded-lg"> <div className="space-y-4 mt-4 p-4 bg-gray-50 rounded-lg">
<h3 className="font-medium text-gray-900 mb-3">Video Settings</h3> <h3 className="font-medium text-gray-900 mb-3">Video Settings</h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<FormField name="start-time"> <div>
<FormLabel>Start Time (seconds)</FormLabel> <Label htmlFor="start-time">Start Time (seconds)</Label>
<Form.Control asChild> <Input
<Input id="start-time"
type="number" type="number"
min="0" min="0"
value={videoDetails.startTime} value={videoDetails.startTime}
onChange={(e) => setVideoDetails({ onChange={(e) => setVideoDetails({
...videoDetails, ...videoDetails,
startTime: Math.max(0, parseInt(e.target.value) || 0) startTime: Math.max(0, parseInt(e.target.value) || 0)
})} })}
placeholder="0" placeholder="0"
/> />
</Form.Control> </div>
</FormField>
<FormField name="end-time"> <div>
<FormLabel>End Time (seconds, optional)</FormLabel> <Label htmlFor="end-time">End Time (seconds, optional)</Label>
<Form.Control asChild> <Input
<Input id="end-time"
type="number" type="number"
min={videoDetails.startTime + 1} min={videoDetails.startTime + 1}
value={videoDetails.endTime || ''} value={videoDetails.endTime || ''}
onChange={(e) => setVideoDetails({ onChange={(e) => setVideoDetails({
...videoDetails, ...videoDetails,
endTime: e.target.value ? parseInt(e.target.value) : null endTime: e.target.value ? parseInt(e.target.value) : null
})} })}
placeholder="Leave empty for full duration" placeholder="Leave empty for full duration"
/> />
</Form.Control> </div>
</FormField>
</div> </div>
<div className="flex items-center space-x-6 mt-4"> <div className="flex items-center space-x-6 mt-4">
@ -164,24 +161,18 @@ function VideoModal({
) )
return ( return (
<FormLayout onSubmit={handleSubmit}> <Form.Root onSubmit={handleSubmit}>
<FormField name="video-activity-name"> <div>
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}> <Label htmlFor="video-activity-name">Activity Name</Label>
<FormLabel>Activity Name</FormLabel> <Input
<FormMessage match="valueMissing"> id="video-activity-name"
Please provide a name for your video activity value={name}
</FormMessage> onChange={(e) => setName(e.target.value)}
</Flex> type="text"
<Form.Control asChild> required
<Input placeholder="Enter activity name..."
value={name} />
onChange={(e) => setName(e.target.value)} </div>
type="text"
required
placeholder="Enter activity name..."
/>
</Form.Control>
</FormField>
<div className="mt-4 rounded-lg border border-gray-200"> <div className="mt-4 rounded-lg border border-gray-200">
<div className="grid grid-cols-2 gap-0"> <div className="grid grid-cols-2 gap-0">
@ -214,10 +205,11 @@ function VideoModal({
<div className="p-6"> <div className="p-6">
{selectedView === 'file' && ( {selectedView === 'file' && (
<div className="space-y-4"> <div className="space-y-4">
<FormField name="video-activity-file"> <div>
<FormLabel>Video File</FormLabel> <Label htmlFor="video-activity-file">Video File</Label>
<div className="mt-2"> <div className="mt-2">
<input <input
id="video-activity-file"
type="file" type="file"
accept={SUPPORTED_FILES} accept={SUPPORTED_FILES}
onChange={handleVideoChange} onChange={handleVideoChange}
@ -225,47 +217,48 @@ function VideoModal({
className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-black file:text-white hover:file:bg-gray-800" className="w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-black file:text-white hover:file:bg-gray-800"
/> />
</div> </div>
</FormField> </div>
<VideoSettingsForm /> <VideoSettingsForm />
</div> </div>
)} )}
{selectedView === 'youtube' && ( {selectedView === 'youtube' && (
<div className="space-y-4"> <div className="space-y-4">
<FormField name="youtube-url"> <div>
<FormLabel>YouTube URL</FormLabel> <Label htmlFor="youtube-url">YouTube URL</Label>
<Form.Control asChild> <Input
<Input id="youtube-url"
value={youtubeUrl} value={youtubeUrl}
onChange={(e) => setYoutubeUrl(e.target.value)} onChange={(e) => setYoutubeUrl(e.target.value)}
type="text" type="text"
required required
placeholder="https://youtube.com/watch?v=..." placeholder="https://youtube.com/watch?v=..."
/> />
</Form.Control> </div>
</FormField>
<VideoSettingsForm /> <VideoSettingsForm />
</div> </div>
)} )}
</div> </div>
</div> </div>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}> <div className="flex justify-end mt-6">
<Form.Submit asChild> <Button
<ButtonBlack type="submit" css={{ marginTop: 10 }}> type="submit"
{isSubmitting ? ( disabled={isSubmitting}
<BarLoader className="bg-black text-white hover:bg-black/90"
cssOverride={{ borderRadius: 60 }} >
width={60} {isSubmitting ? (
color="#ffffff" <BarLoader
/> cssOverride={{ borderRadius: 60 }}
) : ( width={60}
'Create Activity' color="#ffffff"
)} />
</ButtonBlack> ) : (
</Form.Submit> 'Create Activity'
</Flex> )}
</FormLayout> </Button>
</div>
</Form.Root>
) )
} }

View file

@ -75,14 +75,23 @@ export async function createExternalVideoActivity(
data.chapter_id = chapter_id data.chapter_id = chapter_id
data.activity_id = activity.id data.activity_id = activity.id
// Add video details if provided // Add video details with null checking
data.details = { const defaultDetails = {
startTime: data.startTime || 0, startTime: 0,
endTime: data.endTime || null, endTime: null,
autoplay: data.autoplay || false, autoplay: false,
muted: data.muted || false muted: false
} }
const videoDetails = data.details ? {
startTime: data.details.startTime ?? defaultDetails.startTime,
endTime: data.details.endTime ?? defaultDetails.endTime,
autoplay: data.details.autoplay ?? defaultDetails.autoplay,
muted: data.details.muted ?? defaultDetails.muted
} : defaultDetails
data.details = JSON.stringify(videoDetails)
const result = await fetch( const result = await fetch(
`${getAPIUrl()}activities/external_video`, `${getAPIUrl()}activities/external_video`,
RequestBodyWithAuthHeader('POST', data, null, access_token) RequestBodyWithAuthHeader('POST', data, null, access_token)