mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
fix: update details handling in video activities to use JSON strings
This commit is contained in:
parent
31b5104dd5
commit
260bd60c7a
4 changed files with 108 additions and 99 deletions
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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()),
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue