mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: init collections
This commit is contained in:
parent
727f17ba7c
commit
e6adbca562
4 changed files with 219 additions and 178 deletions
|
|
@ -1,3 +1,40 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
|
class CollectionBase(SQLModel):
|
||||||
|
name: str
|
||||||
|
public: bool
|
||||||
|
description: Optional[str] = ""
|
||||||
|
|
||||||
|
|
||||||
|
class Collection(CollectionBase, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||||
|
collection_uuid: str = ""
|
||||||
|
creation_date: str = ""
|
||||||
|
update_date: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionCreate(CollectionBase):
|
||||||
|
courses: list
|
||||||
|
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionUpdate(CollectionBase):
|
||||||
|
collection_id: int
|
||||||
|
courses: Optional[list]
|
||||||
|
name: Optional[str]
|
||||||
|
public: Optional[bool]
|
||||||
|
description: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionRead(CollectionBase):
|
||||||
|
id: int
|
||||||
|
courses: list
|
||||||
|
collection_uuid: str
|
||||||
|
creation_date: str
|
||||||
|
update_date: str
|
||||||
|
pass
|
||||||
|
|
|
||||||
11
apps/api/src/db/collections_courses.py
Normal file
11
apps/api/src/db/collections_courses.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from typing import Optional
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionCourse(SQLModel, table=True):
|
||||||
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
collection_id: int = Field(default=None, foreign_key="collection.id")
|
||||||
|
course_id: int = Field(default=None, foreign_key="course.id")
|
||||||
|
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||||
|
creation_date: str
|
||||||
|
update_date: str
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from src.core.events.database import get_db_session
|
||||||
|
from src.db.collections import CollectionCreate, CollectionUpdate
|
||||||
from src.security.auth import get_current_user
|
from src.security.auth import get_current_user
|
||||||
from src.services.users.users import PublicUser
|
from src.services.users.users import PublicUser
|
||||||
from src.services.courses.collections import (
|
from src.services.courses.collections import (
|
||||||
|
|
@ -17,13 +19,14 @@ router = APIRouter()
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def api_create_collection(
|
async def api_create_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_object: Collection,
|
collection_object: CollectionCreate,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create new Collection
|
Create new Collection
|
||||||
"""
|
"""
|
||||||
return await create_collection(request, collection_object, current_user)
|
return await create_collection(request, collection_object, current_user, db_session)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{collection_id}")
|
@router.get("/{collection_id}")
|
||||||
|
|
@ -31,11 +34,12 @@ async def api_get_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_id: str,
|
collection_id: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get single collection by ID
|
Get single collection by ID
|
||||||
"""
|
"""
|
||||||
return await get_collection(request, collection_id, current_user)
|
return await get_collection(request, collection_id, current_user, db_session)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/org_id/{org_id}/page/{page}/limit/{limit}")
|
@router.get("/org_id/{org_id}/page/{page}/limit/{limit}")
|
||||||
|
|
@ -45,26 +49,25 @@ async def api_get_collections_by(
|
||||||
limit: int,
|
limit: int,
|
||||||
org_id: str,
|
org_id: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get collections by page and limit
|
Get collections by page and limit
|
||||||
"""
|
"""
|
||||||
return await get_collections(request, org_id, current_user, page, limit)
|
return await get_collections(request, org_id, current_user, db_session, page, limit)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{collection_id}")
|
@router.put("/{collection_id}")
|
||||||
async def api_update_collection(
|
async def api_update_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_object: Collection,
|
collection_object: CollectionUpdate,
|
||||||
collection_id: str,
|
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update collection by ID
|
Update collection by ID
|
||||||
"""
|
"""
|
||||||
return await update_collection(
|
return await update_collection(request, collection_object, current_user, db_session)
|
||||||
request, collection_object, collection_id, current_user
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{collection_id}")
|
@router.delete("/{collection_id}")
|
||||||
|
|
@ -72,9 +75,10 @@ async def api_delete_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_id: str,
|
collection_id: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Delete collection by ID
|
Delete collection by ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return await delete_collection(request, collection_id, current_user)
|
return await delete_collection(request, collection_id, current_user, db_session)
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,31 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from gc import collect
|
||||||
from typing import List, Literal
|
from typing import List, Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from src.security.rbac.rbac import authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon
|
from sqlmodel import Session, select
|
||||||
|
from src.db.collections import (
|
||||||
|
Collection,
|
||||||
|
CollectionCreate,
|
||||||
|
CollectionRead,
|
||||||
|
CollectionUpdate,
|
||||||
|
)
|
||||||
|
from src.db.collections_courses import CollectionCourse
|
||||||
|
from src.db.courses import Course
|
||||||
|
from src.security.rbac.rbac import (
|
||||||
|
authorization_verify_based_on_roles_and_authorship,
|
||||||
|
authorization_verify_if_user_is_anon,
|
||||||
|
)
|
||||||
from src.services.users.users import PublicUser
|
from src.services.users.users import PublicUser
|
||||||
from fastapi import HTTPException, status, Request
|
from fastapi import HTTPException, status, Request
|
||||||
|
from typing import List
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from src.db.collections import Collection
|
||||||
|
from src.db.courses import Course
|
||||||
|
from src.db.collections_courses import CollectionCourse
|
||||||
|
from src.services.users.users import PublicUser
|
||||||
|
|
||||||
#### Classes ####################################################
|
|
||||||
|
|
||||||
|
|
||||||
class Collection(BaseModel):
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
courses: List[str] # course_id
|
|
||||||
public: bool
|
|
||||||
org_id: str # org_id
|
|
||||||
|
|
||||||
|
|
||||||
class CollectionInDB(Collection):
|
|
||||||
collection_id: str
|
|
||||||
authors: List[str] # user_id
|
|
||||||
|
|
||||||
|
|
||||||
#### Classes ####################################################
|
|
||||||
|
|
||||||
####################################################
|
####################################################
|
||||||
# CRUD
|
# CRUD
|
||||||
|
|
@ -29,134 +33,164 @@ class CollectionInDB(Collection):
|
||||||
|
|
||||||
|
|
||||||
async def get_collection(
|
async def get_collection(
|
||||||
request: Request, collection_id: str, current_user: PublicUser
|
request: Request, collection_id: str, current_user: PublicUser, db_session: Session
|
||||||
):
|
) -> CollectionRead:
|
||||||
collections = request.app.db["collections"]
|
statement = select(Collection).where(Collection.id == collection_id)
|
||||||
|
collection = db_session.exec(statement).first()
|
||||||
collection = await collections.find_one({"collection_id": collection_id})
|
|
||||||
|
|
||||||
# verify collection rights
|
|
||||||
await verify_collection_rights(
|
|
||||||
request, collection_id, current_user, "read", collection["org_id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not collection:
|
if not collection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||||
)
|
)
|
||||||
|
|
||||||
collection = Collection(**collection)
|
# get courses in collection
|
||||||
|
statement = (
|
||||||
|
select(Course)
|
||||||
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
|
.distinct(Course.id)
|
||||||
|
)
|
||||||
|
courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
# add courses to collection
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
courses = request.app.db["courses"]
|
|
||||||
courseids = [course for course in collection.courses]
|
|
||||||
|
|
||||||
collection.courses = []
|
|
||||||
collection.courses = courses.find({"course_id": {"$in": courseids}}, {"_id": 0})
|
|
||||||
|
|
||||||
collection.courses = [
|
|
||||||
course for course in await collection.courses.to_list(length=100)
|
|
||||||
]
|
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
|
|
||||||
|
|
||||||
async def create_collection(
|
async def create_collection(
|
||||||
request: Request, collection_object: Collection, current_user: PublicUser
|
request: Request,
|
||||||
):
|
collection_object: CollectionCreate,
|
||||||
collections = request.app.db["collections"]
|
current_user: PublicUser,
|
||||||
|
db_session: Session,
|
||||||
|
) -> CollectionRead:
|
||||||
|
collection = Collection.from_orm(collection_object)
|
||||||
|
|
||||||
# find if collection already exists using name
|
# Complete the collection object
|
||||||
isCollectionNameAvailable = await collections.find_one(
|
collection.collection_uuid = f"collection_{uuid4()}"
|
||||||
{"name": collection_object.name}
|
collection.creation_date = str(datetime.now())
|
||||||
|
collection.update_date = str(datetime.now())
|
||||||
|
|
||||||
|
# Add collection to database
|
||||||
|
db_session.add(collection)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
db_session.refresh(collection)
|
||||||
|
|
||||||
|
# Link courses to collection
|
||||||
|
for course in collection_object.courses:
|
||||||
|
collection_course = CollectionCourse(
|
||||||
|
collection_id=int(collection.id is not None),
|
||||||
|
course_id=int(course),
|
||||||
|
org_id=int(collection_object.org_id),
|
||||||
|
creation_date=str(datetime.now()),
|
||||||
|
update_date=str(datetime.now()),
|
||||||
)
|
)
|
||||||
|
# Add collection_course to database
|
||||||
|
db_session.add(collection_course)
|
||||||
|
|
||||||
# TODO
|
db_session.commit()
|
||||||
# await verify_collection_rights("*", current_user, "create")
|
db_session.refresh(collection)
|
||||||
|
|
||||||
if isCollectionNameAvailable:
|
# Get courses once again
|
||||||
raise HTTPException(
|
statement = (
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
select(Course)
|
||||||
detail="Collection name already exists",
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
|
.distinct(Course.id)
|
||||||
)
|
)
|
||||||
|
courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
# generate collection_id with uuid4
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
collection_id = str(f"collection_{uuid4()}")
|
|
||||||
|
|
||||||
collection = CollectionInDB(
|
return CollectionRead.from_orm(collection)
|
||||||
collection_id=collection_id,
|
|
||||||
authors=[current_user.user_id],
|
|
||||||
**collection_object.dict(),
|
|
||||||
)
|
|
||||||
|
|
||||||
collection_in_db = await collections.insert_one(collection.dict())
|
|
||||||
|
|
||||||
if not collection_in_db:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Unavailable database",
|
|
||||||
)
|
|
||||||
|
|
||||||
return collection.dict()
|
|
||||||
|
|
||||||
|
|
||||||
async def update_collection(
|
async def update_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_object: Collection,
|
collection_object: CollectionUpdate,
|
||||||
collection_id: str,
|
|
||||||
current_user: PublicUser,
|
current_user: PublicUser,
|
||||||
):
|
db_session: Session,
|
||||||
# verify collection rights
|
) -> CollectionRead:
|
||||||
|
statement = select(Collection).where(
|
||||||
collections = request.app.db["collections"]
|
Collection.id == collection_object.collection_id
|
||||||
|
|
||||||
collection = await collections.find_one({"collection_id": collection_id})
|
|
||||||
|
|
||||||
await verify_collection_rights(
|
|
||||||
request, collection_id, current_user, "update", collection["org_id"]
|
|
||||||
)
|
)
|
||||||
|
collection = db_session.exec(statement).first()
|
||||||
|
|
||||||
if not collection:
|
if not collection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_collection = CollectionInDB(
|
courses = collection_object.courses
|
||||||
collection_id=collection_id, **collection_object.dict()
|
|
||||||
|
del collection_object.collection_id
|
||||||
|
del collection_object.courses
|
||||||
|
|
||||||
|
# Update only the fields that were passed in
|
||||||
|
for var, value in vars(collection_object).items():
|
||||||
|
if value is not None:
|
||||||
|
setattr(collection, var, value)
|
||||||
|
|
||||||
|
collection.update_date = str(datetime.now())
|
||||||
|
|
||||||
|
# Update only the fields that were passed in
|
||||||
|
for var, value in vars(collection_object).items():
|
||||||
|
if value is not None:
|
||||||
|
setattr(collection, var, value)
|
||||||
|
|
||||||
|
statement = select(CollectionCourse).where(
|
||||||
|
CollectionCourse.collection_id == collection.id
|
||||||
|
)
|
||||||
|
collection_courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
# Delete all collection_courses
|
||||||
|
for collection_course in collection_courses:
|
||||||
|
db_session.delete(collection_course)
|
||||||
|
|
||||||
|
# Add new collection_courses
|
||||||
|
for course in courses or []:
|
||||||
|
collection_course = CollectionCourse(
|
||||||
|
collection_id=int(collection.id is not None),
|
||||||
|
course_id=int(course),
|
||||||
|
org_id=int(collection.org_id),
|
||||||
|
creation_date=str(datetime.now()),
|
||||||
|
update_date=str(datetime.now()),
|
||||||
|
)
|
||||||
|
# Add collection_course to database
|
||||||
|
db_session.add(collection_course)
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(collection)
|
||||||
|
|
||||||
|
# Get courses once again
|
||||||
|
statement = (
|
||||||
|
select(Course)
|
||||||
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
|
.distinct(Course.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
await collections.update_one(
|
courses = db_session.exec(statement).all()
|
||||||
{"collection_id": collection_id}, {"$set": updated_collection.dict()}
|
|
||||||
)
|
|
||||||
|
|
||||||
return Collection(**updated_collection.dict())
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
|
|
||||||
|
return collection
|
||||||
|
|
||||||
|
|
||||||
async def delete_collection(
|
async def delete_collection(
|
||||||
request: Request, collection_id: str, current_user: PublicUser
|
request: Request, collection_id: str, current_user: PublicUser, db_session: Session
|
||||||
):
|
):
|
||||||
collections = request.app.db["collections"]
|
statement = select(Collection).where(Collection.id == collection_id)
|
||||||
|
collection = db_session.exec(statement).first()
|
||||||
collection = await collections.find_one({"collection_id": collection_id})
|
|
||||||
|
|
||||||
await verify_collection_rights(
|
|
||||||
request, collection_id, current_user, "delete", collection["org_id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not collection:
|
if not collection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
status_code=404,
|
||||||
|
detail="Collection not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
isDeleted = await collections.delete_one({"collection_id": collection_id})
|
# delete collection from database
|
||||||
|
db_session.delete(collection)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
if isDeleted:
|
return {"detail": "Collection deleted"}
|
||||||
return {"detail": "collection deleted"}
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
detail="Unavailable database",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
####################################################
|
####################################################
|
||||||
|
|
@ -168,75 +202,30 @@ async def get_collections(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_id: str,
|
org_id: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser,
|
||||||
|
db_session: Session,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
):
|
) -> List[CollectionRead]:
|
||||||
collections = request.app.db["collections"]
|
statement = (
|
||||||
|
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
|
||||||
|
|
||||||
if current_user.user_id == "anonymous":
|
|
||||||
all_collections = collections.find(
|
|
||||||
{"org_id": org_id, "public": True}, {"_id": 0}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# get all collections from database without ObjectId
|
|
||||||
all_collections = (
|
|
||||||
collections.find({"org_id": org_id})
|
|
||||||
.sort("name", 1)
|
|
||||||
.skip(10 * (page - 1))
|
|
||||||
.limit(limit)
|
|
||||||
)
|
)
|
||||||
|
collections = db_session.exec(statement).all()
|
||||||
|
|
||||||
# create list of collections and include courses in each collection
|
if not collections:
|
||||||
collections_list = []
|
|
||||||
for collection in await all_collections.to_list(length=100):
|
|
||||||
collection = CollectionInDB(**collection)
|
|
||||||
collections_list.append(collection)
|
|
||||||
|
|
||||||
collection_courses = [course for course in collection.courses]
|
|
||||||
# add courses to collection
|
|
||||||
courses = request.app.db["courses"]
|
|
||||||
collection.courses = []
|
|
||||||
collection.courses = courses.find(
|
|
||||||
{"course_id": {"$in": collection_courses}}, {"_id": 0}
|
|
||||||
)
|
|
||||||
|
|
||||||
collection.courses = [
|
|
||||||
course for course in await collection.courses.to_list(length=100)
|
|
||||||
]
|
|
||||||
|
|
||||||
return collections_list
|
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
|
||||||
|
|
||||||
|
|
||||||
async def verify_collection_rights(
|
|
||||||
request: Request,
|
|
||||||
collection_id: str,
|
|
||||||
current_user: PublicUser,
|
|
||||||
action: Literal["create", "read", "update", "delete"],
|
|
||||||
org_id: str,
|
|
||||||
):
|
|
||||||
collections = request.app.db["collections"]
|
|
||||||
users = request.app.db["users"]
|
|
||||||
user = await users.find_one({"user_id": current_user.user_id})
|
|
||||||
collection = await collections.find_one({"collection_id": collection_id})
|
|
||||||
|
|
||||||
if not collection and action != "create" and collection_id != "*":
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
status_code=status.HTTP_409_CONFLICT, detail="No collections found"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Collections are public by default for now
|
collections_with_courses = []
|
||||||
if current_user.user_id == "anonymous" and action == "read":
|
for collection in collections:
|
||||||
return True
|
statement = (
|
||||||
|
select(Course)
|
||||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
|
.distinct(Course.id)
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
|
||||||
request, current_user.user_id, action, user["roles"], collection_id
|
|
||||||
)
|
)
|
||||||
|
courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
|
collections_with_courses.append(collection)
|
||||||
|
|
||||||
#### Security ####################################################
|
return collections_with_courses
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue