Merge pull request #133 from learnhouse/feat/psql-migration-and-frontend-improvements

PostgreSQL migration and frontend improvements
This commit is contained in:
Badr B 2023-12-16 12:34:53 +01:00 committed by GitHub
commit 0e2e66d0e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
172 changed files with 7488 additions and 5008 deletions

View file

@ -46,7 +46,8 @@ class HostingConfig(BaseModel):
class DatabaseConfig(BaseModel):
mongodb_connection_string: Optional[str]
sql_connection_string: Optional[str]
mongo_connection_string: Optional[str]
class LearnHouseConfig(BaseModel):
@ -105,9 +106,7 @@ def get_learnhouse_config() -> LearnHouseConfig:
env_allowed_origins = env_allowed_origins.split(",")
env_allowed_regexp = os.environ.get("LEARNHOUSE_ALLOWED_REGEXP")
env_self_hosted = os.environ.get("LEARNHOUSE_SELF_HOSTED")
env_mongodb_connection_string = os.environ.get(
"LEARNHOUSE_MONGODB_CONNECTION_STRING"
)
env_sql_connection_string = os.environ.get("LEARNHOUSE_SQL_CONNECTION_STRING")
# Sentry Config
env_sentry_dsn = os.environ.get("LEARNHOUSE_SENTRY_DSN")
@ -166,9 +165,13 @@ def get_learnhouse_config() -> LearnHouseConfig:
)
# Database config
mongodb_connection_string = env_mongodb_connection_string or yaml_config.get(
sql_connection_string = env_sql_connection_string or yaml_config.get(
"database_config", {}
).get("mongodb_connection_string")
).get("sql_connection_string")
mongo_connection_string = yaml_config.get("database_config", {}).get(
"mongo_connection_string"
)
# Sentry config
# check if the sentry config is provided in the YAML file
@ -210,7 +213,8 @@ def get_learnhouse_config() -> LearnHouseConfig:
content_delivery=content_delivery,
)
database_config = DatabaseConfig(
mongodb_connection_string=mongodb_connection_string
sql_connection_string=sql_connection_string,
mongo_connection_string=mongo_connection_string,
)
# Create LearnHouseConfig object

View file

@ -25,4 +25,5 @@ hosting_config:
endpoint_url: ""
database_config:
mongodb_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/
sql_connection_string: postgresql://learnhouse:learnhouse@db:5432/learnhouse
mongo_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/

View file

@ -1,8 +1,10 @@
fastapi==0.101.1
fastapi==0.104.1
pydantic>=1.8.0,<2.0.0
sqlmodel==0.0.10
uvicorn==0.23.2
pymongo==4.3.3
motor==3.1.1
psycopg2
python-multipart
boto3
botocore

View file

@ -1,21 +1,34 @@
import logging
from config.config import get_learnhouse_config
from fastapi import FastAPI
from sqlmodel import SQLModel, Session, create_engine
import motor.motor_asyncio
learnhouse_config = get_learnhouse_config()
engine = create_engine(
learnhouse_config.database_config.sql_connection_string, echo=False # type: ignore
)
SQLModel.metadata.create_all(engine)
async def connect_to_db(app: FastAPI):
logging.info("Connecting to database...")
try:
app.db_engine = engine # type: ignore
logging.info("LearnHouse database has been started.")
SQLModel.metadata.create_all(engine)
# MongoDB for migration purposes
# mongodb
app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore
app.learnhouse_config.database_config.mongodb_connection_string) # type: ignore
app.learnhouse_config.database_config.mongo_connection_string # type: ignore
) # type: ignore
app.db = app.mongodb_client["learnhouse"] # type: ignore
logging.info("Connected to database!")
except Exception as e:
logging.error("Failed to connect to database!")
logging.error(e)
def get_db_session():
with Session(engine) as session:
yield session
async def close_database(app: FastAPI):
app.mongodb_client.close() # type: ignore
logging.info("LearnHouse has been shut down.")
return app

View file

@ -0,0 +1,72 @@
from typing import Optional
from sqlalchemy import JSON, BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
from enum import Enum
class ActivityTypeEnum(str, Enum):
TYPE_VIDEO = "TYPE_VIDEO"
TYPE_DOCUMENT = "TYPE_DOCUMENT"
TYPE_DYNAMIC = "TYPE_DYNAMIC"
TYPE_ASSESSMENT = "TYPE_ASSESSMENT"
TYPE_CUSTOM = "TYPE_CUSTOM"
class ActivitySubTypeEnum(str, Enum):
# Dynamic
SUBTYPE_DYNAMIC_PAGE = "SUBTYPE_DYNAMIC_PAGE"
# Video
SUBTYPE_VIDEO_YOUTUBE = "SUBTYPE_VIDEO_YOUTUBE"
SUBTYPE_VIDEO_HOSTED = "SUBTYPE_VIDEO_HOSTED"
# Document
SUBTYPE_DOCUMENT_PDF = "SUBTYPE_DOCUMENT_PDF"
SUBTYPE_DOCUMENT_DOC = "SUBTYPE_DOCUMENT_DOC"
# Assessment
SUBTYPE_ASSESSMENT_QUIZ = "SUBTYPE_ASSESSMENT_QUIZ"
# Custom
SUBTYPE_CUSTOM = "SUBTYPE_CUSTOM"
class ActivityBase(SQLModel):
name: str
activity_type: ActivityTypeEnum = ActivityTypeEnum.TYPE_CUSTOM
activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.SUBTYPE_CUSTOM
content: dict = Field(default={}, sa_column=Column(JSON))
published_version: int
version: int
class Activity(ActivityBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(
default=None,
sa_column=Column(
BigInteger, ForeignKey("course.id", ondelete="CASCADE")
),
)
activity_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class ActivityCreate(ActivityBase):
chapter_id: int
pass
class ActivityUpdate(ActivityBase):
name: Optional[str]
activity_type: Optional[ActivityTypeEnum]
activity_sub_type: Optional[ActivitySubTypeEnum]
content: dict = Field(default={}, sa_column=Column(JSON))
published_version: Optional[int]
version: Optional[int]
class ActivityRead(ActivityBase):
id: int
activity_uuid: str
creation_date: str
update_date: str
pass

46
apps/api/src/db/blocks.py Normal file
View file

@ -0,0 +1,46 @@
from typing import Optional
from sqlalchemy import JSON, Column, ForeignKey
from sqlmodel import Field, SQLModel
from enum import Enum
class BlockTypeEnum(str, Enum):
BLOCK_QUIZ = "BLOCK_QUIZ"
BLOCK_VIDEO = "BLOCK_VIDEO"
BLOCK_DOCUMENT_PDF = "BLOCK_DOCUMENT_PDF"
BLOCK_IMAGE = "BLOCK_IMAGE"
BLOCK_CUSTOM = "BLOCK_CUSTOM"
class BlockBase(SQLModel):
id: Optional[int] = Field(default=None, primary_key=True)
block_type: BlockTypeEnum = BlockTypeEnum.BLOCK_CUSTOM
content: dict = Field(default={}, sa_column=Column(JSON))
class Block(BlockBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
content: dict = Field(default={}, sa_column=Column(JSON))
org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(sa_column= Column("course_id", ForeignKey("course.id", ondelete="CASCADE")))
chapter_id: int = Field(sa_column= Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")))
activity_id: int = Field(sa_column= Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")))
block_uuid: str
creation_date: str
update_date: str
class BlockCreate(BlockBase):
pass
class BlockRead(BlockBase):
id: int
org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(default=None, foreign_key="course.id")
chapter_id: int = Field(default=None, foreign_key="chapter.id")
activity_id: int = Field(default=None, foreign_key="activity.id")
block_uuid: str
creation_date: str
update_date: str
pass

View file

@ -0,0 +1,13 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
class ChapterActivity(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
order: int
chapter_id: int = Field(sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE")))
activity_id: int = Field(sa_column=Column(BigInteger, ForeignKey("activity.id", ondelete="CASCADE")))
course_id : int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
org_id : int = Field(default=None, foreign_key="organization.id")
creation_date: str
update_date: str

View file

@ -0,0 +1,68 @@
from typing import Any, List, Optional
from pydantic import BaseModel
from sqlalchemy import Column, ForeignKey
from sqlmodel import Field, SQLModel
from src.db.activities import ActivityRead
class ChapterBase(SQLModel):
name: str
description: Optional[str] = ""
thumbnail_image: Optional[str] = ""
org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(
sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
)
class Chapter(ChapterBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
course_id: int = Field(
sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
)
chapter_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class ChapterCreate(ChapterBase):
# referenced order here will be ignored and just used for validation
# used order will be the next available.
pass
class ChapterUpdate(ChapterBase):
name: Optional[str]
description: Optional[str]
thumbnail_image: Optional[str]
course_id: Optional[int]
org_id: Optional[int]
class ChapterRead(ChapterBase):
id: int
activities: List[ActivityRead]
chapter_uuid: str
creation_date: str
update_date: str
pass
class ActivityOrder(BaseModel):
activity_id: int
class ChapterOrder(BaseModel):
chapter_id: int
activities_order_by_ids: List[ActivityOrder]
class ChapterUpdateOrder(BaseModel):
chapter_order_by_ids: List[ChapterOrder]
class DepreceatedChaptersRead(BaseModel):
chapterOrder: Any
chapters: Any
activities: Any
pass

View file

@ -0,0 +1,39 @@
from typing import Optional
from sqlmodel import Field, SQLModel
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[int]
org_id: int = Field(default=None, foreign_key="organization.id")
pass
class CollectionUpdate(CollectionBase):
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

View file

@ -0,0 +1,12 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
class CollectionCourse(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
collection_id: int = Field(sa_column=Column(BigInteger, ForeignKey("collection.id", ondelete="CASCADE")))
course_id: int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str
update_date: str

View file

@ -0,0 +1,17 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
class CourseChapter(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
order: int
course_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE"))
)
chapter_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE"))
)
org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str
update_date: str

View file

@ -0,0 +1,72 @@
from typing import List, Optional
from sqlmodel import Field, SQLModel
from src.db.users import UserRead
from src.db.trails import TrailRead
from src.db.chapters import ChapterRead
class CourseBase(SQLModel):
name: str
description: Optional[str]
about: Optional[str]
learnings: Optional[str]
tags: Optional[str]
thumbnail_image: Optional[str]
public: bool
class Course(CourseBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
course_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class CourseCreate(CourseBase):
org_id: int = Field(default=None, foreign_key="organization.id")
pass
class CourseUpdate(CourseBase):
name: str
description: Optional[str]
about: Optional[str]
learnings: Optional[str]
tags: Optional[str]
public: Optional[bool]
class CourseRead(CourseBase):
id: int
org_id: int = Field(default=None, foreign_key="organization.id")
authors: List[UserRead]
course_uuid: str
creation_date: str
update_date: str
pass
class FullCourseRead(CourseBase):
id: int
course_uuid: str
creation_date: str
update_date: str
# Chapters, Activities
chapters: List[ChapterRead]
authors: List[UserRead]
pass
class FullCourseReadWithTrail(CourseBase):
id: int
course_uuid: str
creation_date: str
update_date: str
org_id: int = Field(default=None, foreign_key="organization.id")
authors: List[UserRead]
# Chapters, Activities
chapters: List[ChapterRead]
# Trail
trail: TrailRead | None
pass

View file

@ -0,0 +1,31 @@
from typing import Optional
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
class InstallBase(SQLModel):
step: int = Field(default=0)
data: dict = Field(default={}, sa_column=Column(JSON))
class Install(InstallBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
install_uuid: str = Field(default=None)
creation_date: str = ""
update_date: str = ""
class InstallCreate(InstallBase):
pass
class InstallUpdate(InstallBase):
pass
class InstallRead(InstallBase):
id: Optional[int] = Field(default=None, primary_key=True)
install_uuid: str = Field(default=None)
creation_date: str
update_date: str
pass

View file

@ -0,0 +1,21 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
from enum import Enum
class HeaderTypeEnum(str, Enum):
LOGO_MENU_SETTINGS = "LOGO_MENU_SETTINGS"
MENU_LOGO_SETTINGS = "MENU_LOGO_SETTINGS"
class OrganizationSettings(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
)
logo_image: Optional[str] = ""
header_type: HeaderTypeEnum = HeaderTypeEnum.LOGO_MENU_SETTINGS
color: str = ""
creation_date: str
update_date: str

View file

@ -0,0 +1,30 @@
from typing import Optional
from sqlmodel import Field, SQLModel
class OrganizationBase(SQLModel):
name: str
description: Optional[str]
slug: str
email: str
logo_image: Optional[str]
class Organization(OrganizationBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class OrganizationUpdate(OrganizationBase):
pass
class OrganizationCreate(OrganizationBase):
pass
class OrganizationRead(OrganizationBase):
id: int
org_uuid: str
creation_date: str
update_date: str

View file

@ -0,0 +1,18 @@
from enum import Enum
from typing import Optional
from sqlmodel import Field, SQLModel
class ResourceAuthorshipEnum(str, Enum):
CREATOR = "CREATOR"
MAINTAINER = "MAINTAINER"
REPORTER = "REPORTER"
class ResourceAuthor(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
resource_uuid: str
user_id: int = Field(default=None, foreign_key="user.id")
authorship: ResourceAuthorshipEnum = ResourceAuthorshipEnum.CREATOR
creation_date: str = ""
update_date: str = ""

72
apps/api/src/db/roles.py Normal file
View file

@ -0,0 +1,72 @@
from enum import Enum
from typing import Optional, Union
from pydantic import BaseModel
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
# Rights
class Permission(BaseModel):
action_create: bool
action_read: bool
action_update: bool
action_delete: bool
def __getitem__(self, item):
return getattr(self, item)
class Rights(BaseModel):
courses: Permission
users: Permission
collections: Permission
organizations: Permission
coursechapters: Permission
activities: Permission
def __getitem__(self, item):
return getattr(self, item)
# Database Models
class RoleTypeEnum(str, Enum):
TYPE_ORGANIZATION = "TYPE_ORGANIZATION" # Organization roles are associated with an organization, they are used to define the rights of a user in an organization
TYPE_ORGANIZATION_API_TOKEN = "TYPE_ORGANIZATION_API_TOKEN" # Organization API Token roles are associated with an organization, they are used to define the rights of an API Token in an organization
TYPE_GLOBAL = "TYPE_GLOBAL" # Global roles are not associated with an organization, they are used to define the default rights of a user
class RoleBase(SQLModel):
name: str
description: Optional[str]
rights: Optional[Union[Rights, dict]] = Field(default={}, sa_column=Column(JSON))
class Role(RoleBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
role_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL
role_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class RoleRead(RoleBase):
id: Optional[int] = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id")
role_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL
role_uuid: str
creation_date: str
update_date: str
class RoleCreate(RoleBase):
org_id: Optional[int] = Field(default=None, foreign_key="organization.id")
class RoleUpdate(SQLModel):
role_id: int = Field(default=None, foreign_key="role.id")
name: Optional[str]
description: Optional[str]
rights: Optional[Union[Rights, dict]] = Field(default={}, sa_column=Column(JSON))

View file

@ -0,0 +1,57 @@
from typing import Optional
from pydantic import BaseModel
from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel
from enum import Enum
from src.db.trail_steps import TrailStep
class TrailRunEnum(str, Enum):
RUN_TYPE_COURSE = "RUN_TYPE_COURSE"
class StatusEnum(str, Enum):
STATUS_IN_PROGRESS = "STATUS_IN_PROGRESS"
STATUS_COMPLETED = "STATUS_COMPLETED"
STATUS_PAUSED = "STATUS_PAUSED"
STATUS_CANCELLED = "STATUS_CANCELLED"
class TrailRun(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
data: dict = Field(default={}, sa_column=Column(JSON))
status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS
# foreign keys
trail_id: int = Field(default=None, foreign_key="trail.id")
course_id: int = Field(default=None, foreign_key="course.id")
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
# timestamps
creation_date: str
update_date: str
class TrailRunCreate(TrailRun):
pass
# trick because Lists are not supported in SQLModel (runs: list[TrailStep] )
class TrailRunRead(BaseModel):
id: Optional[int] = Field(default=None, primary_key=True)
data: dict = Field(default={}, sa_column=Column(JSON))
status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS
# foreign keys
trail_id: int = Field(default=None, foreign_key="trail.id")
course_id: int = Field(default=None, foreign_key="course.id")
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
# course object
course: dict
# timestamps
creation_date: str
update_date: str
# number of activities in course
course_total_steps: int
steps: list[TrailStep]
pass

View file

@ -0,0 +1,34 @@
from enum import Enum
from typing import Optional
from sqlmodel import Field, SQLModel
from sqlalchemy import BigInteger, ForeignKey, JSON, Column
class TrailStepTypeEnum(str, Enum):
STEP_TYPE_READABLE_ACTIVITY = "STEP_TYPE_READABLE_ACTIVITY"
STEP_TYPE_ASSIGNMENT_ACTIVITY = "STEP_TYPE_ASSIGNMENT_ACTIVITY"
STEP_TYPE_CUSTOM_ACTIVITY = "STEP_TYPE_CUSTOM_ACTIVITY"
class TrailStep(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
complete: bool
teacher_verified: bool
grade: str
data: dict = Field(default={}, sa_column=Column(JSON))
# foreign keys
trailrun_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("trailrun.id", ondelete="CASCADE"))
)
trail_id: int = Field(default=None, foreign_key="trail.id")
activity_id: int = Field(default=None, foreign_key="activity.id")
course_id: int = Field(default=None, foreign_key="course.id")
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
# timestamps
creation_date: str
update_date: str
# note : prepare assignments support
# an assignment object will be linked to a trail step object in the future

34
apps/api/src/db/trails.py Normal file
View file

@ -0,0 +1,34 @@
from typing import Optional
from pydantic import BaseModel
from sqlmodel import Field, SQLModel
from src.db.trail_runs import TrailRunRead
class TrailBase(SQLModel):
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
class Trail(TrailBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
trail_uuid: str = ""
creation_date: str = ""
update_date: str = ""
class TrailCreate(TrailBase):
pass
# trick because Lists are not supported in SQLModel (runs: list[TrailRun] )
class TrailRead(BaseModel):
id: Optional[int] = Field(default=None, primary_key=True)
trail_uuid: str
org_id: int = Field(default=None, foreign_key="organization.id")
user_id: int = Field(default=None, foreign_key="user.id")
creation_date: str
update_date: str
runs: list[TrailRunRead]
class Config:
orm_mode = True

View file

@ -0,0 +1,14 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlmodel import Field, SQLModel
class UserOrganization(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(default=None, foreign_key="user.id")
org_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("organization.id", ondelete="CASCADE"))
)
role_id: int = Field(default=None, foreign_key="role.id")
creation_date: str
update_date: str

50
apps/api/src/db/users.py Normal file
View file

@ -0,0 +1,50 @@
from typing import Optional
from sqlmodel import Field, SQLModel
class UserBase(SQLModel):
username: str
first_name: str
last_name: str
email: str
avatar_image: Optional[str] = ""
bio: Optional[str] = ""
class UserCreate(UserBase):
password: str
class UserUpdate(UserBase):
username: str
first_name: Optional[str]
last_name: Optional[str]
email: str
avatar_image: Optional[str] = ""
bio: Optional[str] = ""
class UserUpdatePassword(SQLModel):
old_password: str
new_password: str
class UserRead(UserBase):
id: int
user_uuid: str
class PublicUser(UserRead):
pass
class AnonymousUser(SQLModel):
id: int = 0
user_uuid: str = "user_anonymous"
username: str = "anonymous"
class User(UserBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
password: str = ""
user_uuid: str = ""
email_verified: bool = False
creation_date: str = ""
update_date: str = ""

View file

@ -35,3 +35,4 @@ v1_router.include_router(
tags=["install"],
dependencies=[Depends(isInstallModeEnabled)],
)

View file

@ -1,8 +1,10 @@
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session
from src.db.users import UserRead
from src.core.events.database import get_db_session
from config.config import get_learnhouse_config
from src.security.auth import AuthJWT, authenticate_user
from src.services.users.users import PublicUser
router = APIRouter()
@ -21,7 +23,12 @@ def refresh(response: Response,Authorize: AuthJWT = Depends()):
current_user = Authorize.get_jwt_subject()
new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore
response.set_cookie(key="access_token_cookie", value=new_access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain)
response.set_cookie(
key="access_token_cookie",
value=new_access_token,
httponly=False,
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
)
return {"access_token": new_access_token}
@ -31,8 +38,11 @@ async def login(
response: Response,
Authorize: AuthJWT = Depends(),
form_data: OAuth2PasswordRequestForm = Depends(),
db_session: Session = Depends(get_db_session),
):
user = await authenticate_user(request, form_data.username, form_data.password)
user = await authenticate_user(
request, form_data.username, form_data.password, db_session
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@ -44,8 +54,14 @@ async def login(
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
Authorize.set_refresh_cookies(refresh_token)
# set cookies using fastapi
response.set_cookie(key="access_token_cookie", value=access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain)
user = PublicUser(**user.dict())
response.set_cookie(
key="access_token_cookie",
value=access_token,
httponly=False,
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
)
user = UserRead.from_orm(user)
result = {
"user": user,

View file

@ -1,9 +1,20 @@
from fastapi import APIRouter, Depends, UploadFile, Form, Request
from src.db.blocks import BlockRead
from src.core.events.database import get_db_session
from src.security.auth import get_current_user
from src.services.blocks.block_types.imageBlock.images import create_image_block, get_image_block
from src.services.blocks.block_types.videoBlock.videoBlock import create_video_block, get_video_block
from src.services.blocks.block_types.pdfBlock.pdfBlock import create_pdf_block, get_pdf_block
from src.services.blocks.block_types.quizBlock.quizBlock import create_quiz_block, get_quiz_block_answers, get_quiz_block_options, quizBlock
from src.services.blocks.block_types.imageBlock.imageBlock import (
create_image_block,
get_image_block,
)
from src.services.blocks.block_types.videoBlock.videoBlock import (
create_video_block,
get_video_block,
)
from src.services.blocks.block_types.pdfBlock.pdfBlock import (
create_pdf_block,
get_pdf_block,
)
from src.services.users.users import PublicUser
router = APIRouter()
@ -12,83 +23,93 @@ router = APIRouter()
# Image Block
####################
@router.post("/image")
async def api_create_image_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
async def api_create_image_file_block(
request: Request,
file_object: UploadFile,
activity_uuid: str = Form(),
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
"""
Create new image file
"""
return await create_image_block(request, file_object, activity_id)
return await create_image_block(request, file_object, activity_uuid, db_session)
@router.get("/image")
async def api_get_image_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_get_image_file_block(
request: Request,
block_uuid: str,
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
"""
Get image file
"""
return await get_image_block(request, file_id, current_user)
return await get_image_block(request, block_uuid, current_user, db_session)
####################
# Video Block
####################
@router.post("/video")
async def api_create_video_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
async def api_create_video_file_block(
request: Request,
file_object: UploadFile,
activity_uuid: str = Form(),
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
"""
Create new video file
"""
return await create_video_block(request, file_object, activity_id)
return await create_video_block(request, file_object, activity_uuid, db_session)
@router.get("/video")
async def api_get_video_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_get_video_file_block(
request: Request,
block_uuid: str,
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
"""
Get video file
"""
return await get_video_block(request, file_id, current_user)
return await get_video_block(request, block_uuid, current_user, db_session)
####################
# PDF Block
####################
@router.post("/pdf")
async def api_create_pdf_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)):
async def api_create_pdf_file_block(
request: Request,
file_object: UploadFile,
activity_uuid: str = Form(),
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
"""
Create new pdf file
"""
return await create_pdf_block(request, file_object, activity_id)
return await create_pdf_block(request, file_object, activity_uuid, db_session)
@router.get("/pdf")
async def api_get_pdf_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_get_pdf_file_block(
request: Request,
block_uuid: str,
db_session=Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> BlockRead:
"""
Get pdf file
"""
return await get_pdf_block(request, file_id, current_user)
####################
# Quiz Block
####################
@router.post("/quiz/{activity_id}")
async def api_create_quiz_block(request: Request, quiz_block: quizBlock, activity_id: str, current_user: PublicUser = Depends(get_current_user)):
"""
Create new document file
"""
return await create_quiz_block(request, quiz_block, activity_id, current_user)
@router.get("/quiz/options")
async def api_get_quiz_options(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)):
"""
Get quiz options
"""
return await get_quiz_block_options(request, block_id, current_user)
@router.get("/quiz/answers")
async def api_get_quiz_answers(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)):
"""
Get quiz answers
"""
return await get_quiz_block_answers(request, block_id, current_user)
return await get_pdf_block(request, block_uuid, current_user, db_session)

View file

@ -1,6 +1,9 @@
from typing import List
from fastapi import APIRouter, Depends, UploadFile, Form, Request
from src.db.activities import ActivityCreate, ActivityRead, ActivityUpdate
from src.db.users import PublicUser
from src.core.events.database import get_db_session
from src.services.courses.activities.activities import (
Activity,
create_activity,
get_activity,
get_activities,
@ -14,7 +17,6 @@ from src.services.courses.activities.video import (
create_external_video_activity,
create_video_activity,
)
from src.services.users.schemas.users import PublicUser
router = APIRouter()
@ -22,17 +24,14 @@ router = APIRouter()
@router.post("/")
async def api_create_activity(
request: Request,
activity_object: Activity,
org_id: str,
coursechapter_id: str,
activity_object: ActivityCreate,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> ActivityRead:
"""
Create new activity
"""
return await create_activity(
request, activity_object, org_id, coursechapter_id, current_user
)
return await create_activity(request, activity_object, current_user, db_session)
@router.get("/{activity_id}")
@ -40,36 +39,43 @@ async def api_get_activity(
request: Request,
activity_id: str,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> ActivityRead:
"""
Get single activity by activity_id
"""
return await get_activity(request, activity_id, current_user=current_user)
return await get_activity(
request, activity_id, current_user=current_user, db_session=db_session
)
@router.get("/coursechapter/{coursechapter_id}")
async def api_get_activities(
@router.get("/chapter/{chapter_id}")
async def api_get_chapter_activities(
request: Request,
coursechapter_id: str,
chapter_id: int,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> List[ActivityRead]:
"""
Get CourseChapter activities
Get Activities for a chapter
"""
return await get_activities(request, coursechapter_id, current_user)
return await get_activities(request, chapter_id, current_user, db_session)
@router.put("/{activity_id}")
@router.put("/{activity_uuid}")
async def api_update_activity(
request: Request,
activity_object: Activity,
activity_id: str,
activity_object: ActivityUpdate,
activity_uuid: str,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> ActivityRead:
"""
Update activity by activity_id
"""
return await update_activity(request, activity_object, activity_id, current_user)
return await update_activity(
request, activity_object, activity_uuid, current_user, db_session
)
@router.delete("/{activity_id}")
@ -77,11 +83,12 @@ async def api_delete_activity(
request: Request,
activity_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Delete activity by activity_id
"""
return await delete_activity(request, activity_id, current_user)
return await delete_activity(request, activity_id, current_user, db_session)
# Video activity
@ -91,15 +98,21 @@ async def api_delete_activity(
async def api_create_video_activity(
request: Request,
name: str = Form(),
coursechapter_id: str = Form(),
chapter_id: str = Form(),
current_user: PublicUser = Depends(get_current_user),
video_file: UploadFile | None = None,
):
db_session=Depends(get_db_session),
) -> ActivityRead:
"""
Create new activity
"""
return await create_video_activity(
request, name, coursechapter_id, current_user, video_file
request,
name,
chapter_id,
current_user,
db_session,
video_file,
)
@ -108,24 +121,28 @@ async def api_create_external_video_activity(
request: Request,
external_video: ExternalVideo,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> ActivityRead:
"""
Create new activity
"""
return await create_external_video_activity(request, current_user, external_video)
return await create_external_video_activity(
request, current_user, external_video, db_session
)
@router.post("/documentpdf")
async def api_create_documentpdf_activity(
request: Request,
name: str = Form(),
coursechapter_id: str = Form(),
chapter_id: str = Form(),
current_user: PublicUser = Depends(get_current_user),
pdf_file: UploadFile | None = None,
):
db_session=Depends(get_db_session),
) -> ActivityRead:
"""
Create new activity
"""
return await create_documentpdf_activity(
request, name, coursechapter_id, current_user, pdf_file
request, name, chapter_id, current_user, db_session, pdf_file
)

View file

@ -1,6 +1,22 @@
from typing import List
from fastapi import APIRouter, Depends, Request
from src.core.events.database import get_db_session
from src.db.chapters import (
ChapterCreate,
ChapterRead,
ChapterUpdate,
ChapterUpdateOrder,
)
from src.services.courses.chapters import (
DEPRECEATED_get_course_chapters,
create_chapter,
delete_chapter,
get_chapter,
get_course_chapters,
reorder_chapters_and_activities,
update_chapter,
)
from src.services.courses.chapters import CourseChapter, CourseChapterMetaData, create_coursechapter, delete_coursechapter, get_coursechapter, get_coursechapters, get_coursechapters_meta, update_coursechapter, update_coursechapters_meta
from src.services.users.users import PublicUser
from src.security.auth import get_current_user
@ -8,57 +24,104 @@ router = APIRouter()
@router.post("/")
async def api_create_coursechapter(request: Request,coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_create_coursechapter(
request: Request,
coursechapter_object: ChapterCreate,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> ChapterRead:
"""
Create new Course Chapter
"""
return await create_coursechapter(request, coursechapter_object, course_id, current_user)
return await create_chapter(request, coursechapter_object, current_user, db_session)
@router.get("/{coursechapter_id}")
async def api_get_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.get("/{chapter_id}")
async def api_get_coursechapter(
request: Request,
chapter_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> ChapterRead:
"""
Get single CourseChapter by coursechapter_id
Get single CourseChapter by chapter_id
"""
return await get_coursechapter(request, coursechapter_id, current_user=current_user)
return await get_chapter(request, chapter_id, current_user, db_session)
@router.get("/meta/{course_id}")
async def api_get_coursechapter_meta(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.get("/course/{course_uuid}/meta", deprecated=True)
async def api_get_chapter_meta(
request: Request,
course_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Get coursechapter metadata
Get Chapters metadata
"""
return await get_coursechapters_meta(request, course_id, current_user=current_user)
return await DEPRECEATED_get_course_chapters(
request, course_uuid, current_user, db_session
)
@router.put("/meta/{course_id}")
async def api_update_coursechapter_meta(request: Request,course_id: str, coursechapters_metadata: CourseChapterMetaData, current_user: PublicUser = Depends(get_current_user)):
@router.put("/course/{course_uuid}/order")
async def api_update_chapter_meta(
request: Request,
course_uuid: str,
order: ChapterUpdateOrder,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Update coursechapter metadata
Update Chapter metadata
"""
return await update_coursechapters_meta(request, course_id, coursechapters_metadata, current_user=current_user)
return await reorder_chapters_and_activities(
request, course_uuid, order, current_user, db_session
)
@router.get("/{course_id}/page/{page}/limit/{limit}")
async def api_get_coursechapter_by(request: Request,course_id: str, page: int, limit: int):
@router.get("/course/{course_id}/page/{page}/limit/{limit}")
async def api_get_chapter_by(
request: Request,
course_id: int,
page: int,
limit: int,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> List[ChapterRead]:
"""
Get Course Chapters by page and limit
"""
return await get_coursechapters(request, course_id, page, limit)
return await get_course_chapters(
request, course_id, db_session, current_user, page, limit
)
@router.put("/{coursechapter_id}")
async def api_update_coursechapter(request: Request,coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.put("/{chapter_id}")
async def api_update_coursechapter(
request: Request,
coursechapter_object: ChapterUpdate,
chapter_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> ChapterRead:
"""
Update CourseChapters by course_id
"""
return await update_coursechapter(request, coursechapter_object, coursechapter_id, current_user)
return await update_chapter(
request, coursechapter_object, chapter_id, current_user, db_session
)
@router.delete("/{coursechapter_id}")
async def api_delete_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.delete("/{chapter_id}")
async def api_delete_coursechapter(
request: Request,
chapter_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Delete CourseChapters by ID
"""
return await delete_coursechapter(request,coursechapter_id, current_user)
return await delete_chapter(request, chapter_id, current_user, db_session)

View file

@ -1,8 +1,10 @@
from typing import List
from fastapi import APIRouter, Depends, Request
from src.core.events.database import get_db_session
from src.db.collections import CollectionCreate, CollectionRead, CollectionUpdate
from src.security.auth import get_current_user
from src.services.users.users import PublicUser
from src.services.courses.collections import (
Collection,
create_collection,
get_collection,
get_collections,
@ -17,64 +19,69 @@ router = APIRouter()
@router.post("/")
async def api_create_collection(
request: Request,
collection_object: Collection,
collection_object: CollectionCreate,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> CollectionRead:
"""
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_uuid}")
async def api_get_collection(
request: Request,
collection_id: str,
collection_uuid: str,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> CollectionRead:
"""
Get single collection by ID
"""
return await get_collection(request, collection_id, current_user)
return await get_collection(request, collection_uuid, current_user, db_session)
@router.get("/org_id/{org_id}/page/{page}/limit/{limit}")
@router.get("/org/{org_id}/page/{page}/limit/{limit}")
async def api_get_collections_by(
request: Request,
page: int,
limit: int,
org_id: str,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> List[CollectionRead]:
"""
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_uuid}")
async def api_update_collection(
request: Request,
collection_object: Collection,
collection_id: str,
collection_object: CollectionUpdate,
collection_uuid: str,
current_user: PublicUser = Depends(get_current_user),
):
db_session=Depends(get_db_session),
) -> CollectionRead:
"""
Update collection by ID
"""
return await update_collection(
request, collection_object, collection_id, current_user
request, collection_object, collection_uuid, current_user, db_session
)
@router.delete("/{collection_id}")
@router.delete("/{collection_uuid}")
async def api_delete_collection(
request: Request,
collection_id: str,
collection_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Delete collection by ID
"""
return await delete_collection(request, collection_id, current_user)
return await delete_collection(request, collection_uuid, current_user, db_session)

View file

@ -1,66 +1,147 @@
from typing import List
from fastapi import APIRouter, Depends, UploadFile, Form, Request
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.db.users import PublicUser
from src.db.courses import (
CourseCreate,
CourseRead,
CourseUpdate,
FullCourseReadWithTrail,
)
from src.security.auth import get_current_user
from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses_orgslug, update_course, delete_course, update_course_thumbnail
from src.services.users.users import PublicUser
from src.services.courses.courses import (
create_course,
get_course,
get_course_meta,
get_courses_orgslug,
update_course,
delete_course,
update_course_thumbnail,
)
router = APIRouter()
@router.post("/")
async def api_create_course(request: Request, org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None):
async def api_create_course(
request: Request,
org_id: int,
name: str = Form(),
description: str = Form(),
public: bool = Form(),
learnings: str = Form(),
tags: str = Form(),
about: str = Form(),
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
thumbnail: UploadFile | None = None,
) -> CourseRead:
"""
Create new Course
"""
course = Course(name=name, mini_description=mini_description, description=description,
org_id=org_id, public=public, thumbnail="", chapters=[], chapters_content=[], learnings=[])
return await create_course(request, course, org_id, current_user, thumbnail)
course = CourseCreate(
name=name,
description=description,
org_id=org_id,
public=public,
thumbnail_image="",
about=about,
learnings=learnings,
tags=tags,
)
return await create_course(request, org_id, course, current_user, db_session, thumbnail)
@router.put("/thumbnail/{course_id}")
async def api_create_course_thumbnail(request: Request, course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)):
@router.put("/{course_uuid}/thumbnail")
async def api_create_course_thumbnail(
request: Request,
course_uuid: str,
thumbnail: UploadFile | None = None,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseRead:
"""
Update new Course Thumbnail
"""
return await update_course_thumbnail(request, course_id, current_user, thumbnail)
return await update_course_thumbnail(
request, course_uuid, current_user, db_session, thumbnail
)
@router.get("/{course_id}")
async def api_get_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.get("/{course_uuid}")
async def api_get_course(
request: Request,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseRead:
"""
Get single Course by course_id
Get single Course by course_uuid
"""
return await get_course(request, course_id, current_user=current_user)
return await get_course(
request, course_uuid, current_user=current_user, db_session=db_session
)
@router.get("/meta/{course_id}")
async def api_get_course_meta(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.get("/{course_uuid}/meta")
async def api_get_course_meta(
request: Request,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> FullCourseReadWithTrail:
"""
Get single Course Metadata (chapters, activities) by course_id
Get single Course Metadata (chapters, activities) by course_uuid
"""
return await get_course_meta(request, course_id, current_user=current_user)
return await get_course_meta(
request, course_uuid, current_user=current_user, db_session=db_session
)
@router.get("/org_slug/{org_slug}/page/{page}/limit/{limit}")
async def api_get_course_by_orgslug(request: Request, page: int, limit: int, org_slug: str, current_user: PublicUser = Depends(get_current_user)):
async def api_get_course_by_orgslug(
request: Request,
page: int,
limit: int,
org_slug: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> List[CourseRead]:
"""
Get houses by page and limit
Get courses by page and limit
"""
return await get_courses_orgslug(request, current_user, page, limit, org_slug)
return await get_courses_orgslug(
request, current_user, org_slug, db_session, page, limit
)
@router.put("/{course_id}")
async def api_update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.put("/{course_uuid}")
async def api_update_course(
request: Request,
course_object: CourseUpdate,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseRead:
"""
Update Course by course_id
Update Course by course_uuid
"""
return await update_course(request, course_object, course_id, current_user)
return await update_course(
request, course_object, course_uuid, current_user, db_session
)
@router.delete("/{course_id}")
async def api_delete_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.delete("/{course_uuid}")
async def api_delete_course(
request: Request,
course_uuid: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
):
"""
Delete Course by ID
"""
return await delete_course(request, course_id, current_user)
return await delete_course(request, course_uuid, current_user, db_session)

View file

@ -1,6 +1,8 @@
from fastapi import APIRouter, Request
from fastapi import APIRouter, Depends, Request
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.services.dev.migration_from_mongo import start_migrate_from_mongo
from config.config import get_learnhouse_config
from src.services.dev.mocks.initial import create_initial_data
router = APIRouter()
@ -12,7 +14,9 @@ async def config():
return config.dict()
@router.get("/mock/initial")
async def initial_data(request: Request):
await create_initial_data(request)
return {"Message": "Initial data created 🤖"}
@router.get("/migrate_from_mongo")
async def migrate_from_mongo(
request: Request,
db_session: Session = Depends(get_db_session),
):
return await start_migrate_from_mongo(request, db_session)

View file

@ -1,70 +1,87 @@
from fastapi import APIRouter, Request
from fastapi import APIRouter, Depends, Request
from src.db.install import InstallRead
from src.core.events.database import get_db_session
from src.db.organizations import OrganizationCreate
from src.db.users import UserCreate
from src.services.install.install import (
create_install_instance,
create_sample_data,
get_latest_install_instance,
install_create_organization,
install_create_organization_user,
install_default_elements,
update_install_instance,
)
from src.services.orgs.schemas.orgs import Organization
from src.services.users.schemas.users import UserWithPassword
router = APIRouter()
@router.post("/start")
async def api_create_install_instance(request: Request, data: dict):
async def api_create_install_instance(
request: Request,
data: dict,
db_session=Depends(get_db_session),
) -> InstallRead:
# create install
install = await create_install_instance(request, data)
install = await create_install_instance(request, data, db_session)
return install
@router.get("/latest")
async def api_get_latest_install_instance(request: Request):
async def api_get_latest_install_instance(
request: Request, db_session=Depends(get_db_session)
) -> InstallRead:
# get latest created install
install = await get_latest_install_instance(request)
install = await get_latest_install_instance(request, db_session=db_session)
return install
@router.post("/default_elements")
async def api_install_def_elements(request: Request):
elements = await install_default_elements(request, {})
async def api_install_def_elements(
request: Request,
db_session=Depends(get_db_session),
):
elements = await install_default_elements(request, {}, db_session)
return elements
@router.post("/org")
async def api_install_org(request: Request, org: Organization):
organization = await install_create_organization(request, org)
async def api_install_org(
request: Request,
org: OrganizationCreate,
db_session=Depends(get_db_session),
):
organization = await install_create_organization(request, org, db_session)
return organization
@router.post("/user")
async def api_install_user(request: Request, data: UserWithPassword, org_slug: str):
user = await install_create_organization_user(request, data, org_slug)
async def api_install_user(
request: Request,
data: UserCreate,
org_slug: str,
db_session=Depends(get_db_session),
):
user = await install_create_organization_user(request, data, org_slug, db_session)
return user
@router.post("/sample")
async def api_install_user_sample(request: Request, username: str, org_slug: str):
sample = await create_sample_data(org_slug, username, request)
return sample
@router.post("/update")
async def api_update_install_instance(request: Request, data: dict, step: int):
async def api_update_install_instance(
request: Request,
data: dict,
step: int,
db_session=Depends(get_db_session),
) -> InstallRead:
request.app.db["installs"]
# get latest created install
install = await update_install_instance(request, data, step)
install = await update_install_instance(request, data, step, db_session)
return install

View file

@ -1,63 +1,127 @@
from typing import List
from fastapi import APIRouter, Depends, Request, UploadFile
from sqlmodel import Session
from src.db.users import PublicUser
from src.db.organizations import (
Organization,
OrganizationCreate,
OrganizationRead,
OrganizationUpdate,
)
from src.core.events.database import get_db_session
from src.security.auth import get_current_user
from src.services.orgs.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org, update_org_logo
from src.services.users.users import PublicUser, User
from src.services.orgs.orgs import (
create_org,
delete_org,
get_organization,
get_organization_by_slug,
get_orgs_by_user,
update_org,
update_org_logo,
)
router = APIRouter()
@router.post("/")
async def api_create_org(request: Request, org_object: Organization, current_user: PublicUser = Depends(get_current_user)):
async def api_create_org(
request: Request,
org_object: OrganizationCreate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> OrganizationRead:
"""
Create new organization
"""
return await create_org(request, org_object, current_user)
return await create_org(request, org_object, current_user, db_session)
@router.get("/{org_id}")
async def api_get_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_get_org(
request: Request,
org_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> OrganizationRead:
"""
Get single Org by ID
"""
return await get_organization(request, org_id)
return await get_organization(request, org_id, db_session, current_user)
@router.get("/slug/{org_slug}")
async def api_get_org_by_slug(request: Request, org_slug: str, current_user: User = Depends(get_current_user)):
async def api_get_org_by_slug(
request: Request,
org_slug: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> OrganizationRead:
"""
Get single Org by Slug
"""
return await get_organization_by_slug(request, org_slug)
return await get_organization_by_slug(request, org_slug, db_session, current_user)
@router.put("/{org_id}/logo")
async def api_update_org_logo(request: Request, org_id: str, logo_file:UploadFile, current_user: PublicUser = Depends(get_current_user)):
async def api_update_org_logo(
request: Request,
org_id: str,
logo_file: UploadFile,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Get single Org by Slug
"""
return await update_org_logo(request=request,logo_file=logo_file, org_id=org_id, current_user=current_user)
return await update_org_logo(
request=request,
logo_file=logo_file,
org_id=org_id,
current_user=current_user,
db_session=db_session,
)
@router.get("/user/page/{page}/limit/{limit}")
async def api_user_orgs(request: Request, page: int, limit: int, current_user: PublicUser = Depends(get_current_user)):
async def api_user_orgs(
request: Request,
page: int,
limit: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> List[Organization]:
"""
Get orgs by page and limit by user
Get orgs by page and limit by current user
"""
return await get_orgs_by_user(request, current_user.user_id, page, limit)
return await get_orgs_by_user(
request, db_session, str(current_user.id), page, limit
)
@router.put("/{org_id}")
async def api_update_org(request: Request, org_object: Organization, org_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_update_org(
request: Request,
org_object: OrganizationUpdate,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> OrganizationRead:
"""
Update Org by ID
"""
return await update_org(request, org_object, org_id, current_user)
return await update_org(request, org_object,org_id, current_user, db_session)
@router.delete("/{org_id}")
async def api_delete_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_delete_org(
request: Request,
org_id: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Delete Org by ID
"""
return await delete_org(request, org_id, current_user)
return await delete_org(request, org_id, current_user, db_session)

View file

@ -1,41 +1,63 @@
from fastapi import APIRouter, Depends, Request
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.db.roles import RoleCreate, RoleRead, RoleUpdate
from src.security.auth import get_current_user
from src.services.roles.schemas.roles import Role
from src.services.roles.roles import create_role, delete_role, read_role, update_role
from src.services.users.schemas.users import PublicUser
from src.db.users import PublicUser
router = APIRouter()
@router.post("/")
async def api_create_role(request: Request, role_object: Role, current_user: PublicUser = Depends(get_current_user)):
async def api_create_role(
request: Request,
role_object: RoleCreate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
)-> RoleRead:
"""
Create new role
"""
return await create_role(request, role_object, current_user)
return await create_role(request, db_session, role_object, current_user)
@router.get("/{role_id}")
async def api_get_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_get_role(
request: Request,
role_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
)-> RoleRead:
"""
Get single role by role_id
"""
return await read_role(request, role_id, current_user)
return await read_role(request, db_session, role_id, current_user)
@router.put("/{role_id}")
async def api_update_role(request: Request, role_object: Role, role_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_update_role(
request: Request,
role_object: RoleUpdate,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
)-> RoleRead:
"""
Update role by role_id
"""
return await update_role(request, role_id, role_object, current_user)
return await update_role(request, db_session, role_object, current_user)
@router.delete("/{role_id}")
async def api_delete_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_delete_role(
request: Request,
role_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Delete role by ID
"""
return await delete_role(request, role_id, current_user)
return await delete_role(request, db_session, role_id, current_user)

View file

@ -1,56 +1,97 @@
from fastapi import APIRouter, Depends, Request
from src.core.events.database import get_db_session
from src.db.trails import TrailCreate, TrailRead
from src.security.auth import get_current_user
from src.services.trail.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail
from src.services.trail.trail import (
Trail,
add_activity_to_trail,
add_course_to_trail,
create_user_trail,
get_user_trails,
get_user_trail_with_orgid,
remove_course_from_trail,
)
router = APIRouter()
@router.post("/start")
async def api_start_trail(request: Request, trail_object: Trail, org_id: str, user=Depends(get_current_user)) -> Trail:
async def api_start_trail(
request: Request,
trail_object: TrailCreate,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> Trail:
"""
Start trail
"""
return await create_trail(request, user, org_id, trail_object)
return await create_user_trail(request, user, trail_object, db_session)
@router.get("/org_id/{org_id}/trail")
async def api_get_trail_by_orgid(request: Request, org_slug: str, user=Depends(get_current_user)):
@router.get("/")
async def api_get_user_trail(
request: Request,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
"""
Get a user trails
"""
return await get_user_trail(request, user=user, org_slug=org_slug)
return await get_user_trails(request, user=user, db_session=db_session)
@router.get("/org_slug/{org_slug}/trail")
async def api_get_trail_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)):
@router.get("/org/{org_id}/trail")
async def api_get_trail_by_org_id(
request: Request,
org_id: int,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
"""
Get a user trails using org slug
"""
return await get_user_trail_with_orgslug(request, user, org_slug=org_slug)
# Courses in trail
return await get_user_trail_with_orgid(
request, user, org_id=org_id, db_session=db_session
)
@router.post("/org_slug/{org_slug}/add_course/{course_id}")
async def api_add_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
@router.post("/add_course/{course_uuid}")
async def api_add_course_to_trail(
request: Request,
course_uuid: str,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
"""
Add Course to trail
"""
return await add_course_to_trail(request, user, org_slug, course_id)
return await add_course_to_trail(request, user, course_uuid, db_session)
@router.post("/org_slug/{org_slug}/remove_course/{course_id}")
async def api_remove_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
@router.delete("/remove_course/{course_uuid}")
async def api_remove_course_to_trail(
request: Request,
course_uuid: str,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
"""
Remove Course from trail
"""
return await remove_course_from_trail(request, user, org_slug, course_id)
return await remove_course_from_trail(request, user, course_uuid, db_session)
@router.post("/org_slug/{org_slug}/add_activity/course_id/{course_id}/activity_id/{activity_id}")
async def api_add_activity_to_trail(request: Request, activity_id: str, course_id: str, org_slug: str, user=Depends(get_current_user)):
@router.post("/add_activity/{activity_uuid}")
async def api_add_activity_to_trail(
request: Request,
activity_uuid: str,
user=Depends(get_current_user),
db_session=Depends(get_db_session),
) -> TrailRead:
"""
Add Course to trail
"""
return await add_activity_to_trail(request, user, course_id, org_slug, activity_id)
return await add_activity_to_trail(
request, user, activity_uuid, db_session
)

View file

@ -1,10 +1,27 @@
from fastapi import Depends, APIRouter, Request
from typing import Literal
from fastapi import APIRouter, Depends, Request
from sqlmodel import Session
from src.security.auth import get_current_user
from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserWithPassword
from src.services.users.users import create_user, delete_user, get_profile_metadata, get_user_by_userid, update_user, update_user_password
from src.core.events.database import get_db_session
from src.db.users import (
PublicUser,
User,
UserCreate,
UserRead,
UserUpdate,
UserUpdatePassword,
)
from src.services.users.users import (
authorize_user_action,
create_user,
create_user_without_org,
delete_user_by_id,
read_user_by_id,
read_user_by_uuid,
update_user,
update_user_password,
)
router = APIRouter()
@ -17,50 +34,119 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)):
"""
return current_user.dict()
@router.get("/profile_metadata")
async def api_get_current_user_metadata(request: Request,current_user: User = Depends(get_current_user)):
@router.get("/authorize/ressource/{ressource_uuid}/action/{action}")
async def api_get_authorization_status(
request: Request,
ressource_uuid: str,
action: Literal["create", "read", "update", "delete"],
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
):
"""
Get current user
Get current user authorization status
"""
return await get_profile_metadata(request , current_user.dict())
return await authorize_user_action(
request, db_session, current_user, ressource_uuid, action
)
@router.get("/user_id/{user_id}")
async def api_get_user_by_userid(request: Request,user_id: str):
@router.post("/{org_id}", response_model=UserRead, tags=["users"])
async def api_create_user_with_orgid(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_object: UserCreate,
org_id: int,
) -> UserRead:
"""
Get single user by user_id
Create User with Org ID
"""
return await get_user_by_userid(request, user_id)
return await create_user(request, db_session, current_user, user_object, org_id)
@router.post("/")
async def api_create_user(request: Request,user_object: UserWithPassword, org_slug: str ):
@router.post("/", response_model=UserRead, tags=["users"])
async def api_create_user_without_org(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_object: UserCreate,
) -> UserRead:
"""
Create new user
Create User
"""
return await create_user(request, None, user_object, org_slug)
return await create_user_without_org(request, db_session, current_user, user_object)
@router.delete("/user_id/{user_id}")
async def api_delete_user(request: Request, user_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.get("/id/{user_id}", response_model=UserRead, tags=["users"])
async def api_get_user_by_id(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_id: int,
) -> UserRead:
"""
Delete user by ID
Get User by ID
"""
return await delete_user(request, current_user, user_id)
return await read_user_by_id(request, db_session, current_user, user_id)
@router.put("/user_id/{user_id}")
async def api_update_user(request: Request, user_object: User, user_id: str, current_user: PublicUser = Depends(get_current_user)):
@router.get("/uuid/{user_uuid}", response_model=UserRead, tags=["users"])
async def api_get_user_by_uuid(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_uuid: str,
) -> UserRead:
"""
Update user by ID
Get User by UUID
"""
return await update_user(request, user_id, user_object, current_user)
return await read_user_by_uuid(request, db_session, current_user, user_uuid)
@router.put("/password/user_id/{user_id}")
async def api_update_user_password(request: Request, user_id: str , passwordChangeForm : PasswordChangeForm, current_user: PublicUser = Depends(get_current_user)):
@router.put("/{user_id}", response_model=UserRead, tags=["users"])
async def api_update_user(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_id: int,
user_object: UserUpdate,
) -> UserRead:
"""
Update user password by ID
Update User
"""
return await update_user_password(request,current_user, user_id, passwordChangeForm)
return await update_user(request, db_session, user_id, current_user, user_object)
@router.put("/change_password/{user_id}", response_model=UserRead, tags=["users"])
async def api_update_user_password(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_id: int,
form: UserUpdatePassword,
) -> UserRead:
"""
Update User Password
"""
return await update_user_password(request, db_session, current_user, user_id, form)
@router.delete("/user_id/{user_id}", tags=["users"])
async def api_delete_user(
*,
request: Request,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
user_id: int,
):
"""
Delete User
"""
return await delete_user_by_id(request, db_session, current_user, user_id)

View file

@ -1,3 +1,7 @@
from sqlmodel import Session
from src.core.events.database import get_db_session
from src.db.users import AnonymousUser, PublicUser, User, UserRead
from src.services.users.users import security_get_user
from config.config import get_learnhouse_config
from pydantic import BaseModel
from fastapi import Depends, HTTPException, Request, status
@ -5,8 +9,7 @@ from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from datetime import datetime, timedelta
from src.services.dev.dev import isDevModeEnabled
from src.services.users.schemas.users import AnonymousUser, PublicUser
from src.services.users.users import security_get_user, security_verify_password
from src.services.users.users import security_verify_password
from src.security.security import ALGORITHM, SECRET_KEY
from fastapi_jwt_auth import AuthJWT
@ -45,10 +48,13 @@ class TokenData(BaseModel):
#### Classes ####################################################
async def authenticate_user(request: Request, email: str, password: str):
user = await security_get_user(request, email)
async def authenticate_user(
request: Request,
email: str,
password: str,
db_session: Session,
) -> User | bool:
user = await security_get_user(request, db_session, email)
if not user:
return False
if not await security_verify_password(password, user.password):
@ -67,7 +73,11 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None):
return encoded_jwt
async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
async def get_current_user(
request: Request,
Authorize: AuthJWT = Depends(),
db_session: Session = Depends(get_db_session),
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
@ -81,7 +91,7 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
except JWTError:
raise credentials_exception
if username:
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
user = await security_get_user(request, db_session, email=token_data.username) # type: ignore # treated as an email
if user is None:
raise credentials_exception
return PublicUser(**user.dict())
@ -89,6 +99,6 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
return AnonymousUser()
async def non_public_endpoint(current_user: PublicUser):
async def non_public_endpoint(current_user: UserRead | AnonymousUser):
if isinstance(current_user, AnonymousUser):
raise HTTPException(status_code=401, detail="Not authenticated")

View file

@ -1,29 +1,31 @@
from typing import Literal
from fastapi import HTTPException, status, Request
from src.security.rbac.utils import check_element_type, get_id_identifier_of_element
from src.services.roles.schemas.roles import RoleInDB
from src.services.users.schemas.users import UserRolesInOrganization
from sqlalchemy import null
from sqlmodel import Session, select
from src.db.collections import Collection
from src.db.courses import Course
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.roles import Role
from src.db.user_organizations import UserOrganization
from src.security.rbac.utils import check_element_type
# Tested and working
async def authorization_verify_if_element_is_public(
request,
element_id: str,
user_id: str,
element_uuid: str,
action: Literal["read"],
db_session: Session,
):
element_nature = await check_element_type(element_id)
element_nature = await check_element_type(element_uuid)
# Verifies if the element is public
if (
element_nature == ("courses" or "collections")
and action == "read"
and user_id == "anonymous"
):
if element_nature == ("courses" or "collections") and action == "read":
if element_nature == "courses":
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": element_id})
if course["public"]:
statement = select(Course).where(
Course.public is True, Course.course_uuid == element_uuid
)
course = db_session.exec(statement).first()
if course:
return True
else:
raise HTTPException(
@ -32,10 +34,12 @@ async def authorization_verify_if_element_is_public(
)
if element_nature == "collections":
collections = request.app.db["collections"]
collection = await collections.find_one({"collection_id": element_id})
statement = select(Collection).where(
Collection.public is True, Collection.collection_uuid == element_uuid
)
collection = db_session.exec(statement).first()
if collection["public"]:
if collection:
return True
else:
raise HTTPException(
@ -49,87 +53,81 @@ async def authorization_verify_if_element_is_public(
)
# Tested and working
async def authorization_verify_if_user_is_author(
request,
user_id: str,
user_id: int,
action: Literal["read", "update", "delete", "create"],
element_id: str,
element_uuid: str,
db_session: Session,
):
if action == "update" or "delete" or "read":
element_nature = await check_element_type(element_id)
elements = request.app.db[element_nature]
element_identifier = await get_id_identifier_of_element(element_id)
element = await elements.find_one({element_identifier: element_id})
if user_id in element["authors"]:
statement = select(ResourceAuthor).where(
ResourceAuthor.resource_uuid == element_uuid
)
resource_author = db_session.exec(statement).first()
if resource_author:
if resource_author.user_id == int(user_id):
if (resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or (
resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER
):
return True
else:
return False
else:
return False
else:
return False
# Tested and working
async def authorization_verify_based_on_roles(
request: Request,
user_id: str,
user_id: int,
action: Literal["read", "update", "delete", "create"],
roles_list: list[UserRolesInOrganization],
element_id: str,
element_uuid: str,
db_session: Session,
):
element_type = await check_element_type(element_id)
element = request.app.db[element_type]
roles = request.app.db["roles"]
element_type = await check_element_type(element_uuid)
# Get the element
element_identifier = await get_id_identifier_of_element(element_id)
element = await element.find_one({element_identifier: element_id})
# Get the roles of the user
roles_id_list = [role["role_id"] for role in roles_list]
roles = await roles.find({"role_id": {"$in": roles_id_list}}).to_list(length=100)
async def checkRoles():
# Check Roles
for role in roles:
role = RoleInDB(**role)
if role.elements[element_type][f"action_{action}"] is True:
return True
else:
return False
async def checkOrgRoles():
# Check Org Roles
users = request.app.db["users"]
user = await users.find_one({"user_id": user_id})
if element is not None:
for org in user["orgs"]:
if org["org_id"] == element["org_id"]:
if org["org_role"] == "owner" or org["org_role"] == "editor":
return True
else:
return False
if await checkRoles() or await checkOrgRoles():
return True
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (roless) : You don't have the right to perform this action",
# Get user roles bound to an organization and standard roles
statement = (
select(Role)
.join(UserOrganization)
.where((UserOrganization.org_id == Role.org_id) | (Role.org_id == null()))
.where(UserOrganization.user_id == user_id)
)
user_roles_in_organization_and_standard_roles = db_session.exec(statement).all()
# Find in roles list if there is a role that matches users action for this type of element
for role in user_roles_in_organization_and_standard_roles:
role = Role.from_orm(role)
if role.rights:
rights = role.rights
if rights[element_type][f"action_{action}"] is True:
return True
else:
return False
else:
return False
# Tested and working
async def authorization_verify_based_on_roles_and_authorship(
request: Request,
user_id: str,
user_id: int,
action: Literal["read", "update", "delete", "create"],
roles_list: list[UserRolesInOrganization],
element_id: str,
element_uuid: str,
db_session: Session,
):
isAuthor = await authorization_verify_if_user_is_author(
request, user_id, action, element_id
request, user_id, action, element_uuid, db_session
)
isRole = await authorization_verify_based_on_roles(
request, user_id, action, roles_list, element_id
request, user_id, action, element_uuid, db_session
)
if isAuthor or isRole:
@ -141,8 +139,8 @@ async def authorization_verify_based_on_roles_and_authorship(
)
async def authorization_verify_if_user_is_anon(user_id: str):
if user_id == "anonymous":
async def authorization_verify_if_user_is_anon(user_id: int):
if user_id == 0:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You should be logged in to perform this action",

View file

@ -5,6 +5,7 @@ async def check_element_type(element_id):
"""
Check if the element is a course, a user, a house or a collection, by checking its prefix
"""
print("element_id", element_id)
if element_id.startswith("course_"):
return "courses"
elif element_id.startswith("user_"):
@ -13,12 +14,14 @@ async def check_element_type(element_id):
return "houses"
elif element_id.startswith("org_"):
return "organizations"
elif element_id.startswith("coursechapter_"):
elif element_id.startswith("chapter_"):
return "coursechapters"
elif element_id.startswith("collection_"):
return "collections"
elif element_id.startswith("activity_"):
return "activities"
elif element_id.startswith("role_"):
return "roles"
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,

View file

@ -0,0 +1,89 @@
from datetime import datetime
from uuid import uuid4
from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request
from sqlmodel import Session, select
from src.db.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser
async def create_image_block(
request: Request, image_file: UploadFile, activity_uuid: str, db_session: Session
):
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
)
block_type = "imageBlock"
# get org_uuid
statement = select(Organization).where(Organization.id == activity.org_id)
org = db_session.exec(statement).first()
# get course
statement = select(Course).where(Course.id == activity.course_id)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
# get block id
block_uuid = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(
request,
image_file,
activity_uuid,
block_uuid,
["jpg", "jpeg", "png", "gif"],
block_type,
org.org_uuid,
str(course.course_uuid),
)
# create block
block = Block(
activity_id=activity.id if activity.id else 0,
block_type=BlockTypeEnum.BLOCK_IMAGE,
content=block_data.dict(),
org_id=org.id if org.id else 0,
course_id=course.id if course.id else 0,
block_uuid=block_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# insert block
db_session.add(block)
db_session.commit()
db_session.refresh(block)
block = BlockRead.from_orm(block)
return block
async def get_image_block(
request: Request, block_uuid: str, current_user: PublicUser, db_session: Session
):
statement = select(Block).where(Block.block_uuid == block_uuid)
block = db_session.exec(statement).first()
if block:
block = BlockRead.from_orm(block)
return block
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist"
)

View file

@ -1,72 +0,0 @@
from uuid import uuid4
from fastapi import HTTPException, status, UploadFile, Request
from src.services.blocks.schemas.blocks import Block
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser
async def create_image_block(
request: Request, image_file: UploadFile, activity_id: str
):
blocks = request.app.db["blocks"]
activity = request.app.db["activities"]
courses = request.app.db["courses"]
block_type = "imageBlock"
# get org_id from activity
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
org_id = activity["org_id"]
coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
# get block id
block_id = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(
request,
image_file,
activity_id,
block_id,
["jpg", "jpeg", "png", "gif"],
block_type,
org_id,
course["course_id"],
)
# create block
block = Block(
block_id=block_id,
activity_id=activity_id,
block_type=block_type,
block_data=block_data,
org_id=org_id,
course_id=course["course_id"],
)
# insert block
await blocks.insert_one(block.dict())
return block
async def get_image_block(request: Request, file_id: str, current_user: PublicUser):
blocks = request.app.db["blocks"]
video_block = await blocks.find_one({"block_id": file_id})
if video_block:
return Block(**video_block)
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist"
)

View file

@ -1,69 +1,89 @@
from datetime import datetime
from uuid import uuid4
from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request
from src.services.blocks.schemas.blocks import Block
from sqlmodel import Session, select
from src.db.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser
async def create_pdf_block(request: Request, pdf_file: UploadFile, activity_id: str):
blocks = request.app.db["blocks"]
activity = request.app.db["activities"]
courses = request.app.db["courses"]
async def create_pdf_block(
request: Request, pdf_file: UploadFile, activity_uuid: str, db_session: Session
):
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
)
block_type = "pdfBlock"
# get org_id from activity
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
org_id = activity["org_id"]
# get org_uuid
statement = select(Organization).where(Organization.id == activity.org_id)
org = db_session.exec(statement).first()
# get course
statement = select(Course).where(Course.id == activity.course_id)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
# get block id
block_id = str(f"block_{uuid4()}")
coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
block_uuid = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(
request,
pdf_file,
activity_id,
block_id,
activity_uuid,
block_uuid,
["pdf"],
block_type,
org_id,
course["course_id"],
org.org_uuid,
str(course.course_uuid),
)
# create block
block = Block(
block_id=block_id,
activity_id=activity_id,
block_type=block_type,
block_data=block_data,
org_id=org_id,
course_id=course["course_id"],
activity_id=activity.id if activity.id else 0,
block_type=BlockTypeEnum.BLOCK_DOCUMENT_PDF,
content=block_data.dict(),
org_id=org.id if org.id else 0,
course_id=course.id if course.id else 0,
block_uuid=block_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# insert block
await blocks.insert_one(block.dict())
db_session.add(block)
db_session.commit()
db_session.refresh(block)
block = BlockRead.from_orm(block)
return block
async def get_pdf_block(request: Request, file_id: str, current_user: PublicUser):
blocks = request.app.db["blocks"]
async def get_pdf_block(
request: Request, block_uuid: str, current_user: PublicUser, db_session: Session
):
statement = select(Block).where(Block.block_uuid == block_uuid)
block = db_session.exec(statement).first()
pdf_block = await blocks.find_one({"block_id": file_id})
if pdf_block:
return Block(**pdf_block)
else:
if not block:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist"
status_code=status.HTTP_404_NOT_FOUND, detail="Video file does not exist"
)
block = BlockRead.from_orm(block)
return block

View file

@ -1,70 +0,0 @@
from typing import List, Literal
from uuid import uuid4
from fastapi import Request
from pydantic import BaseModel
from src.services.blocks.schemas.blocks import Block
from src.services.users.users import PublicUser
class option(BaseModel):
option_id: str
option_type: Literal["text", "image"]
option_data: str
class answer(BaseModel):
question_id: str
option_id: str
class question(BaseModel):
question_id: str
question_value:str
options: List[option]
class quizBlock(BaseModel):
questions: List[question]
answers: List[answer]
async def create_quiz_block(request: Request, quizBlock: quizBlock, activity_id: str, user: PublicUser):
blocks = request.app.db["blocks"]
activities = request.app.db["activities"]
request.app.db["courses"]
# Get org_id from activity
activity = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1})
org_id = activity["org_id"]
# Get course_id from activity
course = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "course_id": 1})
block_id = str(f"block_{uuid4()}")
# create block
block = Block(block_id=block_id, activity_id=activity_id,
block_type="quizBlock", block_data=quizBlock, org_id=org_id, course_id=course["course_id"])
# insert block
await blocks.insert_one(block.dict())
return block
async def get_quiz_block_options(request: Request, block_id: str, user: PublicUser):
blocks = request.app.db["blocks"]
# find block but get only the options
block = await blocks.find_one({"block_id": block_id, }, {
"_id": 0, "block_data.answers": 0})
return block
async def get_quiz_block_answers(request: Request, block_id: str, user: PublicUser):
blocks = request.app.db["blocks"]
# find block but get only the answers
block = await blocks.find_one({"block_id": block_id, }, {
"_id": 0, "block_data.questions": 0})
return block

View file

@ -1,73 +1,89 @@
from datetime import datetime
from uuid import uuid4
from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request
from src.services.blocks.schemas.blocks import Block
from sqlmodel import Session, select
from src.db.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser
async def create_video_block(
request: Request, video_file: UploadFile, activity_id: str
request: Request, video_file: UploadFile, activity_uuid: str, db_session: Session
):
blocks = request.app.db["blocks"]
activity = request.app.db["activities"]
courses = request.app.db["courses"]
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
)
block_type = "videoBlock"
# get org_id from activity
activity = await activity.find_one(
{"activity_id": activity_id}, {"_id": 0}
# get org_uuid
statement = select(Organization).where(Organization.id == activity.org_id)
org = db_session.exec(statement).first()
# get course
statement = select(Course).where(Course.id == activity.course_id)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
org_id = activity["org_id"]
# get block id
block_id = str(f"block_{uuid4()}")
coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
block_uuid = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(
request,
video_file,
activity_id,
block_id,
activity_uuid,
block_uuid,
["mp4", "webm", "ogg"],
block_type,
org_id,
course["course_id"],
org.org_uuid,
str(course.course_uuid),
)
# create block
block = Block(
block_id=block_id,
activity_id=activity_id,
block_type=block_type,
block_data=block_data,
org_id=org_id,
course_id=course["course_id"],
activity_id=activity.id if activity.id else 0,
block_type=BlockTypeEnum.BLOCK_VIDEO,
content=block_data.dict(),
org_id=org.id if org.id else 0,
course_id=course.id if course.id else 0,
block_uuid=block_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# insert block
await blocks.insert_one(block.dict())
db_session.add(block)
db_session.commit()
db_session.refresh(block)
block = BlockRead.from_orm(block)
return block
async def get_video_block(request: Request, file_id: str, current_user: PublicUser):
blocks = request.app.db["blocks"]
async def get_video_block(
request: Request, block_uuid: str, current_user: PublicUser, db_session: Session
):
statement = select(Block).where(Block.block_uuid == block_uuid)
block = db_session.exec(statement).first()
video_block = await blocks.find_one({"block_id": file_id})
if video_block:
return Block(**video_block)
else:
if not block:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist"
status_code=status.HTTP_404_NOT_FOUND, detail="Video file does not exist"
)
block = BlockRead.from_orm(block)
return block

View file

@ -7,4 +7,4 @@ class BlockFile(BaseModel):
file_name: str
file_size: int
file_type: str
activity_id: str
activity_uuid: str

View file

@ -7,12 +7,12 @@ from src.services.utils.upload_content import upload_content
async def upload_file_and_return_file_object(
request: Request,
file: UploadFile,
activity_id: str,
activity_uuid: str,
block_id: str,
list_of_allowed_file_formats: list,
type_of_block: str,
org_id: str,
course_id: str,
org_uuid: str,
course_uuid: str,
):
# get file id
file_id = str(uuid.uuid4())
@ -45,12 +45,12 @@ async def upload_file_and_return_file_object(
file_name=file_name,
file_size=file_size,
file_type=file_type,
activity_id=activity_id,
activity_uuid=activity_uuid,
)
await upload_content(
f"courses/{course_id}/activities/{activity_id}/dynamic/blocks/{type_of_block}/{block_id}",
org_id=org_id,
f"courses/{course_uuid}/activities/{activity_uuid}/dynamic/blocks/{type_of_block}/{block_id}",
org_uuid=org_uuid,
file_binary=file_binary,
file_and_format=f"{file_id}.{file_format}",
)

View file

@ -1,35 +1,17 @@
from typing import Literal
from pydantic import BaseModel
from sqlmodel import Session, select
from src.db.chapters import Chapter
from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
authorization_verify_if_element_is_public,
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.services.users.schemas.users import AnonymousUser, PublicUser
from fastapi import HTTPException, status, Request
from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
from src.db.chapter_activities import ChapterActivity
from src.db.users import AnonymousUser, PublicUser
from fastapi import HTTPException, Request
from uuid import uuid4
from datetime import datetime
#### Classes ####################################################
class Activity(BaseModel):
name: str
type: str
content: object
class ActivityInDB(Activity):
activity_id: str
course_id: str
coursechapter_id: str
org_id: str
creationDate: str
updateDate: str
#### Classes ####################################################
####################################################
# CRUD
@ -38,148 +20,162 @@ class ActivityInDB(Activity):
async def create_activity(
request: Request,
activity_object: Activity,
org_id: str,
coursechapter_id: str,
current_user: PublicUser,
activity_object: ActivityCreate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
activities = request.app.db["activities"]
courses = request.app.db["courses"]
users = request.app.db["users"]
activity = Activity.from_orm(activity_object)
# get user
user = await users.find_one({"user_id": current_user.user_id})
# CHeck if org exists
statement = select(Chapter).where(Chapter.id == activity_object.chapter_id)
chapter = db_session.exec(statement).first()
# generate activity_id
activity_id = str(f"activity_{uuid4()}")
# verify activity rights
await authorization_verify_based_on_roles(
request,
current_user.user_id,
"create",
user["roles"],
activity_id,
if not chapter:
raise HTTPException(
status_code=404,
detail="Chapter not found",
)
# get course_id from activity
course = await courses.find_one({"chapters": coursechapter_id})
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session)
# create activity
activity = ActivityInDB(
**activity_object.dict(),
creationDate=str(datetime.now()),
coursechapter_id=coursechapter_id,
updateDate=str(datetime.now()),
activity_id=activity_id,
org_id=org_id,
course_id=course["course_id"],
activity.activity_uuid = str(f"activity_{uuid4()}")
activity.creation_date = str(datetime.now())
activity.update_date = str(datetime.now())
activity.org_id = chapter.org_id
activity.course_id = chapter.course_id
# Insert Activity in DB
db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
# Find the last activity in the Chapter and add it to the list
statement = (
select(ChapterActivity)
.where(ChapterActivity.chapter_id == activity_object.chapter_id)
.order_by(ChapterActivity.order)
)
await activities.insert_one(activity.dict())
chapter_activities = db_session.exec(statement).all()
# update chapter
await courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$addToSet": {"chapters_content.$.activities": activity_id}},
last_order = chapter_activities[-1].order if chapter_activities else 0
to_be_used_order = last_order + 1
# Add activity to chapter
activity_chapter = ChapterActivity(
chapter_id=activity_object.chapter_id,
activity_id=activity.id if activity.id else 0,
course_id=chapter.course_id,
org_id=chapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=to_be_used_order,
)
return activity
# Insert ChapterActivity link in DB
db_session.add(activity_chapter)
db_session.commit()
db_session.refresh(activity_chapter)
return ActivityRead.from_orm(activity)
async def get_activity(request: Request, activity_id: str, current_user: PublicUser):
activities = request.app.db["activities"]
courses = request.app.db["courses"]
activity = await activities.find_one({"activity_id": activity_id})
# get course_id from activity
coursechapter_id = activity["coursechapter_id"]
await courses.find_one({"chapters": coursechapter_id})
# verify course rights
await verify_rights(request, activity["course_id"], current_user, "read")
async def get_activity(
request: Request,
activity_uuid: str,
current_user: PublicUser,
db_session: Session,
):
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
status_code=404,
detail="Activity not found",
)
activity = ActivityInDB(**activity)
# RBAC check
await rbac_check(request, activity.activity_uuid, current_user, "read", db_session)
activity = ActivityRead.from_orm(activity)
return activity
async def update_activity(
request: Request,
activity_object: Activity,
activity_id: str,
current_user: PublicUser,
activity_object: ActivityUpdate,
activity_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
activities = request.app.db["activities"]
activity = await activities.find_one({"activity_id": activity_id})
# verify course rights
await verify_rights(request, activity_id, current_user, "update")
if activity:
creationDate = activity["creationDate"]
# get today's date
datetime_object = datetime.now()
updated_course = ActivityInDB(
activity_id=activity_id,
coursechapter_id=activity["coursechapter_id"],
creationDate=creationDate,
updateDate=str(datetime_object),
course_id=activity["course_id"],
org_id=activity["org_id"],
**activity_object.dict(),
)
await activities.update_one(
{"activity_id": activity_id}, {"$set": updated_course.dict()}
)
return ActivityInDB(**updated_course.dict())
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
)
async def delete_activity(request: Request, activity_id: str, current_user: PublicUser):
activities = request.app.db["activities"]
activity = await activities.find_one({"activity_id": activity_id})
# verify course rights
await verify_rights(request, activity_id, current_user, "delete")
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
status_code=404,
detail="Activity not found",
)
# Remove Activity
isDeleted = await activities.delete_one({"activity_id": activity_id})
# Remove Activity from chapter
courses = request.app.db["courses"]
isDeletedFromChapter = await courses.update_one(
{"chapters_content.activities": activity_id},
{"$pull": {"chapters_content.$.activities": activity_id}},
# RBAC check
await rbac_check(
request, activity.activity_uuid, current_user, "update", db_session
)
if isDeleted and isDeletedFromChapter:
return {"detail": "Activity deleted"}
else:
# Update only the fields that were passed in
for var, value in vars(activity_object).items():
if value is not None:
setattr(activity, var, value)
db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
activity = ActivityRead.from_orm(activity)
return activity
async def delete_activity(
request: Request,
activity_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
status_code=404,
detail="Activity not found",
)
# RBAC check
await rbac_check(
request, activity.activity_uuid, current_user, "delete", db_session
)
# Delete activity from chapter
statement = select(ChapterActivity).where(
ChapterActivity.activity_id == activity.id
)
activity_chapter = db_session.exec(statement).first()
if not activity_chapter:
raise HTTPException(
status_code=404,
detail="Activity not found in chapter",
)
db_session.delete(activity_chapter)
db_session.delete(activity)
db_session.commit()
return {"detail": "Activity deleted"}
####################################################
# Misc
@ -187,64 +183,49 @@ async def delete_activity(request: Request, activity_id: str, current_user: Publ
async def get_activities(
request: Request, coursechapter_id: str, current_user: PublicUser
):
activities = request.app.db["activities"]
activities = activities.find({"coursechapter_id": coursechapter_id})
request: Request,
coursechapter_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> list[ActivityRead]:
statement = select(ChapterActivity).where(
ChapterActivity.chapter_id == coursechapter_id
)
activities = db_session.exec(statement).all()
if not activities:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
status_code=404,
detail="No activities found",
)
activities = [
ActivityInDB(**activity) for activity in await activities.to_list(length=100)
]
# RBAC check
await rbac_check(request, "activity_x", current_user, "read", db_session)
activities = [ActivityRead.from_orm(activity) for activity in activities]
return activities
#### Security ####################################################
## 🔒 RBAC Utils ##
async def verify_rights(
async def rbac_check(
request: Request,
activity_id: str, # course_id in case of read
course_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
if action == "read":
if current_user.user_id == "anonymous":
await authorization_verify_if_element_is_public(
request, activity_id, current_user.user_id, action
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles(
await authorization_verify_based_on_roles_and_authorship(
request,
current_user.user_id,
current_user.id,
action,
user["roles"],
activity_id,
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles(
request,
current_user.user_id,
action,
user["roles"],
activity_id,
course_id,
db_session,
)
#### Security ####################################################
## 🔒 RBAC Utils ##

View file

@ -1,7 +1,22 @@
from src.security.rbac.rbac import authorization_verify_based_on_roles
from typing import Literal
from src.db.courses import Course
from src.db.organizations import Organization
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.chapters import Chapter
from src.db.activities import (
Activity,
ActivityRead,
ActivitySubTypeEnum,
ActivityTypeEnum,
)
from src.db.chapter_activities import ChapterActivity
from src.db.course_chapters import CourseChapter
from src.db.users import AnonymousUser, PublicUser
from src.services.courses.activities.uploads.pdfs import upload_pdf
from src.services.users.users import PublicUser
from src.services.courses.activities.activities import ActivityInDB
from fastapi import HTTPException, status, UploadFile, Request
from uuid import uuid4
from datetime import datetime
@ -10,26 +25,46 @@ from datetime import datetime
async def create_documentpdf_activity(
request: Request,
name: str,
coursechapter_id: str,
current_user: PublicUser,
chapter_id: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
pdf_file: UploadFile | None = None,
):
activities = request.app.db["activities"]
courses = request.app.db["courses"]
users = request.app.db["users"]
# RBAC check
await rbac_check(request, "activity_x", current_user, "create", db_session)
# get user
user = await users.find_one({"user_id": current_user.user_id})
# get chapter_id
statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
# generate activity_id
activity_id = str(f"activity_{uuid4()}")
# get org_id from course
coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id}
if not chapter:
raise HTTPException(
status_code=404,
detail="Chapter not found",
)
org_id = coursechapter["org_id"]
statement = select(CourseChapter).where(CourseChapter.chapter_id == chapter_id)
coursechapter = db_session.exec(statement).first()
if not coursechapter:
raise HTTPException(
status_code=404,
detail="CourseChapter not found",
)
# get org_id
org_id = coursechapter.org_id
# Get org_uuid
statement = select(Organization).where(Organization.id == coursechapter.org_id)
organization = db_session.exec(statement).first()
# Get course_uuid
statement = select(Course).where(Course.id == coursechapter.course_id)
course = db_session.exec(statement).first()
# create activity uuid
activity_uuid = f"activity_{uuid4()}"
# check if pdf_file is not None
if not pdf_file:
@ -51,45 +86,77 @@ async def create_documentpdf_activity(
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
)
activity_object = ActivityInDB(
org_id=org_id,
activity_id=activity_id,
coursechapter_id=coursechapter_id,
# Create activity
activity = Activity(
name=name,
type="documentpdf",
course_id=coursechapter["course_id"],
activity_type=ActivityTypeEnum.TYPE_DOCUMENT,
activity_sub_type=ActivitySubTypeEnum.SUBTYPE_DOCUMENT_PDF,
content={
"documentpdf": {
"filename": "documentpdf." + pdf_format,
"activity_id": activity_id,
}
"activity_uuid": activity_uuid,
},
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
published_version=1,
version=1,
org_id=org_id if org_id else 0,
course_id=coursechapter.course_id,
activity_uuid=activity_uuid,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
await authorization_verify_based_on_roles(
request,
current_user.user_id,
"create",
user["roles"],
activity_id,
)
# Insert Activity in DB
db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
# create activity
activity = ActivityInDB(**activity_object.dict())
await activities.insert_one(activity.dict())
# Add activity to chapter
activity_chapter = ChapterActivity(
chapter_id=(int(chapter_id)),
activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=1,
)
# upload pdf
if pdf_file:
# get pdffile format
await upload_pdf(pdf_file, activity_id, org_id, coursechapter["course_id"])
# todo : choose whether to update the chapter or not
# update chapter
await courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$addToSet": {"chapters_content.$.activities": activity_id}},
await upload_pdf(
pdf_file,
activity.activity_uuid,
organization.org_uuid,
course.course_uuid,
)
return activity
# Insert ChapterActivity link in DB
db_session.add(activity_chapter)
db_session.commit()
db_session.refresh(activity_chapter)
return ActivityRead.from_orm(activity)
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request,
current_user.id,
action,
course_id,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -1,15 +1,14 @@
from src.services.utils.upload_content import upload_content
async def upload_pdf(pdf_file, activity_id, org_id, course_id):
async def upload_pdf(pdf_file, activity_uuid, org_uuid, course_uuid):
contents = pdf_file.file.read()
pdf_format = pdf_file.filename.split(".")[-1]
try:
await upload_content(
f"courses/{course_id}/activities/{activity_id}/documentpdf",
org_id,
f"courses/{course_uuid}/activities/{activity_uuid}/documentpdf",
org_uuid,
contents,
f"documentpdf.{pdf_format}",
)

View file

@ -2,14 +2,14 @@
from src.services.utils.upload_content import upload_content
async def upload_video(video_file, activity_id, org_id, course_id):
async def upload_video(video_file, activity_uuid, org_uuid, course_uuid):
contents = video_file.file.read()
video_format = video_file.filename.split(".")[-1]
try:
await upload_content(
f"courses/{course_id}/activities/{activity_id}/video",
org_id,
f"courses/{course_uuid}/activities/{activity_uuid}/video",
org_uuid,
contents,
f"video.{video_format}",
)

View file

@ -1,12 +1,24 @@
from typing import Literal
from src.db.courses import Course
from src.db.organizations import Organization
from pydantic import BaseModel
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.chapters import Chapter
from src.db.activities import (
Activity,
ActivityRead,
ActivitySubTypeEnum,
ActivityTypeEnum,
)
from src.db.chapter_activities import ChapterActivity
from src.db.course_chapters import CourseChapter
from src.db.users import AnonymousUser, PublicUser
from src.services.courses.activities.uploads.videos import upload_video
from src.services.users.users import PublicUser
from src.services.courses.activities.activities import ActivityInDB
from fastapi import HTTPException, status, UploadFile, Request
from uuid import uuid4
from datetime import datetime
@ -15,32 +27,43 @@ from datetime import datetime
async def create_video_activity(
request: Request,
name: str,
coursechapter_id: str,
chapter_id: str,
current_user: PublicUser,
db_session: Session,
video_file: UploadFile | None = None,
):
activities = request.app.db["activities"]
courses = request.app.db["courses"]
users = request.app.db["users"]
# RBAC check
await rbac_check(request, "activity_x", current_user, "create", db_session)
# get user
user = await users.find_one({"user_id": current_user.user_id})
# get chapter_id
statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
# generate activity_id
activity_id = str(f"activity_{uuid4()}")
# get org_id from course
coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id}
if not chapter:
raise HTTPException(
status_code=404,
detail="Chapter not found",
)
statement = select(CourseChapter).where(CourseChapter.chapter_id == chapter_id)
coursechapter = db_session.exec(statement).first()
if not coursechapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="CourseChapter : No coursechapter found",
status_code=404,
detail="CourseChapter not found",
)
org_id = coursechapter["org_id"]
# Get org_uuid
statement = select(Organization).where(Organization.id == coursechapter.org_id)
organization = db_session.exec(statement).first()
# Get course_uuid
statement = select(Course).where(Course.id == coursechapter.course_id)
course = db_session.exec(statement).first()
# generate activity_uuid
activity_uuid = str(f"activity_{uuid4()}")
# check if video_file is not None
if not video_file:
@ -64,55 +87,63 @@ async def create_video_activity(
detail="Video : No video file provided",
)
activity_object = ActivityInDB(
org_id=org_id,
activity_id=activity_id,
coursechapter_id=coursechapter_id,
course_id=coursechapter["course_id"],
activity_object = Activity(
name=name,
type="video",
activity_type=ActivityTypeEnum.TYPE_VIDEO,
activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_HOSTED,
activity_uuid=activity_uuid,
org_id=coursechapter.org_id,
course_id=coursechapter.course_id,
published_version=1,
content={
"video": {
"filename": "video." + video_format,
"activity_id": activity_id,
}
"activity_uuid": activity_uuid,
},
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
)
await authorization_verify_based_on_roles(
request,
current_user.user_id,
"create",
user["roles"],
activity_id,
version=1,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# create activity
activity = ActivityInDB(**activity_object.dict())
await activities.insert_one(activity.dict())
activity = Activity.from_orm(activity_object)
db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
# upload video
if video_file:
# get videofile format
await upload_video(video_file, activity_id, org_id, coursechapter["course_id"])
# todo : choose whether to update the chapter or not
# update chapter
await courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$addToSet": {"chapters_content.$.activities": activity_id}},
await upload_video(
video_file,
activity.activity_uuid,
organization.org_uuid,
course.course_uuid,
)
return activity
# update chapter
chapter_activity_object = ChapterActivity(
chapter_id=chapter.id, # type: ignore
activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=1,
)
# Insert ChapterActivity link in DB
db_session.add(chapter_activity_object)
db_session.commit()
db_session.refresh(chapter_activity_object)
return ActivityRead.from_orm(activity)
class ExternalVideo(BaseModel):
name: str
uri: str
type: Literal["youtube", "vimeo"]
coursechapter_id: str
chapter_id: str
class ExternalVideoInDB(BaseModel):
@ -121,67 +152,93 @@ class ExternalVideoInDB(BaseModel):
async def create_external_video_activity(
request: Request,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
data: ExternalVideo,
db_session: Session,
):
activities = request.app.db["activities"]
courses = request.app.db["courses"]
users = request.app.db["users"]
# RBAC check
await rbac_check(request, "activity_x", current_user, "create", db_session)
# get user
user = await users.find_one({"user_id": current_user.user_id})
# get chapter_id
statement = select(Chapter).where(Chapter.id == data.chapter_id)
chapter = db_session.exec(statement).first()
# generate activity_id
activity_id = str(f"activity_{uuid4()}")
# get org_id from course
coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": data.coursechapter_id}
if not chapter:
raise HTTPException(
status_code=404,
detail="Chapter not found",
)
statement = select(CourseChapter).where(CourseChapter.chapter_id == data.chapter_id)
coursechapter = db_session.exec(statement).first()
if not coursechapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="CourseChapter : No coursechapter found",
status_code=404,
detail="CourseChapter not found",
)
org_id = coursechapter["org_id"]
# generate activity_uuid
activity_uuid = str(f"activity_{uuid4()}")
activity_object = ActivityInDB(
org_id=org_id,
activity_id=activity_id,
coursechapter_id=data.coursechapter_id,
activity_object = Activity(
name=data.name,
type="video",
activity_type=ActivityTypeEnum.TYPE_VIDEO,
activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_YOUTUBE,
activity_uuid=activity_uuid,
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
published_version=1,
content={
"external_video": {
"uri": data.uri,
"activity_id": activity_id,
"type": data.type,
}
"activity_uuid": activity_uuid,
},
course_id=coursechapter["course_id"],
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
)
await authorization_verify_based_on_roles(
request,
current_user.user_id,
"create",
user["roles"],
activity_id,
version=1,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
# create activity
activity = ActivityInDB(**activity_object.dict())
await activities.insert_one(activity.dict())
activity = Activity.from_orm(activity_object)
db_session.add(activity)
db_session.commit()
db_session.refresh(activity)
# todo : choose whether to update the chapter or not
# update chapter
await courses.update_one(
{"chapters_content.coursechapter_id": data.coursechapter_id},
{"$addToSet": {"chapters_content.$.activities": activity_id}},
chapter_activity_object = ChapterActivity(
chapter_id=coursechapter.chapter_id, # type: ignore
activity_id=activity.id, # type: ignore
course_id=coursechapter.course_id,
org_id=coursechapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=1,
)
return activity
# Insert ChapterActivity link in DB
db_session.add(chapter_activity_object)
db_session.commit()
return ActivityRead.from_orm(activity)
async def rbac_check(
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request,
current_user.id,
action,
course_id,
db_session,
)
## 🔒 RBAC Utils ##

View file

@ -1,367 +1,551 @@
from datetime import datetime
from typing import List, Literal
from uuid import uuid4
from pydantic import BaseModel
from src.security.auth import non_public_endpoint
from sqlmodel import Session, select
from src.db.users import AnonymousUser
from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon,
)
from src.db.course_chapters import CourseChapter
from src.db.activities import Activity, ActivityRead
from src.db.chapter_activities import ChapterActivity
from src.db.chapters import (
Chapter,
ChapterCreate,
ChapterRead,
ChapterUpdate,
ChapterUpdateOrder,
)
from src.services.courses.courses import Course
from src.services.courses.activities.activities import ActivityInDB
from src.services.users.users import PublicUser
from fastapi import HTTPException, status, Request
class CourseChapter(BaseModel):
name: str
description: str
activities: list
class CourseChapterInDB(CourseChapter):
coursechapter_id: str
course_id: str
creationDate: str
updateDate: str
# Frontend
class CourseChapterMetaData(BaseModel):
chapterOrder: List[str]
chapters: dict
activities: object
#### Classes ####################################################
####################################################
# CRUD
####################################################
async def create_coursechapter(
async def create_chapter(
request: Request,
coursechapter_object: CourseChapter,
course_id: str,
current_user: PublicUser,
):
courses = request.app.db["courses"]
users = request.app.db["users"]
# get course org_id and verify rights
await courses.find_one({"course_id": course_id})
user = await users.find_one({"user_id": current_user.user_id})
chapter_object: ChapterCreate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> ChapterRead:
chapter = Chapter.from_orm(chapter_object)
# generate coursechapter_id with uuid4
coursechapter_id = str(f"coursechapter_{uuid4()}")
# Get COurse
statement = select(Course).where(Course.id == chapter_object.course_id)
hasRoleRights = await authorization_verify_based_on_roles(
request, current_user.user_id, "create", user["roles"], course_id
course = db_session.exec(statement).one()
# RBAC check
await rbac_check(request, "chapter_x", current_user, "create", db_session)
# complete chapter object
chapter.course_id = chapter_object.course_id
chapter.chapter_uuid = f"chapter_{uuid4()}"
chapter.creation_date = str(datetime.now())
chapter.update_date = str(datetime.now())
chapter.org_id = course.org_id
# Find the last chapter in the course and add it to the list
statement = (
select(CourseChapter)
.where(CourseChapter.course_id == chapter.course_id)
.order_by(CourseChapter.order)
)
course_chapters = db_session.exec(statement).all()
# get last chapter order
last_order = course_chapters[-1].order if course_chapters else 0
to_be_used_order = last_order + 1
# Add chapter to database
db_session.add(chapter)
db_session.commit()
db_session.refresh(chapter)
chapter = ChapterRead(**chapter.dict(), activities=[])
# Check if COurseChapter link exists
statement = (
select(CourseChapter)
.where(CourseChapter.chapter_id == chapter.id)
.where(CourseChapter.course_id == chapter.course_id)
.where(CourseChapter.order == to_be_used_order)
)
course_chapter = db_session.exec(statement).first()
if not course_chapter:
# Add CourseChapter link
course_chapter = CourseChapter(
course_id=chapter.course_id,
chapter_id=chapter.id,
org_id=chapter.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=to_be_used_order,
)
if not hasRoleRights:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Roles : Insufficient rights to perform this action",
)
# Insert CourseChapter link in DB
db_session.add(course_chapter)
db_session.commit()
coursechapter = CourseChapterInDB(
coursechapter_id=coursechapter_id,
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
course_id=course_id,
**coursechapter_object.dict(),
)
courses.update_one(
{"course_id": course_id},
{
"$addToSet": {
"chapters": coursechapter_id,
"chapters_content": coursechapter.dict(),
}
},
)
return coursechapter.dict()
return chapter
async def get_coursechapter(
request: Request, coursechapter_id: str, current_user: PublicUser
):
courses = request.app.db["courses"]
coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id}
)
if coursechapter:
# verify course rights
await verify_rights(request, coursechapter["course_id"], current_user, "read")
coursechapter = CourseChapter(**coursechapter)
return coursechapter
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="CourseChapter does not exist"
)
async def update_coursechapter(
async def get_chapter(
request: Request,
coursechapter_object: CourseChapter,
coursechapter_id: str,
current_user: PublicUser,
):
courses = request.app.db["courses"]
chapter_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> ChapterRead:
statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id}
)
if coursechapter:
# verify course rights
await verify_rights(request, coursechapter["course_id"], current_user, "update")
coursechapter = CourseChapterInDB(
coursechapter_id=coursechapter_id,
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
course_id=coursechapter["course_id"],
**coursechapter_object.dict(),
)
courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$set": {"chapters_content.$": coursechapter.dict()}},
)
return coursechapter
else:
if not chapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Coursechapter does not exist"
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "read", db_session)
async def delete_coursechapter(
request: Request, coursechapter_id: str, current_user: PublicUser
):
courses = request.app.db["courses"]
course = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id}
# Get activities for this chapter
statement = (
select(Activity)
.join(ChapterActivity, Activity.id == ChapterActivity.activity_id)
.where(ChapterActivity.chapter_id == chapter_id)
.distinct(Activity.id)
)
if course:
# verify course rights
await verify_rights(request, course["course_id"], current_user, "delete")
activities = db_session.exec(statement).all()
# Remove coursechapter from course
await courses.update_one(
{"course_id": course["course_id"]},
{"$pull": {"chapters": coursechapter_id}},
chapter = ChapterRead(
**chapter.dict(),
activities=[ActivityRead(**activity.dict()) for activity in activities],
)
await courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$pull": {"chapters_content": {"coursechapter_id": coursechapter_id}}},
)
return {"message": "Coursechapter deleted"}
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
return chapter
####################################################
# Misc
####################################################
async def get_coursechapters(
request: Request, course_id: str, page: int = 1, limit: int = 10
):
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id})
if course:
course = Course(**course)
coursechapters = course.chapters_content
return coursechapters
async def get_coursechapters_meta(
request: Request, course_id: str, current_user: PublicUser
):
courses = request.app.db["courses"]
activities = request.app.db["activities"]
await non_public_endpoint(current_user)
await verify_rights(request, course_id, current_user, "read")
coursechapters = await courses.find_one(
{"course_id": course_id}, {"chapters": 1, "chapters_content": 1, "_id": 0}
)
coursechapters = coursechapters
if not coursechapters:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
# activities
coursechapter_activityIds_global = []
# chapters
chapters = {}
if coursechapters["chapters_content"]:
for coursechapter in coursechapters["chapters_content"]:
coursechapter = CourseChapterInDB(**coursechapter)
coursechapter_activityIds = []
for activity in coursechapter.activities:
coursechapter_activityIds.append(activity)
coursechapter_activityIds_global.append(activity)
chapters[coursechapter.coursechapter_id] = {
"id": coursechapter.coursechapter_id,
"name": coursechapter.name,
"activityIds": coursechapter_activityIds,
}
# activities
activities_list = {}
for activity in await activities.find(
{"activity_id": {"$in": coursechapter_activityIds_global}}
).to_list(length=100):
activity = ActivityInDB(**activity)
activities_list[activity.activity_id] = {
"id": activity.activity_id,
"name": activity.name,
"type": activity.type,
"content": activity.content,
}
final = {
"chapters": chapters,
"chapterOrder": coursechapters["chapters"],
"activities": activities_list,
}
return final
async def update_coursechapters_meta(
async def update_chapter(
request: Request,
course_id: str,
coursechapters_metadata: CourseChapterMetaData,
current_user: PublicUser,
):
courses = request.app.db["courses"]
chapter_object: ChapterUpdate,
chapter_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> ChapterRead:
statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
await verify_rights(request, course_id, current_user, "update")
# update chapters in course
await courses.update_one(
{"course_id": course_id},
{"$set": {"chapters": coursechapters_metadata.chapterOrder}},
if not chapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
if coursechapters_metadata.chapters is not None:
for (
coursechapter_id,
chapter_metadata,
) in coursechapters_metadata.chapters.items():
filter_query = {"chapters_content.coursechapter_id": coursechapter_id}
update_query = {
"$set": {
"chapters_content.$.activities": chapter_metadata["activityIds"]
}
}
result = await courses.update_one(filter_query, update_query)
if result.matched_count == 0:
# handle error when no documents are matched by the filter query
print(f"No documents found for course chapter ID {coursechapter_id}")
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "update", db_session)
# update activities in coursechapters
activity = request.app.db["activities"]
if coursechapters_metadata.chapters is not None:
for (
coursechapter_id,
chapter_metadata,
) in coursechapters_metadata.chapters.items():
# Update coursechapter_id in activities
filter_query = {"activity_id": {"$in": chapter_metadata["activityIds"]}}
update_query = {"$set": {"coursechapter_id": coursechapter_id}}
# Update only the fields that were passed in
for var, value in vars(chapter_object).items():
if value is not None:
setattr(chapter, var, value)
result = await activity.update_many(filter_query, update_query)
if result.matched_count == 0:
# handle error when no documents are matched by the filter query
print(f"No documents found for course chapter ID {coursechapter_id}")
chapter.update_date = str(datetime.now())
return {"detail": "coursechapters metadata updated"}
db_session.commit()
db_session.refresh(chapter)
if chapter:
chapter = await get_chapter(
request, chapter.id, current_user, db_session # type: ignore
)
return chapter
#### Security ####################################################
async def verify_rights(
async def delete_chapter(
request: Request,
course_id: str,
current_user: PublicUser,
action: Literal["read", "update", "delete"],
chapter_id: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
courses = request.app.db["courses"]
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
course = await courses.find_one({"course_id": course_id})
statement = select(Chapter).where(Chapter.id == chapter_id)
chapter = db_session.exec(statement).first()
if not chapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
)
# RBAC check
await rbac_check(request, chapter.chapter_uuid, current_user, "delete", db_session)
db_session.delete(chapter)
db_session.commit()
# Remove all linked activities
statement = select(ChapterActivity).where(ChapterActivity.id == chapter.id)
chapter_activities = db_session.exec(statement).all()
for chapter_activity in chapter_activities:
db_session.delete(chapter_activity)
db_session.commit()
return {"detail": "chapter deleted"}
async def get_course_chapters(
request: Request,
course_id: int,
db_session: Session,
current_user: PublicUser | AnonymousUser,
page: int = 1,
limit: int = 10,
) -> List[ChapterRead]:
statement = (
select(Chapter)
.join(CourseChapter, Chapter.id == CourseChapter.chapter_id)
.where(CourseChapter.course_id == course_id)
.where(Chapter.course_id == course_id)
.order_by(CourseChapter.order)
.group_by(Chapter.id, CourseChapter.order)
)
chapters = db_session.exec(statement).all()
chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters]
# RBAC check
await rbac_check(request, "chapter_x", current_user, "read", db_session)
# Get activities for each chapter
for chapter in chapters:
statement = (
select(ChapterActivity)
.where(ChapterActivity.chapter_id == chapter.id)
.order_by(ChapterActivity.order)
.distinct(ChapterActivity.id, ChapterActivity.order)
)
chapter_activities = db_session.exec(statement).all()
for chapter_activity in chapter_activities:
statement = (
select(Activity)
.where(Activity.id == chapter_activity.activity_id)
.distinct(Activity.id)
)
activity = db_session.exec(statement).first()
if activity:
chapter.activities.append(ActivityRead(**activity.dict()))
return chapters
# Important Note : this is legacy code that has been used because
# the frontend is still not adapted for the new data structure, this implementation is absolutely not the best one
# and should not be used for future features
async def DEPRECEATED_get_course_chapters(
request: Request,
course_uuid: str,
current_user: PublicUser,
db_session: Session,
):
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
if action == "read":
if current_user.user_id == "anonymous":
await authorization_verify_if_element_is_public(
request, course_id, current_user.user_id, action
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
await authorization_verify_if_user_is_anon(current_user.user_id)
chapters_in_db = await get_course_chapters(request, course.id, db_session, current_user) # type: ignore
# activities
# chapters
chapters = {}
for chapter in chapters_in_db:
chapter_activityIds = []
for activity in chapter.activities:
print("test", activity)
chapter_activityIds.append(activity.activity_uuid)
chapters[chapter.chapter_uuid] = {
"uuid": chapter.chapter_uuid,
"id": chapter.id,
"name": chapter.name,
"activityIds": chapter_activityIds,
}
# activities
activities_list = {}
statement = (
select(Activity)
.join(ChapterActivity, ChapterActivity.activity_id == Activity.id)
.where(ChapterActivity.activity_id == Activity.id)
.group_by(Activity.id)
)
activities_in_db = db_session.exec(statement).all()
for activity in activities_in_db:
activities_list[activity.activity_uuid] = {
"uuid": activity.activity_uuid,
"id": activity.id,
"name": activity.name,
"type": activity.activity_type,
"content": activity.content,
}
# get chapter order
statement = (
select(Chapter)
.join(CourseChapter, CourseChapter.chapter_id == Chapter.id)
.where(CourseChapter.chapter_id == Chapter.id)
.group_by(Chapter.id, CourseChapter.order)
.order_by(CourseChapter.order)
)
chapters_in_db = db_session.exec(statement).all()
chapterOrder = []
for chapter in chapters_in_db:
chapterOrder.append(chapter.chapter_uuid)
final = {
"chapters": chapters,
"chapterOrder": chapterOrder,
"activities": activities_list,
}
return final
async def reorder_chapters_and_activities(
request: Request,
course_uuid: str,
chapters_order: ChapterUpdateOrder,
current_user: PublicUser,
db_session: Session,
):
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
###########
# Chapters
###########
# Delete CourseChapters that are not linked to chapter_id and activity_id and org_id and course_id
statement = (
select(CourseChapter)
.where(
CourseChapter.course_id == course.id, CourseChapter.org_id == course.org_id
)
.order_by(CourseChapter.order)
)
course_chapters = db_session.exec(statement).all()
chapter_ids_to_keep = [
chapter_order.chapter_id
for chapter_order in chapters_order.chapter_order_by_ids
]
for course_chapter in course_chapters:
if course_chapter.chapter_id not in chapter_ids_to_keep:
db_session.delete(course_chapter)
db_session.commit()
# Delete Chapters that are not in the list of chapters_order
statement = select(Chapter).where(Chapter.course_id == course.id)
chapters = db_session.exec(statement).all()
chapter_ids_to_keep = [
chapter_order.chapter_id
for chapter_order in chapters_order.chapter_order_by_ids
]
for chapter in chapters:
if chapter.id not in chapter_ids_to_keep:
db_session.delete(chapter)
db_session.commit()
# If links do not exists, create them
for chapter_order in chapters_order.chapter_order_by_ids:
statement = (
select(CourseChapter)
.where(
CourseChapter.chapter_id == chapter_order.chapter_id,
CourseChapter.course_id == course.id,
)
.order_by(CourseChapter.order)
)
course_chapter = db_session.exec(statement).first()
if not course_chapter:
# Add CourseChapter link
course_chapter = CourseChapter(
chapter_id=chapter_order.chapter_id,
course_id=course.id, # type: ignore
org_id=course.org_id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=chapter_order.chapter_id,
)
# Insert CourseChapter link in DB
db_session.add(course_chapter)
db_session.commit()
# Update order of chapters
for chapter_order in chapters_order.chapter_order_by_ids:
statement = (
select(CourseChapter)
.where(
CourseChapter.chapter_id == chapter_order.chapter_id,
CourseChapter.course_id == course.id,
)
.order_by(CourseChapter.order)
)
course_chapter = db_session.exec(statement).first()
if course_chapter:
# Get the order from the index of the chapter_order_by_ids list
course_chapter.order = chapters_order.chapter_order_by_ids.index(
chapter_order
)
db_session.commit()
###########
# Activities
###########
# Delete ChapterActivities that are no longer part of the new order
statement = (
select(ChapterActivity)
.where(
ChapterActivity.course_id == course.id,
ChapterActivity.org_id == course.org_id,
)
.order_by(ChapterActivity.order)
)
chapter_activities = db_session.exec(statement).all()
activity_ids_to_delete = []
for chapter_activity in chapter_activities:
if (
chapter_activity.chapter_id not in chapter_ids_to_keep
or chapter_activity.activity_id not in activity_ids_to_delete
):
activity_ids_to_delete.append(chapter_activity.activity_id)
for activity_id in activity_ids_to_delete:
statement = (
select(ChapterActivity)
.where(
ChapterActivity.activity_id == activity_id,
ChapterActivity.course_id == course.id,
)
.order_by(ChapterActivity.order)
)
chapter_activity = db_session.exec(statement).first()
db_session.delete(chapter_activity)
db_session.commit()
# If links do not exist, create them
chapter_activity_map = {}
for chapter_order in chapters_order.chapter_order_by_ids:
for activity_order in chapter_order.activities_order_by_ids:
if activity_order.activity_id in chapter_activity_map and chapter_activity_map[activity_order.activity_id] != chapter_order.chapter_id:
continue
statement = (
select(ChapterActivity)
.where(
ChapterActivity.chapter_id == chapter_order.chapter_id,
ChapterActivity.activity_id == activity_order.activity_id,
)
.order_by(ChapterActivity.order)
)
chapter_activity = db_session.exec(statement).first()
if not chapter_activity:
# Add ChapterActivity link
chapter_activity = ChapterActivity(
chapter_id=chapter_order.chapter_id,
activity_id=activity_order.activity_id,
org_id=course.org_id,
course_id=course.id, # type: ignore
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
order=activity_order.activity_id,
)
# Insert ChapterActivity link in DB
db_session.add(chapter_activity)
db_session.commit()
chapter_activity_map[activity_order.activity_id] = chapter_order.chapter_id
# Update order of activities
for chapter_order in chapters_order.chapter_order_by_ids:
for activity_order in chapter_order.activities_order_by_ids:
statement = (
select(ChapterActivity)
.where(
ChapterActivity.chapter_id == chapter_order.chapter_id,
ChapterActivity.activity_id == activity_order.activity_id,
)
.order_by(ChapterActivity.order)
)
chapter_activity = db_session.exec(statement).first()
if chapter_activity:
# Get the order from the index of the chapter_order_by_ids list
chapter_activity.order = chapter_order.activities_order_by_ids.index(
activity_order
)
db_session.commit()
return {"detail": "Chapters reordered"}
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request,
current_user.user_id,
current_user.id,
action,
user["roles"],
course_id,
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles_and_authorship(
request,
current_user.user_id,
action,
user["roles"],
course_id,
db_session,
)
#### Security ####################################################
## 🔒 RBAC Utils ##

View file

@ -1,27 +1,23 @@
from datetime import datetime
from typing import List, Literal
from uuid import uuid4
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.users import AnonymousUser
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.collections import (
Collection,
CollectionCreate,
CollectionRead,
CollectionUpdate,
)
from src.db.collections_courses import CollectionCourse
from src.db.courses import Course
from src.services.users.users import PublicUser
from fastapi import HTTPException, status, Request
#### 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
@ -29,135 +25,182 @@ class CollectionInDB(Collection):
async def get_collection(
request: Request, collection_id: str, current_user: PublicUser
):
collections = request.app.db["collections"]
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"]
)
request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session
) -> CollectionRead:
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collection = db_session.exec(statement).first()
if not collection:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
)
collection = Collection(**collection)
# RBAC check
await rbac_check(
request, collection.collection_uuid, current_user, "read", db_session
)
# add courses to collection
courses = request.app.db["courses"]
courseids = [course for course in collection.courses]
# get courses in collection
statement = (
select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
.distinct(Course.id)
)
courses = db_session.exec(statement).all()
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)
]
collection = CollectionRead(**collection.dict(), courses=courses)
return collection
async def create_collection(
request: Request, collection_object: Collection, current_user: PublicUser
):
collections = request.app.db["collections"]
request: Request,
collection_object: CollectionCreate,
current_user: PublicUser,
db_session: Session,
) -> CollectionRead:
collection = Collection.from_orm(collection_object)
# find if collection already exists using name
isCollectionNameAvailable = await collections.find_one(
{"name": collection_object.name}
# RBAC check
await rbac_check(request, "collection_x", current_user, "create", db_session)
# Complete the collection object
collection.collection_uuid = f"collection_{uuid4()}"
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
if collection:
for course_id in collection_object.courses:
collection_course = CollectionCourse(
collection_id=int(collection.id), # type: ignore
course_id=course_id,
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
# await verify_collection_rights("*", current_user, "create")
db_session.commit()
db_session.refresh(collection)
if isCollectionNameAvailable:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Collection name already exists",
# Get courses once again
statement = (
select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
.distinct(Course.id)
)
courses = db_session.exec(statement).all()
# generate collection_id with uuid4
collection_id = str(f"collection_{uuid4()}")
collection = CollectionRead(**collection.dict(), courses=courses)
collection = CollectionInDB(
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()
return CollectionRead.from_orm(collection)
async def update_collection(
request: Request,
collection_object: Collection,
collection_id: str,
collection_object: CollectionUpdate,
collection_uuid: str,
current_user: PublicUser,
):
# verify collection rights
collections = request.app.db["collections"]
collection = await collections.find_one({"collection_id": collection_id})
await verify_collection_rights(
request, collection_id, current_user, "update", collection["org_id"]
)
db_session: Session,
) -> CollectionRead:
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collection = db_session.exec(statement).first()
if not collection:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
)
updated_collection = CollectionInDB(
collection_id=collection_id, **collection_object.dict()
# RBAC check
await rbac_check(
request, collection.collection_uuid, current_user, "update", db_session
)
await collections.update_one(
{"collection_id": collection_id}, {"$set": updated_collection.dict()}
courses = collection_object.courses
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), # type: ignore
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)
)
return Collection(**updated_collection.dict())
courses = db_session.exec(statement).all()
collection = CollectionRead(**collection.dict(), courses=courses)
return collection
async def delete_collection(
request: Request, collection_id: str, current_user: PublicUser
request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session
):
collections = request.app.db["collections"]
collection = await collections.find_one({"collection_id": collection_id})
await verify_collection_rights(
request, collection_id, current_user, "delete", collection["org_id"]
)
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
collection = db_session.exec(statement).first()
if not collection:
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})
if isDeleted:
return {"detail": "collection deleted"}
else:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
# RBAC check
await rbac_check(
request, collection.collection_uuid, current_user, "delete", db_session
)
# delete collection from database
db_session.delete(collection)
db_session.commit()
return {"detail": "Collection deleted"}
####################################################
# Misc
@ -167,76 +210,55 @@ async def delete_collection(
async def get_collections(
request: Request,
org_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
db_session: Session,
page: int = 1,
limit: int = 10,
):
collections = request.app.db["collections"]
) -> List[CollectionRead]:
# RBAC check
await rbac_check(request, "collection_x", current_user, "read", db_session)
if current_user.user_id == "anonymous":
all_collections = collections.find(
{"org_id": org_id, "public": True}, {"_id": 0}
statement = (
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
)
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()
collections_with_courses = []
for collection in collections:
statement = (
select(Course)
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
.distinct(Course.id)
)
courses = db_session.exec(statement).all()
# create list of collections and include courses in each collection
collections_list = []
for collection in await all_collections.to_list(length=100):
collection = CollectionInDB(**collection)
collections_list.append(collection)
collection = CollectionRead(**collection.dict(), courses=courses)
collections_with_courses.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
return collections_with_courses
#### Security ####################################################
## 🔒 RBAC Utils ##
async def verify_collection_rights(
async def rbac_check(
request: Request,
collection_id: str,
current_user: PublicUser,
course_id: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
org_id: str,
db_session: Session,
):
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(
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
)
# Collections are public by default for now
if current_user.user_id == "anonymous" and action == "read":
return True
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request, current_user.user_id, action, user["roles"], collection_id
request,
current_user.id,
action,
course_id,
db_session,
)
#### Security ####################################################
## 🔒 RBAC Utils ##

View file

@ -1,413 +1,391 @@
import json
from typing import List, Literal, Optional
from typing import Literal
from uuid import uuid4
from pydantic import BaseModel
from sqlmodel import Session, select
from src.db.organizations import Organization
from src.db.trails import TrailRead
from src.services.trail.trail import get_user_trail_with_orgid
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.users import PublicUser, AnonymousUser, User, UserRead
from src.db.courses import (
Course,
CourseCreate,
CourseRead,
CourseUpdate,
FullCourseReadWithTrail,
)
from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon,
)
from src.services.courses.activities.activities import ActivityInDB
from src.services.courses.thumbnails import upload_thumbnail
from src.services.users.schemas.users import AnonymousUser
from src.services.users.users import PublicUser
from fastapi import HTTPException, Request, status, UploadFile
from fastapi import HTTPException, Request, UploadFile
from datetime import datetime
#### Classes ####################################################
class Course(BaseModel):
name: str
mini_description: str
description: str
learnings: List[str]
thumbnail: str
public: bool
chapters: List[str]
chapters_content: Optional[List]
org_id: str
class CourseInDB(Course):
course_id: str
creationDate: str
updateDate: str
authors: List[str]
# TODO : wow terrible, fix this
# those models need to be available only in the chapters service
class CourseChapter(BaseModel):
name: str
description: str
activities: list
class CourseChapterInDB(CourseChapter):
coursechapter_id: str
course_id: str
creationDate: str
updateDate: str
#### Classes ####################################################
# TODO : Add courses photo & cover upload and delete
####################################################
# CRUD
####################################################
async def get_course(request: Request, course_id: str, current_user: PublicUser):
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id})
# verify course rights
await verify_rights(request, course_id, current_user, "read")
async def get_course(
request: Request,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
status_code=404,
detail="Course not found",
)
course = Course(**course)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
return course
async def get_course_meta(request: Request, course_id: str, current_user: PublicUser):
courses = request.app.db["courses"]
trails = request.app.db["trails"]
async def get_course_meta(
request: Request,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
) -> FullCourseReadWithTrail:
# Avoid circular import
from src.services.courses.chapters import get_course_chapters
course = await courses.find_one({"course_id": course_id})
activities = request.app.db["activities"]
# verify course rights
await verify_rights(request, course_id, current_user, "read")
course_statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(course_statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
status_code=404,
detail="Course not found",
)
coursechapters = await courses.find_one(
{"course_id": course_id}, {"chapters_content": 1, "_id": 0}
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
# Get course chapters
chapters = await get_course_chapters(request, course.id, db_session, current_user)
# Trail
trail = await get_user_trail_with_orgid(
request, current_user, course.org_id, db_session
)
# activities
coursechapter_activityIds_global = []
trail = TrailRead.from_orm(trail)
# chapters
chapters = {}
if coursechapters["chapters_content"]:
for coursechapter in coursechapters["chapters_content"]:
coursechapter = CourseChapterInDB(**coursechapter)
coursechapter_activityIds = []
for activity in coursechapter.activities:
coursechapter_activityIds.append(activity)
coursechapter_activityIds_global.append(activity)
chapters[coursechapter.coursechapter_id] = {
"id": coursechapter.coursechapter_id,
"name": coursechapter.name,
"activityIds": coursechapter_activityIds,
}
# activities
activities_list = {}
for activity in await activities.find(
{"activity_id": {"$in": coursechapter_activityIds_global}}
).to_list(length=100):
activity = ActivityInDB(**activity)
activities_list[activity.activity_id] = {
"id": activity.activity_id,
"name": activity.name,
"type": activity.type,
"content": activity.content,
}
chapters_list_with_activities = []
for chapter in chapters:
chapters_list_with_activities.append(
{
"id": chapters[chapter]["id"],
"name": chapters[chapter]["name"],
"activities": [
activities_list[activity]
for activity in chapters[chapter]["activityIds"]
],
}
return FullCourseReadWithTrail(
**course.dict(),
chapters=chapters,
trail=trail if trail else None,
)
course = CourseInDB(**course)
# Get activity by user
trail = await trails.find_one(
{"courses.course_id": course_id, "user_id": current_user.user_id}
)
if trail:
# get only the course where course_id == course_id
trail_course = next(
(course for course in trail["courses"] if course["course_id"] == course_id),
None,
)
else:
trail_course = ""
return {
"course": course,
"chapters": chapters_list_with_activities,
"trail": trail_course,
}
async def create_course(
request: Request,
course_object: Course,
org_id: str,
current_user: PublicUser,
org_id: int,
course_object: CourseCreate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
thumbnail_file: UploadFile | None = None,
):
courses = request.app.db["courses"]
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
course = Course.from_orm(course_object)
# generate course_id with uuid4
course_id = str(f"course_{uuid4()}")
# RBAC check
await rbac_check(request, "course_x", current_user, "create", db_session)
# TODO(fix) : the implementation here is clearly not the best one (this entire function)
course_object.org_id = org_id
course_object.chapters_content = []
# Complete course object
course.org_id = course.org_id
await authorization_verify_based_on_roles(
request,
current_user.user_id,
"create",
user["roles"],
course_id,
)
# Get org uuid
org_statement = select(Organization).where(Organization.id == org_id)
org = db_session.exec(org_statement).first()
course.course_uuid = str(f"course_{uuid4()}")
course.creation_date = str(datetime.now())
course.update_date = str(datetime.now())
# Upload thumbnail
if thumbnail_file and thumbnail_file.filename:
name_in_disk = (
f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
)
name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
await upload_thumbnail(
thumbnail_file, name_in_disk, course_object.org_id, course_id
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
)
course_object.thumbnail = name_in_disk
course.thumbnail_image = name_in_disk
course = CourseInDB(
course_id=course_id,
authors=[current_user.user_id],
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
**course_object.dict(),
# Insert course
db_session.add(course)
db_session.commit()
db_session.refresh(course)
# Make the user the creator of the course
resource_author = ResourceAuthor(
resource_uuid=course.course_uuid,
user_id=current_user.id,
authorship=ResourceAuthorshipEnum.CREATOR,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
course_in_db = await courses.insert_one(course.dict())
# Insert course author
db_session.add(resource_author)
db_session.commit()
db_session.refresh(resource_author)
if not course_in_db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
return course.dict()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
return CourseRead.from_orm(course)
async def update_course_thumbnail(
request: Request,
course_id: str,
current_user: PublicUser,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
thumbnail_file: UploadFile | None = None,
):
courses = request.app.db["courses"]
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
course = await courses.find_one({"course_id": course_id})
# verify course rights
await verify_rights(request, course_id, current_user, "update")
# TODO(fix) : the implementation here is clearly not the best one
if course:
creationDate = course["creationDate"]
authors = course["authors"]
if thumbnail_file and thumbnail_file.filename:
name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
course = Course(**course).copy(update={"thumbnail": name_in_disk})
await upload_thumbnail(
thumbnail_file, name_in_disk, course.org_id, course_id
)
updated_course = CourseInDB(
course_id=course_id,
creationDate=creationDate,
authors=authors,
updateDate=str(datetime.now()),
**course.dict(),
)
await courses.update_one(
{"course_id": course_id}, {"$set": updated_course.dict()}
)
return CourseInDB(**updated_course.dict())
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
async def update_course(
request: Request, course_object: Course, course_id: str, current_user: PublicUser
):
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id})
# verify course rights
await verify_rights(request, course_id, current_user, "update")
if course:
creationDate = course["creationDate"]
authors = course["authors"]
# get today's date
datetime_object = datetime.now()
updated_course = CourseInDB(
course_id=course_id,
creationDate=creationDate,
authors=authors,
updateDate=str(datetime_object),
**course_object.dict(),
)
await courses.update_one(
{"course_id": course_id}, {"$set": updated_course.dict()}
)
return CourseInDB(**updated_course.dict())
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
)
async def delete_course(request: Request, course_id: str, current_user: PublicUser):
courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id})
# verify course rights
await verify_rights(request, course_id, current_user, "delete")
name_in_disk = None
if not course:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
status_code=404,
detail="Course not found",
)
isDeleted = await courses.delete_one({"course_id": course_id})
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
if isDeleted:
return {"detail": "Course deleted"}
# Get org uuid
org_statement = select(Organization).where(Organization.id == course.org_id)
org = db_session.exec(org_statement).first()
# Upload thumbnail
if thumbnail_file and thumbnail_file.filename:
name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
await upload_thumbnail(
thumbnail_file, name_in_disk, org.org_uuid, course.course_uuid
)
# Update course
if name_in_disk:
course.thumbnail_image = name_in_disk
else:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
status_code=500,
detail="Issue with thumbnail upload",
)
# Complete the course object
course.update_date = str(datetime.now())
####################################################
# Misc
####################################################
db_session.add(course)
db_session.commit()
db_session.refresh(course)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
return course
async def update_course(
request: Request,
course_object: CourseUpdate,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
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",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
# Update only the fields that were passed in
for var, value in vars(course_object).items():
if value is not None:
setattr(course, var, value)
# Complete the course object
course.update_date = str(datetime.now())
db_session.add(course)
db_session.commit()
db_session.refresh(course)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course = CourseRead(**course.dict(), authors=authors)
return course
async def delete_course(
request: Request,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
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",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
db_session.delete(course)
db_session.commit()
return {"detail": "Course deleted"}
async def get_courses_orgslug(
request: Request,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
org_slug: str,
db_session: Session,
page: int = 1,
limit: int = 10,
org_slug: str | None = None,
):
courses = request.app.db["courses"]
orgs = request.app.db["organizations"]
# get org_id from slug
org = await orgs.find_one({"slug": org_slug})
if not org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
statement_public = (
select(Course)
.join(Organization)
.where(Organization.slug == org_slug, Course.public is True)
)
statement_all = (
select(Course).join(Organization).where(Organization.slug == org_slug)
)
# show only public courses if user is not logged in
if current_user.user_id == "anonymous":
all_courses = (
courses.find({"org_id": org["org_id"], "public": True})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(limit)
)
if current_user.id == 0:
statement = statement_public
else:
all_courses = (
courses.find({"org_id": org["org_id"]})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(limit)
# RBAC check
await authorization_verify_if_user_is_anon(current_user.id)
statement = statement_all
courses = db_session.exec(statement)
courses = [CourseRead(**course.dict(), authors=[]) for course in courses]
# for every course, get the authors
for course in courses:
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
return [
json.loads(json.dumps(course, default=str))
for course in await all_courses.to_list(length=100)
]
# convert from User to UserRead
authors = [UserRead.from_orm(author) for author in authors]
course.authors = authors
return courses
#### Security ####################################################
## 🔒 RBAC Utils ##
async def verify_rights(
async def rbac_check(
request: Request,
course_id: str,
course_uuid: str,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
if action == "read":
if current_user.user_id == "anonymous":
if current_user.id == 0: # Anonymous user
await authorization_verify_if_element_is_public(
request, course_id, current_user.user_id, action
request, course_uuid, action, db_session
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, course_uuid, db_session
)
else:
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request,
current_user.user_id,
current_user.id,
action,
user["roles"],
course_id,
)
else:
users = request.app.db["users"]
user = await users.find_one({"user_id": current_user.user_id})
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles_and_authorship(
request,
current_user.user_id,
action,
user["roles"],
course_id,
course_uuid,
db_session,
)
#### Security ####################################################
## 🔒 RBAC Utils ##

View file

@ -16,3 +16,5 @@ def isDevModeEnabledOrRaise():
return True
else:
raise HTTPException(status_code=403, detail="Development mode is disabled")

View file

@ -0,0 +1,275 @@
import datetime
from fastapi import Request
from sqlmodel import Session, select
from src.db.blocks import Block, BlockTypeEnum
from src.db.chapter_activities import ChapterActivity
from src.db.activities import Activity, ActivitySubTypeEnum, ActivityTypeEnum
from src.db.course_chapters import CourseChapter
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.user_organizations import UserOrganization
from src.db.chapters import Chapter
from src.db.courses import Course
from src.db.users import User
from src.db.organizations import Organization
async def start_migrate_from_mongo(request: Request, db_session: Session):
orgs = request.app.db["organizations"]
## ----> Organizations migration
org_db_list = await orgs.find().to_list(length=100)
for org in org_db_list:
org_to_add = Organization(
name=org["name"],
description=org["description"],
slug=org["slug"],
logo_image=org["logo"],
email=org["email"],
org_uuid=org["org_id"],
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(org_to_add)
db_session.commit()
print("Migrated organizations.")
## ----> Users migration
users = request.app.db["users"]
users_db_list = await users.find().to_list(length=100)
for user in users_db_list:
user_to_add = User(
email=user["email"],
username=user["username"],
first_name="",
last_name="",
user_uuid=user["user_id"],
password=user["password"],
creation_date=user["creation_date"],
update_date=user["update_date"],
)
db_session.add(user_to_add)
db_session.commit()
# Link Orgs to users and make them owners
for org in user["orgs"]:
statement = select(Organization).where(
Organization.org_uuid == org["org_id"]
)
org_from_db = db_session.exec(statement).first()
statement = select(User).where(User.user_uuid == user["user_id"])
user_from_db = db_session.exec(statement).first()
user_org_object = UserOrganization(
user_id=user_from_db.id, # type: ignore
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
role_id=1,
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(user_org_object)
db_session.commit()
print("Migrated users and linked them to orgs.")
## ----> Courses migration
courses = request.app.db["courses"]
courses_db_list = await courses.find().to_list(length=300)
for course in courses_db_list:
# Get the organization id
statement = select(Organization).where(
Organization.org_uuid == course["org_id"]
)
org_from_db = db_session.exec(statement).first()
course_to_add = Course(
name=course["name"],
description=course["description"],
about=course["description"],
learnings="",
course_uuid=course["course_id"],
thumbnail_image=course["thumbnail"],
tags="",
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
public=course["public"],
creation_date=str(course["creationDate"]),
update_date=str(course["updateDate"]),
)
db_session.add(course_to_add)
db_session.commit()
# Get this course
statement = select(Course).where(Course.course_uuid == course["course_id"])
course_from_db = db_session.exec(statement).first()
# Add Authorship
authors = course["authors"]
for author in authors:
# Get the user id
statement = select(User).where(User.user_uuid == author)
user_from_db = db_session.exec(statement).first()
authorship = ResourceAuthor(
resource_uuid=course_from_db.course_uuid, # type: ignore
user_id=user_from_db.id if user_from_db is not None else None, # type: ignore
authorship=ResourceAuthorshipEnum.CREATOR,
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(authorship)
db_session.commit()
print("Added authorship.")
## ----> Chapters migration & Link
chapter_object = course["chapters_content"]
order = 0
for chapter in chapter_object:
chapter_to_add = Chapter(
name=chapter["name"],
description=chapter["description"],
chapter_uuid=chapter["coursechapter_id"].replace(
"coursechapter", "chapter"
),
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
course_id=course_from_db.id, # type: ignore
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(chapter_to_add)
db_session.commit()
# Get this chapter
statement = select(Chapter).where(
Chapter.chapter_uuid
== chapter["coursechapter_id"].replace("coursechapter", "chapter")
)
chapter_from_db = db_session.exec(statement).first()
# Link chapter to course
coursechapter_to_add = CourseChapter(
chapter_id=chapter_from_db.id, # type: ignore
course_id=course_from_db.id, # type: ignore
order=order,
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(coursechapter_to_add)
db_session.commit()
order += 1
## ----> Activities migration
activities = request.app.db["activities"]
activities_db_list = await activities.find(
{"coursechapter_id": chapter["coursechapter_id"]}
).to_list(length=100)
activity_order = 0
for activity in activities_db_list:
type_to_use = ActivityTypeEnum.TYPE_CUSTOM
sub_type_to_use = ActivityTypeEnum.TYPE_CUSTOM
if activity["type"] == "video":
type_to_use = ActivityTypeEnum.TYPE_VIDEO
sub_type_to_use = ActivitySubTypeEnum.SUBTYPE_VIDEO_HOSTED
if "external_video" in activity["content"]:
type_to_use = ActivityTypeEnum.TYPE_VIDEO
sub_type_to_use = ActivitySubTypeEnum.SUBTYPE_VIDEO_YOUTUBE
if activity["type"] == "documentpdf":
type_to_use = ActivityTypeEnum.TYPE_DOCUMENT
sub_type_to_use = ActivitySubTypeEnum.SUBTYPE_DOCUMENT_PDF
if activity["type"] == "dynamic":
type_to_use = ActivityTypeEnum.TYPE_DYNAMIC
sub_type_to_use = ActivitySubTypeEnum.SUBTYPE_DYNAMIC_PAGE
activity_to_add = Activity(
name=activity["name"],
activity_uuid=activity["activity_id"],
version=1,
published_version=1,
activity_type=type_to_use,
content=activity["content"],
activity_sub_type=sub_type_to_use,
chapter_id=chapter_from_db.id, # type: ignore
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
course_id=course_from_db.id, # type: ignore
creation_date=str(activity["creationDate"]),
update_date=str(activity["updateDate"]),
)
db_session.add(activity_to_add)
db_session.commit()
# Link activity to chapter
statement = select(Activity).where(
Activity.activity_uuid == activity["activity_id"]
)
activity_from_db = db_session.exec(statement).first()
activitychapter_to_add = ChapterActivity(
chapter_id=chapter_from_db.id, # type: ignore
activity_id=activity_from_db.id, # type: ignore
order=activity_order,
course_id=course_from_db.id, # type: ignore
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(activitychapter_to_add)
db_session.commit()
activity_order += 1
## ----> Blocks migration
blocks = request.app.db["blocks"]
blocks_db_list = await blocks.find(
{"activity_id": activity["activity_id"]}
).to_list(length=200)
for block in blocks_db_list:
type_to_use = BlockTypeEnum.BLOCK_CUSTOM
if block["block_type"] == "imageBlock":
type_to_use = BlockTypeEnum.BLOCK_IMAGE
if block["block_type"] == "videoBlock":
type_to_use = BlockTypeEnum.BLOCK_VIDEO
if block["block_type"] == "pdfBlock":
type_to_use = BlockTypeEnum.BLOCK_DOCUMENT_PDF
print('block', block)
block_to_add = Block(
block_uuid=block["block_id"],
content=block["block_data"],
block_type=type_to_use,
activity_id=activity_from_db.id, # type: ignore
org_id=org_from_db.id if org_from_db is not None else None, # type: ignore
course_id=course_from_db.id, # type: ignore
chapter_id=chapter_from_db.id, # type: ignore
creation_date=str(datetime.datetime.now()),
update_date=str(datetime.datetime.now()),
)
db_session.add(block_to_add)
db_session.commit()
return "Migration successfull."

View file

@ -1,214 +0,0 @@
import os
import requests
from datetime import datetime
from uuid import uuid4
from fastapi import Request
from src.security.security import security_hash_password
from src.services.courses.chapters import CourseChapter, create_coursechapter
from src.services.courses.activities.activities import Activity, create_activity
from src.services.users.users import PublicUser, UserInDB
from src.services.orgs.orgs import Organization, create_org
from src.services.roles.schemas.roles import Permission, Elements, RoleInDB
from src.services.courses.courses import CourseInDB
from faker import Faker
async def create_initial_data(request: Request):
fake = Faker(['en_US'])
fake_multilang = Faker(
['en_US', 'de_DE', 'ja_JP', 'es_ES', 'it_IT', 'pt_BR', 'ar_PS'])
# Create users
########################################
database_users = request.app.db["users"]
await database_users.delete_many({})
users = []
admin_user = UserInDB(
user_id="user_admin",
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
roles= [],
orgs=[],
username="admin",
email="admin@admin.admin",
password=str(await security_hash_password("admin")),
)
await database_users.insert_one(admin_user.dict())
# find admin user
users = request.app.db["users"]
admin_user = await users.find_one({"username": "admin"})
if admin_user:
admin_user = UserInDB(**admin_user)
current_user = PublicUser(**admin_user.dict())
else:
raise Exception("Admin user not found")
# Create roles
########################################
database_roles = request.app.db["roles"]
await database_roles.delete_many({})
roles = []
admin_role = RoleInDB(
name="Admin",
description="Admin",
elements=Elements(
courses=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
users=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
houses=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
collections=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
organizations=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
coursechapters=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
activities=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
),
org_id="org_test",
role_id="role_admin",
created_at=str(datetime.now()),
updated_at=str(datetime.now()),
)
roles.append(admin_role)
for role in roles:
database_roles.insert_one(role.dict())
# Create organizations
########################################
database_orgs = request.app.db["organizations"]
await database_orgs.delete_many({})
organizations = []
for i in range(0, 2):
company = fake.company()
# remove whitespace and special characters and make lowercase
slug = ''.join(e for e in company if e.isalnum()).lower()
org = Organization(
name=company,
description=fake.unique.text(),
email=fake.unique.email(),
slug=slug,
logo="",
default=False
)
organizations.append(org)
await create_org(request, org, current_user)
# Generate Courses and CourseChapters
########################################
database_courses = request.app.db["courses"]
await database_courses.delete_many({})
courses = []
orgs = request.app.db["organizations"]
if await orgs.count_documents({}) > 0:
for org in await orgs.find().to_list(length=100):
for i in range(0, 5):
# get image in BinaryIO format from unsplash and save it to disk
image = requests.get(
"https://source.unsplash.com/random/800x600")
with open("thumbnail.jpg", "wb") as f:
f.write(image.content)
course_id = f"course_{uuid4()}"
course = CourseInDB(
name=fake_multilang.unique.sentence(),
description=fake_multilang.unique.text(),
mini_description=fake_multilang.unique.text(),
thumbnail="thumbnail",
org_id=org['org_id'],
learnings=[fake_multilang.unique.sentence()
for i in range(0, 5)],
public=True,
chapters=[],
course_id=course_id,
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
authors=[current_user.user_id],
chapters_content=[],
)
courses = request.app.db["courses"]
name_in_disk = f"test_mock{course_id}.jpeg"
image = requests.get(
"https://source.unsplash.com/random/800x600/?img=1")
# check if folder exists and create it if not
if not os.path.exists("content/uploads/img"):
os.makedirs("content/uploads/img")
with open(f"content/uploads/img/{name_in_disk}", "wb") as f:
f.write(image.content)
course.thumbnail = name_in_disk
course = CourseInDB(**course.dict())
await courses.insert_one(course.dict())
# create chapters
for i in range(0, 5):
coursechapter = CourseChapter(
name=fake_multilang.unique.sentence(),
description=fake_multilang.unique.text(),
activities=[],
)
coursechapter = await create_coursechapter(request,coursechapter, course_id, current_user)
if coursechapter:
# create activities
for i in range(0, 5):
activity = Activity(
name=fake_multilang.unique.sentence(),
type="dynamic",
content={},
)
activity = await create_activity(request,activity, "org_test", coursechapter['coursechapter_id'], current_user)

View file

@ -1,35 +1,15 @@
from datetime import datetime
from uuid import uuid4
from fastapi import HTTPException, Request, status
from pydantic import BaseModel
import requests
from fastapi import HTTPException, Request
from sqlalchemy import desc
from sqlmodel import Session, select
from src.db.install import Install, InstallRead
from src.db.organizations import Organization, OrganizationCreate
from src.db.roles import Permission, Rights, Role, RoleTypeEnum
from src.db.user_organizations import UserOrganization
from src.db.users import User, UserCreate, UserRead
from config.config import get_learnhouse_config
from src.security.security import security_hash_password
from src.services.courses.activities.activities import Activity, create_activity
from src.services.courses.chapters import create_coursechapter, CourseChapter
from src.services.courses.courses import CourseInDB
from src.services.orgs.schemas.orgs import Organization, OrganizationInDB
from faker import Faker
from src.services.roles.schemas.roles import Elements, Permission, RoleInDB
from src.services.users.schemas.users import (
PublicUser,
User,
UserInDB,
UserOrganization,
UserRolesInOrganization,
UserWithPassword,
)
class InstallInstance(BaseModel):
install_id: str
created_date: str
updated_date: str
step: int
data: dict
async def isInstallModeEnabled():
@ -44,37 +24,31 @@ async def isInstallModeEnabled():
)
async def create_install_instance(request: Request, data: dict):
installs = request.app.db["installs"]
async def create_install_instance(request: Request, data: dict, db_session: Session):
install = Install.from_orm(data)
# get install_id
install_id = str(f"install_{uuid4()}")
created_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
updated_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
step = 1
# complete install instance
install.install_uuid = str(f"install_{uuid4()}")
install.update_date = str(datetime.now())
install.creation_date = str(datetime.now())
# create install
install = InstallInstance(
install_id=install_id,
created_date=created_date,
updated_date=updated_date,
step=step,
data=data,
)
# insert install instance
db_session.add(install)
# insert install
installs.insert_one(install.dict())
# commit changes
db_session.commit()
# refresh install instance
db_session.refresh(install)
install = InstallRead.from_orm(install)
return install
async def get_latest_install_instance(request: Request):
installs = request.app.db["installs"]
# get latest created install instance using find_one
install = await installs.find_one(
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
)
async def get_latest_install_instance(request: Request, db_session: Session):
statement = select(Install).order_by(desc(Install.creation_date)).limit(1)
install = db_session.exec(statement).first()
if install is None:
raise HTTPException(
@ -82,35 +56,33 @@ async def get_latest_install_instance(request: Request):
detail="No install instance found",
)
else:
install = InstallInstance(**install)
install = InstallRead.from_orm(install)
return install
async def update_install_instance(request: Request, data: dict, step: int):
installs = request.app.db["installs"]
# get latest created install
install = await installs.find_one(
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
)
async def update_install_instance(
request: Request, data: dict, step: int, db_session: Session
):
statement = select(Install).order_by(desc(Install.creation_date)).limit(1)
install = db_session.exec(statement).first()
if install is None:
return None
else:
# update install
install["data"] = data
install["step"] = step
install["updated_date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# update install
await installs.update_one(
{"install_id": install["install_id"]}, {"$set": install}
raise HTTPException(
status_code=404,
detail="No install instance found",
)
install = InstallInstance(**install)
install.step = step
install.data = data
# commit changes
db_session.commit()
# refresh install instance
db_session.refresh(install)
install = InstallRead.from_orm(install)
return install
@ -121,24 +93,34 @@ async def update_install_instance(request: Request, data: dict, step: int):
# Install Default roles
async def install_default_elements(request: Request, data: dict):
roles = request.app.db["roles"]
async def install_default_elements(request: Request, data: dict, db_session: Session):
# remove all default roles
statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL)
roles = db_session.exec(statement).all()
# check if default roles ADMIN_ROLE and USER_ROLE already exist
admin_role = await roles.find_one({"role_id": "role_admin"})
user_role = await roles.find_one({"role_id": "role_member"})
for role in roles:
db_session.delete(role)
if admin_role is not None or user_role is not None:
db_session.commit()
# Check if default roles already exist
statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL)
roles = db_session.exec(statement).all()
if roles and len(roles) == 3:
raise HTTPException(
status_code=400,
status_code=409,
detail="Default roles already exist",
)
# get default roles
ADMIN_ROLE = RoleInDB(
name="Admin Role",
description="This role grants all permissions to the user",
elements=Elements(
# Create default roles
role_global_admin = Role(
name="Admin",
description="Standard Admin Role",
id=1,
role_type=RoleTypeEnum.TYPE_GLOBAL,
role_uuid="role_global_admin",
rights=Rights(
courses=Permission(
action_create=True,
action_read=True,
@ -151,12 +133,6 @@ async def install_default_elements(request: Request, data: dict):
action_update=True,
action_delete=True,
),
houses=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
collections=Permission(
action_create=True,
action_read=True,
@ -182,16 +158,65 @@ async def install_default_elements(request: Request, data: dict):
action_delete=True,
),
),
org_id="*",
role_id="role_admin",
created_at=str(datetime.now()),
updated_at=str(datetime.now()),
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
USER_ROLE = RoleInDB(
name="Member Role",
description="This role grants read-only permissions to the user",
elements=Elements(
role_global_maintainer = Role(
name="Maintainer",
description="Standard Maintainer Role",
id=2,
role_type=RoleTypeEnum.TYPE_GLOBAL,
role_uuid="role_global_maintainer",
rights=Rights(
courses=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
users=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
collections=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
organizations=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
coursechapters=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
activities=Permission(
action_create=True,
action_read=True,
action_update=True,
action_delete=True,
),
),
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
role_global_user = Role(
name="User",
description="Standard User Role",
role_type=RoleTypeEnum.TYPE_GLOBAL,
role_uuid="role_global_user",
id=3,
rights=Rights(
courses=Permission(
action_create=False,
action_read=True,
@ -199,13 +224,7 @@ async def install_default_elements(request: Request, data: dict):
action_delete=False,
),
users=Permission(
action_create=False,
action_read=True,
action_update=False,
action_delete=False,
),
houses=Permission(
action_create=False,
action_create=True,
action_read=True,
action_update=False,
action_delete=False,
@ -235,185 +254,122 @@ async def install_default_elements(request: Request, data: dict):
action_delete=False,
),
),
org_id="*",
role_id="role_member",
created_at=str(datetime.now()),
updated_at=str(datetime.now()),
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
try:
# insert default roles
await roles.insert_many([USER_ROLE.dict(), ADMIN_ROLE.dict()])
# Serialize rights to JSON
role_global_admin.rights = role_global_admin.rights.dict() # type: ignore
role_global_maintainer.rights = role_global_maintainer.rights.dict() # type: ignore
role_global_user.rights = role_global_user.rights.dict() # type: ignore
# Insert roles in DB
db_session.add(role_global_admin)
db_session.add(role_global_maintainer)
db_session.add(role_global_user)
# commit changes
db_session.commit()
# refresh roles
db_session.refresh(role_global_admin)
return True
except Exception:
raise HTTPException(
status_code=400,
detail="Error while inserting default roles",
)
# Organization creation
async def install_create_organization(
request: Request,
org_object: Organization,
request: Request, org_object: OrganizationCreate, db_session: Session
):
orgs = request.app.db["organizations"]
request.app.db["users"]
org = Organization.from_orm(org_object)
# find if org already exists using name
# Complete the org object
org.org_uuid = f"org_{uuid4()}"
org.creation_date = str(datetime.now())
org.update_date = str(datetime.now())
isOrgAvailable = await orgs.find_one({"slug": org_object.slug.lower()})
db_session.add(org)
db_session.commit()
db_session.refresh(org)
if isOrgAvailable:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Organization slug already exists",
)
# generate org_id with uuid4
org_id = str(f"org_{uuid4()}")
org = OrganizationInDB(org_id=org_id, **org_object.dict())
org_in_db = await orgs.insert_one(org.dict())
if not org_in_db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
)
return org.dict()
return org
async def install_create_organization_user(
request: Request, user_object: UserWithPassword, org_slug: str
request: Request, user_object: UserCreate, org_slug: str, db_session: Session
):
users = request.app.db["users"]
user = User.from_orm(user_object)
isUsernameAvailable = await users.find_one({"username": user_object.username})
isEmailAvailable = await users.find_one({"email": user_object.email})
# Complete the user object
user.user_uuid = f"user_{uuid4()}"
user.password = await security_hash_password(user_object.password)
user.email_verified = False
user.creation_date = str(datetime.now())
user.update_date = str(datetime.now())
if isUsernameAvailable:
# Verifications
# Check if Organization exists
statement = select(Organization).where(Organization.slug == org_slug)
org = db_session.exec(statement)
if not org.first():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Username already exists"
status_code=400,
detail="Organization does not exist",
)
if isEmailAvailable:
# Username
statement = select(User).where(User.username == user.username)
result = db_session.exec(statement)
if result.first():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Email already exists"
status_code=400,
detail="Username already exists",
)
# Generate user_id with uuid4
user_id = str(f"user_{uuid4()}")
# Email
statement = select(User).where(User.email == user.email)
result = db_session.exec(statement)
# Set the username & hash the password
user_object.username = user_object.username.lower()
user_object.password = await security_hash_password(user_object.password)
# Get org_id from org_slug
orgs = request.app.db["organizations"]
# Check if the org exists
isOrgExists = await orgs.find_one({"slug": org_slug})
# If the org does not exist, raise an error
if not isOrgExists:
if result.first():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="You are trying to create a user in an organization that does not exist",
status_code=400,
detail="Email already exists",
)
org_id = isOrgExists["org_id"]
# Exclude unset values
user_data = user.dict(exclude_unset=True)
for key, value in user_data.items():
setattr(user, key, value)
# Create initial orgs list with the org_id passed in
orgs = [UserOrganization(org_id=org_id, org_role="owner")]
# Add user to database
db_session.add(user)
db_session.commit()
db_session.refresh(user)
# Give role
roles = [UserRolesInOrganization(role_id="role_admin", org_id=org_id)]
# Create the user
user = UserInDB(
user_id=user_id,
# get org id
statement = select(Organization).where(Organization.slug == org_slug)
org = db_session.exec(statement)
org = org.first()
org_id = org.id if org else 0
# Link user and organization
user_organization = UserOrganization(
user_id=user.id if user.id else 0,
org_id=org_id or 0,
role_id=1,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
orgs=orgs,
roles=roles,
**user_object.dict(),
)
# Insert the user into the database
await users.insert_one(user.dict())
db_session.add(user_organization)
db_session.commit()
db_session.refresh(user_organization)
return User(**user.dict())
user = UserRead.from_orm(user)
async def create_sample_data(org_slug: str, username: str, request: Request):
Faker(["en_US"])
fake_multilang = Faker(
["en_US", "de_DE", "ja_JP", "es_ES", "it_IT", "pt_BR", "ar_PS"]
)
users = request.app.db["users"]
orgs = request.app.db["organizations"]
user = await users.find_one({"username": username})
org = await orgs.find_one({"slug": org_slug.lower()})
user_id = user["user_id"]
org_id = org["org_id"]
current_user = PublicUser(**user)
for i in range(0, 5):
# get image in BinaryIO format from unsplash and save it to disk
image = requests.get("https://source.unsplash.com/random/800x600")
with open("thumbnail.jpg", "wb") as f:
f.write(image.content)
course_id = f"course_{uuid4()}"
course = CourseInDB(
name=fake_multilang.unique.sentence(),
description=fake_multilang.unique.text(),
mini_description=fake_multilang.unique.text(),
thumbnail="thumbnail",
org_id=org_id,
learnings=[fake_multilang.unique.sentence() for i in range(0, 5)],
public=True,
chapters=[],
course_id=course_id,
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
authors=[user_id],
chapters_content=[],
)
courses = request.app.db["courses"]
course = CourseInDB(**course.dict())
await courses.insert_one(course.dict())
# create chapters
for i in range(0, 5):
coursechapter = CourseChapter(
name=fake_multilang.unique.sentence(),
description=fake_multilang.unique.text(),
activities=[],
)
coursechapter = await create_coursechapter(
request, coursechapter, course_id, current_user
)
if coursechapter:
# create activities
for i in range(0, 5):
activity = Activity(
name=fake_multilang.unique.sentence(),
type="dynamic",
content={},
)
activity = await create_activity(
request,
activity,
org_id,
coursechapter["coursechapter_id"],
current_user,
)
return user

View file

@ -3,13 +3,13 @@ from uuid import uuid4
from src.services.utils.upload_content import upload_content
async def upload_org_logo(logo_file, org_id):
async def upload_org_logo(logo_file, org_uuid):
contents = logo_file.file.read()
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
await upload_content(
"logos",
org_id,
org_uuid,
contents,
name_in_disk,
)

View file

@ -1,230 +1,287 @@
import json
from datetime import datetime
from typing import Literal
from uuid import uuid4
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.services.orgs.logos import upload_org_logo
from src.services.orgs.schemas.orgs import (
from src.db.users import AnonymousUser, PublicUser
from src.db.user_organizations import UserOrganization
from src.db.organizations import (
Organization,
OrganizationInDB,
PublicOrganization,
OrganizationCreate,
OrganizationRead,
OrganizationUpdate,
)
from src.services.users.schemas.users import UserOrganization
from src.services.users.users import PublicUser
from src.services.orgs.logos import upload_org_logo
from fastapi import HTTPException, UploadFile, status, Request
async def get_organization(request: Request, org_id: str):
orgs = request.app.db["organizations"]
async def get_organization(
request: Request,
org_id: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = await orgs.find_one({"org_id": org_id})
org = result.first()
if not org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
status_code=404,
detail="Organization not found",
)
org = PublicOrganization(**org)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
org = OrganizationRead.from_orm(org)
return org
async def get_organization_by_slug(request: Request, org_slug: str):
orgs = request.app.db["organizations"]
async def get_organization_by_slug(
request: Request,
org_slug: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
):
statement = select(Organization).where(Organization.slug == org_slug)
result = db_session.exec(statement)
org = await orgs.find_one({"slug": org_slug})
org = result.first()
if not org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
status_code=404,
detail="Organization not found",
)
org = PublicOrganization(**org)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "read", db_session)
org = OrganizationRead.from_orm(org)
return org
async def create_org(
request: Request, org_object: Organization, current_user: PublicUser
request: Request,
org_object: OrganizationCreate,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
orgs = request.app.db["organizations"]
user = request.app.db["users"]
statement = select(Organization).where(Organization.slug == org_object.slug)
result = db_session.exec(statement)
# find if org already exists using name
isOrgAvailable = await orgs.find_one({"slug": org_object.slug})
org = result.first()
if isOrgAvailable:
if org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Organization already exists",
)
org = Organization.from_orm(org_object)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "create", db_session)
# Complete the org object
org.org_uuid = f"org_{uuid4()}"
org.creation_date = str(datetime.now())
org.update_date = str(datetime.now())
db_session.add(org)
db_session.commit()
db_session.refresh(org)
# Link user to org
user_org = UserOrganization(
user_id=int(current_user.id),
org_id=int(org.id if org.id else 0),
role_id=1,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(user_org)
db_session.commit()
db_session.refresh(user_org)
return OrganizationRead.from_orm(org)
async def update_org(
request: Request,
org_object: OrganizationUpdate,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization slug not found",
)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# Verify if the new slug is already in use
statement = select(Organization).where(Organization.slug == org_object.slug)
result = db_session.exec(statement)
slug_available = result.first()
if slug_available and slug_available.id != org_id:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Organization slug already exists",
)
# generate org_id with uuid4
org_id = str(f"org_{uuid4()}")
# Update only the fields that were passed in
for var, value in vars(org_object).items():
if value is not None:
setattr(org, var, value)
# force lowercase slug
org_object.slug = org_object.slug.lower()
# Complete the org object
org.update_date = str(datetime.now())
org = OrganizationInDB(
org_id=org_id, **org_object.dict()
)
db_session.add(org)
db_session.commit()
db_session.refresh(org)
org_in_db = await orgs.insert_one(org.dict())
org = OrganizationRead.from_orm(org)
user_organization: UserOrganization = UserOrganization(
org_id=org_id, org_role="owner"
)
# add org to user
await user.update_one(
{"user_id": current_user.user_id},
{"$addToSet": {"orgs": user_organization.dict()}},
)
# add role admin to org
await user.update_one(
{"user_id": current_user.user_id},
{"$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}},
)
if not org_in_db:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
)
return org.dict()
async def update_org(
request: Request, org_object: Organization, org_id: str, current_user: PublicUser
):
# verify org rights
await verify_org_rights(request, org_id, current_user, "update")
orgs = request.app.db["organizations"]
await orgs.find_one({"org_id": org_id})
updated_org = OrganizationInDB(org_id=org_id, **org_object.dict())
# update org
await orgs.update_one({"org_id": org_id}, {"$set": updated_org.dict()})
return updated_org.dict()
return org
async def update_org_logo(
request: Request, logo_file: UploadFile, org_id: str, current_user: PublicUser
request: Request,
logo_file: UploadFile,
org_id: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
# verify org rights
await verify_org_rights(request, org_id, current_user, "update")
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
orgs = request.app.db["organizations"]
org = result.first()
await orgs.find_one({"org_id": org_id})
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
name_in_disk = await upload_org_logo(logo_file, org_id)
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "update", db_session)
# update org
await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}})
# Upload logo
name_in_disk = await upload_org_logo(logo_file, org.org_uuid)
# Update org
org.logo_image = name_in_disk
# Complete the org object
org.update_date = str(datetime.now())
db_session.add(org)
db_session.commit()
db_session.refresh(org)
return {"detail": "Logo updated"}
async def delete_org(request: Request, org_id: str, current_user: PublicUser):
await verify_org_rights(request, org_id, current_user, "delete")
async def delete_org(
request: Request,
org_id: int,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
orgs = request.app.db["organizations"]
org = await orgs.find_one({"org_id": org_id})
org = result.first()
if not org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
status_code=404,
detail="Organization not found",
)
isDeleted = await orgs.delete_one({"org_id": org_id})
# RBAC check
await rbac_check(request, org.org_uuid, current_user, "delete", db_session)
# remove org from all users
users = request.app.db["users"]
await users.update_many({}, {"$pull": {"orgs": {"org_id": org_id}}})
db_session.delete(org)
db_session.commit()
if isDeleted:
return {"detail": "Org deleted"}
else:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Unavailable database",
)
# Delete links to org
statement = select(UserOrganization).where(UserOrganization.org_id == org_id)
result = db_session.exec(statement)
user_orgs = result.all()
for user_org in user_orgs:
db_session.delete(user_org)
db_session.commit()
db_session.refresh(org)
return {"detail": "Organization deleted"}
async def get_orgs_by_user(
request: Request, user_id: str, page: int = 1, limit: int = 10
):
orgs = request.app.db["organizations"]
user = request.app.db["users"]
if user_id == "anonymous":
# raise error
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User not logged in"
request: Request,
db_session: Session,
user_id: str,
page: int = 1,
limit: int = 10,
) -> list[Organization]:
statement = (
select(Organization)
.join(UserOrganization)
.where(
Organization.id == UserOrganization.org_id,
UserOrganization.user_id == user_id,
)
# get user orgs
user_orgs = await user.find_one({"user_id": user_id})
org_ids: list[UserOrganization] = []
for org in user_orgs["orgs"]:
if (
org["org_role"] == "owner"
or org["org_role"] == "editor"
or org["org_role"] == "member"
):
org_ids.append(org["org_id"])
# find all orgs where org_id is in org_ids array
all_orgs = (
orgs.find({"org_id": {"$in": org_ids}})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(100)
)
result = db_session.exec(statement)
return [
json.loads(json.dumps(org, default=str))
for org in await all_orgs.to_list(length=100)
]
orgs = result.all()
return orgs
#### Security ####################################################
## 🔒 RBAC Utils ##
async def verify_org_rights(
async def rbac_check(
request: Request,
org_id: str,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
db_session: Session,
):
orgs = request.app.db["organizations"]
users = request.app.db["users"]
# Organizations are readable by anyone
if action == "read":
return True
user = await users.find_one({"user_id": current_user.user_id})
else:
await authorization_verify_if_user_is_anon(current_user.id)
org = await orgs.find_one({"org_id": org_id})
if not org:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
)
await authorization_verify_if_user_is_anon(current_user.user_id)
await authorization_verify_based_on_roles(
request, current_user.user_id, action, user["roles"], org_id
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, org_id, db_session
)
#### Security ####################################################
## 🔒 RBAC Utils ##

View file

@ -1,28 +0,0 @@
from typing import Optional
from pydantic import BaseModel
#### Classes ####################################################
class Organization(BaseModel):
name: str
description: str
email: str
slug: str
logo: Optional[str]
default: Optional[bool] = False
class OrganizationInDB(Organization):
org_id: str
class PublicOrganization(Organization):
name: str
description: str
email: str
slug: str
org_id: str
def __getitem__(self, item):
return getattr(self, item)

View file

@ -1,127 +1,141 @@
from typing import Literal
from uuid import uuid4
from src.security.rbac.rbac import authorization_verify_if_user_is_anon
from src.services.roles.schemas.roles import Role, RoleInDB
from src.services.users.schemas.users import PublicUser
from fastapi import HTTPException, status, Request
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.db.users import AnonymousUser, PublicUser
from src.db.roles import Role, RoleCreate, RoleRead, RoleUpdate
from fastapi import HTTPException, Request
from datetime import datetime
async def create_role(request: Request, role_object: Role, current_user: PublicUser):
roles = request.app.db["roles"]
async def create_role(
request: Request,
db_session: Session,
role_object: RoleCreate,
current_user: PublicUser,
):
role = Role.from_orm(role_object)
await verify_user_permissions_on_roles(request, current_user, "create", None)
# RBAC check
await rbac_check(request, current_user, "create", "role_xxx", db_session)
# create the role object in the database and return the object
role_id = "role_" + str(uuid4())
# Complete the role object
role.role_uuid = f"role_{uuid4()}"
role.creation_date = str(datetime.now())
role.update_date = str(datetime.now())
role = RoleInDB(
role_id=role_id,
created_at=str(datetime.now()),
updated_at=str(datetime.now()),
**role_object.dict()
)
db_session.add(role)
db_session.commit()
db_session.refresh(role)
await roles.insert_one(role.dict())
role = RoleRead(**role.dict())
return role
async def read_role(request: Request, role_id: str, current_user: PublicUser):
roles = request.app.db["roles"]
async def read_role(
request: Request, db_session: Session, role_id: str, current_user: PublicUser
):
statement = select(Role).where(Role.id == role_id)
result = db_session.exec(statement)
await verify_user_permissions_on_roles(request, current_user, "read", role_id)
role = result.first()
role = RoleInDB(**await roles.find_one({"role_id": role_id}))
if not role:
raise HTTPException(
status_code=404,
detail="Role not found",
)
# RBAC check
await rbac_check(request, current_user, "read", role.role_uuid, db_session)
role = RoleRead(**role.dict())
return role
async def update_role(
request: Request, role_id: str, role_object: Role, current_user: PublicUser
):
roles = request.app.db["roles"]
await verify_user_permissions_on_roles(request, current_user, "update", role_id)
role_object.updated_at = datetime.now()
# Update the role object in the database and return the object
updated_role = RoleInDB(
**await roles.find_one_and_update(
{"role_id": role_id}, {"$set": role_object.dict()}, return_document=True
)
)
return updated_role
async def delete_role(request: Request, role_id: str, current_user: PublicUser):
roles = request.app.db["roles"]
await verify_user_permissions_on_roles(request, current_user, "delete", role_id)
# Delete the role object in the database and return the object
deleted_role = RoleInDB(**await roles.find_one_and_delete({"role_id": role_id}))
return deleted_role
#### Security ####################################################
async def verify_user_permissions_on_roles(
request: Request,
db_session: Session,
role_object: RoleUpdate,
current_user: PublicUser,
action: Literal["create", "read", "update", "delete"],
role_id: str | None,
):
request.app.db["users"]
roles = request.app.db["roles"]
statement = select(Role).where(Role.id == role_object.role_id)
result = db_session.exec(statement)
# If current user is not authenticated
role = result.first()
if not current_user:
if not role:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Roles : Not authenticated"
status_code=404,
detail="Role not found",
)
await authorization_verify_if_user_is_anon(current_user.user_id)
# RBAC check
await rbac_check(request, current_user, "update", role.role_uuid, db_session)
if action == "create":
if "owner" in [org.org_role for org in current_user.orgs]:
return True
# Complete the role object
role.update_date = str(datetime.now())
if role_id is not None:
role = RoleInDB(**await roles.find_one({"role_id": role_id}))
# Remove the role_id from the role_object
del role_object.role_id
if action == "read":
if "owner" in [org.org_role for org in current_user.orgs]:
return True
# Update only the fields that were passed in
for var, value in vars(role_object).items():
if value is not None:
setattr(role, var, value)
for org in current_user.orgs:
if org.org_id == role.org_id:
return True
db_session.add(role)
db_session.commit()
db_session.refresh(role)
if action == "update":
for org in current_user.orgs:
# If the user is an owner of the organization
if org.org_id == role.org_id:
if org.org_role == "owner" or org.org_role == "editor":
return True
# Can't update a global role
if role.org_id == "*":
return False
role = RoleRead(**role.dict())
if action == "delete":
for org in current_user.orgs:
# If the user is an owner of the organization
if org.org_id == role.org_id:
if org.org_role == "owner":
return True
# Can't delete a global role
if role.org_id == "*":
return False
return role
#### Security ####################################################
async def delete_role(
request: Request, db_session: Session, role_id: str, current_user: PublicUser
):
# RBAC check
await rbac_check(request, current_user, "delete", role_id, db_session)
statement = select(Role).where(Role.id == role_id)
result = db_session.exec(statement)
role = result.first()
if not role:
raise HTTPException(
status_code=404,
detail="Role not found",
)
db_session.delete(role)
db_session.commit()
return "Role deleted"
## 🔒 RBAC Utils ##
async def rbac_check(
request: Request,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
role_uuid: str,
db_session: Session,
):
await authorization_verify_if_user_is_anon(current_user.id)
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, role_uuid, db_session
)
## 🔒 RBAC Utils ##

View file

@ -1,41 +0,0 @@
from typing import Literal
from pydantic import BaseModel
# Database Models
class Permission(BaseModel):
action_create: bool
action_read: bool
action_update: bool
action_delete: bool
def __getitem__(self, item):
return getattr(self, item)
class Elements(BaseModel):
courses: Permission
users: Permission
houses: Permission
collections: Permission
organizations: Permission
coursechapters: Permission
activities: Permission
def __getitem__(self, item):
return getattr(self, item)
class Role(BaseModel):
name: str
description: str
elements : Elements
org_id: str | Literal["*"]
class RoleInDB(Role):
role_id: str
created_at: str
updated_at: str

View file

@ -1,286 +1,423 @@
from datetime import datetime
from typing import List, Literal, Optional
from uuid import uuid4
from src.db.chapter_activities import ChapterActivity
from fastapi import HTTPException, Request, status
from pydantic import BaseModel
from src.services.courses.chapters import get_coursechapters_meta
from src.services.orgs.orgs import PublicOrganization
from src.services.users.users import PublicUser
#### Classes ####################################################
from sqlmodel import Session, select
from src.db.activities import Activity
from src.db.courses import Course
from src.db.trail_runs import TrailRun, TrailRunRead
from src.db.trail_steps import TrailStep
from src.db.trails import Trail, TrailCreate, TrailRead
from src.db.users import PublicUser
class ActivityData(BaseModel):
activity_id: str
activity_type: str
data: Optional[dict]
class TrailCourse(BaseModel):
course_id: str
elements_type: Optional[Literal["course"]] = "course"
status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
course_object: dict
masked: Optional[bool] = False
activities_marked_complete: Optional[List[str]]
activities_data: Optional[List[ActivityData]]
progress: Optional[int]
class Trail(BaseModel):
status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
masked: Optional[bool] = False
courses: Optional[List[TrailCourse]]
class TrailInDB(Trail):
trail_id: str
org_id: str
user_id: str
creationDate: str = datetime.now().isoformat()
updateDate: str = datetime.now().isoformat()
#### Classes ####################################################
async def create_trail(
request: Request, user: PublicUser, org_id: str, trail_object: Trail
async def create_user_trail(
request: Request,
user: PublicUser,
trail_object: TrailCreate,
db_session: Session,
) -> Trail:
trails = request.app.db["trails"]
statement = select(Trail).where(Trail.org_id == trail_object.org_id)
trail = db_session.exec(statement).first()
# get list of courses
if trail_object.courses:
courses = trail_object.courses
# get course ids
course_ids = [course.course_id for course in courses]
# find if the user has already started the course
existing_trail = await trails.find_one(
{"user_id": user.user_id, "courses.course_id": {"$in": course_ids}}
)
if existing_trail:
# update the status of the element with the matching course_id to "ongoing"
for element in existing_trail["courses"]:
if element["course_id"] in course_ids:
element["status"] = "ongoing"
# update the existing trail in the database
await trails.replace_one(
{"trail_id": existing_trail["trail_id"]}, existing_trail
if trail:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Trail already exists",
)
# create trail id
trail_id = f"trail_{uuid4()}"
trail = Trail.from_orm(trail_object)
trail.creation_date = str(datetime.now())
trail.update_date = str(datetime.now())
trail.org_id = trail_object.org_id
trail.trail_uuid = str(f"trail_{uuid4()}")
# create trail
trail = TrailInDB(
**trail_object.dict(), trail_id=trail_id, user_id=user.user_id, org_id=org_id
)
await trails.insert_one(trail.dict())
db_session.add(trail)
db_session.commit()
db_session.refresh(trail)
return trail
async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> Trail:
trails = request.app.db["trails"]
trail = await trails.find_one({"user_id": user.user_id})
async def get_user_trails(
request: Request,
user: PublicUser,
db_session: Session,
) -> TrailRead:
statement = select(Trail).where(Trail.user_id == user.id)
trail = db_session.exec(statement).first()
if not trail:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
)
for element in trail["courses"]:
course_id = element["course_id"]
chapters_meta = await get_coursechapters_meta(request, course_id, user)
activities = chapters_meta["activities"]
num_activities = len(activities)
num_completed_activities = len(element.get("activities_marked_complete", []))
element["progress"] = (
round((num_completed_activities / num_activities) * 100, 2)
if num_activities > 0
else 0
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
# Add course object and total activities in a course to trail runs
for trail_run in trail_runs:
statement = select(Course).where(Course.id == trail_run.course_id)
course = db_session.exec(statement).first()
trail_run.course = course
# Add number of activities (steps) in a course
statement = select(ChapterActivity).where(
ChapterActivity.course_id == trail_run.course_id
)
course_total_steps = db_session.exec(statement)
# count number of activities in a this list
trail_run.course_total_steps = len(course_total_steps.all())
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
return Trail(**trail)
return trail_read
async def get_user_trail_with_orgslug(
request: Request, user: PublicUser, org_slug: str
) -> Trail:
trails = request.app.db["trails"]
orgs = request.app.db["organizations"]
courses_mongo = request.app.db["courses"]
# get org_id from orgslug
org = await orgs.find_one({"slug": org_slug})
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
async def check_trail_presence(
org_id: int,
user_id: int,
request: Request,
user: PublicUser,
db_session: Session,
):
statement = select(Trail).where(Trail.org_id == org_id, Trail.user_id == user.id)
trail = db_session.exec(statement).first()
if not trail:
return Trail(masked=False, courses=[])
trail = await create_user_trail(
request,
user,
TrailCreate(
org_id=org_id,
user_id=user.id,
),
db_session,
)
return trail
course_ids = [course["course_id"] for course in trail["courses"]]
return trail
live_courses = await courses_mongo.find({"course_id": {"$in": course_ids}}).to_list(
length=None
async def get_user_trail_with_orgid(
request: Request, user: PublicUser, org_id: int, db_session: Session
) -> TrailRead:
trail = await check_trail_presence(
org_id=org_id,
user_id=user.id,
request=request,
user=user,
db_session=db_session,
)
for course in trail["courses"]:
course_id = course["course_id"]
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
if course_id not in [course["course_id"] for course in live_courses]:
course["masked"] = True
continue
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
chapters_meta = await get_coursechapters_meta(request, course_id, user)
activities = chapters_meta["activities"]
# Add course object and total activities in a course to trail runs
for trail_run in trail_runs:
statement = select(Course).where(Course.id == trail_run.course_id)
course = db_session.exec(statement).first()
trail_run.course = course
# get course object without _id
course_object = await courses_mongo.find_one(
{"course_id": course_id}, {"_id": 0}
# Add number of activities (steps) in a course
statement = select(ChapterActivity).where(
ChapterActivity.course_id == trail_run.course_id
)
course_total_steps = db_session.exec(statement)
# count number of activities in a this list
trail_run.course_total_steps = len(course_total_steps.all())
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
course["course_object"] = course_object
num_activities = len(activities)
num_completed_activities = len(course.get("activities_marked_complete", []))
course["progress"] = (
round((num_completed_activities / num_activities) * 100, 2)
if num_activities > 0
else 0
)
return Trail(**trail)
return trail_read
async def add_activity_to_trail(
request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str
) -> Trail:
trails = request.app.db["trails"]
orgs = request.app.db["organizations"]
courseid = "course_" + course_id
activityid = "activity_" + activity_id
request: Request,
user: PublicUser,
activity_uuid: str,
db_session: Session,
) -> TrailRead:
# Look for the activity
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
activity = db_session.exec(statement).first()
# get org_id from orgslug
org = await orgs.find_one({"slug": org_slug})
org_id = org["org_id"]
# find a trail with the user_id and course_id in the courses array
trail = await trails.find_one(
{"user_id": user.user_id, "courses.course_id": courseid, "org_id": org_id}
)
if user.user_id == "anonymous":
if not activity:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Anonymous users cannot add activity to trail",
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
)
if not trail:
return Trail(masked=False, courses=[])
statement = select(Course).where(Course.id == activity.course_id)
course = db_session.exec(statement).first()
# if a trail has course_id in the courses array, then add the activity_id to the activities_marked_complete array
for element in trail["courses"]:
if element["course_id"] == courseid:
if "activities_marked_complete" in element:
# check if activity_id is already in the array
if activityid not in element["activities_marked_complete"]:
element["activities_marked_complete"].append(activityid)
else:
if not course:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Activity already marked complete",
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
else:
element["activities_marked_complete"] = [activity_id]
# modify trail object
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
trail = await check_trail_presence(
org_id=course.org_id,
user_id=user.id,
request=request,
user=user,
db_session=db_session,
)
return Trail(**trail)
statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
)
trailrun = db_session.exec(statement).first()
if not trailrun:
trailrun = TrailRun(
trail_id=trail.id if trail.id is not None else 0,
course_id=course.id if course.id is not None else 0,
org_id=course.org_id,
user_id=user.id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(trailrun)
db_session.commit()
db_session.refresh(trailrun)
statement = select(TrailStep).where(
TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity.id
)
trailstep = db_session.exec(statement).first()
if not trailstep:
trailstep = TrailStep(
trailrun_id=trailrun.id if trailrun.id is not None else 0,
activity_id=activity.id if activity.id is not None else 0,
course_id=course.id if course.id is not None else 0,
trail_id=trail.id if trail.id is not None else 0,
org_id=course.org_id,
complete=False,
teacher_verified=False,
grade="",
user_id=user.id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(trailstep)
db_session.commit()
db_session.refresh(trailstep)
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
return trail_read
async def add_course_to_trail(
request: Request, user: PublicUser, orgslug: str, course_id: str
) -> Trail:
trails = request.app.db["trails"]
orgs = request.app.db["organizations"]
request: Request,
user: PublicUser,
course_uuid: str,
db_session: Session,
) -> TrailRead:
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if user.user_id == "anonymous":
if not course:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Anonymous users cannot add activity to trail",
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
org = await orgs.find_one({"slug": orgslug})
# check if run already exists
statement = select(TrailRun).where(TrailRun.course_id == course.id)
trailrun = db_session.exec(statement).first()
org = PublicOrganization(**org)
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
if not trail:
trail_to_insert = TrailInDB(
trail_id=f"trail_{uuid4()}",
user_id=user.user_id,
org_id=org["org_id"],
courses=[],
)
trail_to_insert = await trails.insert_one(trail_to_insert.dict())
trail = await trails.find_one({"_id": trail_to_insert.inserted_id})
# check if course is already present in the trail
for element in trail["courses"]:
if element["course_id"] == course_id:
if trailrun:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Course already present in the trail",
status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists"
)
updated_trail = TrailCourse(
course_id=course_id,
activities_data=[],
activities_marked_complete=[],
progress=0,
course_object={},
status="ongoing",
masked=False,
statement = select(Trail).where(
Trail.org_id == course.org_id, Trail.user_id == user.id
)
trail["courses"].append(updated_trail.dict())
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
return Trail(**trail)
async def remove_course_from_trail(
request: Request, user: PublicUser, orgslug: str, course_id: str
) -> Trail:
trails = request.app.db["trails"]
orgs = request.app.db["organizations"]
if user.user_id == "anonymous":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Anonymous users cannot add activity to trail",
)
org = await orgs.find_one({"slug": orgslug})
org = PublicOrganization(**org)
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
trail = db_session.exec(statement).first()
if not trail:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
)
# check if course is already present in the trail
statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
)
trail_run = db_session.exec(statement).first()
for element in trail["courses"]:
if element["course_id"] == course_id:
trail["courses"].remove(element)
break
if not trail_run:
trail_run = TrailRun(
trail_id=trail.id if trail.id is not None else 0,
course_id=course.id if course.id is not None else 0,
org_id=course.org_id,
user_id=user.id,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(trail_run)
db_session.commit()
db_session.refresh(trail_run)
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
return Trail(**trail)
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
return trail_read
async def remove_course_from_trail(
request: Request,
user: PublicUser,
course_uuid: str,
db_session: Session,
) -> TrailRead:
statement = select(Course).where(Course.course_uuid == course_uuid)
course = db_session.exec(statement).first()
if not course:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
statement = select(Trail).where(
Trail.org_id == course.org_id, Trail.user_id == user.id
)
trail = db_session.exec(statement).first()
if not trail:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
)
statement = select(TrailRun).where(
TrailRun.trail_id == trail.id, TrailRun.course_id == course.id
)
trail_run = db_session.exec(statement).first()
if trail_run:
db_session.delete(trail_run)
db_session.commit()
# Delete all trail steps for this course
statement = select(TrailStep).where(TrailStep.course_id == course.id)
trail_steps = db_session.exec(statement).all()
for trail_step in trail_steps:
db_session.delete(trail_step)
db_session.commit()
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
trail_runs = [
TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0)
for trail_run in trail_runs
]
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps]
trail_run.steps = trail_steps
for trail_step in trail_steps:
statement = select(Course).where(Course.id == trail_step.course_id)
course = db_session.exec(statement).first()
trail_step.data = dict(course=course)
trail_read = TrailRead(
**trail.dict(),
runs=trail_runs,
)
return trail_read

View file

@ -1,70 +0,0 @@
from typing import Literal
from pydantic import BaseModel
class UserOrganization(BaseModel):
org_id: str
org_role: Literal['owner', 'editor', 'member']
def __getitem__(self, item):
return getattr(self, item)
class UserRolesInOrganization(BaseModel):
org_id: str
role_id: str
def __getitem__(self, item):
return getattr(self, item)
class User(BaseModel):
username: str
email: str
full_name: str | None = None
avatar_url: str | None = None
bio: str | None = None
class UserWithPassword(User):
password: str
class UserInDB(User):
user_id: str
password: str
verified: bool | None = False
disabled: bool | None = False
orgs: list[UserOrganization] = []
roles: list[UserRolesInOrganization] = []
creation_date: str
update_date: str
def __getitem__(self, item):
return getattr(self, item)
class PublicUser(User):
user_id: str
orgs: list[UserOrganization] = []
roles: list[UserRolesInOrganization] = []
creation_date: str
update_date: str
class AnonymousUser(BaseModel):
user_id: str = "anonymous"
username: str = "anonymous"
roles: list[UserRolesInOrganization] = [
UserRolesInOrganization(org_id="anonymous", role_id="role_anonymous")
]
# Forms ####################################################
class PasswordChangeForm(BaseModel):
old_password: str
new_password: str

View file

@ -2,214 +2,347 @@ from datetime import datetime
from typing import Literal
from uuid import uuid4
from fastapi import HTTPException, Request, status
from sqlmodel import Session, select
from src.security.rbac.rbac import (
authorization_verify_based_on_roles,
authorization_verify_based_on_roles_and_authorship,
authorization_verify_if_user_is_anon,
)
from src.security.security import security_hash_password, security_verify_password
from src.services.users.schemas.users import (
PasswordChangeForm,
from src.db.organizations import Organization
from src.db.users import (
AnonymousUser,
PublicUser,
User,
UserOrganization,
UserRolesInOrganization,
UserWithPassword,
UserInDB,
UserCreate,
UserRead,
UserUpdate,
UserUpdatePassword,
)
from src.db.user_organizations import UserOrganization
from src.security.security import security_hash_password, security_verify_password
async def create_user(
request: Request,
current_user: PublicUser | None,
user_object: UserWithPassword,
org_slug: str,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_object: UserCreate,
org_id: int,
):
users = request.app.db["users"]
user = User.from_orm(user_object)
isUsernameAvailable = await users.find_one({"username": user_object.username})
isEmailAvailable = await users.find_one({"email": user_object.email})
# RBAC check
await rbac_check(request, current_user, "create", "user_x", db_session)
if isUsernameAvailable:
# Complete the user object
user.user_uuid = f"user_{uuid4()}"
user.password = await security_hash_password(user_object.password)
user.email_verified = False
user.creation_date = str(datetime.now())
user.update_date = str(datetime.now())
# Verifications
# Check if Organization exists
statement = select(Organization).where(Organization.id == org_id)
result = db_session.exec(statement)
if not result.first():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Username already exists"
status_code=400,
detail="Organization does not exist",
)
if isEmailAvailable:
# Username
statement = select(User).where(User.username == user.username)
result = db_session.exec(statement)
if result.first():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Email already exists"
status_code=400,
detail="Username already exists",
)
# Generate user_id with uuid4
user_id = str(f"user_{uuid4()}")
# Email
statement = select(User).where(User.email == user.email)
result = db_session.exec(statement)
# Check if the requesting user is authenticated
if current_user is not None:
# Verify rights
await verify_user_rights_on_user(request, current_user, "create", user_id)
# Set the username & hash the password
user_object.username = user_object.username.lower()
user_object.password = await security_hash_password(user_object.password)
# Get org_id from org_slug
orgs = request.app.db["organizations"]
# Check if the org exists
isOrgExists = await orgs.find_one({"slug": org_slug})
# If the org does not exist, raise an error
if not isOrgExists and (org_slug != "None"):
if result.first():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="You are trying to create a user in an organization that does not exist",
status_code=400,
detail="Email already exists",
)
org_id = isOrgExists["org_id"] if org_slug != "None" else ''
# Exclude unset values
user_data = user.dict(exclude_unset=True)
for key, value in user_data.items():
setattr(user, key, value)
# Create initial orgs list with the org_id passed in
orgs = (
[UserOrganization(org_id=org_id, org_role="member")]
if org_slug != "None"
else []
)
# Add user to database
db_session.add(user)
db_session.commit()
db_session.refresh(user)
# Give role
roles = (
[UserRolesInOrganization(role_id="role_member", org_id=org_id)]
if org_slug != "None"
else []
)
# Create the user
user = UserInDB(
user_id=user_id,
# Link user and organization
user_organization = UserOrganization(
user_id=user.id if user.id else 0,
org_id=int(org_id),
role_id=3,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
orgs=orgs,
roles=roles,
**user_object.dict(),
)
# Insert the user into the database
await users.insert_one(user.dict())
db_session.add(user_organization)
db_session.commit()
db_session.refresh(user_organization)
return User(**user.dict())
user = UserRead.from_orm(user)
return user
async def read_user(request: Request, current_user: PublicUser, user_id: str):
users = request.app.db["users"]
async def create_user_without_org(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_object: UserCreate,
):
user = User.from_orm(user_object)
# Check if the user exists
isUserExists = await users.find_one({"user_id": user_id})
# RBAC check
await rbac_check(request, current_user, "create", "user_x", db_session)
# Verify rights
await verify_user_rights_on_user(request, current_user, "read", user_id)
# Complete the user object
user.user_uuid = f"user_{uuid4()}"
user.password = await security_hash_password(user_object.password)
user.email_verified = False
user.creation_date = str(datetime.now())
user.update_date = str(datetime.now())
# If the user does not exist, raise an error
if not isUserExists:
# Verifications
# Username
statement = select(User).where(User.username == user.username)
result = db_session.exec(statement)
if result.first():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
status_code=400,
detail="Username already exists",
)
return User(**isUserExists)
# Email
statement = select(User).where(User.email == user.email)
result = db_session.exec(statement)
if result.first():
raise HTTPException(
status_code=400,
detail="Email already exists",
)
# Exclude unset values
user_data = user.dict(exclude_unset=True)
for key, value in user_data.items():
setattr(user, key, value)
# Add user to database
db_session.add(user)
db_session.commit()
db_session.refresh(user)
user = UserRead.from_orm(user)
return user
async def update_user(
request: Request, user_id: str, user_object: User, current_user: PublicUser
request: Request,
db_session: Session,
user_id: int,
current_user: PublicUser | AnonymousUser,
user_object: UserUpdate,
):
users = request.app.db["users"]
# Get user
statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first()
# Verify rights
await verify_user_rights_on_user(request, current_user, "update", user_id)
isUserExists = await users.find_one({"user_id": user_id})
isUsernameAvailable = await users.find_one({"username": user_object.username})
isEmailAvailable = await users.find_one({"email": user_object.email})
if not isUserExists:
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
status_code=400,
detail="User does not exist",
)
# okay if username is not changed
if isUserExists["username"] == user_object.username:
user_object.username = user_object.username.lower()
# RBAC check
await rbac_check(request, current_user, "update", user.user_uuid, db_session)
else:
if isUsernameAvailable:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Username already used"
)
# Update user
user_data = user_object.dict(exclude_unset=True)
for key, value in user_data.items():
setattr(user, key, value)
if isEmailAvailable:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Email already used"
)
user.update_date = str(datetime.now())
updated_user = {"$set": user_object.dict()}
users.update_one({"user_id": user_id}, updated_user)
# Update user in database
db_session.add(user)
db_session.commit()
db_session.refresh(user)
return User(**user_object.dict())
user = UserRead.from_orm(user)
return user
async def update_user_password(
request: Request,
current_user: PublicUser,
user_id: str,
password_change_form: PasswordChangeForm,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_id: int,
form: UserUpdatePassword,
):
users = request.app.db["users"]
# Get user
statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first()
isUserExists = await users.find_one({"user_id": user_id})
# Verify rights
await verify_user_rights_on_user(request, current_user, "update", user_id)
if not isUserExists:
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
status_code=400,
detail="User does not exist",
)
if not await security_verify_password(
password_change_form.old_password, isUserExists["password"]
):
# RBAC check
await rbac_check(request, current_user, "update", user.user_uuid, db_session)
if not await security_verify_password(form.old_password, user.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password"
)
new_password = await security_hash_password(password_change_form.new_password)
# Update user
user.password = await security_hash_password(form.new_password)
user.update_date = str(datetime.now())
updated_user = {"$set": {"password": new_password}}
await users.update_one({"user_id": user_id}, updated_user)
# Update user in database
db_session.add(user)
db_session.commit()
db_session.refresh(user)
return {"detail": "Password updated"}
user = UserRead.from_orm(user)
return user
async def delete_user(request: Request, current_user: PublicUser, user_id: str):
users = request.app.db["users"]
async def read_user_by_id(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_id: int,
):
# Get user
statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first()
isUserExists = await users.find_one({"user_id": user_id})
# Verify is user has permission to delete the user
await verify_user_rights_on_user(request, current_user, "delete", user_id)
if not isUserExists:
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
status_code=400,
detail="User does not exist",
)
await users.delete_one({"user_id": user_id})
# RBAC check
await rbac_check(request, current_user, "read", user.user_uuid, db_session)
return {"detail": "User deleted"}
user = UserRead.from_orm(user)
return user
async def read_user_by_uuid(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_uuid: str,
):
# Get user
statement = select(User).where(User.user_uuid == user_uuid)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "read", user.user_uuid, db_session)
user = UserRead.from_orm(user)
return user
async def authorize_user_action(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
ressource_uuid: str,
action: Literal["create", "read", "update", "delete"],
):
# Get user
statement = select(User).where(User.user_uuid == current_user.user_uuid)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
# RBAC check
authorized = await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, ressource_uuid, db_session
)
if authorized:
return True
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You are not authorized to perform this action",
)
async def delete_user_by_id(
request: Request,
db_session: Session,
current_user: PublicUser | AnonymousUser,
user_id: int,
):
# Get user
statement = select(User).where(User.id == user_id)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
status_code=400,
detail="User does not exist",
)
# RBAC check
await rbac_check(request, current_user, "delete", user.user_uuid, db_session)
# Delete user
db_session.delete(user)
db_session.commit()
return "User deleted"
# Utils & Security functions
async def security_get_user(request: Request, email: str):
users = request.app.db["users"]
user = await users.find_one({"email": email})
async def security_get_user(request: Request, db_session: Session, email: str) -> User:
# Check if user exists
statement = select(User).where(User.email == email)
user = db_session.exec(statement).first()
if not user:
raise HTTPException(
@ -217,105 +350,39 @@ async def security_get_user(request: Request, email: str):
detail="User with Email does not exist",
)
return UserInDB(**user)
user = User(**user.dict())
async def get_userid_by_username(request: Request, username: str):
users = request.app.db["users"]
user = await users.find_one({"username": username})
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
)
return user["user_id"]
async def get_user_by_userid(request: Request, user_id: str):
users = request.app.db["users"]
user = await users.find_one({"user_id": user_id})
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
)
user = User(**user)
return user
async def get_profile_metadata(request: Request, user):
users = request.app.db["users"]
request.app.db["roles"]
user = await users.find_one({"user_id": user["user_id"]})
if not user:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
)
return {"user_object": PublicUser(**user), "roles": "random"}
## 🔒 RBAC Utils ##
# Verification of the user's permissions on the roles
async def verify_user_rights_on_user(
async def rbac_check(
request: Request,
current_user: PublicUser,
current_user: PublicUser | AnonymousUser,
action: Literal["create", "read", "update", "delete"],
user_id: str,
user_uuid: str,
db_session: Session,
):
users = request.app.db["users"]
user = UserInDB(**await users.find_one({"user_id": user_id}))
if action == "create":
if current_user.id == 0: # if user is anonymous
return True
if action == "read":
await authorization_verify_if_user_is_anon(current_user.user_id)
if current_user.user_id == user_id:
return True
for org in current_user.orgs:
if org.org_id in [org.org_id for org in user.orgs]:
return True
return False
if action == "update":
await authorization_verify_if_user_is_anon(current_user.user_id)
if current_user.user_id == user_id:
return True
for org in current_user.orgs:
if org.org_id in [org.org_id for org in user.orgs]:
if org.org_role == "owner":
return True
await authorization_verify_based_on_roles(
request, current_user.user_id, "update", user["roles"], user_id
else:
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, "create", "user_x", db_session
)
return False
else:
await authorization_verify_if_user_is_anon(current_user.id)
if action == "delete":
await authorization_verify_if_user_is_anon(current_user.user_id)
if current_user.user_id == user_id:
# if user is the same as the one being read
if current_user.user_uuid == user_uuid:
return True
for org in current_user.orgs:
if org.org_id in [org.org_id for org in user.orgs]:
if org.org_role == "owner":
return True
await authorization_verify_based_on_roles(
request, current_user.user_id, "update", user["roles"], user_id
await authorization_verify_based_on_roles_and_authorship(
request, current_user.id, action, user_uuid, db_session
)
## 🔒 RBAC Utils ##

View file

@ -6,7 +6,7 @@ from config.config import get_learnhouse_config
async def upload_content(
directory: str, org_id: str, file_binary: bytes, file_and_format: str
directory: str, org_uuid: str, file_binary: bytes, file_and_format: str
):
# Get Learnhouse Config
learnhouse_config = get_learnhouse_config()
@ -16,12 +16,12 @@ async def upload_content(
if content_delivery == "filesystem":
# create folder for activity
if not os.path.exists(f"content/{org_id}/{directory}"):
if not os.path.exists(f"content/{org_uuid}/{directory}"):
# create folder for activity
os.makedirs(f"content/{org_id}/{directory}")
os.makedirs(f"content/{org_uuid}/{directory}")
# upload file to server
with open(
f"content/{org_id}/{directory}/{file_and_format}",
f"content/{org_uuid}/{directory}/{file_and_format}",
"wb",
) as f:
f.write(file_binary)
@ -37,13 +37,13 @@ async def upload_content(
)
# Create folder for activity
if not os.path.exists(f"content/{org_id}/{directory}"):
if not os.path.exists(f"content/{org_uuid}/{directory}"):
# create folder for activity
os.makedirs(f"content/{org_id}/{directory}")
os.makedirs(f"content/{org_uuid}/{directory}")
# Upload file to server
with open(
f"content/{org_id}/{directory}/{file_and_format}",
f"content/{org_uuid}/{directory}/{file_and_format}",
"wb",
) as f:
f.write(file_binary)
@ -52,9 +52,9 @@ async def upload_content(
print("Uploading to s3 using boto3...")
try:
s3.upload_file(
f"content/{org_id}/{directory}/{file_and_format}",
f"content/{org_uuid}/{directory}/{file_and_format}",
"learnhouse-media",
f"content/{org_id}/{directory}/{file_and_format}",
f"content/{org_uuid}/{directory}/{file_and_format}",
)
except ClientError as e:
print(e)
@ -63,7 +63,7 @@ async def upload_content(
try:
s3.head_object(
Bucket="learnhouse-media",
Key=f"content/{org_id}/{directory}/{file_and_format}",
Key=f"content/{org_uuid}/{directory}/{file_and_format}",
)
print("File upload successful!")
except Exception as e:

View file

@ -1,11 +1,12 @@
import { default as React, } from "react";
import AuthProvider from "@components/Security/AuthProvider";
import AuthProvider from "@components/Security/AuthProviderDepreceated";
import EditorWrapper from "@components/Objects/Editor/EditorWrapper";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers";
import { Metadata } from "next";
import { getActivityWithAuthHeader } from "@services/courses/activities";
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
import { getOrganizationContextInfo, getOrganizationContextInfoWithId } from "@services/organizations/orgs";
type MetadataProps = {
params: { orgslug: string, courseid: string, activityid: string };
@ -21,26 +22,25 @@ export async function generateMetadata(
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
return {
title: `Edit - ${course_meta.course.name} Activity`,
description: course_meta.course.mini_description,
title: `Edit - ${course_meta.name} Activity`,
description: course_meta.mini_description,
};
}
const EditActivity = async (params: any) => {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const activityid = params.params.activityid;
const activityuuid = params.params.activityuuid;
const courseid = params.params.courseid;
const orgslug = params.params.orgslug;
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
const activity = await getActivityWithAuthHeader(activityuuid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
const org = await getOrganizationContextInfoWithId(courseInfo.org_id, { revalidate: 1800, tags: ['organizations'] });
console.log('courseInfo', courseInfo )
return (
<div>
<AuthProvider>
<EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
<EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
</AuthProvider>
</div>
);

View file

@ -5,7 +5,7 @@ import { deleteOrganizationFromBackend } from "@services/organizations/orgs";
import useSWR, { mutate } from "swr";
import { swrFetcher } from "@services/utils/ts/requests";
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
import AuthProvider from "@components/Security/AuthProvider";
import AuthProvider from "@components/Security/AuthProviderDepreceated";
const Organizations = () => {
const { data: organizations, error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)

View file

@ -48,6 +48,7 @@ export async function generateMetadata(
const CollectionPage = async (params: any) => {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const org = await getOrganizationContextInfo(params.params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const orgslug = params.params.orgslug;
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
@ -62,9 +63,9 @@ const CollectionPage = async (params: any) => {
<br />
<div className="home_courses flex flex-wrap">
{col.courses.map((course: any) => (
<div className="pr-8" key={course.course_id}>
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_id, course.thumbnail)})` }}>
<div className="pr-8" key={course.course_uuid}>
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_uuid))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
</div>
</Link>
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>

View file

@ -41,7 +41,7 @@ function NewCollection(params: any) {
description: description,
courses: selectedCourses,
public: true,
org_id: org.org_id,
org_id: org.id,
};
await createCollection(collection);
await revalidateTags(["collections"], orgslug);
@ -69,26 +69,29 @@ function NewCollection(params: any) {
) : (
<div>
{courses.map((course: any) => (
<div key={course.course_id} className="flex items-center mb-2">
<div key={course.course_uuid} className="flex items-center mb-2">
<input
type="checkbox"
id={course.course_id}
name={course.course_id}
value={course.course_id}
checked={selectedCourses.includes(course.course_id)}
id={course.id}
name={course.name}
value={course.id}
// id is an integer, not a string
onChange={(e) => {
const courseId = e.target.value;
setSelectedCourses((prevSelectedCourses: string[]) => {
if (e.target.checked) {
return [...prevSelectedCourses, courseId];
} else {
return prevSelectedCourses.filter((selectedCourse) => selectedCourse !== courseId);
setSelectedCourses([...selectedCourses, course.id]);
}
});
}}
className="mr-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
else {
setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
}
}
}
className="mr-2"
/>
<label htmlFor={course.course_id} className="text-sm">{course.name}</label>
<label htmlFor={course.course_uuid} className="text-sm">{course.name}</label>
</div>
))}

View file

@ -8,7 +8,7 @@ import { Metadata } from "next";
import { cookies } from "next/headers";
import Link from "next/link";
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
import CollectionThumbnail from "@components/Objects/Other/CollectionThumbnail";
import CollectionThumbnail from "@components/Objects/Thumbnails/CollectionThumbnail";
import NewCollectionButton from "@components/StyledElements/Buttons/NewCollectionButton";
type MetadataProps = {
@ -49,14 +49,17 @@ const CollectionsPage = async (params: any) => {
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const org_id = org.org_id;
const org_id = org.id;
const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
return (
<GeneralWrapperStyled>
<div className="flex justify-between" >
<TypeOfContentTitle title="Collections" type="col" />
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}>
<AuthenticatedClientElement
ressourceType="collection"
action="create"
checkMethod='roles' orgId={org_id}>
<Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton />
</Link>
@ -64,7 +67,7 @@ const CollectionsPage = async (params: any) => {
</div>
<div className="home_collections flex flex-wrap">
{collections.map((collection: any) => (
<div className="flex flex-col py-1 px-3" key={collection.collection_id}>
<div className="flex flex-col py-1 px-3" key={collection.collection_uuid}>
<CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org_id} />
</div>
))}
@ -81,7 +84,10 @@ const CollectionsPage = async (params: any) => {
<h1 className="text-3xl font-bold text-gray-600">No collections yet</h1>
<p className="text-lg text-gray-400">Create a collection to group courses together</p>
</div>
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}>
<AuthenticatedClientElement checkMethod='roles'
ressourceType="collection"
action="create"
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton />
</Link>

View file

@ -1,129 +0,0 @@
"use client";
import Link from "next/link";
import { getUriWithOrg } from "@services/config/config";
import Canva from "@components/Objects/Activities/DynamicCanva/DynamicCanva";
import VideoActivity from "@components/Objects/Activities/Video/Video";
import { Check } from "lucide-react";
import { markActivityAsComplete } from "@services/courses/activity";
import DocumentPdfActivity from "@components/Objects/Activities/DocumentPdf/DocumentPdf";
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { useRouter } from "next/navigation";
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
interface ActivityClientProps {
activityid: string;
courseid: string;
orgslug: string;
activity: any;
course: any;
}
function ActivityClient(props: ActivityClientProps) {
const activityid = props.activityid;
const courseid = props.courseid;
const orgslug = props.orgslug;
const activity = props.activity;
const course = props.course;
function getChapterName(chapterId: string) {
let chapterName = "";
course.chapters.forEach((chapter: any) => {
if (chapter.id === chapterId) {
chapterName = chapter.name;
}
});
return chapterName;
}
return (
<>
<GeneralWrapperStyled>
<div className="space-y-4 pt-4">
<div className="flex space-x-6">
<div className="flex">
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_id, course.course.thumbnail)}`} alt="" />
</Link>
</div>
<div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Course </p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.course.name}</h1>
</div>
</div>
<ActivityIndicators course_id={courseid} current_activity={activityid} orgslug={orgslug} course={course} />
<div className="flex justify-between items-center">
<div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Chapter : {getChapterName(activity.coursechapter_id)}</p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
</div>
<div className="flex space-x-2">
<AuthenticatedClientElement checkMethod="authentication">
<MarkStatus activityid={activityid} course={course} orgslug={orgslug} courseid={courseid} />
</AuthenticatedClientElement>
</div>
</div>
{activity ? (
<div className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.type == 'dynamic' ? 'bg-white' : 'bg-zinc-950'}`}>
<div>
{activity.type == "dynamic" && <Canva content={activity.content} activity={activity} />}
{/* todo : use apis & streams instead of this */}
{activity.type == "video" && <VideoActivity course={course} activity={activity} />}
{activity.type == "documentpdf" && <DocumentPdfActivity course={course} activity={activity} />}
</div>
</div>
) : (<div></div>)}
{<div style={{ height: "100px" }}></div>}
</div>
</GeneralWrapperStyled>
</>
);
}
export function MarkStatus(props: { activityid: string, course: any, orgslug: string, courseid: string }) {
const router = useRouter();
async function markActivityAsCompleteFront() {
const trail = await markActivityAsComplete(props.orgslug, props.courseid, props.activityid);
router.refresh();
// refresh page (FIX for Next.js BUG)
//window.location.reload();
}
return (
<>{props.course.trail.activities_marked_complete &&
props.course.trail.activities_marked_complete.includes("activity_" + props.activityid) &&
props.course.trail.status == "ongoing" ? (
<div className="bg-teal-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" >
<i>
<Check size={15}></Check>
</i>{" "}
Already completed
</div>
) : (
<div className="bg-zinc-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" onClick={markActivityAsCompleteFront}>
{" "}
<i>
<Check size={15}></Check>
</i>{" "}
Mark as complete
</div>
)}</>
)
}
export default ActivityClient;

View file

@ -1,152 +0,0 @@
"use client";
import React, { FC, use, useEffect, useReducer } from 'react'
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import useSWR, { mutate } from 'swr';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import Link from 'next/link';
import CourseEdition from '../subpages/CourseEdition';
import CourseContentEdition from '../subpages/CourseContentEdition';
import ErrorUI from '@components/StyledElements/Error/Error';
import { updateChaptersMetadata } from '@services/courses/chapters';
import { Check, SaveAllIcon, Timer } from 'lucide-react';
import Loading from '../../loading';
import { updateCourse } from '@services/courses/courses';
import { useRouter } from 'next/navigation';
function CourseEditClient({ courseid, subpage, params }: { courseid: string, subpage: string, params: any }) {
const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/meta/course_${courseid}`, swrFetcher);
const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseid}`, swrFetcher);
const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {});
const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {});
const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true);
const router = useRouter();
function courseChaptersReducer(state: any, action: any) {
switch (action.type) {
case 'updated_chapter':
// action will contain the entire state, just update the entire state
return action.payload;
default:
throw new Error();
}
}
function courseReducer(state: any, action: any) {
switch (action.type) {
case 'updated_course':
// action will contain the entire state, just update the entire state
return action.payload;
default:
throw new Error();
}
}
function savedContentReducer(state: any, action: any) {
switch (action.type) {
case 'saved_content':
return true;
case 'unsaved_content':
return false;
default:
throw new Error();
}
}
async function saveCourse() {
if (subpage.toString() === 'content') {
await updateChaptersMetadata(courseid, courseChaptersMetadata)
dispatchSavedContent({ type: 'saved_content' })
await mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`)
await revalidateTags(['courses'], params.params.orgslug)
router.refresh()
}
else if (subpage.toString() === 'general') {
await updateCourse(courseid, courseState)
dispatchSavedContent({ type: 'saved_content' })
await mutate(`${getAPIUrl()}courses/course_${courseid}`)
await revalidateTags(['courses'], params.params.orgslug)
router.refresh()
}
}
useEffect(() => {
if (chapters_meta) {
dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta })
dispatchSavedContent({ type: 'saved_content' })
}
if (course) {
dispatchCourseMetadata({ type: 'updated_course', payload: course })
dispatchSavedContent({ type: 'saved_content' })
}
}, [chapters_meta, course])
return (
<>
<div className='bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
{course_isloading && <div className='text-sm text-gray-500'>Loading...</div>}
{course && <>
<div className='flex items-center'><div className='info flex space-x-5 items-center grow'>
<div className='flex'>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.org_id, "course_" + courseid, course.thumbnail)}`} alt="" />
</Link>
</div>
<div className="flex flex-col ">
<div className='text-sm text-gray-500'>Edit Course</div>
<div className='text-2xl font-bold first-letter:uppercase'>{course.name}</div>
</div>
</div>
<div className='flex space-x-5 items-center'>
{savedContent ? <></> : <div className='text-gray-600 flex space-x-2 items-center antialiased'>
<Timer size={15} />
<div>
Unsaved changes
</div>
</div>}
<div className={`' px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` + (savedContent ? 'bg-gray-600 text-white' : 'bg-black text-white border hover:bg-gray-900 ')
} onClick={saveCourse}>
{savedContent ? <Check size={20} /> : <SaveAllIcon size={20} />}
{savedContent ? <div className=''>Saved</div> : <div className=''>Save</div>}
</div>
</div>
</div>
</>}
<div className='flex space-x-5 pt-3 font-black text-sm'>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/general`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>General</div>
</Link>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/content`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>Content</div>
</Link>
</div>
</div>
</div>
<CoursePageViewer dispatchSavedContent={dispatchSavedContent} courseState={courseState} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseMetadata={dispatchCourseMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseid={courseid} orgslug={params.params.orgslug} />
</>
)
}
const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchCourseMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any, courseState: any }) => {
if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) {
return <CourseEdition data={courseState} dispatchCourseMetadata={dispatchCourseMetadata} dispatchSavedContent={dispatchSavedContent} />
}
else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) {
return <CourseContentEdition data={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} courseid={courseid} orgslug={orgslug} />
}
else if (subpage.toString() === 'content' || subpage.toString() === 'general') {
return <Loading />
}
else {
return <ErrorUI />
}
}
export default CourseEditClient

View file

@ -1,41 +0,0 @@
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import CourseEditClient from "./edit";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers";
import { Metadata } from 'next';
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
type MetadataProps = {
params: { orgslug: string, courseid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
return {
title: `Edit Course - ` + course_meta.course.name,
description: course_meta.course.mini_description,
};
}
function CourseEdit(params: any) {
let subpage = params.params.subpage ? params.params.subpage : 'general';
return (
<>
<CourseEditClient params={params} subpage={subpage} courseid={params.params.courseid} />
</>
);
}
export default CourseEdit;

View file

@ -1,320 +0,0 @@
"use client";
import React from "react";
import { useState, useEffect } from "react";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import Chapter from "@components/Pages/CourseEdit/Draggables/Chapter";
import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters";
import { useRouter } from "next/navigation";
import NewChapterModal from "@components/Objects/Modals/Chapters/NewChapter";
import NewActivityModal from "@components/Objects/Modals/Activities/Create/NewActivity";
import { createActivity, createFileActivity, createExternalVideoActivity } from "@services/courses/activities";
import { getOrganizationContextInfo, getOrganizationContextInfoWithoutCredentials } from "@services/organizations/orgs";
import Modal from "@components/StyledElements/Modal/Modal";
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
import { Folders, Hexagon, SaveIcon } from "lucide-react";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { mutate } from "swr";
import { getAPIUrl } from "@services/config/config";
function CourseContentEdition(props: any) {
const router = useRouter();
// Initial Course State
const data = props.data;
// New Chapter Modal State
const [newChapterModal, setNewChapterModal] = useState(false) as any;
// New Activity Modal State
const [newActivityModal, setNewActivityModal] = useState(false) as any;
const [newActivityModalData, setNewActivityModalData] = useState("") as any;
// Check window availability
const [winReady, setwinReady] = useState(false);
const courseid = props.courseid;
const orgslug = props.orgslug;
useEffect(() => {
setwinReady(true);
}, [courseid, orgslug]);
// get a list of chapters order by chapter order
const getChapters = () => {
const chapterOrder = data.chapterOrder ? data.chapterOrder : [];
return chapterOrder.map((chapterId: any) => {
const chapter = data.chapters[chapterId];
let activities = [];
if (data.activities) {
activities = chapter.activityIds.map((activityId: any) => data.activities[activityId])
? chapter.activityIds.map((activityId: any) => data.activities[activityId])
: [];
}
return {
list: {
chapter: chapter,
activities: activities,
},
};
});
};
// Submit new chapter
const submitChapter = async (chapter: any) => {
await createChapter(chapter, courseid);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
await revalidateTags(['courses'], orgslug);
router.refresh();
setNewChapterModal(false);
};
// Submit new activity
const submitActivity = async (activity: any) => {
let org = await getOrganizationContextInfoWithoutCredentials(orgslug, { revalidate: 1800 });
await updateChaptersMetadata(courseid, data);
await createActivity(activity, activity.chapterId, org.org_id);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
// Submit File Upload
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
await updateChaptersMetadata(courseid, data);
await createFileActivity(file, type, activity, chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
// Submit YouTube Video Upload
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
await updateChaptersMetadata(courseid, data);
await createExternalVideoActivity(external_video_data, activity, chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
const deleteChapterUI = async (chapterId: any) => {
await deleteChapter(chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
await revalidateTags(['courses'], orgslug);
router.refresh();
};
const updateChapters = () => {
updateChaptersMetadata(courseid, data);
revalidateTags(['courses'], orgslug);
router.refresh();
};
/*
Modals
*/
const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true);
setNewActivityModalData(chapterId);
};
// Close new chapter modal
const closeNewChapterModal = () => {
setNewChapterModal(false);
};
const closeNewActivityModal = () => {
setNewActivityModal(false);
};
/*
Drag and drop functions
*/
const onDragEnd = async (result: any) => {
const { destination, source, draggableId, type } = result;
// check if the activity is dropped outside the droppable area
if (!destination) {
return;
}
// check if the activity is dropped in the same place
if (destination.droppableId === source.droppableId && destination.index === source.index) {
return;
}
//////////////////////////// CHAPTERS ////////////////////////////
if (type === "chapter") {
const newChapterOrder = Array.from(data.chapterOrder);
newChapterOrder.splice(source.index, 1);
newChapterOrder.splice(destination.index, 0, draggableId);
const newState = {
...data,
chapterOrder: newChapterOrder,
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
//////////////////////// ACTIVITIES IN SAME CHAPTERS ////////////////////////////
// check if the activity is dropped in the same chapter
const start = data.chapters[source.droppableId];
const finish = data.chapters[destination.droppableId];
// check if the activity is dropped in the same chapter
if (start === finish) {
// create new arrays for chapters and activities
const chapter = data.chapters[source.droppableId];
const newActivityIds = Array.from(chapter.activityIds);
// remove the activity from the old position
newActivityIds.splice(source.index, 1);
// add the activity to the new position
newActivityIds.splice(destination.index, 0, draggableId);
const newChapter = {
...chapter,
activityIds: newActivityIds,
};
const newState = {
...data,
chapters: {
...data.chapters,
[newChapter.id]: newChapter,
},
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
//////////////////////// ACTIVITIES IN DIFF CHAPTERS ////////////////////////////
// check if the activity is dropped in a different chapter
if (start !== finish) {
// create new arrays for chapters and activities
const startChapterActivityIds = Array.from(start.activityIds);
// remove the activity from the old position
startChapterActivityIds.splice(source.index, 1);
const newStart = {
...start,
activityIds: startChapterActivityIds,
};
// add the activity to the new position within the chapter
const finishChapterActivityIds = Array.from(finish.activityIds);
finishChapterActivityIds.splice(destination.index, 0, draggableId);
const newFinish = {
...finish,
activityIds: finishChapterActivityIds,
};
const newState = {
...data,
chapters: {
...data.chapters,
[newStart.id]: newStart,
[newFinish.id]: newFinish,
},
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
};
return (
<>
<div className=""
>
<GeneralWrapperStyled>
<Modal
isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal}
minHeight="no-min"
addDefCloseButton={false}
dialogContent={<NewActivityModal
closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity}
chapterId={newActivityModalData}
></NewActivityModal>}
dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course"
/>
{winReady && (
<div className="flex flex-col">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable key="chapters" droppableId="chapters" type="chapter">
{(provided) => (
<>
<div key={"chapters"} {...provided.droppableProps} ref={provided.innerRef}>
{getChapters().map((info: any, index: any) => (
<>
<Chapter
orgslug={orgslug}
courseid={courseid}
openNewActivityModal={openNewActivityModal}
deleteChapter={deleteChapterUI}
key={index}
info={info}
index={index}
></Chapter>
</>
))}
{provided.placeholder}
</div>
</>
)}
</Droppable>
</DragDropContext>
<Modal
isDialogOpen={newChapterModal}
onOpenChange={setNewChapterModal}
minHeight="sm"
dialogContent={<NewChapterModal
closeModal={closeNewChapterModal}
submitChapter={submitChapter}
></NewChapterModal>}
dialogTitle="Create chapter"
dialogDescription="Add a new chapter to the course"
dialogTrigger={
<div className="flex max-w-7xl bg-black text-sm shadow rounded-md items-center text-white justify-center mx-auto space-x-2 p-3 w-72 hover:bg-gray-900 hover:cursor-pointer">
<Hexagon size={16} />
<div>Add chapter +</div>
</div>
}
/>
</div>
)}
</GeneralWrapperStyled >
</div>
</>
);
}
export default CourseContentEdition;

View file

@ -1,116 +0,0 @@
"use client";
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input, Textarea } from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form';
import { useFormik } from 'formik';
import { AlertTriangle } from "lucide-react";
import React from "react";
const validate = (values: any) => {
const errors: any = {};
if (!values.name) {
errors.name = 'Required';
}
if (values.name.length > 100) {
errors.name = 'Must be 80 characters or less';
}
if (!values.mini_description) {
errors.mini_description = 'Required';
}
if (values.mini_description.length > 200) {
errors.mini_description = 'Must be 200 characters or less';
}
if (!values.description) {
errors.description = 'Required';
}
if (values.description.length > 1000) {
errors.description = 'Must be 1000 characters or less';
}
if (!values.learnings) {
errors.learnings = 'Required';
}
return errors;
};
function CourseEdition(props: any) {
const [error, setError] = React.useState('');
const formik = useFormik({
initialValues: {
name: String(props.data.name),
mini_description: String(props.data.mini_description),
description: String(props.data.description),
learnings: String(props.data.learnings),
},
validate,
onSubmit: async values => {
},
});
React.useEffect(() => {
// This code will run whenever form values are updated
if (formik.values !== formik.initialValues) {
props.dispatchSavedContent({ type: 'unsaved_content' });
const updatedCourse = {
...props.data,
name: formik.values.name,
mini_description: formik.values.mini_description,
description: formik.values.description,
learnings: formik.values.learnings.split(", "),
};
props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse });
}
}, [formik.values, formik.initialValues]);
return (
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
<div className="login-form">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label='Name' message={formik.errors.name} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control>
</FormField>
<FormField name="mini_description">
<FormLabelAndMessage label='Mini description' message={formik.errors.mini_description} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.mini_description} type="text" required />
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label='Description' message={formik.errors.description} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
</Form.Control>
</FormField>
<FormField name="learnings">
<FormLabelAndMessage label='Learnings (Separated by , )' message={formik.errors.learnings} />
<Form.Control asChild>
<Textarea placeholder='Science, Design, Architecture' style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
</Form.Control>
</FormField>
</FormLayout>
</div>
</div>
)
}
export default CourseEdition

View file

@ -0,0 +1,137 @@
"use client";
import Link from "next/link";
import { getUriWithOrg } from "@services/config/config";
import Canva from "@components/Objects/Activities/DynamicCanva/DynamicCanva";
import VideoActivity from "@components/Objects/Activities/Video/Video";
import { Check } from "lucide-react";
import { markActivityAsComplete } from "@services/courses/activity";
import DocumentPdfActivity from "@components/Objects/Activities/DocumentPdf/DocumentPdf";
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { useRouter } from "next/navigation";
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { useOrg } from "@components/Contexts/OrgContext";
import { CourseProvider } from "@components/Contexts/CourseContext";
interface ActivityClientProps {
activityid: string;
courseuuid: string;
orgslug: string;
activity: any;
course: any;
}
function ActivityClient(props: ActivityClientProps) {
const activityid = props.activityid;
const courseuuid = props.courseuuid;
const orgslug = props.orgslug;
const activity = props.activity;
const course = props.course;
const org = useOrg() as any;
function getChapterName(chapterId: string) {
let chapterName = "";
course.chapters.forEach((chapter: any) => {
if (chapter.id === chapterId) {
chapterName = chapter.name;
}
});
return chapterName;
}
return (
<>
<CourseProvider courseuuid={course?.course_uuid}>
<GeneralWrapperStyled>
<div className="space-y-4 pt-4">
<div className="flex space-x-6">
<div className="flex">
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)}`} alt="" />
</Link>
</div>
<div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Course </p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.name}</h1>
</div>
</div>
<ActivityIndicators course_uuid={courseuuid} current_activity={activityid} orgslug={orgslug} course={course} />
<div className="flex justify-between items-center">
<div className="flex flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Chapter : {getChapterName(activity.coursechapter_id)}</p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
</div>
<div className="flex space-x-2">
<AuthenticatedClientElement checkMethod="authentication">
<MarkStatus activity={activity} activityid={activityid} course={course} orgslug={orgslug} />
</AuthenticatedClientElement>
</div>
</div>
{activity ? (
<div className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC' ? 'bg-white' : 'bg-zinc-950'}`}>
<div>
{activity.activity_type == "TYPE_DYNAMIC" && <Canva content={activity.content} activity={activity} />}
{/* todo : use apis & streams instead of this */}
{activity.activity_type == "TYPE_VIDEO" && <VideoActivity course={course} activity={activity} />}
{activity.activity_type == "TYPE_DOCUMENT" && <DocumentPdfActivity course={course} activity={activity} />}
</div>
</div>
) : (<div></div>)}
{<div style={{ height: "100px" }}></div>}
</div>
</GeneralWrapperStyled>
</CourseProvider>
</>
);
}
export function MarkStatus(props: { activity: any, activityid: string, course: any, orgslug: string }) {
const router = useRouter();
console.log(props.course.trail)
async function markActivityAsCompleteFront() {
const trail = await markActivityAsComplete(props.orgslug, props.course.course_uuid, 'activity_' + props.activityid);
router.refresh();
}
const isActivityCompleted = () => {
let run = props.course.trail.runs.find((run: any) => run.course_id == props.course.id);
if (run) {
return run.steps.find((step: any) => step.activity_id == props.activity.id);
}
}
console.log('isActivityCompleted', isActivityCompleted());
return (
<>{isActivityCompleted() ? (
<div className="bg-teal-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" >
<i>
<Check size={15}></Check>
</i>{" "}
Already completed
</div>
) : (
<div className="bg-zinc-600 rounded-md drop-shadow-md flex flex-col p-3 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" onClick={markActivityAsCompleteFront}>
{" "}
<i>
<Check size={15}></Check>
</i>{" "}
Mark as complete
</div>
)}</>
)
}
export default ActivityClient;

View file

@ -8,7 +8,7 @@ import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshToke
type MetadataProps = {
params: { orgslug: string, courseid: string, activityid: string };
params: { orgslug: string, courseuuid: string, activityid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
@ -20,14 +20,14 @@ export async function generateMetadata(
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const activity = await getActivityWithAuthHeader(params.activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
// SEO
return {
title: activity.name + `${course_meta.course.name} Course`,
description: course_meta.course.mini_description,
keywords: course_meta.course.learnings,
title: activity.name + `${course_meta.name} Course`,
description: course_meta.description,
keywords: course_meta.learnings,
robots: {
index: true,
follow: true,
@ -39,11 +39,10 @@ export async function generateMetadata(
}
},
openGraph: {
title: activity.name + `${course_meta.course.name} Course`,
description: course_meta.course.mini_description,
type: activity.type === 'video' ? 'video.other' : 'article',
publishedTime: course_meta.course.creationDate,
tags: course_meta.course.learnings,
title: activity.name + `${course_meta.name} Course`,
description: course_meta.description,
publishedTime: course_meta.creation_date,
tags: course_meta.learnings,
},
};
}
@ -52,16 +51,16 @@ const ActivityPage = async (params: any) => {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const activityid = params.params.activityid;
const courseid = params.params.courseid;
const courseuuid = params.params.courseuuid;
const orgslug = params.params.orgslug;
const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
return (
<>
<ActivityClient
activityid={activityid}
courseid={courseid}
courseuuid={courseuuid}
orgslug={orgslug}
activity={activity}
course={course_meta}

View file

@ -12,26 +12,28 @@ import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
import Avvvatars from "avvvatars-react";
import { getUser } from "@services/users/users";
import { useOrg } from "@components/Contexts/OrgContext";
const CourseClient = (props: any) => {
const [user, setUser] = useState<any>({});
const courseid = props.courseid;
const [learnings, setLearnings] = useState<any>([]);
const courseuuid = props.courseuuid;
const orgslug = props.orgslug;
const course = props.course;
const org = useOrg() as any;
const router = useRouter();
function getLearningTags() {
// create array of learnings from a string object (comma separated)
let learnings = course.learnings.split(",");
setLearnings(learnings);
async function getUserUI() {
let user_id = course.course.authors[0];
const user = await getUser(user_id);
setUser(user);
console.log(user);
}
async function startCourseUI() {
// Create activity
await startCourse("course_" + courseid, orgslug);
await startCourse("course_" + courseuuid, orgslug);
await revalidateTags(['courses'], orgslug);
router.refresh();
@ -39,18 +41,24 @@ const CourseClient = (props: any) => {
// window.location.reload();
}
function isCourseStarted() {
const runs = course.trail.runs;
// checks if one of the obejcts in the array has the property "STATUS_IN_PROGRESS"
return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS");
}
async function quitCourse() {
// Close activity
let activity = await removeCourse("course_" + courseid, orgslug);
let activity = await removeCourse("course_" + courseuuid, orgslug);
// Mutate course
await revalidateTags(['courses'], orgslug);
router.refresh();
}
useEffect(() => {
getUserUI();
}
, []);
, [org]);
return (
<>
@ -61,26 +69,26 @@ const CourseClient = (props: any) => {
<div className="pb-3">
<p className="text-md font-bold text-gray-400 pb-2">Course</p>
<h1 className="text-3xl -mt-3 font-bold">
{course.course.name}
{course.name}
</h1>
</div>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_id, course.course.thumbnail)})` }}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
</div>
<ActivityIndicators course_id={props.course.course.course_id} orgslug={orgslug} course={course} />
<ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} />
<div className="flex flex-row pt-10">
<div className="course_metadata_left grow space-y-2">
<h2 className="py-3 text-2xl font-bold">Description</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
<p className="py-5 px-5">{course.course.description}</p>
<p className="py-5 px-5">{course.description}</p>
</div>
<h2 className="py-3 text-2xl font-bold">What you will learn</h2>
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2">
{course.course.learnings.map((learning: any) => {
{learnings.map((learning: any) => {
return (
<div key={learning}
className="flex space-x-2 items-center font-semibold text-gray-500 capitalize">
@ -118,48 +126,48 @@ const CourseClient = (props: any) => {
</p>
<div className="flex space-x-1 py-2 px-4 items-center">
<div className="courseicon items-center flex space-x-2 text-neutral-400">
{activity.type === "dynamic" &&
{activity.activity_type === "TYPE_DYNAMIC" &&
<div className="bg-gray-100 px-2 py-2 rounded-full">
<Sparkles className="text-gray-400" size={13} />
</div>
}
{activity.type === "video" &&
{activity.activity_type === "TYPE_VIDEO" &&
<div className="bg-gray-100 px-2 py-2 rounded-full">
<Video className="text-gray-400" size={13} />
</div>
}
{activity.type === "documentpdf" &&
{activity.activity_type === "TYPE_DOCUMENT" &&
<div className="bg-gray-100 px-2 py-2 rounded-full">
<File className="text-gray-400" size={13} />
</div>
}
</div>
<Link className="flex font-semibold grow pl-2 text-neutral-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer">
<Link className="flex font-semibold grow pl-2 text-neutral-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<p>{activity.name}</p>
</Link>
<div className="flex ">
{activity.type === "dynamic" &&
{activity.activity_type === "TYPE_DYNAMIC" &&
<>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer">
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Page</p>
<ArrowRight size={13} /></div>
</Link>
</>
}
{activity.type === "video" &&
{activity.activity_type === "TYPE_VIDEO" &&
<>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer">
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Video</p>
<ArrowRight size={13} /></div>
</Link>
</>
}
{activity.type === "documentpdf" &&
{activity.activity_type === "TYPE_DOCUMENT" &&
<>
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseid}/activity/${activity.id.replace("activity_", "")}`} rel="noopener noreferrer">
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Document</p>
<ArrowRight size={13} /></div>
@ -181,16 +189,17 @@ const CourseClient = (props: any) => {
{user &&
<div className="flex mx-auto space-x-3 px-2 py-2 items-center">
<div className="">
<Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.course.authors[0]} style='shape' />
<Avvvatars border borderSize={5} borderColor="white" size={50} shadow value={course.authors[0].username} style='shape' />
</div>
<div className="-space-y-2 ">
<div className="text-[12px] text-neutral-400 font-semibold">Author</div>
<div className="text-xl font-bold text-neutral-800">{user.full_name}</div>
<div className="text-xl font-bold text-neutral-800">{course.authors[0].first_name} {course.authors[0].last_name} { (course.authors[0].first_name && course.authors[0].last_name) ? course.authors[0].first_name + ' ' + course.authors[0].last_name : course.authors[0].username }</div>
</div>
</div>
}
{console.log(course)}
{course.trail.status == "ongoing" ? (
{isCourseStarted() ? (
<button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" onClick={quitCourse}>
Quit Course
</button>

View file

@ -7,7 +7,7 @@ import { Metadata } from 'next';
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth';
type MetadataProps = {
params: { orgslug: string, courseid: string };
params: { orgslug: string, courseuuid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
@ -19,14 +19,14 @@ export async function generateMetadata(
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
// SEO
return {
title: course_meta.course.name + `${org.name}`,
description: course_meta.course.mini_description,
keywords: course_meta.course.learnings,
title: course_meta.name + `${org.name}`,
description: course_meta.description,
keywords: course_meta.learnings,
robots: {
index: true,
follow: true,
@ -38,11 +38,11 @@ export async function generateMetadata(
}
},
openGraph: {
title: course_meta.course.name + `${org.name}`,
description: course_meta.course.mini_description,
title: course_meta.name + `${org.name}`,
description: course_meta.description ? course_meta.description : '',
type: 'article',
publishedTime: course_meta.course.creationDate,
tags: course_meta.course.learnings,
publishedTime: course_meta.creation_date ? course_meta.creation_date : '',
tags: course_meta.learnings ? course_meta.learnings : [],
},
};
}
@ -50,14 +50,14 @@ export async function generateMetadata(
const CoursePage = async (params: any) => {
const cookieStore = cookies();
const courseid = params.params.courseid
const courseuuid = params.params.courseuuid
const orgslug = params.params.orgslug;
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
return (
<div>
<CourseClient courseid={courseid} orgslug={orgslug} course={course_meta} />
<CourseClient courseuuid={courseuuid} orgslug={orgslug} course={course_meta} />
</div>
)
}

View file

@ -6,7 +6,7 @@ import { useSearchParams } from 'next/navigation';
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import CourseThumbnail from '@components/Objects/Other/CourseThumbnail';
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
interface CourseProps {
@ -32,7 +32,10 @@ function Courses(props: CourseProps) {
<div className='flex flex-wrap justify-between'>
<TypeOfContentTitle title="Courses" type="cou" />
<AuthenticatedClientElement checkMethod='roles' orgId={props.org_id}>
<AuthenticatedClientElement checkMethod='roles'
action='create'
ressourceType='course'
orgId={props.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
@ -56,7 +59,7 @@ function Courses(props: CourseProps) {
<div className="flex flex-wrap">
{courses.map((course: any) => (
<div className="px-3" key={course.course_id}>
<div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
@ -73,7 +76,10 @@ function Courses(props: CourseProps) {
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
<p className="text-lg text-gray-400">Create a course to add content</p>
</div>
<AuthenticatedClientElement checkMethod='roles' orgId={props.org_id}>
<AuthenticatedClientElement
action='create'
ressourceType='course'
checkMethod='roles' orgId={props.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}

View file

@ -1,6 +1,6 @@
import "@styles/globals.css";
import { Menu } from "@components/Objects/Menu/Menu";
import AuthProvider from "@components/Security/AuthProvider";
import AuthProvider from "@components/Security/AuthProviderDepreceated";
export default function RootLayout({ children, params }: { children: React.ReactNode , params :any}) {
return (

View file

@ -9,8 +9,8 @@ import { cookies } from 'next/headers';
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
import CourseThumbnail from '@components/Objects/Other/CourseThumbnail';
import CollectionThumbnail from '@components/Objects/Other/CollectionThumbnail';
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import { Plus, PlusCircle } from 'lucide-react';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
@ -56,8 +56,8 @@ const OrgHomePage = async (params: any) => {
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const org_id = org.org_id;
const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] });
const org_id = org.id;
const collections = await getOrgCollectionsWithAuthHeader(org.id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] });
return (
<div>
@ -67,7 +67,11 @@ const OrgHomePage = async (params: any) => {
<div className='flex grow'>
<TypeOfContentTitle title="Collections" type="col" />
</div>
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}>
<AuthenticatedClientElement
checkMethod='roles'
ressourceType='collection'
action='create'
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton />
</Link>
@ -105,7 +109,11 @@ const OrgHomePage = async (params: any) => {
<div className='flex grow'>
<TypeOfContentTitle title="Courses" type="cou" />
</div>
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}>
<AuthenticatedClientElement
ressourceType='course'
action='create'
checkMethod='roles'
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/courses?new=true")}>
<NewCourseButton />
</Link>
@ -113,7 +121,7 @@ const OrgHomePage = async (params: any) => {
</div>
<div className="home_courses flex flex-wrap">
{courses.map((course: any) => (
<div className="py-3 px-3" key={course.course_id}>
<div className="py-3 px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}

View file

@ -1,4 +1,5 @@
"use client";
import { useOrg } from "@components/Contexts/OrgContext";
import PageLoading from "@components/Objects/Loaders/PageLoading";
import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement";
import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
@ -6,13 +7,18 @@ import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWra
import { getAPIUrl } from "@services/config/config";
import { removeCourse } from "@services/courses/activity";
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import React from "react";
import React, { useEffect } from "react";
import useSWR, { mutate } from "swr";
function Trail(params: any) {
let orgslug = params.orgslug;
const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org_slug/${orgslug}/trail`, swrFetcher);
const org = useOrg() as any;
const orgID = org?.id;
const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org/${orgID}/trail`, swrFetcher);
useEffect(() => {
}
, [trail,org]);
return (
<GeneralWrapperStyled>
@ -21,12 +27,10 @@ function Trail(params: any) {
<PageLoading></PageLoading>
) : (
<div className="space-y-6">
{trail.courses.map((course: any) => (
!course.masked ? (
<TrailCourseElement key={trail.trail_id} orgslug={orgslug} course={course} />
) : (
<></>
)
{trail.runs.map((run: any) => (
<>
<TrailCourseElement run={run} course={run.course} orgslug={orgslug} />
</>
))}

View file

@ -0,0 +1,109 @@
'use client';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
import Modal from '@components/StyledElements/Modal/Modal';
import Link from 'next/link'
import { useSearchParams } from 'next/navigation';
import React from 'react'
type CourseProps = {
orgslug: string;
courses: any;
org_id: string;
}
function CoursesHome(params: CourseProps) {
const searchParams = useSearchParams();
const isCreatingCourse = searchParams.get('new') ? true : false;
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse);
const orgslug = params.orgslug;
const courses = params.courses;
async function closeNewCourseModal() {
setNewCourseModal(false);
}
return (
<div className='h-full w-full bg-[#f8f8f8]'>
<div >
<div className='pl-10 mr-10 tracking-tighter'>
<BreadCrumbs type='courses' />
<div className='w-100 flex justify-between'>
<div className='pt-3 flex font-bold text-4xl'>Courses</div>
<AuthenticatedClientElement checkMethod='roles'
action='create'
ressourceType='course'
orgId={params.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>}
/>
</AuthenticatedClientElement>
</div>
</div>
</div>
<div className="flex flex-wrap mx-8 mt-7">
{courses.map((course: any) => (
<div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
{courses.length == 0 &&
<div className="flex mx-auto h-[400px]">
<div className="flex flex-col justify-center text-center items-center space-y-5">
<div className='mx-auto'>
<svg width="120" height="120" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.51" x="10" y="10" width="275" height="275" rx="75" stroke="#4B5564" strokeOpacity="0.15" strokeWidth="20" />
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#4B5564" fillOpacity="0.08" />
</svg>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
<p className="text-lg text-gray-400">Create a course to add content</p>
</div>
<AuthenticatedClientElement
action='create'
ressourceType='course'
checkMethod='roles' orgId={params.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>}
/>
</AuthenticatedClientElement>
</div>
</div>
}
</div>
</div>
)
}
export default CoursesHome

View file

@ -0,0 +1,79 @@
'use client';
import EditCourseStructure from '../../../../../../../../components/Dashboard/Course/EditCourseStructure/EditCourseStructure'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import PageLoading from '@components/Objects/Loaders/PageLoading';
import ClientComponentSkeleton from '@components/Utils/ClientComp';
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import React, { createContext, use, useEffect, useState } from 'react'
import useSWR from 'swr';
import { CourseProvider, useCourse } from '../../../../../../../../components/Contexts/CourseContext';
import SaveState from '@components/Dashboard/UI/SaveState';
import Link from 'next/link';
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
import { CSSTransition } from 'react-transition-group';
import { motion } from 'framer-motion';
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral';
import { GalleryVertical, GalleryVerticalEnd, Info } from 'lucide-react';
export type CourseOverviewParams = {
orgslug: string,
courseuuid: string,
subpage: string
}
function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
function getEntireCourseUUID(courseuuid: string) {
// add course_ to uuid
return `course_${courseuuid}`
}
return (
<div className='h-full w-full bg-[#f8f8f8]'>
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<CourseOverviewTop params={params} />
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Info size={16} />
<div>General</div>
</div>
</div>
</Link>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/content`}>
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<GalleryVerticalEnd size={16} />
<div>Content</div>
</div>
</div>
</Link>
</div>
</div>
<div className='h-6'></div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
>
{params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
{params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''}
</motion.div>
</CourseProvider>
</div>
)
}
export default CourseOverviewPage

View file

@ -0,0 +1,57 @@
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses';
import { getOrganizationContextInfo } from '@services/organizations/orgs';
import { Metadata } from 'next';
import { cookies } from 'next/headers';
import React from 'react'
import CoursesHome from './client';
type MetadataProps = {
params: { orgslug: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
// SEO
return {
title: "Courses — " + org.name,
description: org.description,
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
},
openGraph: {
title: "Courses — " + org.name,
description: org.description,
type: 'website',
},
};
}
async function CoursesPage(params: any) {
const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
return (
<CoursesHome org_id={org.org_id} orgslug={orgslug} courses={courses} />
)
}
export default CoursesPage

View file

@ -0,0 +1,20 @@
import LeftMenu from '@components/Dashboard/UI/LeftMenu'
import AuthProvider from '@components/Security/AuthProviderDepreceated'
import React from 'react'
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
return (
<>
<AuthProvider>
<div className='flex'>
<LeftMenu/>
<div className='flex w-full'>
{children}
</div>
</div>
</AuthProvider>
</>
)
}
export default DashboardLayout

View file

@ -0,0 +1,51 @@
'use client';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { getUriWithOrg } from '@services/config/config'
import { Info } from 'lucide-react'
import Link from 'next/link'
import React from 'react'
import { motion } from 'framer-motion'
import OrgEditGeneral from '@components/Dashboard/Org/OrgEditGeneral/OrgEditGeneral'
export type OrgParams = {
subpage: string
orgslug: string
}
function OrgPage({ params }: { params: OrgParams }) {
return (
<div className='h-full w-full bg-[#f8f8f8]'>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<BreadCrumbs type='org' ></BreadCrumbs>
<div className='my-2 tracking-tighter'>
<div className='w-100 flex justify-between'>
<div className='pt-3 flex font-bold text-4xl'>Organization Settings</div>
</div>
</div>
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/org/settings/general`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Info size={16} />
<div>General</div>
</div>
</div>
</Link>
</div>
</div>
<div className='h-6'></div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
>
{params.subpage == 'general' ? <OrgEditGeneral /> : ''}
</motion.div>
</div>
)
}
export default OrgPage

View file

@ -0,0 +1,13 @@
import PageLoading from '@components/Objects/Loaders/PageLoading'
import React from 'react'
function DashboardHome() {
return (
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
<PageLoading />
<div className='text-neutral-400 font-bold animate-pulse text-2xl'>This page is work in progress</div>
</div>
)
}
export default DashboardHome

View file

@ -0,0 +1,69 @@
'use client';
import React, { useEffect } from 'react'
import { motion } from 'framer-motion';
import UserEditGeneral from '@components/Dashboard/User/UserEditGeneral/UserEditGeneral';
import UserEditPassword from '@components/Dashboard/User/UserEditPassword/UserEditPassword';
import Link from 'next/link';
import { getUriWithOrg } from '@services/config/config';
import { Info, Lock } from 'lucide-react';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
import { useAuth } from '@components/Security/AuthContext';
export type SettingsParams = {
subpage: string
orgslug: string
}
function SettingsPage({ params }: { params: SettingsParams }) {
const auth = useAuth() as any;
useEffect(() => {
}
, [auth])
return (
<div className='h-full w-full bg-[#f8f8f8]'>
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<BreadCrumbs type='user' last_breadcrumb={auth?.user?.username} ></BreadCrumbs>
<div className='my-2 tracking-tighter'>
<div className='w-100 flex justify-between'>
<div className='pt-3 flex font-bold text-4xl'>Account Settings</div>
</div>
</div>
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/general`}>
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Info size={16} />
<div>General</div>
</div>
</div>
</Link>
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user/settings/security`}>
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'security' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
<div className='flex items-center space-x-2.5 mx-2'>
<Lock size={16} />
<div>Password</div>
</div>
</div>
</Link>
</div>
</div>
<div className='h-6'></div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
>
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
{params.subpage == 'security' ? <UserEditPassword /> : ''}
</motion.div>
</div>
)
}
export default SettingsPage

Some files were not shown because too many files have changed in this diff Show more