feat: implement author roles in course management

This commit is contained in:
swve 2025-03-20 13:38:10 +01:00
parent 5f302106a9
commit 4ab8f52b09
11 changed files with 623 additions and 96 deletions

View file

@ -4,6 +4,15 @@ from sqlmodel import Field, SQLModel
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 AuthorWithRole(SQLModel):
user: UserRead
authorship: ResourceAuthorshipEnum
authorship_status: ResourceAuthorshipStatusEnum
creation_date: str
update_date: str
class CourseBase(SQLModel):
@ -41,10 +50,11 @@ class CourseUpdate(CourseBase):
public: Optional[bool]
open_to_contributors: Optional[bool]
class CourseRead(CourseBase):
id: int
org_id: int = Field(default=None, foreign_key="organization.id")
authors: Optional[List[UserRead]]
authors: List[AuthorWithRole]
course_uuid: str
creation_date: str
update_date: str
@ -58,7 +68,7 @@ class FullCourseRead(CourseBase):
update_date: Optional[str]
# Chapters, Activities
chapters: List[ChapterRead]
authors: List[UserRead]
authors: List[AuthorWithRole]
pass
@ -68,7 +78,7 @@ class FullCourseReadWithTrail(CourseBase):
creation_date: Optional[str]
update_date: Optional[str]
org_id: int = Field(default=None, foreign_key="organization.id")
authors: List[UserRead]
authors: List[AuthorWithRole]
# Chapters, Activities
chapters: List[ChapterRead]
# Trail

View file

@ -22,7 +22,7 @@ class ResourceAuthor(SQLModel, table=True):
user_id: int = Field(
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
)
authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR
authorship_status: ResourceAuthorshipStatusEnum = ResourceAuthorshipStatusEnum.ACTIVE
authorship: ResourceAuthorshipEnum
authorship_status: ResourceAuthorshipStatusEnum
creation_date: str = ""
update_date: str = ""

View file

@ -294,7 +294,7 @@ async def api_get_course_contributors(
return await get_course_contributors(request, course_uuid, current_user, db_session)
@router.put("/{course_uuid}/contributors/{contributor_id}")
@router.put("/{course_uuid}/contributors/{contributor_user_id}")
async def api_update_course_contributor(
request: Request,
course_uuid: str,

View file

@ -10,7 +10,7 @@ from src.security.features_utils.usage import (
increase_feature_usage,
)
from src.services.trail.trail import get_user_trail_with_orgid
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
from src.db.users import PublicUser, AnonymousUser, User, UserRead
from src.db.courses.courses import (
Course,
@ -18,6 +18,7 @@ from src.db.courses.courses import (
CourseRead,
CourseUpdate,
FullCourseReadWithTrail,
AuthorWithRole,
)
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
@ -48,16 +49,28 @@ async def get_course(
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Get course authors
# Get course authors with their roles
authors_statement = (
select(User)
.join(ResourceAuthor)
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
.order_by(
ResourceAuthor.id.asc()
)
)
authors = db_session.exec(authors_statement).all()
author_results = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.model_validate(author) for author in authors]
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
course = CourseRead(**course.model_dump(), authors=authors)
@ -82,16 +95,28 @@ async def get_course_by_id(
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Get course authors
# Get course authors with their roles
authors_statement = (
select(User)
.join(ResourceAuthor)
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
.order_by(
ResourceAuthor.id.asc()
)
)
authors = db_session.exec(authors_statement).all()
author_results = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.model_validate(author) for author in authors]
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
course = CourseRead(**course.model_dump(), authors=authors)
@ -123,12 +148,15 @@ async def get_course_meta(
# Start async tasks concurrently
tasks = []
# Task 1: Get course authors
# Task 1: Get course authors with their roles
async def get_authors():
authors_statement = (
select(User)
.join(ResourceAuthor)
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
.order_by(
ResourceAuthor.id.asc()
)
)
return db_session.exec(authors_statement).all()
@ -153,10 +181,19 @@ async def get_course_meta(
tasks.append(get_trail())
# Run all tasks concurrently
authors_raw, chapters, trail = await asyncio.gather(*tasks)
author_results, chapters, trail = await asyncio.gather(*tasks)
# Convert authors from User to UserRead
authors = [UserRead.model_validate(author) for author in authors_raw]
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
# Create course read model
course_read = CourseRead(**course.model_dump(), authors=authors)
@ -167,6 +204,7 @@ async def get_course_meta(
trail=trail,
)
async def get_courses_orgslug(
request: Request,
current_user: PublicUser | AnonymousUser,
@ -225,6 +263,9 @@ async def get_courses_orgslug(
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id) # type: ignore
.where(ResourceAuthor.resource_uuid.in_(course_uuids)) # type: ignore
.order_by(
ResourceAuthor.id.asc()
)
)
author_results = db_session.exec(authors_query).all()
@ -234,13 +275,23 @@ async def get_courses_orgslug(
for resource_author, user in author_results:
if resource_author.resource_uuid not in course_authors:
course_authors[resource_author.resource_uuid] = []
course_authors[resource_author.resource_uuid].append(UserRead.model_validate(user))
course_authors[resource_author.resource_uuid].append(
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
)
# Create CourseRead objects with authors
course_reads = []
for course in courses:
course_read = CourseRead.model_validate(course)
course_read.authors = course_authors.get(course.course_uuid, [])
course_read = CourseRead(
**course.model_dump(),
authors=course_authors.get(course.course_uuid, [])
)
course_reads.append(course_read)
return course_reads
@ -306,15 +357,31 @@ async def search_courses(
# Fetch authors for each course
course_reads = []
for course in courses:
authors_query = (
select(User)
.join(ResourceAuthor, ResourceAuthor.user_id == User.id) # type: ignore
# Get course authors with their roles
authors_statement = (
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
.order_by(
ResourceAuthor.id.asc()
)
)
authors = db_session.exec(authors_query).all()
author_results = db_session.exec(authors_statement).all()
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
course_read = CourseRead.model_validate(course)
course_read.authors = [UserRead.model_validate(author) for author in authors]
course_read.authors = authors
course_reads.append(course_read)
return course_reads
@ -368,6 +435,7 @@ async def create_course(
resource_uuid=course.course_uuid,
user_id=current_user.id,
authorship=ResourceAuthorshipEnum.CREATOR,
authorship_status=ResourceAuthorshipStatusEnum.ACTIVE,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
@ -377,20 +445,32 @@ async def create_course(
db_session.commit()
db_session.refresh(resource_author)
# Get course authors
# Get course authors with their roles
authors_statement = (
select(User)
.join(ResourceAuthor)
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
.order_by(
ResourceAuthor.id.asc()
)
)
authors = db_session.exec(authors_statement).all()
author_results = db_session.exec(authors_statement).all()
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
# Feature usage
increase_feature_usage("courses", course.org_id, db_session)
# convert from User to UserRead
authors = [UserRead.model_validate(author) for author in authors]
course = CourseRead(**course.model_dump(), authors=authors)
return CourseRead.model_validate(course)
@ -444,16 +524,28 @@ async def update_course_thumbnail(
db_session.commit()
db_session.refresh(course)
# Get course authors
# Get course authors with their roles
authors_statement = (
select(User)
.join(ResourceAuthor)
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
.order_by(
ResourceAuthor.id.asc()
)
)
authors = db_session.exec(authors_statement).all()
author_results = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.model_validate(author) for author in authors]
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
course = CourseRead(**course.model_dump(), authors=authors)
@ -491,16 +583,28 @@ async def update_course(
db_session.commit()
db_session.refresh(course)
# Get course authors
# Get course authors with their roles
authors_statement = (
select(User)
.join(ResourceAuthor)
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
.order_by(
ResourceAuthor.id.asc()
)
)
authors = db_session.exec(authors_statement).all()
author_results = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.model_validate(author) for author in authors]
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
course = CourseRead(**course.model_dump(), authors=authors)

View file

@ -3,8 +3,10 @@ from fastapi import HTTPException, Request
from sqlmodel import Session, select
from sqlalchemy import text
from src.db.courses.courses import Course, CourseRead
from src.db.courses.courses import Course, CourseRead, AuthorWithRole
from src.db.organizations import Organization, OrganizationRead
from src.db.users import User, UserRead
from src.db.resource_authors import ResourceAuthor
def _get_sort_expression(salt: str):
@ -96,7 +98,27 @@ async def get_course_for_explore(
detail="Course not found",
)
return CourseRead.model_validate(course)
# Get course authors with their roles
authors_statement = (
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
author_results = db_session.exec(authors_statement).all()
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
return CourseRead(**course.model_dump(), authors=authors)
async def search_orgs_for_explore(
request: Request,

View file

@ -1,7 +1,7 @@
from fastapi import HTTPException, Request
from sqlmodel import Session, select
from typing import Any
from src.db.courses.courses import Course, CourseRead
from typing import Any, List
from src.db.courses.courses import Course, CourseRead, AuthorWithRole
from src.db.payments.payments_courses import PaymentsCourse
from src.db.payments.payments_users import PaymentsUser, PaymentStatusEnum, ProviderSpecificData
from src.db.payments.payments_products import PaymentsProduct
@ -231,19 +231,28 @@ async def get_owned_courses(
# Get authors for each course and convert to CourseRead
course_reads = []
for course in unique_courses:
# Get course authors
# Get course authors with their roles
authors_statement = (
select(User)
.join(ResourceAuthor)
select(ResourceAuthor, User)
.join(User, ResourceAuthor.user_id == User.id)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
author_results = db_session.exec(authors_statement).all()
# Convert authors to UserRead
author_reads = [UserRead.model_validate(author) for author in authors]
# Convert to AuthorWithRole objects
authors = [
AuthorWithRole(
user=UserRead.model_validate(user),
authorship=resource_author.authorship,
authorship_status=resource_author.authorship_status,
creation_date=resource_author.creation_date,
update_date=resource_author.update_date
)
for resource_author, user in author_results
]
# Create CourseRead object
course_read = CourseRead(**course.model_dump(), authors=author_reads)
course_read = CourseRead(**course.model_dump(), authors=authors)
course_reads.append(course_read)
return course_reads