mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #133 from learnhouse/feat/psql-migration-and-frontend-improvements
PostgreSQL migration and frontend improvements
This commit is contained in:
commit
0e2e66d0e6
172 changed files with 7488 additions and 5008 deletions
|
|
@ -46,7 +46,8 @@ class HostingConfig(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig(BaseModel):
|
class DatabaseConfig(BaseModel):
|
||||||
mongodb_connection_string: Optional[str]
|
sql_connection_string: Optional[str]
|
||||||
|
mongo_connection_string: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class LearnHouseConfig(BaseModel):
|
class LearnHouseConfig(BaseModel):
|
||||||
|
|
@ -105,9 +106,7 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
||||||
env_allowed_origins = env_allowed_origins.split(",")
|
env_allowed_origins = env_allowed_origins.split(",")
|
||||||
env_allowed_regexp = os.environ.get("LEARNHOUSE_ALLOWED_REGEXP")
|
env_allowed_regexp = os.environ.get("LEARNHOUSE_ALLOWED_REGEXP")
|
||||||
env_self_hosted = os.environ.get("LEARNHOUSE_SELF_HOSTED")
|
env_self_hosted = os.environ.get("LEARNHOUSE_SELF_HOSTED")
|
||||||
env_mongodb_connection_string = os.environ.get(
|
env_sql_connection_string = os.environ.get("LEARNHOUSE_SQL_CONNECTION_STRING")
|
||||||
"LEARNHOUSE_MONGODB_CONNECTION_STRING"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sentry Config
|
# Sentry Config
|
||||||
env_sentry_dsn = os.environ.get("LEARNHOUSE_SENTRY_DSN")
|
env_sentry_dsn = os.environ.get("LEARNHOUSE_SENTRY_DSN")
|
||||||
|
|
@ -166,9 +165,13 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Database config
|
# 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", {}
|
"database_config", {}
|
||||||
).get("mongodb_connection_string")
|
).get("sql_connection_string")
|
||||||
|
|
||||||
|
mongo_connection_string = yaml_config.get("database_config", {}).get(
|
||||||
|
"mongo_connection_string"
|
||||||
|
)
|
||||||
|
|
||||||
# Sentry config
|
# Sentry config
|
||||||
# check if the sentry config is provided in the YAML file
|
# check if the sentry config is provided in the YAML file
|
||||||
|
|
@ -210,7 +213,8 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
||||||
content_delivery=content_delivery,
|
content_delivery=content_delivery,
|
||||||
)
|
)
|
||||||
database_config = DatabaseConfig(
|
database_config = DatabaseConfig(
|
||||||
mongodb_connection_string=mongodb_connection_string
|
sql_connection_string=sql_connection_string,
|
||||||
|
mongo_connection_string=mongo_connection_string,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create LearnHouseConfig object
|
# Create LearnHouseConfig object
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ hosting_config:
|
||||||
domain: learnhouse.app
|
domain: learnhouse.app
|
||||||
ssl: true
|
ssl: true
|
||||||
allowed_origins:
|
allowed_origins:
|
||||||
- http://localhost:3000
|
- http://localhost:3000
|
||||||
- http://localhost:3001
|
- http://localhost:3001
|
||||||
cookies_config:
|
cookies_config:
|
||||||
domain: ".localhost"
|
domain: ".localhost"
|
||||||
allowed_regexp: '\b((?:https?://)[^\s/$.?#].[^\s]*)\b'
|
allowed_regexp: '\b((?:https?://)[^\s/$.?#].[^\s]*)\b'
|
||||||
|
|
@ -25,4 +25,5 @@ hosting_config:
|
||||||
endpoint_url: ""
|
endpoint_url: ""
|
||||||
|
|
||||||
database_config:
|
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/
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
fastapi==0.101.1
|
fastapi==0.104.1
|
||||||
pydantic>=1.8.0,<2.0.0
|
pydantic>=1.8.0,<2.0.0
|
||||||
|
sqlmodel==0.0.10
|
||||||
uvicorn==0.23.2
|
uvicorn==0.23.2
|
||||||
pymongo==4.3.3
|
pymongo==4.3.3
|
||||||
motor==3.1.1
|
motor==3.1.1
|
||||||
|
psycopg2
|
||||||
python-multipart
|
python-multipart
|
||||||
boto3
|
boto3
|
||||||
botocore
|
botocore
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,34 @@
|
||||||
import logging
|
import logging
|
||||||
|
from config.config import get_learnhouse_config
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from sqlmodel import SQLModel, Session, create_engine
|
||||||
import motor.motor_asyncio
|
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):
|
async def connect_to_db(app: FastAPI):
|
||||||
logging.info("Connecting to database...")
|
app.db_engine = engine # type: ignore
|
||||||
try:
|
logging.info("LearnHouse database has been started.")
|
||||||
app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore
|
SQLModel.metadata.create_all(engine)
|
||||||
app.learnhouse_config.database_config.mongodb_connection_string) # type: ignore
|
|
||||||
app.db = app.mongodb_client["learnhouse"] # type: ignore
|
# MongoDB for migration purposes
|
||||||
logging.info("Connected to database!")
|
# mongodb
|
||||||
except Exception as e:
|
app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore
|
||||||
logging.error("Failed to connect to database!")
|
app.learnhouse_config.database_config.mongo_connection_string # type: ignore
|
||||||
logging.error(e)
|
) # type: ignore
|
||||||
|
app.db = app.mongodb_client["learnhouse"] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def get_db_session():
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
async def close_database(app: FastAPI):
|
async def close_database(app: FastAPI):
|
||||||
app.mongodb_client.close() # type: ignore
|
|
||||||
logging.info("LearnHouse has been shut down.")
|
logging.info("LearnHouse has been shut down.")
|
||||||
return app
|
return app
|
||||||
|
|
|
||||||
72
apps/api/src/db/activities.py
Normal file
72
apps/api/src/db/activities.py
Normal 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
46
apps/api/src/db/blocks.py
Normal 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
|
||||||
13
apps/api/src/db/chapter_activities.py
Normal file
13
apps/api/src/db/chapter_activities.py
Normal 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
|
||||||
68
apps/api/src/db/chapters.py
Normal file
68
apps/api/src/db/chapters.py
Normal 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
|
||||||
39
apps/api/src/db/collections.py
Normal file
39
apps/api/src/db/collections.py
Normal 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
|
||||||
12
apps/api/src/db/collections_courses.py
Normal file
12
apps/api/src/db/collections_courses.py
Normal 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
|
||||||
17
apps/api/src/db/course_chapters.py
Normal file
17
apps/api/src/db/course_chapters.py
Normal 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
|
||||||
72
apps/api/src/db/courses.py
Normal file
72
apps/api/src/db/courses.py
Normal 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
|
||||||
31
apps/api/src/db/install.py
Normal file
31
apps/api/src/db/install.py
Normal 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
|
||||||
21
apps/api/src/db/organization_settings.py
Normal file
21
apps/api/src/db/organization_settings.py
Normal 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
|
||||||
30
apps/api/src/db/organizations.py
Normal file
30
apps/api/src/db/organizations.py
Normal 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
|
||||||
18
apps/api/src/db/resource_authors.py
Normal file
18
apps/api/src/db/resource_authors.py
Normal 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
72
apps/api/src/db/roles.py
Normal 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))
|
||||||
57
apps/api/src/db/trail_runs.py
Normal file
57
apps/api/src/db/trail_runs.py
Normal 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
|
||||||
34
apps/api/src/db/trail_steps.py
Normal file
34
apps/api/src/db/trail_steps.py
Normal 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
34
apps/api/src/db/trails.py
Normal 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
|
||||||
14
apps/api/src/db/user_organizations.py
Normal file
14
apps/api/src/db/user_organizations.py
Normal 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
50
apps/api/src/db/users.py
Normal 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 = ""
|
||||||
|
|
@ -35,3 +35,4 @@ v1_router.include_router(
|
||||||
tags=["install"],
|
tags=["install"],
|
||||||
dependencies=[Depends(isInstallModeEnabled)],
|
dependencies=[Depends(isInstallModeEnabled)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
|
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
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 config.config import get_learnhouse_config
|
||||||
from src.security.auth import AuthJWT, authenticate_user
|
from src.security.auth import AuthJWT, authenticate_user
|
||||||
from src.services.users.users import PublicUser
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/refresh")
|
@router.post("/refresh")
|
||||||
def refresh(response: Response,Authorize: AuthJWT = Depends()):
|
def refresh(response: Response, Authorize: AuthJWT = Depends()):
|
||||||
"""
|
"""
|
||||||
The jwt_refresh_token_required() function insures a valid refresh
|
The jwt_refresh_token_required() function insures a valid refresh
|
||||||
token is present in the request before running any code below that function.
|
token is present in the request before running any code below that function.
|
||||||
|
|
@ -21,7 +23,12 @@ def refresh(response: Response,Authorize: AuthJWT = Depends()):
|
||||||
current_user = Authorize.get_jwt_subject()
|
current_user = Authorize.get_jwt_subject()
|
||||||
new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore
|
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}
|
return {"access_token": new_access_token}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,8 +38,11 @@ async def login(
|
||||||
response: Response,
|
response: Response,
|
||||||
Authorize: AuthJWT = Depends(),
|
Authorize: AuthJWT = Depends(),
|
||||||
form_data: OAuth2PasswordRequestForm = 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:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -44,8 +54,14 @@ async def login(
|
||||||
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
|
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
|
||||||
Authorize.set_refresh_cookies(refresh_token)
|
Authorize.set_refresh_cookies(refresh_token)
|
||||||
# set cookies using fastapi
|
# 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)
|
response.set_cookie(
|
||||||
user = PublicUser(**user.dict())
|
key="access_token_cookie",
|
||||||
|
value=access_token,
|
||||||
|
httponly=False,
|
||||||
|
domain=get_learnhouse_config().hosting_config.cookie_config.domain,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = UserRead.from_orm(user)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"user": user,
|
"user": user,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
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.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.imageBlock.imageBlock import (
|
||||||
from src.services.blocks.block_types.videoBlock.videoBlock import create_video_block, get_video_block
|
create_image_block,
|
||||||
from src.services.blocks.block_types.pdfBlock.pdfBlock import create_pdf_block, get_pdf_block
|
get_image_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.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
|
from src.services.users.users import PublicUser
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
@ -12,83 +23,93 @@ router = APIRouter()
|
||||||
# Image Block
|
# Image Block
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/image")
|
@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
|
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")
|
@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
|
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
|
# Video Block
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/video")
|
@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
|
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")
|
@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
|
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
|
# PDF Block
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
@router.post("/pdf")
|
@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
|
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")
|
@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
|
Get pdf file
|
||||||
"""
|
"""
|
||||||
return await get_pdf_block(request, file_id, current_user)
|
return await get_pdf_block(request, block_uuid, current_user, db_session)
|
||||||
|
|
||||||
|
|
||||||
####################
|
|
||||||
# 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)
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
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 (
|
from src.services.courses.activities.activities import (
|
||||||
Activity,
|
|
||||||
create_activity,
|
create_activity,
|
||||||
get_activity,
|
get_activity,
|
||||||
get_activities,
|
get_activities,
|
||||||
|
|
@ -14,7 +17,6 @@ from src.services.courses.activities.video import (
|
||||||
create_external_video_activity,
|
create_external_video_activity,
|
||||||
create_video_activity,
|
create_video_activity,
|
||||||
)
|
)
|
||||||
from src.services.users.schemas.users import PublicUser
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -22,17 +24,14 @@ router = APIRouter()
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def api_create_activity(
|
async def api_create_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
activity_object: Activity,
|
activity_object: ActivityCreate,
|
||||||
org_id: str,
|
|
||||||
coursechapter_id: str,
|
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> ActivityRead:
|
||||||
"""
|
"""
|
||||||
Create new activity
|
Create new activity
|
||||||
"""
|
"""
|
||||||
return await create_activity(
|
return await create_activity(request, activity_object, current_user, db_session)
|
||||||
request, activity_object, org_id, coursechapter_id, current_user
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{activity_id}")
|
@router.get("/{activity_id}")
|
||||||
|
|
@ -40,36 +39,43 @@ async def api_get_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
activity_id: str,
|
activity_id: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> ActivityRead:
|
||||||
"""
|
"""
|
||||||
Get single activity by activity_id
|
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}")
|
@router.get("/chapter/{chapter_id}")
|
||||||
async def api_get_activities(
|
async def api_get_chapter_activities(
|
||||||
request: Request,
|
request: Request,
|
||||||
coursechapter_id: str,
|
chapter_id: int,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
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(
|
async def api_update_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
activity_object: Activity,
|
activity_object: ActivityUpdate,
|
||||||
activity_id: str,
|
activity_uuid: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> ActivityRead:
|
||||||
"""
|
"""
|
||||||
Update activity by activity_id
|
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}")
|
@router.delete("/{activity_id}")
|
||||||
|
|
@ -77,11 +83,12 @@ async def api_delete_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
activity_id: str,
|
activity_id: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Delete activity by activity_id
|
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
|
# Video activity
|
||||||
|
|
@ -91,15 +98,21 @@ async def api_delete_activity(
|
||||||
async def api_create_video_activity(
|
async def api_create_video_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
name: str = Form(),
|
name: str = Form(),
|
||||||
coursechapter_id: str = Form(),
|
chapter_id: str = Form(),
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
video_file: UploadFile | None = None,
|
video_file: UploadFile | None = None,
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> ActivityRead:
|
||||||
"""
|
"""
|
||||||
Create new activity
|
Create new activity
|
||||||
"""
|
"""
|
||||||
return await create_video_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,
|
request: Request,
|
||||||
external_video: ExternalVideo,
|
external_video: ExternalVideo,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> ActivityRead:
|
||||||
"""
|
"""
|
||||||
Create new activity
|
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")
|
@router.post("/documentpdf")
|
||||||
async def api_create_documentpdf_activity(
|
async def api_create_documentpdf_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
name: str = Form(),
|
name: str = Form(),
|
||||||
coursechapter_id: str = Form(),
|
chapter_id: str = Form(),
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
pdf_file: UploadFile | None = None,
|
pdf_file: UploadFile | None = None,
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> ActivityRead:
|
||||||
"""
|
"""
|
||||||
Create new activity
|
Create new activity
|
||||||
"""
|
"""
|
||||||
return await create_documentpdf_activity(
|
return await create_documentpdf_activity(
|
||||||
request, name, coursechapter_id, current_user, pdf_file
|
request, name, chapter_id, current_user, db_session, pdf_file
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,22 @@
|
||||||
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, Request
|
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.services.users.users import PublicUser
|
||||||
from src.security.auth import get_current_user
|
from src.security.auth import get_current_user
|
||||||
|
|
||||||
|
|
@ -8,57 +24,104 @@ router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@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 CourseChapter
|
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}")
|
@router.get("/{chapter_id}")
|
||||||
async def api_get_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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}")
|
@router.get("/course/{course_uuid}/meta", deprecated=True)
|
||||||
async def api_get_coursechapter_meta(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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}")
|
@router.put("/course/{course_uuid}/order")
|
||||||
async def api_update_coursechapter_meta(request: Request,course_id: str, coursechapters_metadata: CourseChapterMetaData, current_user: PublicUser = Depends(get_current_user)):
|
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}")
|
@router.get("/course/{course_id}/page/{page}/limit/{limit}")
|
||||||
async def api_get_coursechapter_by(request: Request,course_id: str, page: int, limit: int):
|
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 CourseChapters by page and limit
|
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}")
|
@router.put("/{chapter_id}")
|
||||||
async def api_update_coursechapter(request: Request,coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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
|
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}")
|
@router.delete("/{chapter_id}")
|
||||||
async def api_delete_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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
|
Delete CourseChapters by ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return await delete_coursechapter(request,coursechapter_id, current_user)
|
return await delete_chapter(request, chapter_id, current_user, db_session)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from src.core.events.database import get_db_session
|
||||||
|
from src.db.collections import CollectionCreate, CollectionRead, CollectionUpdate
|
||||||
from src.security.auth import get_current_user
|
from src.security.auth import get_current_user
|
||||||
from src.services.users.users import PublicUser
|
from src.services.users.users import PublicUser
|
||||||
from src.services.courses.collections import (
|
from src.services.courses.collections import (
|
||||||
Collection,
|
|
||||||
create_collection,
|
create_collection,
|
||||||
get_collection,
|
get_collection,
|
||||||
get_collections,
|
get_collections,
|
||||||
|
|
@ -17,64 +19,69 @@ router = APIRouter()
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def api_create_collection(
|
async def api_create_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_object: Collection,
|
collection_object: CollectionCreate,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> CollectionRead:
|
||||||
"""
|
"""
|
||||||
Create new Collection
|
Create new Collection
|
||||||
"""
|
"""
|
||||||
return await create_collection(request, collection_object, current_user)
|
return await create_collection(request, collection_object, current_user, db_session)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{collection_id}")
|
@router.get("/{collection_uuid}")
|
||||||
async def api_get_collection(
|
async def api_get_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_id: str,
|
collection_uuid: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> CollectionRead:
|
||||||
"""
|
"""
|
||||||
Get single collection by ID
|
Get single collection by ID
|
||||||
"""
|
"""
|
||||||
return await get_collection(request, collection_id, current_user)
|
return await get_collection(request, collection_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(
|
async def api_get_collections_by(
|
||||||
request: Request,
|
request: Request,
|
||||||
page: int,
|
page: int,
|
||||||
limit: int,
|
limit: int,
|
||||||
org_id: str,
|
org_id: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> List[CollectionRead]:
|
||||||
"""
|
"""
|
||||||
Get collections by page and limit
|
Get collections by page and limit
|
||||||
"""
|
"""
|
||||||
return await get_collections(request, org_id, current_user, page, limit)
|
return await get_collections(request, org_id, current_user, db_session, page, limit)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{collection_id}")
|
@router.put("/{collection_uuid}")
|
||||||
async def api_update_collection(
|
async def api_update_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_object: Collection,
|
collection_object: CollectionUpdate,
|
||||||
collection_id: str,
|
collection_uuid: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
):
|
db_session=Depends(get_db_session),
|
||||||
|
) -> CollectionRead:
|
||||||
"""
|
"""
|
||||||
Update collection by ID
|
Update collection by ID
|
||||||
"""
|
"""
|
||||||
return await update_collection(
|
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(
|
async def api_delete_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_id: str,
|
collection_uuid: str,
|
||||||
current_user: PublicUser = Depends(get_current_user),
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Delete collection by ID
|
Delete collection by ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return await delete_collection(request, collection_id, current_user)
|
return await delete_collection(request, collection_uuid, current_user, db_session)
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,147 @@
|
||||||
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
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.security.auth import get_current_user
|
||||||
|
from src.services.courses.courses import (
|
||||||
from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses_orgslug, update_course, delete_course, update_course_thumbnail
|
create_course,
|
||||||
from src.services.users.users import PublicUser
|
get_course,
|
||||||
|
get_course_meta,
|
||||||
|
get_courses_orgslug,
|
||||||
|
update_course,
|
||||||
|
delete_course,
|
||||||
|
update_course_thumbnail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@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
|
Create new Course
|
||||||
"""
|
"""
|
||||||
course = Course(name=name, mini_description=mini_description, description=description,
|
course = CourseCreate(
|
||||||
org_id=org_id, public=public, thumbnail="", chapters=[], chapters_content=[], learnings=[])
|
name=name,
|
||||||
return await create_course(request, course, org_id, current_user, thumbnail)
|
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}")
|
@router.put("/{course_uuid}/thumbnail")
|
||||||
async def api_create_course_thumbnail(request: Request, course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)):
|
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
|
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}")
|
@router.get("/{course_uuid}")
|
||||||
async def api_get_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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}")
|
@router.get("/{course_uuid}/meta")
|
||||||
async def api_get_course_meta(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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}")
|
@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}")
|
@router.put("/{course_uuid}")
|
||||||
async def api_update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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}")
|
@router.delete("/{course_uuid}")
|
||||||
async def api_delete_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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
|
Delete Course by ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return await delete_course(request, course_id, current_user)
|
return await delete_course(request, course_uuid, current_user, db_session)
|
||||||
|
|
|
||||||
|
|
@ -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 config.config import get_learnhouse_config
|
||||||
from src.services.dev.mocks.initial import create_initial_data
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
@ -12,7 +14,9 @@ async def config():
|
||||||
return config.dict()
|
return config.dict()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/mock/initial")
|
@router.get("/migrate_from_mongo")
|
||||||
async def initial_data(request: Request):
|
async def migrate_from_mongo(
|
||||||
await create_initial_data(request)
|
request: Request,
|
||||||
return {"Message": "Initial data created 🤖"}
|
db_session: Session = Depends(get_db_session),
|
||||||
|
):
|
||||||
|
return await start_migrate_from_mongo(request, db_session)
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
from src.services.install.install import (
|
||||||
create_install_instance,
|
create_install_instance,
|
||||||
create_sample_data,
|
|
||||||
get_latest_install_instance,
|
get_latest_install_instance,
|
||||||
install_create_organization,
|
install_create_organization,
|
||||||
install_create_organization_user,
|
install_create_organization_user,
|
||||||
install_default_elements,
|
install_default_elements,
|
||||||
update_install_instance,
|
update_install_instance,
|
||||||
)
|
)
|
||||||
from src.services.orgs.schemas.orgs import Organization
|
|
||||||
from src.services.users.schemas.users import UserWithPassword
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start")
|
@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
|
# create install
|
||||||
install = await create_install_instance(request, data)
|
install = await create_install_instance(request, data, db_session)
|
||||||
|
|
||||||
return install
|
return install
|
||||||
|
|
||||||
|
|
||||||
@router.get("/latest")
|
@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
|
# get latest created install
|
||||||
install = await get_latest_install_instance(request)
|
install = await get_latest_install_instance(request, db_session=db_session)
|
||||||
|
|
||||||
return install
|
return install
|
||||||
|
|
||||||
|
|
||||||
@router.post("/default_elements")
|
@router.post("/default_elements")
|
||||||
async def api_install_def_elements(request: Request):
|
async def api_install_def_elements(
|
||||||
elements = await install_default_elements(request, {})
|
request: Request,
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
|
):
|
||||||
|
elements = await install_default_elements(request, {}, db_session)
|
||||||
|
|
||||||
return elements
|
return elements
|
||||||
|
|
||||||
|
|
||||||
@router.post("/org")
|
@router.post("/org")
|
||||||
async def api_install_org(request: Request, org: Organization):
|
async def api_install_org(
|
||||||
organization = await install_create_organization(request, org)
|
request: Request,
|
||||||
|
org: OrganizationCreate,
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
|
):
|
||||||
|
organization = await install_create_organization(request, org, db_session)
|
||||||
|
|
||||||
return organization
|
return organization
|
||||||
|
|
||||||
|
|
||||||
@router.post("/user")
|
@router.post("/user")
|
||||||
async def api_install_user(request: Request, data: UserWithPassword, org_slug: str):
|
async def api_install_user(
|
||||||
user = await install_create_organization_user(request, data, org_slug)
|
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
|
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")
|
@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"]
|
request.app.db["installs"]
|
||||||
|
|
||||||
# get latest created install
|
# get latest created install
|
||||||
install = await update_install_instance(request, data, step)
|
install = await update_install_instance(request, data, step, db_session)
|
||||||
|
|
||||||
return install
|
return install
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,127 @@
|
||||||
|
from typing import List
|
||||||
from fastapi import APIRouter, Depends, Request, UploadFile
|
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.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.orgs.orgs import (
|
||||||
from src.services.users.users import PublicUser, User
|
create_org,
|
||||||
|
delete_org,
|
||||||
|
get_organization,
|
||||||
|
get_organization_by_slug,
|
||||||
|
get_orgs_by_user,
|
||||||
|
update_org,
|
||||||
|
update_org_logo,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@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
|
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}")
|
@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
|
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}")
|
@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
|
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")
|
@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
|
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}")
|
@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}")
|
@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
|
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}")
|
@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
|
Delete Org by ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return await delete_org(request, org_id, current_user)
|
return await delete_org(request, org_id, current_user, db_session)
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,63 @@
|
||||||
from fastapi import APIRouter, Depends, Request
|
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.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.roles.roles import create_role, delete_role, read_role, update_role
|
from src.db.users import PublicUser
|
||||||
from src.services.users.schemas.users import PublicUser
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@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
|
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}")
|
@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
|
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}")
|
@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
|
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}")
|
@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
|
Delete role by ID
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return await delete_role(request, role_id, current_user)
|
return await delete_role(request, db_session, role_id, current_user)
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,97 @@
|
||||||
from fastapi import APIRouter, Depends, Request
|
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.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 = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start")
|
@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
|
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")
|
@router.get("/")
|
||||||
async def api_get_trail_by_orgid(request: Request, org_slug: str, user=Depends(get_current_user)):
|
async def api_get_user_trail(
|
||||||
|
request: Request,
|
||||||
|
user=Depends(get_current_user),
|
||||||
|
db_session=Depends(get_db_session),
|
||||||
|
) -> TrailRead:
|
||||||
"""
|
"""
|
||||||
Get a user trails
|
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")
|
@router.get("/org/{org_id}/trail")
|
||||||
async def api_get_trail_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)):
|
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
|
Get a user trails using org slug
|
||||||
"""
|
"""
|
||||||
return await get_user_trail_with_orgslug(request, user, org_slug=org_slug)
|
return await get_user_trail_with_orgid(
|
||||||
|
request, user, org_id=org_id, db_session=db_session
|
||||||
# Courses in trail
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/org_slug/{org_slug}/add_course/{course_id}")
|
@router.post("/add_course/{course_uuid}")
|
||||||
async def api_add_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
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
|
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}")
|
@router.delete("/remove_course/{course_uuid}")
|
||||||
async def api_remove_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
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
|
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}")
|
@router.post("/add_activity/{activity_uuid}")
|
||||||
async def api_add_activity_to_trail(request: Request, activity_id: str, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
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
|
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
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.security.auth import get_current_user
|
||||||
from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserWithPassword
|
from src.core.events.database import get_db_session
|
||||||
from src.services.users.users import create_user, delete_user, get_profile_metadata, get_user_by_userid, update_user, update_user_password
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
router = APIRouter()
|
||||||
|
|
@ -17,50 +34,119 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)):
|
||||||
"""
|
"""
|
||||||
return current_user.dict()
|
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.post("/{org_id}", response_model=UserRead, tags=["users"])
|
||||||
@router.get("/user_id/{user_id}")
|
async def api_create_user_with_orgid(
|
||||||
async def api_get_user_by_userid(request: Request,user_id: str):
|
*,
|
||||||
|
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("/")
|
@router.post("/", response_model=UserRead, tags=["users"])
|
||||||
async def api_create_user(request: Request,user_object: UserWithPassword, org_slug: str ):
|
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}")
|
@router.get("/id/{user_id}", response_model=UserRead, tags=["users"])
|
||||||
async def api_delete_user(request: Request, user_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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 read_user_by_id(request, db_session, current_user, user_id)
|
||||||
return await delete_user(request, current_user, user_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/user_id/{user_id}")
|
@router.get("/uuid/{user_uuid}", response_model=UserRead, tags=["users"])
|
||||||
async def api_update_user(request: Request, user_object: User, user_id: str, current_user: PublicUser = Depends(get_current_user)):
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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 config.config import get_learnhouse_config
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import Depends, HTTPException, Request, status
|
from fastapi import Depends, HTTPException, Request, status
|
||||||
|
|
@ -5,8 +9,7 @@ from fastapi.security import OAuth2PasswordBearer
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from src.services.dev.dev import isDevModeEnabled
|
from src.services.dev.dev import isDevModeEnabled
|
||||||
from src.services.users.schemas.users import AnonymousUser, PublicUser
|
from src.services.users.users import security_verify_password
|
||||||
from src.services.users.users import security_get_user, security_verify_password
|
|
||||||
from src.security.security import ALGORITHM, SECRET_KEY
|
from src.security.security import ALGORITHM, SECRET_KEY
|
||||||
from fastapi_jwt_auth import AuthJWT
|
from fastapi_jwt_auth import AuthJWT
|
||||||
|
|
||||||
|
|
@ -45,10 +48,13 @@ class TokenData(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
#### Classes ####################################################
|
#### Classes ####################################################
|
||||||
|
async def authenticate_user(
|
||||||
|
request: Request,
|
||||||
async def authenticate_user(request: Request, email: str, password: str):
|
email: str,
|
||||||
user = await security_get_user(request, email)
|
password: str,
|
||||||
|
db_session: Session,
|
||||||
|
) -> User | bool:
|
||||||
|
user = await security_get_user(request, db_session, email)
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return False
|
||||||
if not await security_verify_password(password, user.password):
|
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
|
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(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Could not validate credentials",
|
detail="Could not validate credentials",
|
||||||
|
|
@ -81,7 +91,7 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
if username:
|
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:
|
if user is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
return PublicUser(**user.dict())
|
return PublicUser(**user.dict())
|
||||||
|
|
@ -89,6 +99,6 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
|
||||||
return AnonymousUser()
|
return AnonymousUser()
|
||||||
|
|
||||||
|
|
||||||
async def non_public_endpoint(current_user: PublicUser):
|
async def non_public_endpoint(current_user: UserRead | AnonymousUser):
|
||||||
if isinstance(current_user, AnonymousUser):
|
if isinstance(current_user, AnonymousUser):
|
||||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,31 @@
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from fastapi import HTTPException, status, Request
|
from fastapi import HTTPException, status, Request
|
||||||
from src.security.rbac.utils import check_element_type, get_id_identifier_of_element
|
from sqlalchemy import null
|
||||||
from src.services.roles.schemas.roles import RoleInDB
|
from sqlmodel import Session, select
|
||||||
from src.services.users.schemas.users import UserRolesInOrganization
|
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(
|
async def authorization_verify_if_element_is_public(
|
||||||
request,
|
request,
|
||||||
element_id: str,
|
element_uuid: str,
|
||||||
user_id: str,
|
|
||||||
action: Literal["read"],
|
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
|
# Verifies if the element is public
|
||||||
if (
|
if element_nature == ("courses" or "collections") and action == "read":
|
||||||
element_nature == ("courses" or "collections")
|
|
||||||
and action == "read"
|
|
||||||
and user_id == "anonymous"
|
|
||||||
):
|
|
||||||
if element_nature == "courses":
|
if element_nature == "courses":
|
||||||
courses = request.app.db["courses"]
|
statement = select(Course).where(
|
||||||
course = await courses.find_one({"course_id": element_id})
|
Course.public is True, Course.course_uuid == element_uuid
|
||||||
|
)
|
||||||
if course["public"]:
|
course = db_session.exec(statement).first()
|
||||||
|
if course:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -32,10 +34,12 @@ async def authorization_verify_if_element_is_public(
|
||||||
)
|
)
|
||||||
|
|
||||||
if element_nature == "collections":
|
if element_nature == "collections":
|
||||||
collections = request.app.db["collections"]
|
statement = select(Collection).where(
|
||||||
collection = await collections.find_one({"collection_id": element_id})
|
Collection.public is True, Collection.collection_uuid == element_uuid
|
||||||
|
)
|
||||||
|
collection = db_session.exec(statement).first()
|
||||||
|
|
||||||
if collection["public"]:
|
if collection:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
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(
|
async def authorization_verify_if_user_is_author(
|
||||||
request,
|
request,
|
||||||
user_id: str,
|
user_id: int,
|
||||||
action: Literal["read", "update", "delete", "create"],
|
action: Literal["read", "update", "delete", "create"],
|
||||||
element_id: str,
|
element_uuid: str,
|
||||||
|
db_session: Session,
|
||||||
):
|
):
|
||||||
if action == "update" or "delete" or "read":
|
if action == "update" or "delete" or "read":
|
||||||
element_nature = await check_element_type(element_id)
|
statement = select(ResourceAuthor).where(
|
||||||
elements = request.app.db[element_nature]
|
ResourceAuthor.resource_uuid == element_uuid
|
||||||
element_identifier = await get_id_identifier_of_element(element_id)
|
)
|
||||||
element = await elements.find_one({element_identifier: element_id})
|
resource_author = db_session.exec(statement).first()
|
||||||
if user_id in element["authors"]:
|
|
||||||
return True
|
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:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Tested and working
|
||||||
|
async def authorization_verify_based_on_roles(
|
||||||
|
request: Request,
|
||||||
|
user_id: int,
|
||||||
|
action: Literal["read", "update", "delete", "create"],
|
||||||
|
element_uuid: str,
|
||||||
|
db_session: Session,
|
||||||
|
):
|
||||||
|
element_type = await check_element_type(element_uuid)
|
||||||
|
|
||||||
|
# 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:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def authorization_verify_based_on_roles(
|
# Tested and working
|
||||||
request: Request,
|
|
||||||
user_id: str,
|
|
||||||
action: Literal["read", "update", "delete", "create"],
|
|
||||||
roles_list: list[UserRolesInOrganization],
|
|
||||||
element_id: str,
|
|
||||||
):
|
|
||||||
element_type = await check_element_type(element_id)
|
|
||||||
element = request.app.db[element_type]
|
|
||||||
roles = request.app.db["roles"]
|
|
||||||
|
|
||||||
# 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",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def authorization_verify_based_on_roles_and_authorship(
|
async def authorization_verify_based_on_roles_and_authorship(
|
||||||
request: Request,
|
request: Request,
|
||||||
user_id: str,
|
user_id: int,
|
||||||
action: Literal["read", "update", "delete", "create"],
|
action: Literal["read", "update", "delete", "create"],
|
||||||
roles_list: list[UserRolesInOrganization],
|
element_uuid: str,
|
||||||
element_id: str,
|
db_session: Session,
|
||||||
):
|
):
|
||||||
isAuthor = await authorization_verify_if_user_is_author(
|
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(
|
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:
|
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):
|
async def authorization_verify_if_user_is_anon(user_id: int):
|
||||||
if user_id == "anonymous":
|
if user_id == 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="You should be logged in to perform this action",
|
detail="You should be logged in to perform this action",
|
||||||
|
|
|
||||||
|
|
@ -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
|
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_"):
|
if element_id.startswith("course_"):
|
||||||
return "courses"
|
return "courses"
|
||||||
elif element_id.startswith("user_"):
|
elif element_id.startswith("user_"):
|
||||||
|
|
@ -13,12 +14,14 @@ async def check_element_type(element_id):
|
||||||
return "houses"
|
return "houses"
|
||||||
elif element_id.startswith("org_"):
|
elif element_id.startswith("org_"):
|
||||||
return "organizations"
|
return "organizations"
|
||||||
elif element_id.startswith("coursechapter_"):
|
elif element_id.startswith("chapter_"):
|
||||||
return "coursechapters"
|
return "coursechapters"
|
||||||
elif element_id.startswith("collection_"):
|
elif element_id.startswith("collection_"):
|
||||||
return "collections"
|
return "collections"
|
||||||
elif element_id.startswith("activity_"):
|
elif element_id.startswith("activity_"):
|
||||||
return "activities"
|
return "activities"
|
||||||
|
elif element_id.startswith("role_"):
|
||||||
|
return "roles"
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
|
@ -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"
|
|
||||||
)
|
|
||||||
|
|
@ -1,69 +1,89 @@
|
||||||
|
from datetime import datetime
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from src.db.organizations import Organization
|
||||||
from fastapi import HTTPException, status, UploadFile, Request
|
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.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||||
|
|
||||||
from src.services.users.users import PublicUser
|
from src.services.users.users import PublicUser
|
||||||
|
|
||||||
|
|
||||||
async def create_pdf_block(request: Request, pdf_file: UploadFile, activity_id: str):
|
async def create_pdf_block(
|
||||||
blocks = request.app.db["blocks"]
|
request: Request, pdf_file: UploadFile, activity_uuid: str, db_session: Session
|
||||||
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 = "pdfBlock"
|
block_type = "pdfBlock"
|
||||||
|
|
||||||
# get org_id from activity
|
# get org_uuid
|
||||||
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
|
statement = select(Organization).where(Organization.id == activity.org_id)
|
||||||
org_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
|
# get block id
|
||||||
block_id = str(f"block_{uuid4()}")
|
block_uuid = 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_data = await upload_file_and_return_file_object(
|
block_data = await upload_file_and_return_file_object(
|
||||||
request,
|
request,
|
||||||
pdf_file,
|
pdf_file,
|
||||||
activity_id,
|
activity_uuid,
|
||||||
block_id,
|
block_uuid,
|
||||||
["pdf"],
|
["pdf"],
|
||||||
block_type,
|
block_type,
|
||||||
org_id,
|
org.org_uuid,
|
||||||
course["course_id"],
|
str(course.course_uuid),
|
||||||
)
|
)
|
||||||
|
|
||||||
# create block
|
# create block
|
||||||
block = Block(
|
block = Block(
|
||||||
block_id=block_id,
|
activity_id=activity.id if activity.id else 0,
|
||||||
activity_id=activity_id,
|
block_type=BlockTypeEnum.BLOCK_DOCUMENT_PDF,
|
||||||
block_type=block_type,
|
content=block_data.dict(),
|
||||||
block_data=block_data,
|
org_id=org.id if org.id else 0,
|
||||||
org_id=org_id,
|
course_id=course.id if course.id else 0,
|
||||||
course_id=course["course_id"],
|
block_uuid=block_uuid,
|
||||||
|
creation_date=str(datetime.now()),
|
||||||
|
update_date=str(datetime.now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
# insert block
|
# 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
|
return block
|
||||||
|
|
||||||
|
|
||||||
async def get_pdf_block(request: Request, file_id: str, current_user: PublicUser):
|
async def get_pdf_block(
|
||||||
blocks = request.app.db["blocks"]
|
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 not block:
|
||||||
|
|
||||||
if pdf_block:
|
|
||||||
return Block(**pdf_block)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,73 +1,89 @@
|
||||||
|
from datetime import datetime
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from src.db.organizations import Organization
|
||||||
from fastapi import HTTPException, status, UploadFile, Request
|
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.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||||
|
|
||||||
from src.services.users.users import PublicUser
|
from src.services.users.users import PublicUser
|
||||||
|
|
||||||
|
|
||||||
async def create_video_block(
|
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"]
|
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
|
||||||
activity = request.app.db["activities"]
|
activity = db_session.exec(statement).first()
|
||||||
courses = request.app.db["courses"]
|
|
||||||
|
if not activity:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
|
||||||
|
)
|
||||||
|
|
||||||
block_type = "videoBlock"
|
block_type = "videoBlock"
|
||||||
|
|
||||||
# get org_id from activity
|
# get org_uuid
|
||||||
activity = await activity.find_one(
|
statement = select(Organization).where(Organization.id == activity.org_id)
|
||||||
{"activity_id": activity_id}, {"_id": 0}
|
org = db_session.exec(statement).first()
|
||||||
)
|
|
||||||
org_id = activity["org_id"]
|
# 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
|
# get block id
|
||||||
block_id = str(f"block_{uuid4()}")
|
block_uuid = 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_data = await upload_file_and_return_file_object(
|
block_data = await upload_file_and_return_file_object(
|
||||||
request,
|
request,
|
||||||
video_file,
|
video_file,
|
||||||
activity_id,
|
activity_uuid,
|
||||||
block_id,
|
block_uuid,
|
||||||
["mp4", "webm", "ogg"],
|
["mp4", "webm", "ogg"],
|
||||||
block_type,
|
block_type,
|
||||||
org_id,
|
org.org_uuid,
|
||||||
course["course_id"],
|
str(course.course_uuid),
|
||||||
)
|
)
|
||||||
|
|
||||||
# create block
|
# create block
|
||||||
block = Block(
|
block = Block(
|
||||||
block_id=block_id,
|
activity_id=activity.id if activity.id else 0,
|
||||||
activity_id=activity_id,
|
block_type=BlockTypeEnum.BLOCK_VIDEO,
|
||||||
block_type=block_type,
|
content=block_data.dict(),
|
||||||
block_data=block_data,
|
org_id=org.id if org.id else 0,
|
||||||
org_id=org_id,
|
course_id=course.id if course.id else 0,
|
||||||
course_id=course["course_id"],
|
block_uuid=block_uuid,
|
||||||
|
creation_date=str(datetime.now()),
|
||||||
|
update_date=str(datetime.now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
# insert block
|
# 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
|
return block
|
||||||
|
|
||||||
|
|
||||||
async def get_video_block(request: Request, file_id: str, current_user: PublicUser):
|
async def get_video_block(
|
||||||
blocks = request.app.db["blocks"]
|
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 not block:
|
||||||
|
|
||||||
if video_block:
|
|
||||||
return Block(**video_block)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
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
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,4 @@ class BlockFile(BaseModel):
|
||||||
file_name: str
|
file_name: str
|
||||||
file_size: int
|
file_size: int
|
||||||
file_type: str
|
file_type: str
|
||||||
activity_id: str
|
activity_uuid: str
|
||||||
|
|
@ -7,12 +7,12 @@ from src.services.utils.upload_content import upload_content
|
||||||
async def upload_file_and_return_file_object(
|
async def upload_file_and_return_file_object(
|
||||||
request: Request,
|
request: Request,
|
||||||
file: UploadFile,
|
file: UploadFile,
|
||||||
activity_id: str,
|
activity_uuid: str,
|
||||||
block_id: str,
|
block_id: str,
|
||||||
list_of_allowed_file_formats: list,
|
list_of_allowed_file_formats: list,
|
||||||
type_of_block: str,
|
type_of_block: str,
|
||||||
org_id: str,
|
org_uuid: str,
|
||||||
course_id: str,
|
course_uuid: str,
|
||||||
):
|
):
|
||||||
# get file id
|
# get file id
|
||||||
file_id = str(uuid.uuid4())
|
file_id = str(uuid.uuid4())
|
||||||
|
|
@ -45,12 +45,12 @@ async def upload_file_and_return_file_object(
|
||||||
file_name=file_name,
|
file_name=file_name,
|
||||||
file_size=file_size,
|
file_size=file_size,
|
||||||
file_type=file_type,
|
file_type=file_type,
|
||||||
activity_id=activity_id,
|
activity_uuid=activity_uuid,
|
||||||
)
|
)
|
||||||
|
|
||||||
await upload_content(
|
await upload_content(
|
||||||
f"courses/{course_id}/activities/{activity_id}/dynamic/blocks/{type_of_block}/{block_id}",
|
f"courses/{course_uuid}/activities/{activity_uuid}/dynamic/blocks/{type_of_block}/{block_id}",
|
||||||
org_id=org_id,
|
org_uuid=org_uuid,
|
||||||
file_binary=file_binary,
|
file_binary=file_binary,
|
||||||
file_and_format=f"{file_id}.{file_format}",
|
file_and_format=f"{file_id}.{file_format}",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,17 @@
|
||||||
from typing import Literal
|
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 (
|
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,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.services.users.schemas.users import AnonymousUser, PublicUser
|
from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
|
||||||
from fastapi import HTTPException, status, Request
|
from src.db.chapter_activities import ChapterActivity
|
||||||
|
from src.db.users import AnonymousUser, PublicUser
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
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
|
# CRUD
|
||||||
|
|
@ -38,148 +20,162 @@ class ActivityInDB(Activity):
|
||||||
|
|
||||||
async def create_activity(
|
async def create_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
activity_object: Activity,
|
activity_object: ActivityCreate,
|
||||||
org_id: str,
|
current_user: PublicUser | AnonymousUser,
|
||||||
coursechapter_id: str,
|
db_session: Session,
|
||||||
current_user: PublicUser,
|
|
||||||
):
|
):
|
||||||
activities = request.app.db["activities"]
|
activity = Activity.from_orm(activity_object)
|
||||||
courses = request.app.db["courses"]
|
|
||||||
users = request.app.db["users"]
|
|
||||||
|
|
||||||
# get user
|
# CHeck if org exists
|
||||||
user = await users.find_one({"user_id": current_user.user_id})
|
statement = select(Chapter).where(Chapter.id == activity_object.chapter_id)
|
||||||
|
chapter = db_session.exec(statement).first()
|
||||||
|
|
||||||
# generate activity_id
|
if not chapter:
|
||||||
activity_id = str(f"activity_{uuid4()}")
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Chapter not found",
|
||||||
|
)
|
||||||
|
|
||||||
# verify activity rights
|
# RBAC check
|
||||||
await authorization_verify_based_on_roles(
|
await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session)
|
||||||
request,
|
|
||||||
current_user.user_id,
|
activity.activity_uuid = str(f"activity_{uuid4()}")
|
||||||
"create",
|
activity.creation_date = str(datetime.now())
|
||||||
user["roles"],
|
activity.update_date = str(datetime.now())
|
||||||
activity_id,
|
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)
|
||||||
|
)
|
||||||
|
chapter_activities = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# get course_id from activity
|
# Insert ChapterActivity link in DB
|
||||||
course = await courses.find_one({"chapters": coursechapter_id})
|
db_session.add(activity_chapter)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(activity_chapter)
|
||||||
|
|
||||||
# create activity
|
return ActivityRead.from_orm(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"],
|
|
||||||
)
|
|
||||||
await activities.insert_one(activity.dict())
|
|
||||||
|
|
||||||
# update chapter
|
|
||||||
await courses.update_one(
|
|
||||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
|
||||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
|
||||||
)
|
|
||||||
|
|
||||||
return activity
|
|
||||||
|
|
||||||
|
|
||||||
async def get_activity(request: Request, activity_id: str, current_user: PublicUser):
|
async def get_activity(
|
||||||
activities = request.app.db["activities"]
|
request: Request,
|
||||||
courses = request.app.db["courses"]
|
activity_uuid: str,
|
||||||
|
current_user: PublicUser,
|
||||||
activity = await activities.find_one({"activity_id": activity_id})
|
db_session: Session,
|
||||||
|
):
|
||||||
# get course_id from activity
|
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
|
||||||
coursechapter_id = activity["coursechapter_id"]
|
activity = db_session.exec(statement).first()
|
||||||
await courses.find_one({"chapters": coursechapter_id})
|
|
||||||
|
|
||||||
# verify course rights
|
|
||||||
await verify_rights(request, activity["course_id"], current_user, "read")
|
|
||||||
|
|
||||||
if not activity:
|
if not activity:
|
||||||
raise HTTPException(
|
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
|
return activity
|
||||||
|
|
||||||
|
|
||||||
async def update_activity(
|
async def update_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
activity_object: Activity,
|
activity_object: ActivityUpdate,
|
||||||
activity_id: str,
|
activity_uuid: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
db_session: Session,
|
||||||
):
|
):
|
||||||
activities = request.app.db["activities"]
|
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
|
||||||
|
activity = db_session.exec(statement).first()
|
||||||
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")
|
|
||||||
|
|
||||||
if not activity:
|
if not activity:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
|
status_code=404,
|
||||||
|
detail="Activity not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove Activity
|
# RBAC check
|
||||||
isDeleted = await activities.delete_one({"activity_id": activity_id})
|
await rbac_check(
|
||||||
|
request, activity.activity_uuid, current_user, "update", db_session
|
||||||
# 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}},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if isDeleted and isDeletedFromChapter:
|
# Update only the fields that were passed in
|
||||||
return {"detail": "Activity deleted"}
|
for var, value in vars(activity_object).items():
|
||||||
else:
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=404,
|
||||||
detail="Unavailable database",
|
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
|
# Misc
|
||||||
|
|
@ -187,64 +183,49 @@ async def delete_activity(request: Request, activity_id: str, current_user: Publ
|
||||||
|
|
||||||
|
|
||||||
async def get_activities(
|
async def get_activities(
|
||||||
request: Request, coursechapter_id: str, current_user: PublicUser
|
request: Request,
|
||||||
):
|
coursechapter_id: int,
|
||||||
activities = request.app.db["activities"]
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
db_session: Session,
|
||||||
activities = activities.find({"coursechapter_id": coursechapter_id})
|
) -> list[ActivityRead]:
|
||||||
|
statement = select(ChapterActivity).where(
|
||||||
|
ChapterActivity.chapter_id == coursechapter_id
|
||||||
|
)
|
||||||
|
activities = db_session.exec(statement).all()
|
||||||
|
|
||||||
if not activities:
|
if not activities:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
status_code=404,
|
||||||
|
detail="No activities found",
|
||||||
)
|
)
|
||||||
|
|
||||||
activities = [
|
# RBAC check
|
||||||
ActivityInDB(**activity) for activity in await activities.to_list(length=100)
|
await rbac_check(request, "activity_x", current_user, "read", db_session)
|
||||||
]
|
|
||||||
|
activities = [ActivityRead.from_orm(activity) for activity in activities]
|
||||||
|
|
||||||
return activities
|
return activities
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
## 🔒 RBAC Utils ##
|
||||||
|
|
||||||
|
|
||||||
async def verify_rights(
|
async def rbac_check(
|
||||||
request: Request,
|
request: Request,
|
||||||
activity_id: str, # course_id in case of read
|
course_id: str,
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
action: Literal["create", "read", "update", "delete"],
|
action: Literal["create", "read", "update", "delete"],
|
||||||
|
db_session: Session,
|
||||||
):
|
):
|
||||||
if action == "read":
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
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.user_id)
|
await authorization_verify_based_on_roles_and_authorship(
|
||||||
|
request,
|
||||||
await authorization_verify_based_on_roles(
|
current_user.id,
|
||||||
request,
|
action,
|
||||||
current_user.user_id,
|
course_id,
|
||||||
action,
|
db_session,
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
## 🔒 RBAC Utils ##
|
||||||
|
|
|
||||||
|
|
@ -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.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 fastapi import HTTPException, status, UploadFile, Request
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -10,26 +25,46 @@ from datetime import datetime
|
||||||
async def create_documentpdf_activity(
|
async def create_documentpdf_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
name: str,
|
name: str,
|
||||||
coursechapter_id: str,
|
chapter_id: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
db_session: Session,
|
||||||
pdf_file: UploadFile | None = None,
|
pdf_file: UploadFile | None = None,
|
||||||
):
|
):
|
||||||
activities = request.app.db["activities"]
|
# RBAC check
|
||||||
courses = request.app.db["courses"]
|
await rbac_check(request, "activity_x", current_user, "create", db_session)
|
||||||
users = request.app.db["users"]
|
|
||||||
|
|
||||||
# get user
|
# get chapter_id
|
||||||
user = await users.find_one({"user_id": current_user.user_id})
|
statement = select(Chapter).where(Chapter.id == chapter_id)
|
||||||
|
chapter = db_session.exec(statement).first()
|
||||||
|
|
||||||
# generate activity_id
|
if not chapter:
|
||||||
activity_id = str(f"activity_{uuid4()}")
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Chapter not found",
|
||||||
|
)
|
||||||
|
|
||||||
# get org_id from course
|
statement = select(CourseChapter).where(CourseChapter.chapter_id == chapter_id)
|
||||||
coursechapter = await courses.find_one(
|
coursechapter = db_session.exec(statement).first()
|
||||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
org_id = coursechapter["org_id"]
|
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
|
# check if pdf_file is not None
|
||||||
if not pdf_file:
|
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"
|
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_object = ActivityInDB(
|
# Create activity
|
||||||
org_id=org_id,
|
activity = Activity(
|
||||||
activity_id=activity_id,
|
|
||||||
coursechapter_id=coursechapter_id,
|
|
||||||
name=name,
|
name=name,
|
||||||
type="documentpdf",
|
activity_type=ActivityTypeEnum.TYPE_DOCUMENT,
|
||||||
course_id=coursechapter["course_id"],
|
activity_sub_type=ActivitySubTypeEnum.SUBTYPE_DOCUMENT_PDF,
|
||||||
content={
|
content={
|
||||||
"documentpdf": {
|
"filename": "documentpdf." + pdf_format,
|
||||||
"filename": "documentpdf." + pdf_format,
|
"activity_uuid": activity_uuid,
|
||||||
"activity_id": activity_id,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
creationDate=str(datetime.now()),
|
published_version=1,
|
||||||
updateDate=str(datetime.now()),
|
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(
|
# Insert Activity in DB
|
||||||
request,
|
db_session.add(activity)
|
||||||
current_user.user_id,
|
db_session.commit()
|
||||||
"create",
|
db_session.refresh(activity)
|
||||||
user["roles"],
|
|
||||||
activity_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# create activity
|
# Add activity to chapter
|
||||||
activity = ActivityInDB(**activity_object.dict())
|
activity_chapter = ChapterActivity(
|
||||||
await activities.insert_one(activity.dict())
|
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
|
# upload pdf
|
||||||
if pdf_file:
|
if pdf_file:
|
||||||
# get pdffile format
|
# get pdffile format
|
||||||
await upload_pdf(pdf_file, activity_id, org_id, coursechapter["course_id"])
|
await upload_pdf(
|
||||||
|
pdf_file,
|
||||||
|
activity.activity_uuid,
|
||||||
|
organization.org_uuid,
|
||||||
|
course.course_uuid,
|
||||||
|
)
|
||||||
|
|
||||||
# todo : choose whether to update the chapter or not
|
# Insert ChapterActivity link in DB
|
||||||
# update chapter
|
db_session.add(activity_chapter)
|
||||||
await courses.update_one(
|
db_session.commit()
|
||||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
db_session.refresh(activity_chapter)
|
||||||
{"$addToSet": {"chapters_content.$.activities": activity_id}},
|
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
return activity
|
|
||||||
|
## 🔒 RBAC Utils ##
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
|
|
||||||
from src.services.utils.upload_content import upload_content
|
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()
|
contents = pdf_file.file.read()
|
||||||
pdf_format = pdf_file.filename.split(".")[-1]
|
pdf_format = pdf_file.filename.split(".")[-1]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await upload_content(
|
await upload_content(
|
||||||
f"courses/{course_id}/activities/{activity_id}/documentpdf",
|
f"courses/{course_uuid}/activities/{activity_uuid}/documentpdf",
|
||||||
org_id,
|
org_uuid,
|
||||||
contents,
|
contents,
|
||||||
f"documentpdf.{pdf_format}",
|
f"documentpdf.{pdf_format}",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
from src.services.utils.upload_content import upload_content
|
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()
|
contents = video_file.file.read()
|
||||||
video_format = video_file.filename.split(".")[-1]
|
video_format = video_file.filename.split(".")[-1]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await upload_content(
|
await upload_content(
|
||||||
f"courses/{course_id}/activities/{activity_id}/video",
|
f"courses/{course_uuid}/activities/{activity_uuid}/video",
|
||||||
org_id,
|
org_uuid,
|
||||||
contents,
|
contents,
|
||||||
f"video.{video_format}",
|
f"video.{video_format}",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,24 @@
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
from src.db.courses import Course
|
||||||
|
from src.db.organizations import Organization
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from sqlmodel import Session, select
|
||||||
from src.security.rbac.rbac import (
|
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.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 fastapi import HTTPException, status, UploadFile, Request
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -15,32 +27,43 @@ from datetime import datetime
|
||||||
async def create_video_activity(
|
async def create_video_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
name: str,
|
name: str,
|
||||||
coursechapter_id: str,
|
chapter_id: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser,
|
||||||
|
db_session: Session,
|
||||||
video_file: UploadFile | None = None,
|
video_file: UploadFile | None = None,
|
||||||
):
|
):
|
||||||
activities = request.app.db["activities"]
|
# RBAC check
|
||||||
courses = request.app.db["courses"]
|
await rbac_check(request, "activity_x", current_user, "create", db_session)
|
||||||
users = request.app.db["users"]
|
|
||||||
|
|
||||||
# get user
|
# get chapter_id
|
||||||
user = await users.find_one({"user_id": current_user.user_id})
|
statement = select(Chapter).where(Chapter.id == chapter_id)
|
||||||
|
chapter = db_session.exec(statement).first()
|
||||||
|
|
||||||
# generate activity_id
|
if not chapter:
|
||||||
activity_id = str(f"activity_{uuid4()}")
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Chapter not found",
|
||||||
|
)
|
||||||
|
|
||||||
# get org_id from course
|
statement = select(CourseChapter).where(CourseChapter.chapter_id == chapter_id)
|
||||||
coursechapter = await courses.find_one(
|
coursechapter = db_session.exec(statement).first()
|
||||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not coursechapter:
|
if not coursechapter:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=404,
|
||||||
detail="CourseChapter : No coursechapter found",
|
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
|
# check if video_file is not None
|
||||||
if not video_file:
|
if not video_file:
|
||||||
|
|
@ -64,55 +87,63 @@ async def create_video_activity(
|
||||||
detail="Video : No video file provided",
|
detail="Video : No video file provided",
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_object = ActivityInDB(
|
activity_object = Activity(
|
||||||
org_id=org_id,
|
|
||||||
activity_id=activity_id,
|
|
||||||
coursechapter_id=coursechapter_id,
|
|
||||||
course_id=coursechapter["course_id"],
|
|
||||||
name=name,
|
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={
|
content={
|
||||||
"video": {
|
"filename": "video." + video_format,
|
||||||
"filename": "video." + video_format,
|
"activity_uuid": activity_uuid,
|
||||||
"activity_id": activity_id,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
creationDate=str(datetime.now()),
|
version=1,
|
||||||
updateDate=str(datetime.now()),
|
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# create activity
|
# create activity
|
||||||
activity = ActivityInDB(**activity_object.dict())
|
activity = Activity.from_orm(activity_object)
|
||||||
await activities.insert_one(activity.dict())
|
db_session.add(activity)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(activity)
|
||||||
|
|
||||||
# upload video
|
# upload video
|
||||||
if video_file:
|
if video_file:
|
||||||
# get videofile format
|
# get videofile format
|
||||||
await upload_video(video_file, activity_id, org_id, coursechapter["course_id"])
|
await upload_video(
|
||||||
|
video_file,
|
||||||
|
activity.activity_uuid,
|
||||||
|
organization.org_uuid,
|
||||||
|
course.course_uuid,
|
||||||
|
)
|
||||||
|
|
||||||
# todo : choose whether to update the chapter or not
|
|
||||||
# update chapter
|
# update chapter
|
||||||
await courses.update_one(
|
chapter_activity_object = ChapterActivity(
|
||||||
{"chapters_content.coursechapter_id": coursechapter_id},
|
chapter_id=chapter.id, # type: ignore
|
||||||
{"$addToSet": {"chapters_content.$.activities": activity_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,
|
||||||
)
|
)
|
||||||
|
|
||||||
return activity
|
# 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):
|
class ExternalVideo(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
uri: str
|
uri: str
|
||||||
type: Literal["youtube", "vimeo"]
|
type: Literal["youtube", "vimeo"]
|
||||||
coursechapter_id: str
|
chapter_id: str
|
||||||
|
|
||||||
|
|
||||||
class ExternalVideoInDB(BaseModel):
|
class ExternalVideoInDB(BaseModel):
|
||||||
|
|
@ -121,67 +152,93 @@ class ExternalVideoInDB(BaseModel):
|
||||||
|
|
||||||
async def create_external_video_activity(
|
async def create_external_video_activity(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
data: ExternalVideo,
|
data: ExternalVideo,
|
||||||
|
db_session: Session,
|
||||||
):
|
):
|
||||||
activities = request.app.db["activities"]
|
# RBAC check
|
||||||
courses = request.app.db["courses"]
|
await rbac_check(request, "activity_x", current_user, "create", db_session)
|
||||||
users = request.app.db["users"]
|
|
||||||
|
|
||||||
# get user
|
# get chapter_id
|
||||||
user = await users.find_one({"user_id": current_user.user_id})
|
statement = select(Chapter).where(Chapter.id == data.chapter_id)
|
||||||
|
chapter = db_session.exec(statement).first()
|
||||||
|
|
||||||
# generate activity_id
|
if not chapter:
|
||||||
activity_id = str(f"activity_{uuid4()}")
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Chapter not found",
|
||||||
|
)
|
||||||
|
|
||||||
# get org_id from course
|
statement = select(CourseChapter).where(CourseChapter.chapter_id == data.chapter_id)
|
||||||
coursechapter = await courses.find_one(
|
coursechapter = db_session.exec(statement).first()
|
||||||
{"chapters_content.coursechapter_id": data.coursechapter_id}
|
|
||||||
)
|
|
||||||
|
|
||||||
if not coursechapter:
|
if not coursechapter:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=404,
|
||||||
detail="CourseChapter : No coursechapter found",
|
detail="CourseChapter not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
org_id = coursechapter["org_id"]
|
# generate activity_uuid
|
||||||
|
activity_uuid = str(f"activity_{uuid4()}")
|
||||||
|
|
||||||
activity_object = ActivityInDB(
|
activity_object = Activity(
|
||||||
org_id=org_id,
|
|
||||||
activity_id=activity_id,
|
|
||||||
coursechapter_id=data.coursechapter_id,
|
|
||||||
name=data.name,
|
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={
|
content={
|
||||||
"external_video": {
|
"uri": data.uri,
|
||||||
"uri": data.uri,
|
"type": data.type,
|
||||||
"activity_id": activity_id,
|
"activity_uuid": activity_uuid,
|
||||||
"type": data.type,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
course_id=coursechapter["course_id"],
|
version=1,
|
||||||
creationDate=str(datetime.now()),
|
creation_date=str(datetime.now()),
|
||||||
updateDate=str(datetime.now()),
|
update_date=str(datetime.now()),
|
||||||
)
|
|
||||||
|
|
||||||
await authorization_verify_based_on_roles(
|
|
||||||
request,
|
|
||||||
current_user.user_id,
|
|
||||||
"create",
|
|
||||||
user["roles"],
|
|
||||||
activity_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# create activity
|
# create activity
|
||||||
activity = ActivityInDB(**activity_object.dict())
|
activity = Activity.from_orm(activity_object)
|
||||||
await activities.insert_one(activity.dict())
|
db_session.add(activity)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(activity)
|
||||||
|
|
||||||
# todo : choose whether to update the chapter or not
|
|
||||||
# update chapter
|
# update chapter
|
||||||
await courses.update_one(
|
chapter_activity_object = ChapterActivity(
|
||||||
{"chapters_content.coursechapter_id": data.coursechapter_id},
|
chapter_id=coursechapter.chapter_id, # type: ignore
|
||||||
{"$addToSet": {"chapters_content.$.activities": activity_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,
|
||||||
)
|
)
|
||||||
|
|
||||||
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 ##
|
||||||
|
|
|
||||||
|
|
@ -1,367 +1,551 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Literal
|
from typing import List, Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from pydantic import BaseModel
|
from sqlmodel import Session, select
|
||||||
from src.security.auth import non_public_endpoint
|
from src.db.users import AnonymousUser
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles,
|
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship,
|
||||||
authorization_verify_if_element_is_public,
|
|
||||||
authorization_verify_if_user_is_anon,
|
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.courses import Course
|
||||||
from src.services.courses.activities.activities import ActivityInDB
|
|
||||||
from src.services.users.users import PublicUser
|
from src.services.users.users import PublicUser
|
||||||
from fastapi import HTTPException, status, Request
|
from fastapi import HTTPException, status, Request
|
||||||
|
|
||||||
|
|
||||||
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
|
# CRUD
|
||||||
####################################################
|
####################################################
|
||||||
|
|
||||||
|
|
||||||
async def create_coursechapter(
|
async def create_chapter(
|
||||||
request: Request,
|
request: Request,
|
||||||
coursechapter_object: CourseChapter,
|
chapter_object: ChapterCreate,
|
||||||
course_id: str,
|
current_user: PublicUser | AnonymousUser,
|
||||||
current_user: PublicUser,
|
db_session: Session,
|
||||||
):
|
) -> ChapterRead:
|
||||||
courses = request.app.db["courses"]
|
chapter = Chapter.from_orm(chapter_object)
|
||||||
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})
|
|
||||||
|
|
||||||
# generate coursechapter_id with uuid4
|
# Get COurse
|
||||||
coursechapter_id = str(f"coursechapter_{uuid4()}")
|
statement = select(Course).where(Course.id == chapter_object.course_id)
|
||||||
|
|
||||||
hasRoleRights = await authorization_verify_based_on_roles(
|
course = db_session.exec(statement).one()
|
||||||
request, current_user.user_id, "create", user["roles"], course_id
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
if not hasRoleRights:
|
# get last chapter order
|
||||||
raise HTTPException(
|
last_order = course_chapters[-1].order if course_chapters else 0
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
to_be_used_order = last_order + 1
|
||||||
detail="Roles : Insufficient rights to perform this action",
|
|
||||||
|
# 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
coursechapter = CourseChapterInDB(
|
# Insert CourseChapter link in DB
|
||||||
coursechapter_id=coursechapter_id,
|
db_session.add(course_chapter)
|
||||||
creationDate=str(datetime.now()),
|
db_session.commit()
|
||||||
updateDate=str(datetime.now()),
|
|
||||||
course_id=course_id,
|
|
||||||
**coursechapter_object.dict(),
|
|
||||||
)
|
|
||||||
|
|
||||||
courses.update_one(
|
return chapter
|
||||||
{"course_id": course_id},
|
|
||||||
{
|
|
||||||
"$addToSet": {
|
|
||||||
"chapters": coursechapter_id,
|
|
||||||
"chapters_content": coursechapter.dict(),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return coursechapter.dict()
|
|
||||||
|
|
||||||
|
|
||||||
async def get_coursechapter(
|
async def get_chapter(
|
||||||
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(
|
|
||||||
request: Request,
|
request: Request,
|
||||||
coursechapter_object: CourseChapter,
|
chapter_id: int,
|
||||||
coursechapter_id: str,
|
current_user: PublicUser | AnonymousUser,
|
||||||
current_user: PublicUser,
|
db_session: Session,
|
||||||
):
|
) -> ChapterRead:
|
||||||
courses = request.app.db["courses"]
|
statement = select(Chapter).where(Chapter.id == chapter_id)
|
||||||
|
chapter = db_session.exec(statement).first()
|
||||||
|
|
||||||
coursechapter = await courses.find_one(
|
if not chapter:
|
||||||
{"chapters_content.coursechapter_id": coursechapter_id}
|
raise HTTPException(
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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 coursechapter:
|
activities = db_session.exec(statement).all()
|
||||||
# verify course rights
|
|
||||||
await verify_rights(request, coursechapter["course_id"], current_user, "update")
|
|
||||||
|
|
||||||
coursechapter = CourseChapterInDB(
|
chapter = ChapterRead(
|
||||||
coursechapter_id=coursechapter_id,
|
**chapter.dict(),
|
||||||
creationDate=str(datetime.now()),
|
activities=[ActivityRead(**activity.dict()) for activity in activities],
|
||||||
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:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Coursechapter does not exist"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if course:
|
return chapter
|
||||||
# verify course rights
|
|
||||||
await verify_rights(request, course["course_id"], current_user, "delete")
|
|
||||||
|
|
||||||
# Remove coursechapter from course
|
|
||||||
await courses.update_one(
|
|
||||||
{"course_id": course["course_id"]},
|
|
||||||
{"$pull": {"chapters": coursechapter_id}},
|
|
||||||
)
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
####################################################
|
async def update_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(
|
|
||||||
request: Request,
|
request: Request,
|
||||||
course_id: str,
|
chapter_object: ChapterUpdate,
|
||||||
coursechapters_metadata: CourseChapterMetaData,
|
chapter_id: int,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
):
|
db_session: Session,
|
||||||
courses = request.app.db["courses"]
|
) -> ChapterRead:
|
||||||
|
statement = select(Chapter).where(Chapter.id == chapter_id)
|
||||||
|
chapter = db_session.exec(statement).first()
|
||||||
|
|
||||||
await verify_rights(request, course_id, current_user, "update")
|
if not chapter:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist"
|
||||||
|
)
|
||||||
|
|
||||||
# update chapters in course
|
# RBAC check
|
||||||
await courses.update_one(
|
await rbac_check(request, chapter.chapter_uuid, current_user, "update", db_session)
|
||||||
{"course_id": course_id},
|
|
||||||
{"$set": {"chapters": coursechapters_metadata.chapterOrder}},
|
|
||||||
)
|
|
||||||
|
|
||||||
if coursechapters_metadata.chapters is not None:
|
# Update only the fields that were passed in
|
||||||
for (
|
for var, value in vars(chapter_object).items():
|
||||||
coursechapter_id,
|
if value is not None:
|
||||||
chapter_metadata,
|
setattr(chapter, var, value)
|
||||||
) 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}")
|
|
||||||
|
|
||||||
# update activities in coursechapters
|
chapter.update_date = str(datetime.now())
|
||||||
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}}
|
|
||||||
|
|
||||||
result = await activity.update_many(filter_query, update_query)
|
db_session.commit()
|
||||||
if result.matched_count == 0:
|
db_session.refresh(chapter)
|
||||||
# handle error when no documents are matched by the filter query
|
|
||||||
print(f"No documents found for course chapter ID {coursechapter_id}")
|
|
||||||
|
|
||||||
return {"detail": "coursechapters metadata updated"}
|
if chapter:
|
||||||
|
chapter = await get_chapter(
|
||||||
|
request, chapter.id, current_user, db_session # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
return chapter
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
async def delete_chapter(
|
||||||
|
|
||||||
|
|
||||||
async def verify_rights(
|
|
||||||
request: Request,
|
request: Request,
|
||||||
course_id: str,
|
chapter_id: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
action: Literal["read", "update", "delete"],
|
db_session: Session,
|
||||||
):
|
):
|
||||||
courses = request.app.db["courses"]
|
statement = select(Chapter).where(Chapter.id == chapter_id)
|
||||||
users = request.app.db["users"]
|
chapter = db_session.exec(statement).first()
|
||||||
user = await users.find_one({"user_id": current_user.user_id})
|
|
||||||
course = await courses.find_one({"course_id": course_id})
|
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:
|
if not course:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist"
|
||||||
)
|
)
|
||||||
|
|
||||||
if action == "read":
|
# RBAC check
|
||||||
if current_user.user_id == "anonymous":
|
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||||
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})
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
# activities
|
||||||
request,
|
|
||||||
current_user.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)
|
# chapters
|
||||||
|
chapters = {}
|
||||||
|
|
||||||
await authorization_verify_based_on_roles_and_authorship(
|
for chapter in chapters_in_db:
|
||||||
request,
|
chapter_activityIds = []
|
||||||
current_user.user_id,
|
|
||||||
action,
|
for activity in chapter.activities:
|
||||||
user["roles"],
|
print("test", activity)
|
||||||
course_id,
|
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)
|
||||||
|
|
||||||
#### Security ####################################################
|
###########
|
||||||
|
# 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.id,
|
||||||
|
action,
|
||||||
|
course_id,
|
||||||
|
db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## 🔒 RBAC Utils ##
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,23 @@
|
||||||
|
from datetime import datetime
|
||||||
from typing import List, Literal
|
from typing import List, Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from pydantic import BaseModel
|
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
|
||||||
|
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 src.services.users.users import PublicUser
|
||||||
from fastapi import HTTPException, status, Request
|
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
|
# CRUD
|
||||||
|
|
@ -29,134 +25,181 @@ class CollectionInDB(Collection):
|
||||||
|
|
||||||
|
|
||||||
async def get_collection(
|
async def get_collection(
|
||||||
request: Request, collection_id: str, current_user: PublicUser
|
request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session
|
||||||
):
|
) -> CollectionRead:
|
||||||
collections = request.app.db["collections"]
|
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
|
||||||
|
collection = db_session.exec(statement).first()
|
||||||
collection = await collections.find_one({"collection_id": collection_id})
|
|
||||||
|
|
||||||
# verify collection rights
|
|
||||||
await verify_collection_rights(
|
|
||||||
request, collection_id, current_user, "read", collection["org_id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not collection:
|
if not collection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||||
)
|
)
|
||||||
|
|
||||||
collection = Collection(**collection)
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request, collection.collection_uuid, current_user, "read", db_session
|
||||||
|
)
|
||||||
|
|
||||||
# add courses to collection
|
# get courses in collection
|
||||||
courses = request.app.db["courses"]
|
statement = (
|
||||||
courseids = [course for course in collection.courses]
|
select(Course)
|
||||||
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
|
.distinct(Course.id)
|
||||||
|
)
|
||||||
|
courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
collection.courses = []
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
collection.courses = courses.find({"course_id": {"$in": courseids}}, {"_id": 0})
|
|
||||||
|
|
||||||
collection.courses = [
|
|
||||||
course for course in await collection.courses.to_list(length=100)
|
|
||||||
]
|
|
||||||
|
|
||||||
return collection
|
return collection
|
||||||
|
|
||||||
|
|
||||||
async def create_collection(
|
async def create_collection(
|
||||||
request: Request, collection_object: Collection, current_user: PublicUser
|
request: Request,
|
||||||
):
|
collection_object: CollectionCreate,
|
||||||
collections = request.app.db["collections"]
|
current_user: PublicUser,
|
||||||
|
db_session: Session,
|
||||||
|
) -> CollectionRead:
|
||||||
|
collection = Collection.from_orm(collection_object)
|
||||||
|
|
||||||
# find if collection already exists using name
|
# RBAC check
|
||||||
isCollectionNameAvailable = await collections.find_one(
|
await rbac_check(request, "collection_x", current_user, "create", db_session)
|
||||||
{"name": collection_object.name}
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(collection)
|
||||||
|
|
||||||
|
# Get courses once again
|
||||||
|
statement = (
|
||||||
|
select(Course)
|
||||||
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
|
.distinct(Course.id)
|
||||||
)
|
)
|
||||||
|
courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
# TODO
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
# await verify_collection_rights("*", current_user, "create")
|
|
||||||
|
|
||||||
if isCollectionNameAvailable:
|
return CollectionRead.from_orm(collection)
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
|
||||||
detail="Collection name already exists",
|
|
||||||
)
|
|
||||||
|
|
||||||
# generate collection_id with uuid4
|
|
||||||
collection_id = str(f"collection_{uuid4()}")
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
async def update_collection(
|
async def update_collection(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_object: Collection,
|
collection_object: CollectionUpdate,
|
||||||
collection_id: str,
|
collection_uuid: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser,
|
||||||
):
|
db_session: Session,
|
||||||
# verify collection rights
|
) -> CollectionRead:
|
||||||
|
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
|
||||||
collections = request.app.db["collections"]
|
collection = db_session.exec(statement).first()
|
||||||
|
|
||||||
collection = await collections.find_one({"collection_id": collection_id})
|
|
||||||
|
|
||||||
await verify_collection_rights(
|
|
||||||
request, collection_id, current_user, "update", collection["org_id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not collection:
|
if not collection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
||||||
)
|
)
|
||||||
|
|
||||||
updated_collection = CollectionInDB(
|
# RBAC check
|
||||||
collection_id=collection_id, **collection_object.dict()
|
await rbac_check(
|
||||||
|
request, collection.collection_uuid, current_user, "update", db_session
|
||||||
)
|
)
|
||||||
|
|
||||||
await collections.update_one(
|
courses = collection_object.courses
|
||||||
{"collection_id": collection_id}, {"$set": updated_collection.dict()}
|
|
||||||
|
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(
|
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"]
|
statement = select(Collection).where(Collection.collection_uuid == collection_uuid)
|
||||||
|
collection = db_session.exec(statement).first()
|
||||||
collection = await collections.find_one({"collection_id": collection_id})
|
|
||||||
|
|
||||||
await verify_collection_rights(
|
|
||||||
request, collection_id, current_user, "delete", collection["org_id"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not collection:
|
if not collection:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist"
|
status_code=404,
|
||||||
|
detail="Collection not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
isDeleted = await collections.delete_one({"collection_id": collection_id})
|
# RBAC check
|
||||||
|
await rbac_check(
|
||||||
|
request, collection.collection_uuid, current_user, "delete", db_session
|
||||||
|
)
|
||||||
|
|
||||||
if isDeleted:
|
# delete collection from database
|
||||||
return {"detail": "collection deleted"}
|
db_session.delete(collection)
|
||||||
else:
|
db_session.commit()
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
return {"detail": "Collection deleted"}
|
||||||
detail="Unavailable database",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
####################################################
|
####################################################
|
||||||
|
|
@ -167,76 +210,55 @@ async def delete_collection(
|
||||||
async def get_collections(
|
async def get_collections(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_id: str,
|
org_id: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
db_session: Session,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
):
|
) -> List[CollectionRead]:
|
||||||
collections = request.app.db["collections"]
|
# RBAC check
|
||||||
|
await rbac_check(request, "collection_x", current_user, "read", db_session)
|
||||||
|
|
||||||
|
statement = (
|
||||||
|
select(Collection).where(Collection.org_id == org_id).distinct(Collection.id)
|
||||||
|
)
|
||||||
|
collections = db_session.exec(statement).all()
|
||||||
|
|
||||||
|
|
||||||
if current_user.user_id == "anonymous":
|
|
||||||
all_collections = collections.find(
|
collections_with_courses = []
|
||||||
{"org_id": org_id, "public": True}, {"_id": 0}
|
for collection in collections:
|
||||||
)
|
statement = (
|
||||||
else:
|
select(Course)
|
||||||
# get all collections from database without ObjectId
|
.join(CollectionCourse, Course.id == CollectionCourse.course_id)
|
||||||
all_collections = (
|
.distinct(Course.id)
|
||||||
collections.find({"org_id": org_id})
|
|
||||||
.sort("name", 1)
|
|
||||||
.skip(10 * (page - 1))
|
|
||||||
.limit(limit)
|
|
||||||
)
|
)
|
||||||
|
courses = db_session.exec(statement).all()
|
||||||
|
|
||||||
# create list of collections and include courses in each collection
|
collection = CollectionRead(**collection.dict(), courses=courses)
|
||||||
collections_list = []
|
collections_with_courses.append(collection)
|
||||||
for collection in await all_collections.to_list(length=100):
|
|
||||||
collection = CollectionInDB(**collection)
|
|
||||||
collections_list.append(collection)
|
|
||||||
|
|
||||||
collection_courses = [course for course in collection.courses]
|
return collections_with_courses
|
||||||
# add courses to collection
|
|
||||||
courses = request.app.db["courses"]
|
|
||||||
collection.courses = []
|
|
||||||
collection.courses = courses.find(
|
|
||||||
{"course_id": {"$in": collection_courses}}, {"_id": 0}
|
|
||||||
)
|
|
||||||
|
|
||||||
collection.courses = [
|
|
||||||
course for course in await collection.courses.to_list(length=100)
|
|
||||||
]
|
|
||||||
|
|
||||||
return collections_list
|
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
## 🔒 RBAC Utils ##
|
||||||
|
|
||||||
|
|
||||||
async def verify_collection_rights(
|
async def rbac_check(
|
||||||
request: Request,
|
request: Request,
|
||||||
collection_id: str,
|
course_id: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
action: Literal["create", "read", "update", "delete"],
|
action: Literal["create", "read", "update", "delete"],
|
||||||
org_id: str,
|
db_session: Session,
|
||||||
):
|
):
|
||||||
collections = request.app.db["collections"]
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
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_based_on_roles_and_authorship(
|
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 ##
|
||||||
|
|
|
||||||
|
|
@ -1,413 +1,391 @@
|
||||||
import json
|
from typing import Literal
|
||||||
from typing import List, Literal, Optional
|
|
||||||
from uuid import uuid4
|
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 (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles,
|
|
||||||
authorization_verify_based_on_roles_and_authorship,
|
authorization_verify_based_on_roles_and_authorship,
|
||||||
authorization_verify_if_element_is_public,
|
authorization_verify_if_element_is_public,
|
||||||
authorization_verify_if_user_is_anon,
|
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.courses.thumbnails import upload_thumbnail
|
||||||
from src.services.users.schemas.users import AnonymousUser
|
from fastapi import HTTPException, Request, UploadFile
|
||||||
from src.services.users.users import PublicUser
|
|
||||||
from fastapi import HTTPException, Request, status, UploadFile
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
#### Classes ####################################################
|
|
||||||
|
|
||||||
|
async def get_course(
|
||||||
class Course(BaseModel):
|
request: Request,
|
||||||
name: str
|
course_uuid: str,
|
||||||
mini_description: str
|
current_user: PublicUser | AnonymousUser,
|
||||||
description: str
|
db_session: Session,
|
||||||
learnings: List[str]
|
):
|
||||||
thumbnail: str
|
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||||
public: bool
|
course = db_session.exec(statement).first()
|
||||||
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")
|
|
||||||
|
|
||||||
if not course:
|
if not course:
|
||||||
raise HTTPException(
|
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
|
return course
|
||||||
|
|
||||||
|
|
||||||
async def get_course_meta(request: Request, course_id: str, current_user: PublicUser):
|
async def get_course_meta(
|
||||||
courses = request.app.db["courses"]
|
request: Request,
|
||||||
trails = request.app.db["trails"]
|
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})
|
course_statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||||
activities = request.app.db["activities"]
|
course = db_session.exec(course_statement).first()
|
||||||
|
|
||||||
# verify course rights
|
|
||||||
await verify_rights(request, course_id, current_user, "read")
|
|
||||||
|
|
||||||
if not course:
|
if not course:
|
||||||
raise HTTPException(
|
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(
|
# RBAC check
|
||||||
{"course_id": course_id}, {"chapters_content": 1, "_id": 0}
|
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
|
trail = TrailRead.from_orm(trail)
|
||||||
coursechapter_activityIds_global = []
|
|
||||||
|
|
||||||
# chapters
|
return FullCourseReadWithTrail(
|
||||||
chapters = {}
|
**course.dict(),
|
||||||
if coursechapters["chapters_content"]:
|
chapters=chapters,
|
||||||
for coursechapter in coursechapters["chapters_content"]:
|
trail=trail if trail else None,
|
||||||
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"]
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
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(
|
async def create_course(
|
||||||
request: Request,
|
request: Request,
|
||||||
course_object: Course,
|
org_id: int,
|
||||||
org_id: str,
|
course_object: CourseCreate,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
db_session: Session,
|
||||||
thumbnail_file: UploadFile | None = None,
|
thumbnail_file: UploadFile | None = None,
|
||||||
):
|
):
|
||||||
courses = request.app.db["courses"]
|
course = Course.from_orm(course_object)
|
||||||
users = request.app.db["users"]
|
|
||||||
user = await users.find_one({"user_id": current_user.user_id})
|
|
||||||
|
|
||||||
# generate course_id with uuid4
|
# RBAC check
|
||||||
course_id = str(f"course_{uuid4()}")
|
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)
|
# Complete course object
|
||||||
course_object.org_id = org_id
|
course.org_id = course.org_id
|
||||||
course_object.chapters_content = []
|
|
||||||
|
|
||||||
await authorization_verify_based_on_roles(
|
# Get org uuid
|
||||||
request,
|
org_statement = select(Organization).where(Organization.id == org_id)
|
||||||
current_user.user_id,
|
org = db_session.exec(org_statement).first()
|
||||||
"create",
|
|
||||||
user["roles"],
|
|
||||||
course_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
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:
|
if thumbnail_file and thumbnail_file.filename:
|
||||||
name_in_disk = (
|
name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
||||||
f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
|
|
||||||
)
|
|
||||||
await upload_thumbnail(
|
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(
|
# Insert course
|
||||||
course_id=course_id,
|
db_session.add(course)
|
||||||
authors=[current_user.user_id],
|
db_session.commit()
|
||||||
creationDate=str(datetime.now()),
|
db_session.refresh(course)
|
||||||
updateDate=str(datetime.now()),
|
|
||||||
**course_object.dict(),
|
# 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:
|
# Get course authors
|
||||||
raise HTTPException(
|
authors_statement = (
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
select(User)
|
||||||
detail="Unavailable database",
|
.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(
|
async def update_course_thumbnail(
|
||||||
request: Request,
|
request: Request,
|
||||||
course_id: str,
|
course_uuid: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
db_session: Session,
|
||||||
thumbnail_file: UploadFile | None = None,
|
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})
|
name_in_disk = None
|
||||||
|
|
||||||
# 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")
|
|
||||||
|
|
||||||
if not course:
|
if not course:
|
||||||
raise HTTPException(
|
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:
|
# Get org uuid
|
||||||
return {"detail": "Course deleted"}
|
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:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=500,
|
||||||
detail="Unavailable database",
|
detail="Issue with thumbnail upload",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Complete the course object
|
||||||
|
course.update_date = str(datetime.now())
|
||||||
|
|
||||||
####################################################
|
db_session.add(course)
|
||||||
# Misc
|
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(
|
async def get_courses_orgslug(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
org_slug: str,
|
||||||
|
db_session: Session,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
org_slug: str | None = None,
|
|
||||||
):
|
):
|
||||||
courses = request.app.db["courses"]
|
statement_public = (
|
||||||
orgs = request.app.db["organizations"]
|
select(Course)
|
||||||
|
.join(Organization)
|
||||||
|
.where(Organization.slug == org_slug, Course.public is True)
|
||||||
|
)
|
||||||
|
statement_all = (
|
||||||
|
select(Course).join(Organization).where(Organization.slug == org_slug)
|
||||||
|
)
|
||||||
|
|
||||||
# get org_id from slug
|
if current_user.id == 0:
|
||||||
org = await orgs.find_one({"slug": org_slug})
|
statement = statement_public
|
||||||
|
|
||||||
if not org:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
all_courses = (
|
# RBAC check
|
||||||
courses.find({"org_id": org["org_id"]})
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
.sort("name", 1)
|
|
||||||
.skip(10 * (page - 1))
|
statement = statement_all
|
||||||
.limit(limit)
|
|
||||||
|
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 [
|
# convert from User to UserRead
|
||||||
json.loads(json.dumps(course, default=str))
|
authors = [UserRead.from_orm(author) for author in authors]
|
||||||
for course in await all_courses.to_list(length=100)
|
|
||||||
]
|
course.authors = authors
|
||||||
|
|
||||||
|
return courses
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
## 🔒 RBAC Utils ##
|
||||||
|
|
||||||
|
|
||||||
async def verify_rights(
|
async def rbac_check(
|
||||||
request: Request,
|
request: Request,
|
||||||
course_id: str,
|
course_uuid: str,
|
||||||
current_user: PublicUser | AnonymousUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
action: Literal["create", "read", "update", "delete"],
|
action: Literal["create", "read", "update", "delete"],
|
||||||
|
db_session: Session,
|
||||||
):
|
):
|
||||||
if action == "read":
|
if action == "read":
|
||||||
if current_user.user_id == "anonymous":
|
if current_user.id == 0: # Anonymous user
|
||||||
await authorization_verify_if_element_is_public(
|
await authorization_verify_if_element_is_public(
|
||||||
request, course_id, current_user.user_id, action
|
request, course_uuid, action, db_session
|
||||||
)
|
)
|
||||||
else:
|
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(
|
await authorization_verify_based_on_roles_and_authorship(
|
||||||
request,
|
request, current_user.id, action, course_uuid, db_session
|
||||||
current_user.user_id,
|
|
||||||
action,
|
|
||||||
user["roles"],
|
|
||||||
course_id,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
users = request.app.db["users"]
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
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(
|
await authorization_verify_based_on_roles_and_authorship(
|
||||||
request,
|
request,
|
||||||
current_user.user_id,
|
current_user.id,
|
||||||
action,
|
action,
|
||||||
user["roles"],
|
course_uuid,
|
||||||
course_id,
|
db_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
## 🔒 RBAC Utils ##
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,5 @@ def isDevModeEnabledOrRaise():
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=403, detail="Development mode is disabled")
|
raise HTTPException(status_code=403, detail="Development mode is disabled")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
275
apps/api/src/services/dev/migration_from_mongo.py
Normal file
275
apps/api/src/services/dev/migration_from_mongo.py
Normal 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."
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -1,35 +1,15 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from fastapi import HTTPException, Request, status
|
from fastapi import HTTPException, Request
|
||||||
from pydantic import BaseModel
|
from sqlalchemy import desc
|
||||||
import requests
|
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 config.config import get_learnhouse_config
|
||||||
from src.security.security import security_hash_password
|
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():
|
async def isInstallModeEnabled():
|
||||||
|
|
@ -44,37 +24,48 @@ async def isInstallModeEnabled():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def create_install_instance(request: Request, data: dict):
|
async def create_install_instance(request: Request, data: dict, db_session: Session):
|
||||||
installs = request.app.db["installs"]
|
install = Install.from_orm(data)
|
||||||
|
|
||||||
# get install_id
|
# complete install instance
|
||||||
install_id = str(f"install_{uuid4()}")
|
install.install_uuid = str(f"install_{uuid4()}")
|
||||||
created_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
install.update_date = str(datetime.now())
|
||||||
updated_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
install.creation_date = str(datetime.now())
|
||||||
step = 1
|
|
||||||
|
|
||||||
# create install
|
# insert install instance
|
||||||
install = InstallInstance(
|
db_session.add(install)
|
||||||
install_id=install_id,
|
|
||||||
created_date=created_date,
|
|
||||||
updated_date=updated_date,
|
|
||||||
step=step,
|
|
||||||
data=data,
|
|
||||||
)
|
|
||||||
|
|
||||||
# insert install
|
# commit changes
|
||||||
installs.insert_one(install.dict())
|
db_session.commit()
|
||||||
|
|
||||||
|
# refresh install instance
|
||||||
|
db_session.refresh(install)
|
||||||
|
|
||||||
|
install = InstallRead.from_orm(install)
|
||||||
|
|
||||||
return install
|
return install
|
||||||
|
|
||||||
|
|
||||||
async def get_latest_install_instance(request: Request):
|
async def get_latest_install_instance(request: Request, db_session: Session):
|
||||||
installs = request.app.db["installs"]
|
statement = select(Install).order_by(desc(Install.creation_date)).limit(1)
|
||||||
|
install = db_session.exec(statement).first()
|
||||||
|
|
||||||
# get latest created install instance using find_one
|
if install is None:
|
||||||
install = await installs.find_one(
|
raise HTTPException(
|
||||||
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
|
status_code=404,
|
||||||
)
|
detail="No install instance found",
|
||||||
|
)
|
||||||
|
|
||||||
|
install = InstallRead.from_orm(install)
|
||||||
|
|
||||||
|
return install
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
if install is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -82,37 +73,18 @@ async def get_latest_install_instance(request: Request):
|
||||||
detail="No install instance found",
|
detail="No install instance found",
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
install.step = step
|
||||||
install = InstallInstance(**install)
|
install.data = data
|
||||||
|
|
||||||
return install
|
# commit changes
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# refresh install instance
|
||||||
|
db_session.refresh(install)
|
||||||
|
|
||||||
async def update_install_instance(request: Request, data: dict, step: int):
|
install = InstallRead.from_orm(install)
|
||||||
installs = request.app.db["installs"]
|
|
||||||
|
|
||||||
# get latest created install
|
return install
|
||||||
install = await installs.find_one(
|
|
||||||
sort=[("created_date", -1)], limit=1, projection={"_id": 0}
|
|
||||||
)
|
|
||||||
|
|
||||||
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}
|
|
||||||
)
|
|
||||||
|
|
||||||
install = InstallInstance(**install)
|
|
||||||
|
|
||||||
return install
|
|
||||||
|
|
||||||
|
|
||||||
############################################################################################################
|
############################################################################################################
|
||||||
|
|
@ -121,24 +93,34 @@ async def update_install_instance(request: Request, data: dict, step: int):
|
||||||
|
|
||||||
|
|
||||||
# Install Default roles
|
# Install Default roles
|
||||||
async def install_default_elements(request: Request, data: dict):
|
async def install_default_elements(request: Request, data: dict, db_session: Session):
|
||||||
roles = request.app.db["roles"]
|
# 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
|
for role in roles:
|
||||||
admin_role = await roles.find_one({"role_id": "role_admin"})
|
db_session.delete(role)
|
||||||
user_role = await roles.find_one({"role_id": "role_member"})
|
|
||||||
|
|
||||||
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(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=409,
|
||||||
detail="Default roles already exist",
|
detail="Default roles already exist",
|
||||||
)
|
)
|
||||||
|
|
||||||
# get default roles
|
# Create default roles
|
||||||
ADMIN_ROLE = RoleInDB(
|
role_global_admin = Role(
|
||||||
name="Admin Role",
|
name="Admin",
|
||||||
description="This role grants all permissions to the user",
|
description="Standard Admin Role",
|
||||||
elements=Elements(
|
id=1,
|
||||||
|
role_type=RoleTypeEnum.TYPE_GLOBAL,
|
||||||
|
role_uuid="role_global_admin",
|
||||||
|
rights=Rights(
|
||||||
courses=Permission(
|
courses=Permission(
|
||||||
action_create=True,
|
action_create=True,
|
||||||
action_read=True,
|
action_read=True,
|
||||||
|
|
@ -151,12 +133,6 @@ async def install_default_elements(request: Request, data: dict):
|
||||||
action_update=True,
|
action_update=True,
|
||||||
action_delete=True,
|
action_delete=True,
|
||||||
),
|
),
|
||||||
houses=Permission(
|
|
||||||
action_create=True,
|
|
||||||
action_read=True,
|
|
||||||
action_update=True,
|
|
||||||
action_delete=True,
|
|
||||||
),
|
|
||||||
collections=Permission(
|
collections=Permission(
|
||||||
action_create=True,
|
action_create=True,
|
||||||
action_read=True,
|
action_read=True,
|
||||||
|
|
@ -182,16 +158,65 @@ async def install_default_elements(request: Request, data: dict):
|
||||||
action_delete=True,
|
action_delete=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
org_id="*",
|
creation_date=str(datetime.now()),
|
||||||
role_id="role_admin",
|
update_date=str(datetime.now()),
|
||||||
created_at=str(datetime.now()),
|
|
||||||
updated_at=str(datetime.now()),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
USER_ROLE = RoleInDB(
|
role_global_maintainer = Role(
|
||||||
name="Member Role",
|
name="Maintainer",
|
||||||
description="This role grants read-only permissions to the user",
|
description="Standard Maintainer Role",
|
||||||
elements=Elements(
|
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(
|
courses=Permission(
|
||||||
action_create=False,
|
action_create=False,
|
||||||
action_read=True,
|
action_read=True,
|
||||||
|
|
@ -199,13 +224,7 @@ async def install_default_elements(request: Request, data: dict):
|
||||||
action_delete=False,
|
action_delete=False,
|
||||||
),
|
),
|
||||||
users=Permission(
|
users=Permission(
|
||||||
action_create=False,
|
action_create=True,
|
||||||
action_read=True,
|
|
||||||
action_update=False,
|
|
||||||
action_delete=False,
|
|
||||||
),
|
|
||||||
houses=Permission(
|
|
||||||
action_create=False,
|
|
||||||
action_read=True,
|
action_read=True,
|
||||||
action_update=False,
|
action_update=False,
|
||||||
action_delete=False,
|
action_delete=False,
|
||||||
|
|
@ -235,185 +254,122 @@ async def install_default_elements(request: Request, data: dict):
|
||||||
action_delete=False,
|
action_delete=False,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
org_id="*",
|
creation_date=str(datetime.now()),
|
||||||
role_id="role_member",
|
update_date=str(datetime.now()),
|
||||||
created_at=str(datetime.now()),
|
|
||||||
updated_at=str(datetime.now()),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
# Serialize rights to JSON
|
||||||
# insert default roles
|
role_global_admin.rights = role_global_admin.rights.dict() # type: ignore
|
||||||
await roles.insert_many([USER_ROLE.dict(), ADMIN_ROLE.dict()])
|
role_global_maintainer.rights = role_global_maintainer.rights.dict() # type: ignore
|
||||||
return True
|
role_global_user.rights = role_global_user.rights.dict() # type: ignore
|
||||||
|
|
||||||
except Exception:
|
# Insert roles in DB
|
||||||
raise HTTPException(
|
db_session.add(role_global_admin)
|
||||||
status_code=400,
|
db_session.add(role_global_maintainer)
|
||||||
detail="Error while inserting default roles",
|
db_session.add(role_global_user)
|
||||||
)
|
|
||||||
|
# commit changes
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# refresh roles
|
||||||
|
db_session.refresh(role_global_admin)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Organization creation
|
# Organization creation
|
||||||
async def install_create_organization(
|
async def install_create_organization(
|
||||||
request: Request,
|
request: Request, org_object: OrganizationCreate, db_session: Session
|
||||||
org_object: Organization,
|
|
||||||
):
|
):
|
||||||
orgs = request.app.db["organizations"]
|
org = Organization.from_orm(org_object)
|
||||||
request.app.db["users"]
|
|
||||||
|
|
||||||
# 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:
|
return org
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
async def install_create_organization_user(
|
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})
|
# Complete the user object
|
||||||
isEmailAvailable = await users.find_one({"email": user_object.email})
|
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(
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Email already exists"
|
status_code=400,
|
||||||
|
detail="Username already exists",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate user_id with uuid4
|
# Email
|
||||||
user_id = str(f"user_{uuid4()}")
|
statement = select(User).where(User.email == user.email)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
# Set the username & hash the password
|
if result.first():
|
||||||
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:
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=400,
|
||||||
detail="You are trying to create a user in an organization that does not exist",
|
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
|
# Add user to database
|
||||||
orgs = [UserOrganization(org_id=org_id, org_role="owner")]
|
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
|
# get org id
|
||||||
user = UserInDB(
|
statement = select(Organization).where(Organization.slug == org_slug)
|
||||||
user_id=user_id,
|
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()),
|
creation_date=str(datetime.now()),
|
||||||
update_date=str(datetime.now()),
|
update_date=str(datetime.now()),
|
||||||
orgs=orgs,
|
|
||||||
roles=roles,
|
|
||||||
**user_object.dict(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Insert the user into the database
|
db_session.add(user_organization)
|
||||||
await users.insert_one(user.dict())
|
db_session.commit()
|
||||||
|
db_session.refresh(user_organization)
|
||||||
|
|
||||||
return User(**user.dict())
|
user = UserRead.from_orm(user)
|
||||||
|
|
||||||
|
return 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,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ from uuid import uuid4
|
||||||
from src.services.utils.upload_content import upload_content
|
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()
|
contents = logo_file.file.read()
|
||||||
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
|
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
|
||||||
|
|
||||||
await upload_content(
|
await upload_content(
|
||||||
"logos",
|
"logos",
|
||||||
org_id,
|
org_uuid,
|
||||||
contents,
|
contents,
|
||||||
name_in_disk,
|
name_in_disk,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,230 +1,287 @@
|
||||||
import json
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from sqlmodel import Session, select
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles,
|
authorization_verify_based_on_roles_and_authorship,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.services.orgs.logos import upload_org_logo
|
from src.db.users import AnonymousUser, PublicUser
|
||||||
from src.services.orgs.schemas.orgs import (
|
from src.db.user_organizations import UserOrganization
|
||||||
|
from src.db.organizations import (
|
||||||
Organization,
|
Organization,
|
||||||
OrganizationInDB,
|
OrganizationCreate,
|
||||||
PublicOrganization,
|
OrganizationRead,
|
||||||
|
OrganizationUpdate,
|
||||||
)
|
)
|
||||||
from src.services.users.schemas.users import UserOrganization
|
from src.services.orgs.logos import upload_org_logo
|
||||||
from src.services.users.users import PublicUser
|
|
||||||
from fastapi import HTTPException, UploadFile, status, Request
|
from fastapi import HTTPException, UploadFile, status, Request
|
||||||
|
|
||||||
|
|
||||||
async def get_organization(request: Request, org_id: str):
|
async def get_organization(
|
||||||
orgs = request.app.db["organizations"]
|
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:
|
if not org:
|
||||||
raise HTTPException(
|
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
|
return org
|
||||||
|
|
||||||
|
|
||||||
async def get_organization_by_slug(request: Request, org_slug: str):
|
async def get_organization_by_slug(
|
||||||
orgs = request.app.db["organizations"]
|
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:
|
if not org:
|
||||||
raise HTTPException(
|
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
|
return org
|
||||||
|
|
||||||
|
|
||||||
async def create_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"]
|
statement = select(Organization).where(Organization.slug == org_object.slug)
|
||||||
user = request.app.db["users"]
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
# find if org already exists using name
|
org = result.first()
|
||||||
isOrgAvailable = await orgs.find_one({"slug": org_object.slug})
|
|
||||||
|
|
||||||
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
detail="Organization slug already exists",
|
detail="Organization slug already exists",
|
||||||
)
|
)
|
||||||
|
|
||||||
# generate org_id with uuid4
|
# Update only the fields that were passed in
|
||||||
org_id = str(f"org_{uuid4()}")
|
for var, value in vars(org_object).items():
|
||||||
|
if value is not None:
|
||||||
|
setattr(org, var, value)
|
||||||
|
|
||||||
# force lowercase slug
|
# Complete the org object
|
||||||
org_object.slug = org_object.slug.lower()
|
org.update_date = str(datetime.now())
|
||||||
|
|
||||||
org = OrganizationInDB(
|
db_session.add(org)
|
||||||
org_id=org_id, **org_object.dict()
|
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(
|
return org
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
async def update_org_logo(
|
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
|
statement = select(Organization).where(Organization.id == org_id)
|
||||||
await verify_org_rights(request, org_id, current_user, "update")
|
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
|
# Upload logo
|
||||||
await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}})
|
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"}
|
return {"detail": "Logo updated"}
|
||||||
|
|
||||||
|
|
||||||
async def delete_org(request: Request, org_id: str, current_user: PublicUser):
|
async def delete_org(
|
||||||
await verify_org_rights(request, org_id, current_user, "delete")
|
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 = result.first()
|
||||||
|
|
||||||
org = await orgs.find_one({"org_id": org_id})
|
|
||||||
|
|
||||||
if not org:
|
if not org:
|
||||||
raise HTTPException(
|
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
|
db_session.delete(org)
|
||||||
users = request.app.db["users"]
|
db_session.commit()
|
||||||
await users.update_many({}, {"$pull": {"orgs": {"org_id": org_id}}})
|
|
||||||
|
|
||||||
if isDeleted:
|
# Delete links to org
|
||||||
return {"detail": "Org deleted"}
|
statement = select(UserOrganization).where(UserOrganization.org_id == org_id)
|
||||||
else:
|
result = db_session.exec(statement)
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
user_orgs = result.all()
|
||||||
detail="Unavailable database",
|
|
||||||
)
|
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(
|
async def get_orgs_by_user(
|
||||||
request: Request, user_id: str, page: int = 1, limit: int = 10
|
request: Request,
|
||||||
):
|
db_session: Session,
|
||||||
orgs = request.app.db["organizations"]
|
user_id: str,
|
||||||
user = request.app.db["users"]
|
page: int = 1,
|
||||||
|
limit: int = 10,
|
||||||
if user_id == "anonymous":
|
) -> list[Organization]:
|
||||||
# raise error
|
statement = (
|
||||||
raise HTTPException(
|
select(Organization)
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="User not logged in"
|
.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 [
|
orgs = result.all()
|
||||||
json.loads(json.dumps(org, default=str))
|
|
||||||
for org in await all_orgs.to_list(length=100)
|
return orgs
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
## 🔒 RBAC Utils ##
|
||||||
|
|
||||||
|
|
||||||
async def verify_org_rights(
|
async def rbac_check(
|
||||||
request: Request,
|
request: Request,
|
||||||
org_id: str,
|
org_id: str,
|
||||||
current_user: PublicUser,
|
current_user: PublicUser | AnonymousUser,
|
||||||
action: Literal["create", "read", "update", "delete"],
|
action: Literal["create", "read", "update", "delete"],
|
||||||
|
db_session: Session,
|
||||||
):
|
):
|
||||||
orgs = request.app.db["organizations"]
|
# Organizations are readable by anyone
|
||||||
users = request.app.db["users"]
|
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})
|
await authorization_verify_based_on_roles_and_authorship(
|
||||||
|
request, current_user.id, action, org_id, db_session
|
||||||
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(
|
## 🔒 RBAC Utils ##
|
||||||
request, current_user.user_id, action, user["roles"], org_id
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -1,127 +1,141 @@
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from src.security.rbac.rbac import authorization_verify_if_user_is_anon
|
from sqlmodel import Session, select
|
||||||
from src.services.roles.schemas.roles import Role, RoleInDB
|
from src.security.rbac.rbac import (
|
||||||
from src.services.users.schemas.users import PublicUser
|
authorization_verify_based_on_roles_and_authorship,
|
||||||
from fastapi import HTTPException, status, Request
|
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
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
async def create_role(request: Request, role_object: Role, current_user: PublicUser):
|
async def create_role(
|
||||||
roles = request.app.db["roles"]
|
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
|
# Complete the role object
|
||||||
role_id = "role_" + str(uuid4())
|
role.role_uuid = f"role_{uuid4()}"
|
||||||
|
role.creation_date = str(datetime.now())
|
||||||
|
role.update_date = str(datetime.now())
|
||||||
|
|
||||||
role = RoleInDB(
|
db_session.add(role)
|
||||||
role_id=role_id,
|
db_session.commit()
|
||||||
created_at=str(datetime.now()),
|
db_session.refresh(role)
|
||||||
updated_at=str(datetime.now()),
|
|
||||||
**role_object.dict()
|
|
||||||
)
|
|
||||||
|
|
||||||
await roles.insert_one(role.dict())
|
role = RoleRead(**role.dict())
|
||||||
|
|
||||||
return role
|
return role
|
||||||
|
|
||||||
|
|
||||||
async def read_role(request: Request, role_id: str, current_user: PublicUser):
|
async def read_role(
|
||||||
roles = request.app.db["roles"]
|
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
|
return role
|
||||||
|
|
||||||
|
|
||||||
async def update_role(
|
async def update_role(
|
||||||
request: Request, role_id: str, role_object: Role, current_user: PublicUser
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
role_object: RoleUpdate,
|
||||||
|
current_user: PublicUser,
|
||||||
):
|
):
|
||||||
roles = request.app.db["roles"]
|
statement = select(Role).where(Role.id == role_object.role_id)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
await verify_user_permissions_on_roles(request, current_user, "update", role_id)
|
role = result.first()
|
||||||
|
|
||||||
role_object.updated_at = datetime.now()
|
if not role:
|
||||||
|
raise HTTPException(
|
||||||
# Update the role object in the database and return the object
|
status_code=404,
|
||||||
updated_role = RoleInDB(
|
detail="Role not found",
|
||||||
**await roles.find_one_and_update(
|
|
||||||
{"role_id": role_id}, {"$set": role_object.dict()}, return_document=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# RBAC check
|
||||||
|
await rbac_check(request, current_user, "update", role.role_uuid, db_session)
|
||||||
|
|
||||||
|
# Complete the role object
|
||||||
|
role.update_date = str(datetime.now())
|
||||||
|
|
||||||
|
# Remove the role_id from the role_object
|
||||||
|
del role_object.role_id
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
db_session.add(role)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(role)
|
||||||
|
|
||||||
|
role = RoleRead(**role.dict())
|
||||||
|
|
||||||
|
return role
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
return updated_role
|
|
||||||
|
|
||||||
|
## 🔒 RBAC Utils ##
|
||||||
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,
|
|
||||||
current_user: PublicUser,
|
|
||||||
action: Literal["create", "read", "update", "delete"],
|
|
||||||
role_id: str | None,
|
|
||||||
):
|
|
||||||
request.app.db["users"]
|
|
||||||
roles = request.app.db["roles"]
|
|
||||||
|
|
||||||
# If current user is not authenticated
|
|
||||||
|
|
||||||
if not current_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Roles : Not authenticated"
|
|
||||||
)
|
|
||||||
|
|
||||||
await authorization_verify_if_user_is_anon(current_user.user_id)
|
|
||||||
|
|
||||||
if action == "create":
|
|
||||||
if "owner" in [org.org_role for org in current_user.orgs]:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if role_id is not None:
|
|
||||||
role = RoleInDB(**await roles.find_one({"role_id": role_id}))
|
|
||||||
|
|
||||||
if action == "read":
|
|
||||||
if "owner" in [org.org_role for org in current_user.orgs]:
|
|
||||||
return True
|
|
||||||
|
|
||||||
for org in current_user.orgs:
|
|
||||||
if org.org_id == role.org_id:
|
|
||||||
return True
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
#### Security ####################################################
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
|
|
@ -1,286 +1,423 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Literal, Optional
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from src.db.chapter_activities import ChapterActivity
|
||||||
from fastapi import HTTPException, Request, status
|
from fastapi import HTTPException, Request, status
|
||||||
from pydantic import BaseModel
|
from sqlmodel import Session, select
|
||||||
from src.services.courses.chapters import get_coursechapters_meta
|
from src.db.activities import Activity
|
||||||
from src.services.orgs.orgs import PublicOrganization
|
from src.db.courses import Course
|
||||||
|
from src.db.trail_runs import TrailRun, TrailRunRead
|
||||||
from src.services.users.users import PublicUser
|
from src.db.trail_steps import TrailStep
|
||||||
|
from src.db.trails import Trail, TrailCreate, TrailRead
|
||||||
#### Classes ####################################################
|
from src.db.users import PublicUser
|
||||||
|
|
||||||
|
|
||||||
class ActivityData(BaseModel):
|
async def create_user_trail(
|
||||||
activity_id: str
|
request: Request,
|
||||||
activity_type: str
|
user: PublicUser,
|
||||||
data: Optional[dict]
|
trail_object: TrailCreate,
|
||||||
|
db_session: Session,
|
||||||
|
|
||||||
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
|
|
||||||
) -> Trail:
|
) -> 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:
|
||||||
if trail_object.courses:
|
raise HTTPException(
|
||||||
courses = trail_object.courses
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
# get course ids
|
detail="Trail already exists",
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# create trail id
|
trail = Trail.from_orm(trail_object)
|
||||||
trail_id = f"trail_{uuid4()}"
|
|
||||||
|
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
|
# create trail
|
||||||
trail = TrailInDB(
|
db_session.add(trail)
|
||||||
**trail_object.dict(), trail_id=trail_id, user_id=user.user_id, org_id=org_id
|
db_session.commit()
|
||||||
)
|
db_session.refresh(trail)
|
||||||
|
|
||||||
await trails.insert_one(trail.dict())
|
|
||||||
|
|
||||||
return trail
|
return trail
|
||||||
|
|
||||||
|
|
||||||
async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> Trail:
|
async def get_user_trails(
|
||||||
trails = request.app.db["trails"]
|
request: Request,
|
||||||
trail = await trails.find_one({"user_id": user.user_id})
|
user: PublicUser,
|
||||||
|
db_session: Session,
|
||||||
|
) -> TrailRead:
|
||||||
|
statement = select(Trail).where(Trail.user_id == user.id)
|
||||||
|
trail = db_session.exec(statement).first()
|
||||||
|
|
||||||
if not trail:
|
if not trail:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
|
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", []))
|
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
|
||||||
element["progress"] = (
|
trail_runs = db_session.exec(statement).all()
|
||||||
round((num_completed_activities / num_activities) * 100, 2)
|
|
||||||
if num_activities > 0
|
trail_runs = [
|
||||||
else 0
|
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())
|
||||||
|
|
||||||
return Trail(**trail)
|
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
|
||||||
|
|
||||||
async def get_user_trail_with_orgslug(
|
for trail_step in trail_steps:
|
||||||
request: Request, user: PublicUser, org_slug: str
|
statement = select(Course).where(Course.id == trail_step.course_id)
|
||||||
) -> Trail:
|
course = db_session.exec(statement).first()
|
||||||
trails = request.app.db["trails"]
|
trail_step.data = dict(course=course)
|
||||||
orgs = request.app.db["organizations"]
|
|
||||||
courses_mongo = request.app.db["courses"]
|
|
||||||
|
|
||||||
# get org_id from orgslug
|
trail_read = TrailRead(
|
||||||
org = await orgs.find_one({"slug": org_slug})
|
**trail.dict(),
|
||||||
|
runs=trail_runs,
|
||||||
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
|
||||||
|
|
||||||
if not trail:
|
|
||||||
return Trail(masked=False, courses=[])
|
|
||||||
|
|
||||||
course_ids = [course["course_id"] for course in trail["courses"]]
|
|
||||||
|
|
||||||
live_courses = await courses_mongo.find({"course_id": {"$in": course_ids}}).to_list(
|
|
||||||
length=None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for course in trail["courses"]:
|
return trail_read
|
||||||
course_id = course["course_id"]
|
|
||||||
|
|
||||||
if course_id not in [course["course_id"] for course in live_courses]:
|
|
||||||
course["masked"] = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
chapters_meta = await get_coursechapters_meta(request, course_id, user)
|
async def check_trail_presence(
|
||||||
activities = chapters_meta["activities"]
|
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()
|
||||||
|
|
||||||
# get course object without _id
|
if not trail:
|
||||||
course_object = await courses_mongo.find_one(
|
trail = await create_user_trail(
|
||||||
{"course_id": course_id}, {"_id": 0}
|
request,
|
||||||
|
user,
|
||||||
|
TrailCreate(
|
||||||
|
org_id=org_id,
|
||||||
|
user_id=user.id,
|
||||||
|
),
|
||||||
|
db_session,
|
||||||
)
|
)
|
||||||
|
return trail
|
||||||
|
|
||||||
course["course_object"] = course_object
|
return trail
|
||||||
num_activities = len(activities)
|
|
||||||
|
|
||||||
num_completed_activities = len(course.get("activities_marked_complete", []))
|
|
||||||
course["progress"] = (
|
async def get_user_trail_with_orgid(
|
||||||
round((num_completed_activities / num_activities) * 100, 2)
|
request: Request, user: PublicUser, org_id: int, db_session: Session
|
||||||
if num_activities > 0
|
) -> TrailRead:
|
||||||
else 0
|
|
||||||
|
trail = await check_trail_presence(
|
||||||
|
org_id=org_id,
|
||||||
|
user_id=user.id,
|
||||||
|
request=request,
|
||||||
|
user=user,
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
return Trail(**trail)
|
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_activity_to_trail(
|
async def add_activity_to_trail(
|
||||||
request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str
|
request: Request,
|
||||||
) -> Trail:
|
user: PublicUser,
|
||||||
trails = request.app.db["trails"]
|
activity_uuid: str,
|
||||||
orgs = request.app.db["organizations"]
|
db_session: Session,
|
||||||
courseid = "course_" + course_id
|
) -> TrailRead:
|
||||||
activityid = "activity_" + activity_id
|
# Look for the activity
|
||||||
|
statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
|
||||||
|
activity = db_session.exec(statement).first()
|
||||||
|
|
||||||
# get org_id from orgslug
|
if not activity:
|
||||||
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":
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found"
|
||||||
detail="Anonymous users cannot add activity to trail",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not trail:
|
statement = select(Course).where(Course.id == activity.course_id)
|
||||||
return Trail(masked=False, courses=[])
|
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
|
if not course:
|
||||||
for element in trail["courses"]:
|
raise HTTPException(
|
||||||
if element["course_id"] == courseid:
|
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
|
||||||
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:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Activity already marked complete",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
element["activities_marked_complete"] = [activity_id]
|
|
||||||
|
|
||||||
# modify trail object
|
trail = await check_trail_presence(
|
||||||
await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
|
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(
|
async def add_course_to_trail(
|
||||||
request: Request, user: PublicUser, orgslug: str, course_id: str
|
request: Request,
|
||||||
) -> Trail:
|
user: PublicUser,
|
||||||
trails = request.app.db["trails"]
|
course_uuid: str,
|
||||||
orgs = request.app.db["organizations"]
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
|
||||||
detail="Anonymous users cannot add activity to trail",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
if trailrun:
|
||||||
|
raise HTTPException(
|
||||||
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists"
|
||||||
|
|
||||||
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})
|
statement = select(Trail).where(
|
||||||
|
Trail.org_id == course.org_id, Trail.user_id == user.id
|
||||||
# check if course is already present in the trail
|
|
||||||
for element in trail["courses"]:
|
|
||||||
if element["course_id"] == course_id:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="Course already present in the trail",
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_trail = TrailCourse(
|
|
||||||
course_id=course_id,
|
|
||||||
activities_data=[],
|
|
||||||
activities_marked_complete=[],
|
|
||||||
progress=0,
|
|
||||||
course_object={},
|
|
||||||
status="ongoing",
|
|
||||||
masked=False,
|
|
||||||
)
|
)
|
||||||
trail["courses"].append(updated_trail.dict())
|
trail = db_session.exec(statement).first()
|
||||||
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"]})
|
|
||||||
|
|
||||||
if not trail:
|
if not trail:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
|
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 not trail_run:
|
||||||
if element["course_id"] == course_id:
|
trail_run = TrailRun(
|
||||||
trail["courses"].remove(element)
|
trail_id=trail.id if trail.id is not None else 0,
|
||||||
break
|
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)
|
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
|
||||||
return Trail(**trail)
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -2,214 +2,347 @@ from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from fastapi import HTTPException, Request, status
|
from fastapi import HTTPException, Request, status
|
||||||
|
from sqlmodel import Session, select
|
||||||
from src.security.rbac.rbac import (
|
from src.security.rbac.rbac import (
|
||||||
authorization_verify_based_on_roles,
|
authorization_verify_based_on_roles_and_authorship,
|
||||||
authorization_verify_if_user_is_anon,
|
authorization_verify_if_user_is_anon,
|
||||||
)
|
)
|
||||||
from src.security.security import security_hash_password, security_verify_password
|
from src.db.organizations import Organization
|
||||||
from src.services.users.schemas.users import (
|
from src.db.users import (
|
||||||
PasswordChangeForm,
|
AnonymousUser,
|
||||||
PublicUser,
|
PublicUser,
|
||||||
User,
|
User,
|
||||||
UserOrganization,
|
UserCreate,
|
||||||
UserRolesInOrganization,
|
UserRead,
|
||||||
UserWithPassword,
|
UserUpdate,
|
||||||
UserInDB,
|
UserUpdatePassword,
|
||||||
)
|
)
|
||||||
|
from src.db.user_organizations import UserOrganization
|
||||||
|
from src.security.security import security_hash_password, security_verify_password
|
||||||
|
|
||||||
|
|
||||||
async def create_user(
|
async def create_user(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: PublicUser | None,
|
db_session: Session,
|
||||||
user_object: UserWithPassword,
|
current_user: PublicUser | AnonymousUser,
|
||||||
org_slug: str,
|
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})
|
# RBAC check
|
||||||
isEmailAvailable = await users.find_one({"email": user_object.email})
|
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(
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Email already exists"
|
status_code=400,
|
||||||
|
detail="Username already exists",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate user_id with uuid4
|
# Email
|
||||||
user_id = str(f"user_{uuid4()}")
|
statement = select(User).where(User.email == user.email)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
# Check if the requesting user is authenticated
|
if result.first():
|
||||||
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"):
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
status_code=400,
|
||||||
detail="You are trying to create a user in an organization that does not exist",
|
detail="Email already exists",
|
||||||
)
|
)
|
||||||
|
|
||||||
org_id = isOrgExists["org_id"] if org_slug != "None" else ''
|
|
||||||
|
|
||||||
# Create initial orgs list with the org_id passed in
|
# Exclude unset values
|
||||||
orgs = (
|
user_data = user.dict(exclude_unset=True)
|
||||||
[UserOrganization(org_id=org_id, org_role="member")]
|
for key, value in user_data.items():
|
||||||
if org_slug != "None"
|
setattr(user, key, value)
|
||||||
else []
|
|
||||||
)
|
|
||||||
|
|
||||||
# Give role
|
# Add user to database
|
||||||
roles = (
|
db_session.add(user)
|
||||||
[UserRolesInOrganization(role_id="role_member", org_id=org_id)]
|
db_session.commit()
|
||||||
if org_slug != "None"
|
db_session.refresh(user)
|
||||||
else []
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the user
|
# Link user and organization
|
||||||
user = UserInDB(
|
user_organization = UserOrganization(
|
||||||
user_id=user_id,
|
user_id=user.id if user.id else 0,
|
||||||
|
org_id=int(org_id),
|
||||||
|
role_id=3,
|
||||||
creation_date=str(datetime.now()),
|
creation_date=str(datetime.now()),
|
||||||
update_date=str(datetime.now()),
|
update_date=str(datetime.now()),
|
||||||
orgs=orgs,
|
|
||||||
roles=roles,
|
|
||||||
**user_object.dict(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Insert the user into the database
|
db_session.add(user_organization)
|
||||||
await users.insert_one(user.dict())
|
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):
|
async def create_user_without_org(
|
||||||
users = request.app.db["users"]
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
user_object: UserCreate,
|
||||||
|
):
|
||||||
|
user = User.from_orm(user_object)
|
||||||
|
|
||||||
# Check if the user exists
|
# RBAC check
|
||||||
isUserExists = await users.find_one({"user_id": user_id})
|
await rbac_check(request, current_user, "create", "user_x", db_session)
|
||||||
|
|
||||||
# Verify rights
|
# Complete the user object
|
||||||
await verify_user_rights_on_user(request, current_user, "read", user_id)
|
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
|
# Verifications
|
||||||
if not isUserExists:
|
|
||||||
|
# Username
|
||||||
|
statement = select(User).where(User.username == user.username)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
if result.first():
|
||||||
raise HTTPException(
|
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(
|
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
|
if not user:
|
||||||
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:
|
|
||||||
raise HTTPException(
|
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
|
# RBAC check
|
||||||
if isUserExists["username"] == user_object.username:
|
await rbac_check(request, current_user, "update", user.user_uuid, db_session)
|
||||||
user_object.username = user_object.username.lower()
|
|
||||||
|
|
||||||
else:
|
# Update user
|
||||||
if isUsernameAvailable:
|
user_data = user_object.dict(exclude_unset=True)
|
||||||
raise HTTPException(
|
for key, value in user_data.items():
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Username already used"
|
setattr(user, key, value)
|
||||||
)
|
|
||||||
|
|
||||||
if isEmailAvailable:
|
user.update_date = str(datetime.now())
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Email already used"
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_user = {"$set": user_object.dict()}
|
# Update user in database
|
||||||
users.update_one({"user_id": user_id}, updated_user)
|
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(
|
async def update_user_password(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: PublicUser,
|
db_session: Session,
|
||||||
user_id: str,
|
current_user: PublicUser | AnonymousUser,
|
||||||
password_change_form: PasswordChangeForm,
|
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})
|
if not user:
|
||||||
|
|
||||||
# Verify rights
|
|
||||||
await verify_user_rights_on_user(request, current_user, "update", user_id)
|
|
||||||
|
|
||||||
if not isUserExists:
|
|
||||||
raise HTTPException(
|
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(
|
# RBAC check
|
||||||
password_change_form.old_password, isUserExists["password"]
|
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(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password"
|
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}}
|
# Update user in database
|
||||||
await users.update_one({"user_id": user_id}, updated_user)
|
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):
|
async def read_user_by_id(
|
||||||
users = request.app.db["users"]
|
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})
|
if not user:
|
||||||
|
|
||||||
# Verify is user has permission to delete the user
|
|
||||||
await verify_user_rights_on_user(request, current_user, "delete", user_id)
|
|
||||||
|
|
||||||
if not isUserExists:
|
|
||||||
raise HTTPException(
|
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
|
# Utils & Security functions
|
||||||
|
|
||||||
|
|
||||||
async def security_get_user(request: Request, email: str):
|
async def security_get_user(request: Request, db_session: Session, email: str) -> User:
|
||||||
users = request.app.db["users"]
|
# Check if user exists
|
||||||
|
statement = select(User).where(User.email == email)
|
||||||
user = await users.find_one({"email": email})
|
user = db_session.exec(statement).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -217,105 +350,39 @@ async def security_get_user(request: Request, email: str):
|
||||||
detail="User with Email does not exist",
|
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
|
return user
|
||||||
|
|
||||||
|
|
||||||
async def get_profile_metadata(request: Request, user):
|
## 🔒 RBAC Utils ##
|
||||||
users = request.app.db["users"]
|
|
||||||
request.app.db["roles"]
|
|
||||||
|
|
||||||
user = await users.find_one({"user_id": user["user_id"]})
|
|
||||||
|
|
||||||
if not user:
|
async def rbac_check(
|
||||||
raise HTTPException(
|
request: Request,
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="User does not exist"
|
current_user: PublicUser | AnonymousUser,
|
||||||
|
action: Literal["create", "read", "update", "delete"],
|
||||||
|
user_uuid: str,
|
||||||
|
db_session: Session,
|
||||||
|
):
|
||||||
|
if action == "create":
|
||||||
|
if current_user.id == 0: # if user is anonymous
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
await authorization_verify_based_on_roles_and_authorship(
|
||||||
|
request, current_user.id, "create", "user_x", db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
await authorization_verify_if_user_is_anon(current_user.id)
|
||||||
|
|
||||||
|
# if user is the same as the one being read
|
||||||
|
if current_user.user_uuid == user_uuid:
|
||||||
|
return True
|
||||||
|
|
||||||
|
await authorization_verify_based_on_roles_and_authorship(
|
||||||
|
request, current_user.id, action, user_uuid, db_session
|
||||||
)
|
)
|
||||||
|
|
||||||
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(
|
|
||||||
request: Request,
|
|
||||||
current_user: PublicUser,
|
|
||||||
action: Literal["create", "read", "update", "delete"],
|
|
||||||
user_id: str,
|
|
||||||
):
|
|
||||||
users = request.app.db["users"]
|
|
||||||
user = UserInDB(**await users.find_one({"user_id": user_id}))
|
|
||||||
|
|
||||||
if action == "create":
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
if action == "delete":
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from config.config import get_learnhouse_config
|
||||||
|
|
||||||
|
|
||||||
async def upload_content(
|
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
|
# Get Learnhouse Config
|
||||||
learnhouse_config = get_learnhouse_config()
|
learnhouse_config = get_learnhouse_config()
|
||||||
|
|
@ -16,12 +16,12 @@ async def upload_content(
|
||||||
|
|
||||||
if content_delivery == "filesystem":
|
if content_delivery == "filesystem":
|
||||||
# create folder for activity
|
# 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
|
# create folder for activity
|
||||||
os.makedirs(f"content/{org_id}/{directory}")
|
os.makedirs(f"content/{org_uuid}/{directory}")
|
||||||
# upload file to server
|
# upload file to server
|
||||||
with open(
|
with open(
|
||||||
f"content/{org_id}/{directory}/{file_and_format}",
|
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
"wb",
|
"wb",
|
||||||
) as f:
|
) as f:
|
||||||
f.write(file_binary)
|
f.write(file_binary)
|
||||||
|
|
@ -37,13 +37,13 @@ async def upload_content(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create folder for activity
|
# 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
|
# create folder for activity
|
||||||
os.makedirs(f"content/{org_id}/{directory}")
|
os.makedirs(f"content/{org_uuid}/{directory}")
|
||||||
|
|
||||||
# Upload file to server
|
# Upload file to server
|
||||||
with open(
|
with open(
|
||||||
f"content/{org_id}/{directory}/{file_and_format}",
|
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
"wb",
|
"wb",
|
||||||
) as f:
|
) as f:
|
||||||
f.write(file_binary)
|
f.write(file_binary)
|
||||||
|
|
@ -52,9 +52,9 @@ async def upload_content(
|
||||||
print("Uploading to s3 using boto3...")
|
print("Uploading to s3 using boto3...")
|
||||||
try:
|
try:
|
||||||
s3.upload_file(
|
s3.upload_file(
|
||||||
f"content/{org_id}/{directory}/{file_and_format}",
|
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
"learnhouse-media",
|
"learnhouse-media",
|
||||||
f"content/{org_id}/{directory}/{file_and_format}",
|
f"content/{org_uuid}/{directory}/{file_and_format}",
|
||||||
)
|
)
|
||||||
except ClientError as e:
|
except ClientError as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
@ -63,7 +63,7 @@ async def upload_content(
|
||||||
try:
|
try:
|
||||||
s3.head_object(
|
s3.head_object(
|
||||||
Bucket="learnhouse-media",
|
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!")
|
print("File upload successful!")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { default as React, } from "react";
|
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 EditorWrapper from "@components/Objects/Editor/EditorWrapper";
|
||||||
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth";
|
||||||
|
import { getOrganizationContextInfo, getOrganizationContextInfoWithId } from "@services/organizations/orgs";
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string, activityid: string };
|
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)
|
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `Edit - ${course_meta.course.name} Activity`,
|
title: `Edit - ${course_meta.name} Activity`,
|
||||||
description: course_meta.course.mini_description,
|
description: course_meta.mini_description,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditActivity = async (params: any) => {
|
const EditActivity = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const activityid = params.params.activityid;
|
const activityuuid = params.params.activityuuid;
|
||||||
const courseid = params.params.courseid;
|
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 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
<EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -5,7 +5,7 @@ import { deleteOrganizationFromBackend } from "@services/organizations/orgs";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { swrFetcher } from "@services/utils/ts/requests";
|
import { swrFetcher } from "@services/utils/ts/requests";
|
||||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||||
import AuthProvider from "@components/Security/AuthProvider";
|
import AuthProvider from "@components/Security/AuthProviderDepreceated";
|
||||||
|
|
||||||
const Organizations = () => {
|
const Organizations = () => {
|
||||||
const { data: organizations, error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
|
const { data: organizations, error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export async function generateMetadata(
|
||||||
const CollectionPage = async (params: any) => {
|
const CollectionPage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
|
const org = await getOrganizationContextInfo(params.params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
|
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 />
|
<br />
|
||||||
<div className="home_courses flex flex-wrap">
|
<div className="home_courses flex flex-wrap">
|
||||||
{col.courses.map((course: any) => (
|
{col.courses.map((course: any) => (
|
||||||
<div className="pr-8" key={course.course_id}>
|
<div className="pr-8" key={course.course_uuid}>
|
||||||
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}>
|
<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(course.org_id, course.course_id, course.thumbnail)})` }}>
|
<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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
|
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ function NewCollection(params: any) {
|
||||||
description: description,
|
description: description,
|
||||||
courses: selectedCourses,
|
courses: selectedCourses,
|
||||||
public: true,
|
public: true,
|
||||||
org_id: org.org_id,
|
org_id: org.id,
|
||||||
};
|
};
|
||||||
await createCollection(collection);
|
await createCollection(collection);
|
||||||
await revalidateTags(["collections"], orgslug);
|
await revalidateTags(["collections"], orgslug);
|
||||||
|
|
@ -69,26 +69,29 @@ function NewCollection(params: any) {
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{courses.map((course: any) => (
|
{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"
|
<input
|
||||||
id={course.course_id}
|
|
||||||
name={course.course_id}
|
type="checkbox"
|
||||||
value={course.course_id}
|
id={course.id}
|
||||||
checked={selectedCourses.includes(course.course_id)}
|
name={course.name}
|
||||||
onChange={(e) => {
|
value={course.id}
|
||||||
const courseId = e.target.value;
|
// id is an integer, not a string
|
||||||
setSelectedCourses((prevSelectedCourses: string[]) => {
|
|
||||||
if (e.target.checked) {
|
onChange={(e) => {
|
||||||
return [...prevSelectedCourses, courseId];
|
if (e.target.checked) {
|
||||||
} else {
|
setSelectedCourses([...selectedCourses, course.id]);
|
||||||
return prevSelectedCourses.filter((selectedCourse) => selectedCourse !== courseId);
|
}
|
||||||
}
|
else {
|
||||||
});
|
setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
|
||||||
}}
|
}
|
||||||
className="mr-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
}
|
||||||
/>
|
}
|
||||||
<label htmlFor={course.course_id} className="text-sm">{course.name}</label>
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label htmlFor={course.course_uuid} className="text-sm">{course.name}</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Metadata } from "next";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
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";
|
import NewCollectionButton from "@components/StyledElements/Buttons/NewCollectionButton";
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
|
|
@ -49,14 +49,17 @@ const CollectionsPage = async (params: any) => {
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
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'] });
|
const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
<div className="flex justify-between" >
|
<div className="flex justify-between" >
|
||||||
<TypeOfContentTitle title="Collections" type="col" />
|
<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")}>
|
<Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}>
|
||||||
<NewCollectionButton />
|
<NewCollectionButton />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -64,7 +67,7 @@ const CollectionsPage = async (params: any) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="home_collections flex flex-wrap">
|
<div className="home_collections flex flex-wrap">
|
||||||
{collections.map((collection: any) => (
|
{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} />
|
<CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org_id} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -81,7 +84,10 @@ const CollectionsPage = async (params: any) => {
|
||||||
<h1 className="text-3xl font-bold text-gray-600">No collections yet</h1>
|
<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>
|
<p className="text-lg text-gray-400">Create a collection to group courses together</p>
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}>
|
<AuthenticatedClientElement checkMethod='roles'
|
||||||
|
ressourceType="collection"
|
||||||
|
action="create"
|
||||||
|
orgId={org_id}>
|
||||||
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
|
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
|
||||||
<NewCollectionButton />
|
<NewCollectionButton />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -8,7 +8,7 @@ import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshToke
|
||||||
|
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string, activityid: string };
|
params: { orgslug: string, courseuuid: string, activityid: string };
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -20,14 +20,14 @@ export async function generateMetadata(
|
||||||
|
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
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)
|
const activity = await getActivityWithAuthHeader(params.activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
return {
|
return {
|
||||||
title: activity.name + ` — ${course_meta.course.name} Course`,
|
title: activity.name + ` — ${course_meta.name} Course`,
|
||||||
description: course_meta.course.mini_description,
|
description: course_meta.description,
|
||||||
keywords: course_meta.course.learnings,
|
keywords: course_meta.learnings,
|
||||||
robots: {
|
robots: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
|
|
@ -39,11 +39,10 @@ export async function generateMetadata(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: activity.name + ` — ${course_meta.course.name} Course`,
|
title: activity.name + ` — ${course_meta.name} Course`,
|
||||||
description: course_meta.course.mini_description,
|
description: course_meta.description,
|
||||||
type: activity.type === 'video' ? 'video.other' : 'article',
|
publishedTime: course_meta.creation_date,
|
||||||
publishedTime: course_meta.course.creationDate,
|
tags: course_meta.learnings,
|
||||||
tags: course_meta.course.learnings,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -52,16 +51,16 @@ const ActivityPage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const activityid = params.params.activityid;
|
const activityid = params.params.activityid;
|
||||||
const courseid = params.params.courseid;
|
const courseuuid = params.params.courseuuid;
|
||||||
const orgslug = params.params.orgslug;
|
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)
|
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ActivityClient
|
<ActivityClient
|
||||||
activityid={activityid}
|
activityid={activityid}
|
||||||
courseid={courseid}
|
courseuuid={courseuuid}
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
activity={activity}
|
activity={activity}
|
||||||
course={course_meta}
|
course={course_meta}
|
||||||
|
|
@ -12,26 +12,28 @@ import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
||||||
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
|
import { ArrowRight, Check, File, Sparkles, Star, Video } from "lucide-react";
|
||||||
import Avvvatars from "avvvatars-react";
|
import Avvvatars from "avvvatars-react";
|
||||||
import { getUser } from "@services/users/users";
|
import { getUser } from "@services/users/users";
|
||||||
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
|
|
||||||
const CourseClient = (props: any) => {
|
const CourseClient = (props: any) => {
|
||||||
const [user, setUser] = useState<any>({});
|
const [user, setUser] = useState<any>({});
|
||||||
const courseid = props.courseid;
|
const [learnings, setLearnings] = useState<any>([]);
|
||||||
|
const courseuuid = props.courseuuid;
|
||||||
const orgslug = props.orgslug;
|
const orgslug = props.orgslug;
|
||||||
const course = props.course;
|
const course = props.course;
|
||||||
|
const org = useOrg() as any;
|
||||||
const router = useRouter();
|
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() {
|
async function startCourseUI() {
|
||||||
// Create activity
|
// Create activity
|
||||||
await startCourse("course_" + courseid, orgslug);
|
await startCourse("course_" + courseuuid, orgslug);
|
||||||
await revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
||||||
|
|
@ -39,18 +41,24 @@ const CourseClient = (props: any) => {
|
||||||
// window.location.reload();
|
// 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() {
|
async function quitCourse() {
|
||||||
// Close activity
|
// Close activity
|
||||||
let activity = await removeCourse("course_" + courseid, orgslug);
|
let activity = await removeCourse("course_" + courseuuid, orgslug);
|
||||||
// Mutate course
|
// Mutate course
|
||||||
await revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserUI();
|
|
||||||
}
|
}
|
||||||
, []);
|
, [org]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -61,26 +69,26 @@ const CourseClient = (props: any) => {
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<p className="text-md font-bold text-gray-400 pb-2">Course</p>
|
<p className="text-md font-bold text-gray-400 pb-2">Course</p>
|
||||||
<h1 className="text-3xl -mt-3 font-bold">
|
<h1 className="text-3xl -mt-3 font-bold">
|
||||||
{course.course.name}
|
{course.name}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</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>
|
</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="flex flex-row pt-10">
|
||||||
<div className="course_metadata_left grow space-y-2">
|
<div className="course_metadata_left grow space-y-2">
|
||||||
<h2 className="py-3 text-2xl font-bold">Description</h2>
|
<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">
|
<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>
|
</div>
|
||||||
|
|
||||||
<h2 className="py-3 text-2xl font-bold">What you will learn</h2>
|
<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">
|
<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 (
|
return (
|
||||||
<div key={learning}
|
<div key={learning}
|
||||||
className="flex space-x-2 items-center font-semibold text-gray-500 capitalize">
|
className="flex space-x-2 items-center font-semibold text-gray-500 capitalize">
|
||||||
|
|
@ -118,48 +126,48 @@ const CourseClient = (props: any) => {
|
||||||
</p>
|
</p>
|
||||||
<div className="flex space-x-1 py-2 px-4 items-center">
|
<div className="flex space-x-1 py-2 px-4 items-center">
|
||||||
<div className="courseicon items-center flex space-x-2 text-neutral-400">
|
<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">
|
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
||||||
<Sparkles className="text-gray-400" size={13} />
|
<Sparkles className="text-gray-400" size={13} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{activity.type === "video" &&
|
{activity.activity_type === "TYPE_VIDEO" &&
|
||||||
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
||||||
<Video className="text-gray-400" size={13} />
|
<Video className="text-gray-400" size={13} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{activity.type === "documentpdf" &&
|
{activity.activity_type === "TYPE_DOCUMENT" &&
|
||||||
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
||||||
<File className="text-gray-400" size={13} />
|
<File className="text-gray-400" size={13} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
</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>
|
<p>{activity.name}</p>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex ">
|
<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">
|
<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>
|
<p>Page</p>
|
||||||
<ArrowRight size={13} /></div>
|
<ArrowRight size={13} /></div>
|
||||||
</Link>
|
</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">
|
<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>
|
<p>Video</p>
|
||||||
<ArrowRight size={13} /></div>
|
<ArrowRight size={13} /></div>
|
||||||
</Link>
|
</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">
|
<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>
|
<p>Document</p>
|
||||||
<ArrowRight size={13} /></div>
|
<ArrowRight size={13} /></div>
|
||||||
|
|
@ -178,19 +186,20 @@ const CourseClient = (props: any) => {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="course_metadata_right space-y-3 w-64 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
<div className="course_metadata_right space-y-3 w-64 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
||||||
{ user &&
|
{user &&
|
||||||
<div className="flex mx-auto space-x-3 px-2 py-2 items-center">
|
<div className="flex mx-auto space-x-3 px-2 py-2 items-center">
|
||||||
<div className="">
|
<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">{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>
|
</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>
|
|
||||||
</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}>
|
<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
|
Quit Course
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -7,7 +7,7 @@ import { Metadata } from 'next';
|
||||||
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth';
|
import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth';
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string };
|
params: { orgslug: string, courseuuid: string };
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,14 +19,14 @@ export async function generateMetadata(
|
||||||
|
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
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
|
// SEO
|
||||||
return {
|
return {
|
||||||
title: course_meta.course.name + ` — ${org.name}`,
|
title: course_meta.name + ` — ${org.name}`,
|
||||||
description: course_meta.course.mini_description,
|
description: course_meta.description,
|
||||||
keywords: course_meta.course.learnings,
|
keywords: course_meta.learnings,
|
||||||
robots: {
|
robots: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
|
|
@ -38,11 +38,11 @@ export async function generateMetadata(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: course_meta.course.name + ` — ${org.name}`,
|
title: course_meta.name + ` — ${org.name}`,
|
||||||
description: course_meta.course.mini_description,
|
description: course_meta.description ? course_meta.description : '',
|
||||||
type: 'article',
|
type: 'article',
|
||||||
publishedTime: course_meta.course.creationDate,
|
publishedTime: course_meta.creation_date ? course_meta.creation_date : '',
|
||||||
tags: course_meta.course.learnings,
|
tags: course_meta.learnings ? course_meta.learnings : [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -50,14 +50,14 @@ export async function generateMetadata(
|
||||||
|
|
||||||
const CoursePage = async (params: any) => {
|
const CoursePage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const courseid = params.params.courseid
|
const courseuuid = params.params.courseuuid
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CourseClient courseid={courseid} orgslug={orgslug} course={course_meta} />
|
<CourseClient courseuuid={courseuuid} orgslug={orgslug} course={course_meta} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import { useSearchParams } from 'next/navigation';
|
||||||
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
|
||||||
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
|
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
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';
|
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
||||||
|
|
||||||
interface CourseProps {
|
interface CourseProps {
|
||||||
|
|
@ -32,7 +32,10 @@ function Courses(props: CourseProps) {
|
||||||
|
|
||||||
<div className='flex flex-wrap justify-between'>
|
<div className='flex flex-wrap justify-between'>
|
||||||
<TypeOfContentTitle title="Courses" type="cou" />
|
<TypeOfContentTitle title="Courses" type="cou" />
|
||||||
<AuthenticatedClientElement checkMethod='roles' orgId={props.org_id}>
|
<AuthenticatedClientElement checkMethod='roles'
|
||||||
|
action='create'
|
||||||
|
ressourceType='course'
|
||||||
|
orgId={props.org_id}>
|
||||||
<Modal
|
<Modal
|
||||||
isDialogOpen={newCourseModal}
|
isDialogOpen={newCourseModal}
|
||||||
onOpenChange={setNewCourseModal}
|
onOpenChange={setNewCourseModal}
|
||||||
|
|
@ -56,7 +59,7 @@ function Courses(props: CourseProps) {
|
||||||
|
|
||||||
<div className="flex flex-wrap">
|
<div className="flex flex-wrap">
|
||||||
{courses.map((course: any) => (
|
{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} />
|
<CourseThumbnail course={course} orgslug={orgslug} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -73,7 +76,10 @@ function Courses(props: CourseProps) {
|
||||||
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
|
<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>
|
<p className="text-lg text-gray-400">Create a course to add content</p>
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement checkMethod='roles' orgId={props.org_id}>
|
<AuthenticatedClientElement
|
||||||
|
action='create'
|
||||||
|
ressourceType='course'
|
||||||
|
checkMethod='roles' orgId={props.org_id}>
|
||||||
<Modal
|
<Modal
|
||||||
isDialogOpen={newCourseModal}
|
isDialogOpen={newCourseModal}
|
||||||
onOpenChange={setNewCourseModal}
|
onOpenChange={setNewCourseModal}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import "@styles/globals.css";
|
import "@styles/globals.css";
|
||||||
import { Menu } from "@components/Objects/Menu/Menu";
|
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}) {
|
export default function RootLayout({ children, params }: { children: React.ReactNode , params :any}) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import { cookies } from 'next/headers';
|
||||||
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
|
||||||
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
|
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
||||||
import CourseThumbnail from '@components/Objects/Other/CourseThumbnail';
|
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
||||||
import CollectionThumbnail from '@components/Objects/Other/CollectionThumbnail';
|
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail';
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||||
import { Plus, PlusCircle } from 'lucide-react';
|
import { Plus, PlusCircle } from 'lucide-react';
|
||||||
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
||||||
|
|
@ -56,8 +56,8 @@ const OrgHomePage = async (params: any) => {
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
|
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 = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||||
const org_id = org.org_id;
|
const org_id = org.id;
|
||||||
const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] });
|
const collections = await getOrgCollectionsWithAuthHeader(org.id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -67,7 +67,11 @@ const OrgHomePage = async (params: any) => {
|
||||||
<div className='flex grow'>
|
<div className='flex grow'>
|
||||||
<TypeOfContentTitle title="Collections" type="col" />
|
<TypeOfContentTitle title="Collections" type="col" />
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}>
|
<AuthenticatedClientElement
|
||||||
|
checkMethod='roles'
|
||||||
|
ressourceType='collection'
|
||||||
|
action='create'
|
||||||
|
orgId={org_id}>
|
||||||
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
|
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
|
||||||
<NewCollectionButton />
|
<NewCollectionButton />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -105,7 +109,11 @@ const OrgHomePage = async (params: any) => {
|
||||||
<div className='flex grow'>
|
<div className='flex grow'>
|
||||||
<TypeOfContentTitle title="Courses" type="cou" />
|
<TypeOfContentTitle title="Courses" type="cou" />
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement checkMethod='roles' orgId={org_id}>
|
<AuthenticatedClientElement
|
||||||
|
ressourceType='course'
|
||||||
|
action='create'
|
||||||
|
checkMethod='roles'
|
||||||
|
orgId={org_id}>
|
||||||
<Link href={getUriWithOrg(orgslug, "/courses?new=true")}>
|
<Link href={getUriWithOrg(orgslug, "/courses?new=true")}>
|
||||||
<NewCourseButton />
|
<NewCourseButton />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -113,7 +121,7 @@ const OrgHomePage = async (params: any) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="home_courses flex flex-wrap">
|
<div className="home_courses flex flex-wrap">
|
||||||
{courses.map((course: any) => (
|
{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} />
|
<CourseThumbnail course={course} orgslug={orgslug} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { useOrg } from "@components/Contexts/OrgContext";
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
||||||
import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement";
|
import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement";
|
||||||
import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
|
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 { getAPIUrl } from "@services/config/config";
|
||||||
import { removeCourse } from "@services/courses/activity";
|
import { removeCourse } from "@services/courses/activity";
|
||||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
function Trail(params: any) {
|
function Trail(params: any) {
|
||||||
let orgslug = params.orgslug;
|
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 (
|
return (
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
|
|
@ -21,12 +27,10 @@ function Trail(params: any) {
|
||||||
<PageLoading></PageLoading>
|
<PageLoading></PageLoading>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{trail.courses.map((course: any) => (
|
{trail.runs.map((run: any) => (
|
||||||
!course.masked ? (
|
<>
|
||||||
<TrailCourseElement key={trail.trail_id} orgslug={orgslug} course={course} />
|
<TrailCourseElement run={run} course={run.course} orgslug={orgslug} />
|
||||||
) : (
|
</>
|
||||||
<></>
|
|
||||||
)
|
|
||||||
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
||||||
109
apps/web/app/orgs/[orgslug]/dash/courses/client.tsx
Normal file
109
apps/web/app/orgs/[orgslug]/dash/courses/client.tsx
Normal 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
|
||||||
|
|
@ -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
|
||||||
57
apps/web/app/orgs/[orgslug]/dash/courses/page.tsx
Normal file
57
apps/web/app/orgs/[orgslug]/dash/courses/page.tsx
Normal 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
|
||||||
20
apps/web/app/orgs/[orgslug]/dash/layout.tsx
Normal file
20
apps/web/app/orgs/[orgslug]/dash/layout.tsx
Normal 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
|
||||||
|
|
@ -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
|
||||||
13
apps/web/app/orgs/[orgslug]/dash/page.tsx
Normal file
13
apps/web/app/orgs/[orgslug]/dash/page.tsx
Normal 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
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue