feat: add video thumbnails to courses

This commit is contained in:
swve 2025-06-20 22:43:42 +02:00
parent d72abd15fb
commit 2966ac91b7
9 changed files with 518 additions and 164 deletions

View file

@ -0,0 +1,33 @@
"""Video Thumbnails
Revision ID: 9e031a0358d1
Revises: eb10d15465b3
Create Date: 2025-06-20 21:28:50.735540
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa # noqa: F401
import sqlmodel # noqa: F401
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '9e031a0358d1'
down_revision: Union[str, None] = 'eb10d15465b3'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('course', sa.Column('thumbnail_type', postgresql.ENUM('IMAGE', 'VIDEO', 'BOTH', name='thumbnailtype', create_type=False), nullable=True))
op.add_column('course', sa.Column('thumbnail_video', sqlmodel.sql.sqltypes.AutoString(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('course', 'thumbnail_video')
op.drop_column('course', 'thumbnail_type')
# ### end Alembic commands ###

View file

@ -1,12 +1,19 @@
from typing import List, Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel
from enum import Enum
from src.db.users import UserRead
from src.db.trails import TrailRead
from src.db.courses.chapters import ChapterRead
from src.db.resource_authors import ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
class ThumbnailType(str, Enum):
IMAGE = "image"
VIDEO = "video"
BOTH = "both"
class AuthorWithRole(SQLModel):
user: UserRead
authorship: ResourceAuthorshipEnum
@ -21,7 +28,9 @@ class CourseBase(SQLModel):
about: Optional[str]
learnings: Optional[str]
tags: Optional[str]
thumbnail_image: Optional[str]
thumbnail_type: Optional[ThumbnailType] = Field(default=ThumbnailType.IMAGE)
thumbnail_image: Optional[str] = Field(default="")
thumbnail_video: Optional[str] = Field(default="")
public: bool
open_to_contributors: bool
@ -38,6 +47,9 @@ class Course(CourseBase, table=True):
class CourseCreate(CourseBase):
org_id: int = Field(default=None, foreign_key="organization.id")
thumbnail_type: Optional[ThumbnailType] = Field(default=ThumbnailType.IMAGE)
thumbnail_image: Optional[str] = Field(default="")
thumbnail_video: Optional[str] = Field(default="")
pass
@ -47,6 +59,9 @@ class CourseUpdate(CourseBase):
about: Optional[str]
learnings: Optional[str]
tags: Optional[str]
thumbnail_type: Optional[ThumbnailType] = Field(default=ThumbnailType.IMAGE)
thumbnail_image: Optional[str] = Field(default="")
thumbnail_video: Optional[str] = Field(default="")
public: Optional[bool]
open_to_contributors: Optional[bool]
@ -58,6 +73,9 @@ class CourseRead(CourseBase):
course_uuid: str
creation_date: str
update_date: str
thumbnail_type: Optional[ThumbnailType] = Field(default=ThumbnailType.IMAGE)
thumbnail_image: Optional[str] = Field(default="")
thumbnail_video: Optional[str] = Field(default="")
pass
@ -67,6 +85,9 @@ class FullCourseRead(CourseBase):
course_uuid: Optional[str]
creation_date: Optional[str]
update_date: Optional[str]
thumbnail_type: Optional[ThumbnailType] = Field(default=ThumbnailType.IMAGE)
thumbnail_image: Optional[str] = Field(default="")
thumbnail_video: Optional[str] = Field(default="")
# Chapters, Activities
chapters: List[ChapterRead]
authors: List[AuthorWithRole]

View file

@ -12,7 +12,9 @@ from src.db.courses.courses import (
CourseCreate,
CourseRead,
CourseUpdate,
FullCourseRead,
FullCourseReadWithTrail,
ThumbnailType,
)
from src.security.auth import get_current_user
from src.services.courses.courses import (
@ -55,6 +57,7 @@ async def api_create_course(
learnings: str = Form(None),
tags: str = Form(None),
about: str = Form(),
thumbnail_type: ThumbnailType = Form(default=ThumbnailType.IMAGE),
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
thumbnail: UploadFile | None = None,
@ -67,14 +70,16 @@ async def api_create_course(
description=description,
org_id=org_id,
public=public,
thumbnail_type=thumbnail_type,
thumbnail_image="",
thumbnail_video="",
about=about,
learnings=learnings,
tags=tags,
open_to_contributors=False,
)
return await create_course(
request, org_id, course, current_user, db_session, thumbnail
request, org_id, course, current_user, db_session, thumbnail, thumbnail_type
)
@ -82,15 +87,16 @@ async def api_create_course(
async def api_create_course_thumbnail(
request: Request,
course_uuid: str,
thumbnail_type: ThumbnailType = Form(default=ThumbnailType.IMAGE),
thumbnail: UploadFile | None = None,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseRead:
"""
Update new Course Thumbnail
Update Course Thumbnail (Image or Video)
"""
return await update_course_thumbnail(
request, course_uuid, current_user, db_session, thumbnail
request, course_uuid, current_user, db_session, thumbnail, thumbnail_type
)
@ -131,7 +137,7 @@ async def api_get_course_meta(
with_unpublished_activities: bool = False,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> FullCourseReadWithTrail:
) -> FullCourseRead:
"""
Get single Course Metadata (chapters, activities) by course_uuid
"""

View file

@ -19,6 +19,7 @@ from src.db.courses.courses import (
CourseUpdate,
FullCourseRead,
AuthorWithRole,
ThumbnailType,
)
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
@ -398,6 +399,7 @@ async def create_course(
current_user: PublicUser | AnonymousUser,
db_session: Session,
thumbnail_file: UploadFile | None = None,
thumbnail_type: ThumbnailType = ThumbnailType.IMAGE,
):
course = Course.model_validate(course_object)
@ -424,10 +426,16 @@ async def create_course(
await upload_thumbnail(
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid # type: ignore
)
course.thumbnail_image = name_in_disk
if thumbnail_type == ThumbnailType.IMAGE:
course.thumbnail_image = name_in_disk
course.thumbnail_type = ThumbnailType.IMAGE
elif thumbnail_type == ThumbnailType.VIDEO:
course.thumbnail_video = name_in_disk
course.thumbnail_type = ThumbnailType.VIDEO
else:
course.thumbnail_image = ""
course.thumbnail_video = ""
course.thumbnail_type = ThumbnailType.IMAGE
# Insert course
db_session.add(course)
@ -486,6 +494,7 @@ async def update_course_thumbnail(
current_user: PublicUser | AnonymousUser,
db_session: Session,
thumbnail_file: UploadFile | None = None,
thumbnail_type: ThumbnailType = ThumbnailType.IMAGE,
):
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
@ -514,7 +523,12 @@ async def update_course_thumbnail(
# Update course
if name_in_disk:
course.thumbnail_image = name_in_disk
if thumbnail_type == ThumbnailType.IMAGE:
course.thumbnail_image = name_in_disk
course.thumbnail_type = ThumbnailType.IMAGE if not course.thumbnail_video else ThumbnailType.BOTH
elif thumbnail_type == ThumbnailType.VIDEO:
course.thumbnail_video = name_in_disk
course.thumbnail_type = ThumbnailType.VIDEO if not course.thumbnail_image else ThumbnailType.BOTH
else:
raise HTTPException(
status_code=500,