mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: multi-contributors backend code
This commit is contained in:
parent
8d2e61ff39
commit
75500bacd2
5 changed files with 245 additions and 1 deletions
|
|
@ -14,6 +14,7 @@ class CourseBase(SQLModel):
|
|||
tags: Optional[str]
|
||||
thumbnail_image: Optional[str]
|
||||
public: bool
|
||||
open_to_contributors: bool
|
||||
|
||||
|
||||
class Course(CourseBase, table=True):
|
||||
|
|
@ -38,7 +39,7 @@ class CourseUpdate(CourseBase):
|
|||
learnings: Optional[str]
|
||||
tags: Optional[str]
|
||||
public: Optional[bool]
|
||||
|
||||
open_to_contributors: Optional[bool]
|
||||
|
||||
class CourseRead(CourseBase):
|
||||
id: int
|
||||
|
|
|
|||
|
|
@ -6,9 +6,15 @@ from sqlmodel import Field, SQLModel
|
|||
|
||||
class ResourceAuthorshipEnum(str, Enum):
|
||||
CREATOR = "CREATOR"
|
||||
CONTRIBUTOR = "CONTRIBUTOR"
|
||||
MAINTAINER = "MAINTAINER"
|
||||
REPORTER = "REPORTER"
|
||||
|
||||
class ResourceAuthorshipStatusEnum(str, Enum):
|
||||
ACTIVE = "ACTIVE"
|
||||
PENDING = "PENDING"
|
||||
INACTIVE = "INACTIVE"
|
||||
|
||||
|
||||
class ResourceAuthor(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
|
|
@ -17,5 +23,6 @@ class ResourceAuthor(SQLModel, table=True):
|
|||
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
|
||||
)
|
||||
authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR
|
||||
authorship_status: ResourceAuthorshipStatusEnum = ResourceAuthorshipStatusEnum.ACTIVE
|
||||
creation_date: str = ""
|
||||
update_date: str = ""
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ from src.services.courses.updates import (
|
|||
get_updates_by_course_uuid,
|
||||
update_update,
|
||||
)
|
||||
from src.services.courses.contributors import (
|
||||
apply_course_contributor,
|
||||
update_course_contributor,
|
||||
get_course_contributors,
|
||||
)
|
||||
from src.db.resource_authors import ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -63,6 +69,7 @@ async def api_create_course(
|
|||
about=about,
|
||||
learnings=learnings,
|
||||
tags=tags,
|
||||
open_to_contributors=False,
|
||||
)
|
||||
return await create_course(
|
||||
request, org_id, course, current_user, db_session, thumbnail
|
||||
|
|
@ -195,6 +202,19 @@ async def api_delete_course(
|
|||
return await delete_course(request, course_uuid, current_user, db_session)
|
||||
|
||||
|
||||
@router.post("/{course_uuid}/apply-contributor")
|
||||
async def api_apply_course_contributor(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
db_session: Session = Depends(get_db_session),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Apply to be a contributor for a course
|
||||
"""
|
||||
return await apply_course_contributor(request, course_uuid, current_user, db_session)
|
||||
|
||||
|
||||
@router.get("/{course_uuid}/updates")
|
||||
async def api_get_course_updates(
|
||||
request: Request,
|
||||
|
|
@ -259,3 +279,41 @@ async def api_delete_course_update(
|
|||
"""
|
||||
|
||||
return await delete_update(request, courseupdate_uuid, current_user, db_session)
|
||||
|
||||
|
||||
@router.get("/{course_uuid}/contributors")
|
||||
async def api_get_course_contributors(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
db_session: Session = Depends(get_db_session),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get all contributors for a course
|
||||
"""
|
||||
return await get_course_contributors(request, course_uuid, current_user, db_session)
|
||||
|
||||
|
||||
@router.put("/{course_uuid}/contributors/{contributor_id}")
|
||||
async def api_update_course_contributor(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
contributor_user_id: int,
|
||||
authorship: ResourceAuthorshipEnum,
|
||||
authorship_status: ResourceAuthorshipStatusEnum,
|
||||
db_session: Session = Depends(get_db_session),
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update a course contributor's role and status
|
||||
Only administrators can perform this action
|
||||
"""
|
||||
return await update_course_contributor(
|
||||
request,
|
||||
course_uuid,
|
||||
contributor_user_id,
|
||||
authorship,
|
||||
authorship_status,
|
||||
current_user,
|
||||
db_session
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ async def authorization_verify_if_user_is_author(
|
|||
if resource_author.user_id == int(user_id):
|
||||
if (resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or (
|
||||
resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER
|
||||
) or (
|
||||
resource_author.authorship == ResourceAuthorshipEnum.CONTRIBUTOR
|
||||
):
|
||||
return True
|
||||
else:
|
||||
|
|
|
|||
176
apps/api/src/services/courses/contributors.py
Normal file
176
apps/api/src/services/courses/contributors.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
from datetime import datetime
|
||||
from fastapi import HTTPException, Request, status
|
||||
from sqlmodel import Session, select, and_
|
||||
from src.db.users import PublicUser, AnonymousUser, User, UserRead
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum
|
||||
from src.security.rbac.rbac import authorization_verify_if_user_is_anon, authorization_verify_based_on_roles_and_authorship
|
||||
from typing import List
|
||||
|
||||
|
||||
async def apply_course_contributor(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Check if user already has any authorship role for this course
|
||||
existing_authorship = db_session.exec(
|
||||
select(ResourceAuthor).where(
|
||||
and_(
|
||||
ResourceAuthor.resource_uuid == course_uuid,
|
||||
ResourceAuthor.user_id == current_user.id
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing_authorship:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="You already have an authorship role for this course",
|
||||
)
|
||||
|
||||
# Create pending contributor application
|
||||
resource_author = ResourceAuthor(
|
||||
resource_uuid=course_uuid,
|
||||
user_id=current_user.id,
|
||||
authorship=ResourceAuthorshipEnum.CONTRIBUTOR,
|
||||
authorship_status=ResourceAuthorshipStatusEnum.PENDING,
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
||||
db_session.add(resource_author)
|
||||
db_session.commit()
|
||||
db_session.refresh(resource_author)
|
||||
|
||||
return {
|
||||
"detail": "Contributor application submitted successfully",
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
async def update_course_contributor(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
contributor_user_id: int,
|
||||
authorship: ResourceAuthorshipEnum,
|
||||
authorship_status: ResourceAuthorshipStatusEnum,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
):
|
||||
"""
|
||||
Update a course contributor's role and status
|
||||
Only administrators can perform this action
|
||||
"""
|
||||
# Verify user is not anonymous
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
# RBAC check - verify if user has admin rights
|
||||
authorized = await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, "update", course_uuid, db_session
|
||||
)
|
||||
|
||||
if not authorized:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="You are not authorized to update course contributors",
|
||||
)
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Check if the contributor exists for this course
|
||||
existing_authorship = db_session.exec(
|
||||
select(ResourceAuthor).where(
|
||||
and_(
|
||||
ResourceAuthor.resource_uuid == course_uuid,
|
||||
ResourceAuthor.user_id == contributor_user_id
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
if not existing_authorship:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Contributor not found for this course",
|
||||
)
|
||||
|
||||
# Don't allow changing the role of the creator
|
||||
if existing_authorship.authorship == ResourceAuthorshipEnum.CREATOR:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot modify the role of the course creator",
|
||||
)
|
||||
|
||||
# Update the contributor's role and status
|
||||
existing_authorship.authorship = authorship
|
||||
existing_authorship.authorship_status = authorship_status
|
||||
existing_authorship.update_date = str(datetime.now())
|
||||
|
||||
db_session.add(existing_authorship)
|
||||
db_session.commit()
|
||||
db_session.refresh(existing_authorship)
|
||||
|
||||
return {
|
||||
"detail": "Contributor updated successfully",
|
||||
"status": "success"
|
||||
}
|
||||
|
||||
async def get_course_contributors(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Get all contributors for a course with their user information
|
||||
"""
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Get all contributors for this course with user information
|
||||
statement = (
|
||||
select(ResourceAuthor, User)
|
||||
.join(User) # SQLModel will automatically join on foreign key
|
||||
.where(ResourceAuthor.resource_uuid == course_uuid)
|
||||
)
|
||||
results = db_session.exec(statement).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"user_id": contributor.user_id,
|
||||
"authorship": contributor.authorship,
|
||||
"authorship_status": contributor.authorship_status,
|
||||
"creation_date": contributor.creation_date,
|
||||
"update_date": contributor.update_date,
|
||||
"user": UserRead.model_validate(user).model_dump()
|
||||
}
|
||||
for contributor, user in results
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue