From afaf1d6dfe3ceac0ee8a8e28a1279983dbff9086 Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 28 Oct 2023 14:53:48 +0200 Subject: [PATCH 01/39] feat: add postgresql to docker-compose --- docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index ad4ae4b4..eb8a2dab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,15 @@ services: - .:/usr/learnhouse environment: - LEARNHOUSE_COOKIE_DOMAIN=.localhost + postgresql: + image: postgres:16-alpine + restart: always + ports: + - "5432:5432" + environment: + - POSTGRES_USER=learnhouse + - POSTGRES_PASSWORD=learnhouse + - POSTGRES_DB=learnhouse mongo: image: mongo:5.0 restart: always From 732b14866c1af37dc593ece211104ce4895d207f Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 28 Oct 2023 19:36:58 +0200 Subject: [PATCH 02/39] wip: initiate user creation --- apps/api/app.py | 3 +- apps/api/requirements.txt | 2 + apps/api/src/core/events/database.py | 26 ++++++++---- apps/api/src/rewrite/__init__.py | 0 apps/api/src/rewrite/routers/users.py | 23 +++++++++++ apps/api/src/rewrite/services/db/__init__.py | 0 apps/api/src/rewrite/services/db/users.py | 32 +++++++++++++++ apps/api/src/rewrite/services/users/users.py | 43 ++++++++++++++++++++ apps/api/src/router.py | 6 +++ docker-compose.yml | 2 +- 10 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 apps/api/src/rewrite/__init__.py create mode 100644 apps/api/src/rewrite/routers/users.py create mode 100644 apps/api/src/rewrite/services/db/__init__.py create mode 100644 apps/api/src/rewrite/services/db/users.py create mode 100644 apps/api/src/rewrite/services/users/users.py diff --git a/apps/api/app.py b/apps/api/app.py index 3a7f62c6..dc066251 100644 --- a/apps/api/app.py +++ b/apps/api/app.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, Request from config.config import LearnHouseConfig, get_learnhouse_config from src.core.events.events import shutdown_app, startup_app -from src.router import v1_router +from src.router import v1_router, rewrite from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles @@ -60,6 +60,7 @@ app.mount("/content", StaticFiles(directory="content"), name="content") # Global Routes app.include_router(v1_router) +app.include_router(rewrite) # General Routes @app.get("/") diff --git a/apps/api/requirements.txt b/apps/api/requirements.txt index 3f69c553..afec2b0a 100644 --- a/apps/api/requirements.txt +++ b/apps/api/requirements.txt @@ -1,8 +1,10 @@ fastapi==0.101.1 pydantic>=1.8.0,<2.0.0 +sqlmodel==0.0.10 uvicorn==0.23.2 pymongo==4.3.3 motor==3.1.1 +psycopg2 python-multipart boto3 botocore diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index 174575a6..984c23c4 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -1,19 +1,27 @@ import logging from fastapi import FastAPI import motor.motor_asyncio +from sqlmodel import Field, SQLModel, Session, create_engine +from src.rewrite.services.db import users +engine = create_engine('postgresql://learnhouse:learnhouse@db:5432/learnhouse', echo=True) +SQLModel.metadata.create_all(engine) async def connect_to_db(app: FastAPI): - logging.info("Connecting to database...") - try: - app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore - app.learnhouse_config.database_config.mongodb_connection_string) # type: ignore - app.db = app.mongodb_client["learnhouse"] # type: ignore - logging.info("Connected to database!") - except Exception as e: - logging.error("Failed to connect to database!") - logging.error(e) + app.db_engine = engine # type: ignore + logging.info("LearnHouse database has been started.") + + SQLModel.metadata.create_all(engine) + + # mongodb + app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore + app.learnhouse_config.database_config.mongodb_connection_string) # 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): app.mongodb_client.close() # type: ignore diff --git a/apps/api/src/rewrite/__init__.py b/apps/api/src/rewrite/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/rewrite/routers/users.py b/apps/api/src/rewrite/routers/users.py new file mode 100644 index 00000000..bd01e15e --- /dev/null +++ b/apps/api/src/rewrite/routers/users.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter, Depends, Request +from sqlmodel import Session +from src.core.events.database import get_db_session + +from src.rewrite.services.db.users import UserCreate, UserRead +from src.rewrite.services.users.users import create_user + + +router = APIRouter() + + +@router.post("/", response_model=UserRead, tags=["users"]) +async def api_create_user( + *, + request: Request, + db_session: Session = Depends(get_db_session), + user_object: UserCreate, + org_slug: str +): + """ + Create new user + """ + return await create_user(request, db_session, None, user_object, org_slug) diff --git a/apps/api/src/rewrite/services/db/__init__.py b/apps/api/src/rewrite/services/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/rewrite/services/db/users.py b/apps/api/src/rewrite/services/db/users.py new file mode 100644 index 00000000..a02d6c19 --- /dev/null +++ b/apps/api/src/rewrite/services/db/users.py @@ -0,0 +1,32 @@ +from typing import Optional +from sqlmodel import Field, Session, SQLModel, create_engine, select + + +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): + password: Optional[str] = None + + +class UserRead(UserBase): + id: int + + +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 = "" diff --git a/apps/api/src/rewrite/services/users/users.py b/apps/api/src/rewrite/services/users/users.py new file mode 100644 index 00000000..5bb69ecb --- /dev/null +++ b/apps/api/src/rewrite/services/users/users.py @@ -0,0 +1,43 @@ +from datetime import datetime +from uuid import uuid4 +from fastapi import Depends, Request +from sqlmodel import Session +from src.rewrite.services.db.users import User, UserCreate +from src.security.security import security_hash_password +from src.services.users.schemas.users import PublicUser + + +async def create_user( + request: Request, + db_session: Session, + current_user: PublicUser | None, + user_object: UserCreate, + org_slug: str, +): + user = User.from_orm(user_object) + + # 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 + #todo: add username uniqueness verification + #todo: add email uniqueness verification + + + #todo: add user to org as member if org is not None + + # 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) + + return user diff --git a/apps/api/src/router.py b/apps/api/src/router.py index 895873b3..08a5325b 100644 --- a/apps/api/src/router.py +++ b/apps/api/src/router.py @@ -1,5 +1,7 @@ from fastapi import APIRouter, Depends +from src import rewrite from src.routers import blocks, dev, trail, users, auth, orgs, roles +from src.rewrite.routers import users as rw_users from src.routers.courses import chapters, collections, courses, activities from src.routers.install import install from src.services.dev.dev import isDevModeEnabledOrRaise @@ -7,6 +9,7 @@ from src.services.install.install import isInstallModeEnabled v1_router = APIRouter(prefix="/api/v1") +rewrite = APIRouter(prefix="/api/rewrite") # API Routes @@ -35,3 +38,6 @@ v1_router.include_router( tags=["install"], dependencies=[Depends(isInstallModeEnabled)], ) + +# Rewrite Routes +rewrite.include_router(rw_users.router, prefix="/users", tags=["users"]) diff --git a/docker-compose.yml b/docker-compose.yml index eb8a2dab..a1bd0ef5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: - .:/usr/learnhouse environment: - LEARNHOUSE_COOKIE_DOMAIN=.localhost - postgresql: + db: image: postgres:16-alpine restart: always ports: From b4dcc1474954311cd85e5583d2d75875ff24c141 Mon Sep 17 00:00:00 2001 From: swve Date: Sun, 29 Oct 2023 13:46:02 +0100 Subject: [PATCH 03/39] wip: add more schemas --- apps/api/src/core/events/database.py | 32 ++++++++-- .../api/src/rewrite/services/db/activities.py | 58 +++++++++++++++++++ apps/api/src/rewrite/services/db/blocks.py | 44 ++++++++++++++ .../rewrite/services/db/chapter_activities.py | 13 +++++ apps/api/src/rewrite/services/db/chapters.py | 30 ++++++++++ .../src/rewrite/services/db/collections.py | 3 + .../src/rewrite/services/db/course_authors.py | 18 ++++++ .../rewrite/services/db/course_chapters.py | 12 ++++ apps/api/src/rewrite/services/db/courses.py | 29 ++++++++++ .../services/db/organization_settings.py | 17 ++++++ .../src/rewrite/services/db/organizations.py | 26 +++++++++ apps/api/src/rewrite/services/db/roles.py | 30 ++++++++++ .../rewrite/services/db/user_organizations.py | 11 ++++ apps/api/src/rewrite/services/db/users.py | 2 +- apps/api/src/rewrite/services/users/users.py | 2 +- 15 files changed, 319 insertions(+), 8 deletions(-) create mode 100644 apps/api/src/rewrite/services/db/activities.py create mode 100644 apps/api/src/rewrite/services/db/blocks.py create mode 100644 apps/api/src/rewrite/services/db/chapter_activities.py create mode 100644 apps/api/src/rewrite/services/db/chapters.py create mode 100644 apps/api/src/rewrite/services/db/collections.py create mode 100644 apps/api/src/rewrite/services/db/course_authors.py create mode 100644 apps/api/src/rewrite/services/db/course_chapters.py create mode 100644 apps/api/src/rewrite/services/db/courses.py create mode 100644 apps/api/src/rewrite/services/db/organization_settings.py create mode 100644 apps/api/src/rewrite/services/db/organizations.py create mode 100644 apps/api/src/rewrite/services/db/roles.py create mode 100644 apps/api/src/rewrite/services/db/user_organizations.py diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index 984c23c4..f7b2d2d8 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -2,27 +2,47 @@ import logging from fastapi import FastAPI import motor.motor_asyncio from sqlmodel import Field, SQLModel, Session, create_engine -from src.rewrite.services.db import users -engine = create_engine('postgresql://learnhouse:learnhouse@db:5432/learnhouse', echo=True) +from src.rewrite.services.db import ( + user_organizations, + users, + roles, + organization_settings, + organizations, + courses, + course_authors, + chapters, + activities, + course_chapters, + chapter_activities, + blocks, + collections, +) + +engine = create_engine( + "postgresql://learnhouse:learnhouse@db:5432/learnhouse", echo=True +) SQLModel.metadata.create_all(engine) + async def connect_to_db(app: FastAPI): app.db_engine = engine # type: ignore logging.info("LearnHouse database has been started.") - SQLModel.metadata.create_all(engine) - # mongodb + # mongodb app.mongodb_client = motor.motor_asyncio.AsyncIOMotorClient( # type: ignore - app.learnhouse_config.database_config.mongodb_connection_string) # type: ignore - app.db = app.mongodb_client["learnhouse"] # type: ignore + app.learnhouse_config.database_config.mongodb_connection_string # type: ignore + ) # 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): app.mongodb_client.close() # type: ignore logging.info("LearnHouse has been shut down.") diff --git a/apps/api/src/rewrite/services/db/activities.py b/apps/api/src/rewrite/services/db/activities.py new file mode 100644 index 00000000..ca05a622 --- /dev/null +++ b/apps/api/src/rewrite/services/db/activities.py @@ -0,0 +1,58 @@ +from typing import Literal, Optional +from sqlalchemy import JSON, Column +from sqlmodel import Field, Session, SQLModel, create_engine, select +from enum import Enum + + +class ActivityTypeEnum(str, Enum): + VIDEO = "VIDEO" + DOCUMENT = "DOCUMENT" + DYNAMIC = "DYNAMIC" + ASSESSMENT = "ASSESSMENT" + CUSTOM = "CUSTOM" + + +class ActivitySubTypeEnum(str, Enum): + # Dynamic + DYNAMIC_PAGE = "DYNAMIC_PAGE" + # Video + VIDEO_YOUTUBE = "VIDEO_YOUTUBE" + VIDEO_HOSTED = "VIDEO_HOSTED" + # Document + DOCUMENT_PDF = "DOCUMENT_PDF" + DOCUMENT_DOC = "DOCUMENT_GDOC" + # Assessment + ASSESSMENT_QUIZ = "ASSESSMENT_QUIZ" + # Custom + CUSTOM = "CUSTOM" + + +class ActivityBase(SQLModel): + name: str + activity_type: ActivityTypeEnum = ActivityTypeEnum.CUSTOM + activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.CUSTOM + slug: str + content: dict = Field(default={}, sa_column=Column(JSON)) + published_version: int + version: int + org_id: int = Field(default=None, foreign_key="organization.id") + course_id: int = Field(default=None, foreign_key="course.id") + + +class Activity(ActivityBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + activity_uuid: str + creation_date: str + update_date: str + + +class ActivityCreate(ActivityBase): + pass + + +class ActivityRead(ActivityBase): + id: int + activity_uuid: str + creation_date: str + update_date: str + pass diff --git a/apps/api/src/rewrite/services/db/blocks.py b/apps/api/src/rewrite/services/db/blocks.py new file mode 100644 index 00000000..24f67c2d --- /dev/null +++ b/apps/api/src/rewrite/services/db/blocks.py @@ -0,0 +1,44 @@ +from typing import Optional +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel +from enum import Enum + + +class BlockTypeEnum(str, Enum): + QUIZ_BLOCK = "QUIZ_BLOCK" + VIDEO_BLOCK = "VIDEO_BLOCK" + DOCUMENT_PDF_BLOCK = "DOCUMENT_PDF_BLOCK" + IMAGE_BLOCK = "IMAGE_BLOCK" + CUSTOM = "CUSTOM" + + +class BlockBase(SQLModel): + id: Optional[int] = Field(default=None, primary_key=True) + block_type: BlockTypeEnum = BlockTypeEnum.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(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 + +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 \ No newline at end of file diff --git a/apps/api/src/rewrite/services/db/chapter_activities.py b/apps/api/src/rewrite/services/db/chapter_activities.py new file mode 100644 index 00000000..73d20598 --- /dev/null +++ b/apps/api/src/rewrite/services/db/chapter_activities.py @@ -0,0 +1,13 @@ +from typing import Optional +from sqlmodel import Field, SQLModel +from enum import Enum + +class ChapterActivity(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + order: int + chapter_id: int = Field(default=None, foreign_key="chapter.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") + creation_date: str + update_date: str \ No newline at end of file diff --git a/apps/api/src/rewrite/services/db/chapters.py b/apps/api/src/rewrite/services/db/chapters.py new file mode 100644 index 00000000..c7490c4a --- /dev/null +++ b/apps/api/src/rewrite/services/db/chapters.py @@ -0,0 +1,30 @@ +from typing import Optional +from sqlmodel import Field, SQLModel + + +class ChapterBase(SQLModel): + name: str + description: Optional[str] = "" + thumbnail_image: Optional[str] = "" + org_id: int = Field(default=None, foreign_key="organization.id") + creation_date: str + update_date: str + + +class Chapter(ChapterBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + chapter_uuid: str + creation_date: str + update_date: str + + +class ChapterCreate(ChapterBase): + pass + + +class ChapterRead(ChapterBase): + id: int + chapter_uuid: str + creation_date: str + update_date: str + pass diff --git a/apps/api/src/rewrite/services/db/collections.py b/apps/api/src/rewrite/services/db/collections.py new file mode 100644 index 00000000..63960f70 --- /dev/null +++ b/apps/api/src/rewrite/services/db/collections.py @@ -0,0 +1,3 @@ +from typing import Optional +from sqlmodel import Field, SQLModel +from enum import Enum \ No newline at end of file diff --git a/apps/api/src/rewrite/services/db/course_authors.py b/apps/api/src/rewrite/services/db/course_authors.py new file mode 100644 index 00000000..1edaa47f --- /dev/null +++ b/apps/api/src/rewrite/services/db/course_authors.py @@ -0,0 +1,18 @@ +from typing import Optional +from sqlmodel import Field, SQLModel +from enum import Enum + + +class CourseAuthorshipEnum(str, Enum): + CREATOR = "CREATOR" + MAINTAINER = "MAINTAINER" + REPORTER = "REPORTER" + + +class CourseAuthor(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + course_id: int = Field(default=None, foreign_key="course.id") + user_id: int = Field(default=None, foreign_key="user.id") + authorship: CourseAuthorshipEnum = CourseAuthorshipEnum.CREATOR + creation_date: str + update_date: str diff --git a/apps/api/src/rewrite/services/db/course_chapters.py b/apps/api/src/rewrite/services/db/course_chapters.py new file mode 100644 index 00000000..600eef0c --- /dev/null +++ b/apps/api/src/rewrite/services/db/course_chapters.py @@ -0,0 +1,12 @@ +from typing import Optional +from sqlmodel import Field, SQLModel +from enum import Enum + +class CourseChapter(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + order: int + course_id: int = Field(default=None, foreign_key="course.id") + chapter_id: int = Field(default=None, foreign_key="chapter.id") + org_id : int = Field(default=None, foreign_key="organization.id") + creation_date: str + update_date: str \ No newline at end of file diff --git a/apps/api/src/rewrite/services/db/courses.py b/apps/api/src/rewrite/services/db/courses.py new file mode 100644 index 00000000..faca51b0 --- /dev/null +++ b/apps/api/src/rewrite/services/db/courses.py @@ -0,0 +1,29 @@ +from typing import Optional +from sqlmodel import Field, SQLModel + +class CourseBase(SQLModel): + name: str + description: Optional[str] = "" + about: Optional[str] = "" + course_slug: 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): + pass + +class CourseRead(CourseBase): + id: int + course_uuid: str + creation_date: str + update_date: str + pass \ No newline at end of file diff --git a/apps/api/src/rewrite/services/db/organization_settings.py b/apps/api/src/rewrite/services/db/organization_settings.py new file mode 100644 index 00000000..4c464b5d --- /dev/null +++ b/apps/api/src/rewrite/services/db/organization_settings.py @@ -0,0 +1,17 @@ +from typing import Optional +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(default=None, foreign_key="organization.id") + logo_image: Optional[str] = "" + header_type: HeaderTypeEnum = HeaderTypeEnum.LOGO_MENU_SETTINGS + color: str = "" + creation_date: str + update_date: str diff --git a/apps/api/src/rewrite/services/db/organizations.py b/apps/api/src/rewrite/services/db/organizations.py new file mode 100644 index 00000000..3cd147e6 --- /dev/null +++ b/apps/api/src/rewrite/services/db/organizations.py @@ -0,0 +1,26 @@ +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 OrganizationCreate(OrganizationBase): + pass + + +class OrganizationRead(OrganizationBase): + id: int + org_uuid: str diff --git a/apps/api/src/rewrite/services/db/roles.py b/apps/api/src/rewrite/services/db/roles.py new file mode 100644 index 00000000..794a332e --- /dev/null +++ b/apps/api/src/rewrite/services/db/roles.py @@ -0,0 +1,30 @@ +from enum import Enum +from typing import Optional +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel + + +class RoleTypeEnum(str, Enum): + ORGANIZATION = "ORGANIZATION" + ORGANIZATION_API_TOKEN = "ORGANIZATION_API_TOKEN" + DEFAULT = "DEFAULT" + + +class RoleBase(SQLModel): + name: str + description: Optional[str] = "" + 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.DEFAULT + role_uuid: str + creation_date: str + update_date: str + + +class RoleCreate(RoleBase): + org_id: int = Field(default=None, foreign_key="organization.id") + pass diff --git a/apps/api/src/rewrite/services/db/user_organizations.py b/apps/api/src/rewrite/services/db/user_organizations.py new file mode 100644 index 00000000..bb70a5fd --- /dev/null +++ b/apps/api/src/rewrite/services/db/user_organizations.py @@ -0,0 +1,11 @@ +from typing import Optional +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(default=None, foreign_key="organization.id") + role_id: int = Field(default=None, foreign_key="role.id") + creation_date: str + update_date: str diff --git a/apps/api/src/rewrite/services/db/users.py b/apps/api/src/rewrite/services/db/users.py index a02d6c19..acfa3c03 100644 --- a/apps/api/src/rewrite/services/db/users.py +++ b/apps/api/src/rewrite/services/db/users.py @@ -1,5 +1,5 @@ from typing import Optional -from sqlmodel import Field, Session, SQLModel, create_engine, select +from sqlmodel import Field, SQLModel class UserBase(SQLModel): diff --git a/apps/api/src/rewrite/services/users/users.py b/apps/api/src/rewrite/services/users/users.py index 5bb69ecb..c5e602e2 100644 --- a/apps/api/src/rewrite/services/users/users.py +++ b/apps/api/src/rewrite/services/users/users.py @@ -1,6 +1,6 @@ from datetime import datetime from uuid import uuid4 -from fastapi import Depends, Request +from fastapi import Request from sqlmodel import Session from src.rewrite.services.db.users import User, UserCreate from src.security.security import security_hash_password From a50fc6710470aa73a7228c2503794ecdf9c90f6e Mon Sep 17 00:00:00 2001 From: swve Date: Sun, 12 Nov 2023 23:16:34 +0100 Subject: [PATCH 04/39] feat: user init & refactors --- apps/api/app.py | 3 +- apps/api/src/core/events/database.py | 2 +- apps/api/src/{rewrite => db}/__init__.py | 0 .../{rewrite/services => }/db/activities.py | 0 .../src/{rewrite/services => }/db/blocks.py | 0 .../services => }/db/chapter_activities.py | 0 .../src/{rewrite/services => }/db/chapters.py | 0 .../{rewrite/services => }/db/collections.py | 0 .../services => }/db/course_authors.py | 0 .../services => }/db/course_chapters.py | 0 .../src/{rewrite/services => }/db/courses.py | 0 .../services => }/db/organization_settings.py | 0 .../services => }/db/organizations.py | 0 .../src/{rewrite/services => }/db/roles.py | 4 +- .../services => }/db/user_organizations.py | 0 .../src/{rewrite/services => }/db/users.py | 13 +- apps/api/src/rewrite/routers/users.py | 23 - apps/api/src/rewrite/services/db/__init__.py | 0 apps/api/src/rewrite/services/users/users.py | 43 -- apps/api/src/router.py | 5 - apps/api/src/routers/auth.py | 27 +- apps/api/src/routers/users.py | 121 +++-- apps/api/src/security/auth.py | 27 +- apps/api/src/services/dev/mocks/initial.py | 3 +- apps/api/src/services/users/users.py | 443 ++++++++---------- 25 files changed, 356 insertions(+), 358 deletions(-) rename apps/api/src/{rewrite => db}/__init__.py (100%) rename apps/api/src/{rewrite/services => }/db/activities.py (100%) rename apps/api/src/{rewrite/services => }/db/blocks.py (100%) rename apps/api/src/{rewrite/services => }/db/chapter_activities.py (100%) rename apps/api/src/{rewrite/services => }/db/chapters.py (100%) rename apps/api/src/{rewrite/services => }/db/collections.py (100%) rename apps/api/src/{rewrite/services => }/db/course_authors.py (100%) rename apps/api/src/{rewrite/services => }/db/course_chapters.py (100%) rename apps/api/src/{rewrite/services => }/db/courses.py (100%) rename apps/api/src/{rewrite/services => }/db/organization_settings.py (100%) rename apps/api/src/{rewrite/services => }/db/organizations.py (100%) rename apps/api/src/{rewrite/services => }/db/roles.py (90%) rename apps/api/src/{rewrite/services => }/db/user_organizations.py (100%) rename apps/api/src/{rewrite/services => }/db/users.py (69%) delete mode 100644 apps/api/src/rewrite/routers/users.py delete mode 100644 apps/api/src/rewrite/services/db/__init__.py delete mode 100644 apps/api/src/rewrite/services/users/users.py diff --git a/apps/api/app.py b/apps/api/app.py index dc066251..3a7f62c6 100644 --- a/apps/api/app.py +++ b/apps/api/app.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, Request from config.config import LearnHouseConfig, get_learnhouse_config from src.core.events.events import shutdown_app, startup_app -from src.router import v1_router, rewrite +from src.router import v1_router from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles @@ -60,7 +60,6 @@ app.mount("/content", StaticFiles(directory="content"), name="content") # Global Routes app.include_router(v1_router) -app.include_router(rewrite) # General Routes @app.get("/") diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index f7b2d2d8..8c16e65b 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -3,7 +3,7 @@ from fastapi import FastAPI import motor.motor_asyncio from sqlmodel import Field, SQLModel, Session, create_engine -from src.rewrite.services.db import ( +from src.db import ( user_organizations, users, roles, diff --git a/apps/api/src/rewrite/__init__.py b/apps/api/src/db/__init__.py similarity index 100% rename from apps/api/src/rewrite/__init__.py rename to apps/api/src/db/__init__.py diff --git a/apps/api/src/rewrite/services/db/activities.py b/apps/api/src/db/activities.py similarity index 100% rename from apps/api/src/rewrite/services/db/activities.py rename to apps/api/src/db/activities.py diff --git a/apps/api/src/rewrite/services/db/blocks.py b/apps/api/src/db/blocks.py similarity index 100% rename from apps/api/src/rewrite/services/db/blocks.py rename to apps/api/src/db/blocks.py diff --git a/apps/api/src/rewrite/services/db/chapter_activities.py b/apps/api/src/db/chapter_activities.py similarity index 100% rename from apps/api/src/rewrite/services/db/chapter_activities.py rename to apps/api/src/db/chapter_activities.py diff --git a/apps/api/src/rewrite/services/db/chapters.py b/apps/api/src/db/chapters.py similarity index 100% rename from apps/api/src/rewrite/services/db/chapters.py rename to apps/api/src/db/chapters.py diff --git a/apps/api/src/rewrite/services/db/collections.py b/apps/api/src/db/collections.py similarity index 100% rename from apps/api/src/rewrite/services/db/collections.py rename to apps/api/src/db/collections.py diff --git a/apps/api/src/rewrite/services/db/course_authors.py b/apps/api/src/db/course_authors.py similarity index 100% rename from apps/api/src/rewrite/services/db/course_authors.py rename to apps/api/src/db/course_authors.py diff --git a/apps/api/src/rewrite/services/db/course_chapters.py b/apps/api/src/db/course_chapters.py similarity index 100% rename from apps/api/src/rewrite/services/db/course_chapters.py rename to apps/api/src/db/course_chapters.py diff --git a/apps/api/src/rewrite/services/db/courses.py b/apps/api/src/db/courses.py similarity index 100% rename from apps/api/src/rewrite/services/db/courses.py rename to apps/api/src/db/courses.py diff --git a/apps/api/src/rewrite/services/db/organization_settings.py b/apps/api/src/db/organization_settings.py similarity index 100% rename from apps/api/src/rewrite/services/db/organization_settings.py rename to apps/api/src/db/organization_settings.py diff --git a/apps/api/src/rewrite/services/db/organizations.py b/apps/api/src/db/organizations.py similarity index 100% rename from apps/api/src/rewrite/services/db/organizations.py rename to apps/api/src/db/organizations.py diff --git a/apps/api/src/rewrite/services/db/roles.py b/apps/api/src/db/roles.py similarity index 90% rename from apps/api/src/rewrite/services/db/roles.py rename to apps/api/src/db/roles.py index 794a332e..0653a411 100644 --- a/apps/api/src/rewrite/services/db/roles.py +++ b/apps/api/src/db/roles.py @@ -7,7 +7,7 @@ from sqlmodel import Field, SQLModel class RoleTypeEnum(str, Enum): ORGANIZATION = "ORGANIZATION" ORGANIZATION_API_TOKEN = "ORGANIZATION_API_TOKEN" - DEFAULT = "DEFAULT" + GLOBAL = "GLOBAL" class RoleBase(SQLModel): @@ -19,7 +19,7 @@ class RoleBase(SQLModel): 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.DEFAULT + role_type: RoleTypeEnum = RoleTypeEnum.GLOBAL role_uuid: str creation_date: str update_date: str diff --git a/apps/api/src/rewrite/services/db/user_organizations.py b/apps/api/src/db/user_organizations.py similarity index 100% rename from apps/api/src/rewrite/services/db/user_organizations.py rename to apps/api/src/db/user_organizations.py diff --git a/apps/api/src/rewrite/services/db/users.py b/apps/api/src/db/users.py similarity index 69% rename from apps/api/src/rewrite/services/db/users.py rename to apps/api/src/db/users.py index acfa3c03..726cedfc 100644 --- a/apps/api/src/rewrite/services/db/users.py +++ b/apps/api/src/db/users.py @@ -16,7 +16,18 @@ class UserCreate(UserBase): class UserUpdate(UserBase): - password: Optional[str] = None + username: str + first_name: Optional[str] + last_name: Optional[str] + email: str + avatar_image: Optional[str] = "" + bio: Optional[str] = "" + + +class UserUpdatePassword(SQLModel): + user_id: int + old_password: str + new_password: str class UserRead(UserBase): diff --git a/apps/api/src/rewrite/routers/users.py b/apps/api/src/rewrite/routers/users.py deleted file mode 100644 index bd01e15e..00000000 --- a/apps/api/src/rewrite/routers/users.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import APIRouter, Depends, Request -from sqlmodel import Session -from src.core.events.database import get_db_session - -from src.rewrite.services.db.users import UserCreate, UserRead -from src.rewrite.services.users.users import create_user - - -router = APIRouter() - - -@router.post("/", response_model=UserRead, tags=["users"]) -async def api_create_user( - *, - request: Request, - db_session: Session = Depends(get_db_session), - user_object: UserCreate, - org_slug: str -): - """ - Create new user - """ - return await create_user(request, db_session, None, user_object, org_slug) diff --git a/apps/api/src/rewrite/services/db/__init__.py b/apps/api/src/rewrite/services/db/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/src/rewrite/services/users/users.py b/apps/api/src/rewrite/services/users/users.py deleted file mode 100644 index c5e602e2..00000000 --- a/apps/api/src/rewrite/services/users/users.py +++ /dev/null @@ -1,43 +0,0 @@ -from datetime import datetime -from uuid import uuid4 -from fastapi import Request -from sqlmodel import Session -from src.rewrite.services.db.users import User, UserCreate -from src.security.security import security_hash_password -from src.services.users.schemas.users import PublicUser - - -async def create_user( - request: Request, - db_session: Session, - current_user: PublicUser | None, - user_object: UserCreate, - org_slug: str, -): - user = User.from_orm(user_object) - - # 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 - #todo: add username uniqueness verification - #todo: add email uniqueness verification - - - #todo: add user to org as member if org is not None - - # 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) - - return user diff --git a/apps/api/src/router.py b/apps/api/src/router.py index 08a5325b..6931d66b 100644 --- a/apps/api/src/router.py +++ b/apps/api/src/router.py @@ -1,7 +1,5 @@ from fastapi import APIRouter, Depends -from src import rewrite from src.routers import blocks, dev, trail, users, auth, orgs, roles -from src.rewrite.routers import users as rw_users from src.routers.courses import chapters, collections, courses, activities from src.routers.install import install from src.services.dev.dev import isDevModeEnabledOrRaise @@ -9,7 +7,6 @@ from src.services.install.install import isInstallModeEnabled v1_router = APIRouter(prefix="/api/v1") -rewrite = APIRouter(prefix="/api/rewrite") # API Routes @@ -39,5 +36,3 @@ v1_router.include_router( dependencies=[Depends(isInstallModeEnabled)], ) -# Rewrite Routes -rewrite.include_router(rw_users.router, prefix="/users", tags=["users"]) diff --git a/apps/api/src/routers/auth.py b/apps/api/src/routers/auth.py index 54e4dc7a..589782e0 100644 --- a/apps/api/src/routers/auth.py +++ b/apps/api/src/routers/auth.py @@ -1,5 +1,8 @@ from fastapi import Depends, APIRouter, HTTPException, Response, status, Request from fastapi.security import OAuth2PasswordRequestForm +from sqlmodel import Session +from src.db.users import UserRead +from src.core.events.database import get_db_session from config.config import get_learnhouse_config from src.security.auth import AuthJWT, authenticate_user from src.services.users.users import PublicUser @@ -9,7 +12,7 @@ router = APIRouter() @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 token is present in the request before running any code below that function. @@ -21,7 +24,12 @@ def refresh(response: Response,Authorize: AuthJWT = Depends()): current_user = Authorize.get_jwt_subject() new_access_token = Authorize.create_access_token(subject=current_user) # type: ignore - response.set_cookie(key="access_token_cookie", value=new_access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain) + response.set_cookie( + key="access_token_cookie", + value=new_access_token, + httponly=False, + domain=get_learnhouse_config().hosting_config.cookie_config.domain, + ) return {"access_token": new_access_token} @@ -31,8 +39,11 @@ async def login( response: Response, Authorize: AuthJWT = Depends(), form_data: OAuth2PasswordRequestForm = Depends(), + db_session: Session = Depends(get_db_session), ): - user = await authenticate_user(request, form_data.username, form_data.password) + user = await authenticate_user( + request, form_data.username, form_data.password, db_session + ) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -44,8 +55,14 @@ async def login( refresh_token = Authorize.create_refresh_token(subject=form_data.username) Authorize.set_refresh_cookies(refresh_token) # set cookies using fastapi - response.set_cookie(key="access_token_cookie", value=access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain) - user = PublicUser(**user.dict()) + response.set_cookie( + key="access_token_cookie", + value=access_token, + httponly=False, + domain=get_learnhouse_config().hosting_config.cookie_config.domain, + ) + + user = UserRead.from_orm(user) result = { "user": user, diff --git a/apps/api/src/routers/users.py b/apps/api/src/routers/users.py index 3b4edd1a..1fb8ebc6 100644 --- a/apps/api/src/routers/users.py +++ b/apps/api/src/routers/users.py @@ -1,10 +1,24 @@ -from fastapi import Depends, APIRouter, Request +from fastapi import APIRouter, Depends, Request +from sqlmodel import Session from src.security.auth import get_current_user -from src.services.users.schemas.users import PasswordChangeForm, PublicUser, User, UserWithPassword -from src.services.users.users import create_user, delete_user, get_profile_metadata, get_user_by_userid, update_user, update_user_password - - +from src.core.events.database import get_db_session +from src.db.users import ( + User, + UserCreate, + UserRead, + UserUpdate, + UserUpdatePassword, +) +from src.services.users.users import ( + create_user, + create_user_without_org, + delete_user_by_id, + read_user_by_id, + read_user_by_uuid, + update_user, + update_user_password, +) router = APIRouter() @@ -17,50 +31,95 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)): """ return current_user.dict() -@router.get("/profile_metadata") -async def api_get_current_user_metadata(request: Request,current_user: User = Depends(get_current_user)): + +@router.post("/org_id/{org_id}", response_model=UserRead, tags=["users"]) +async def api_create_user_with_orgid( + *, + request: Request, + db_session: Session = Depends(get_db_session), + user_object: UserCreate, + org_id: int, +) -> UserRead: """ - Get current user + Create User with Org ID """ - return await get_profile_metadata(request , current_user.dict()) + return await create_user(request, db_session, None, user_object, org_id) - -@router.get("/user_id/{user_id}") -async def api_get_user_by_userid(request: Request,user_id: str): +@router.post("/", response_model=UserRead, tags=["users"]) +async def api_create_user_without_org( + *, + request: Request, + db_session: Session = Depends(get_db_session), + user_object: UserCreate, + org_id: int, +) -> UserRead: """ - Get single user by user_id + Create User """ - return await get_user_by_userid(request, user_id) + return await create_user_without_org(request, db_session, None, user_object) -@router.post("/") -async def api_create_user(request: Request,user_object: UserWithPassword, org_slug: str ): +@router.get("/user_id/{user_id}", response_model=UserRead, tags=["users"]) +async def api_get_user_by_id( + *, + request: Request, + db_session: Session = Depends(get_db_session), + user_id: int, +) -> UserRead: """ - Create new user + Get User by ID """ - return await create_user(request, None, user_object, org_slug) + return await read_user_by_id(request, db_session, None, user_id) -@router.delete("/user_id/{user_id}") -async def api_delete_user(request: Request, user_id: str, current_user: PublicUser = Depends(get_current_user)): +@router.get("/user_uuid/{user_uuid}", response_model=UserRead, tags=["users"]) +async def api_get_user_by_uuid( + *, + request: Request, + db_session: Session = Depends(get_db_session), + user_uuid: str, +) -> UserRead: """ - Delete user by ID + Get User by UUID """ - - return await delete_user(request, current_user, user_id) + return await read_user_by_uuid(request, db_session, None, user_uuid) -@router.put("/user_id/{user_id}") -async def api_update_user(request: Request, user_object: User, user_id: str, current_user: PublicUser = Depends(get_current_user)): +@router.put("/", response_model=UserRead, tags=["users"]) +async def api_update_user( + *, + request: Request, + db_session: Session = Depends(get_db_session), + user_object: UserUpdate, +) -> UserRead: """ - Update user by ID + Update User """ - return await update_user(request, user_id, user_object, current_user) + return await update_user(request, db_session, None, user_object) -@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("/change_password/", response_model=UserRead, tags=["users"]) +async def api_update_user_password( + *, + request: Request, + db_session: Session = Depends(get_db_session), + form: UserUpdatePassword, +) -> UserRead: """ - Update user password by ID + Update User Password """ - return await update_user_password(request,current_user, user_id, passwordChangeForm) + return await update_user_password(request, db_session, None, form) + + +@router.delete("/user_id/{user_id}", tags=["users"]) +async def api_delete_user( + *, + request: Request, + db_session: Session = Depends(get_db_session), + user_id: int, +): + """ + Delete User + """ + return await delete_user_by_id(request, db_session, None, user_id) diff --git a/apps/api/src/security/auth.py b/apps/api/src/security/auth.py index d125c15d..d6691864 100644 --- a/apps/api/src/security/auth.py +++ b/apps/api/src/security/auth.py @@ -1,3 +1,7 @@ +from sqlmodel import Session +from src.core.events.database import get_db_session +from src.db.users import User, UserRead +from src.services.users.users import security_get_user from config.config import get_learnhouse_config from pydantic import BaseModel from fastapi import Depends, HTTPException, Request, status @@ -6,7 +10,7 @@ from jose import JWTError, jwt from datetime import datetime, timedelta from src.services.dev.dev import isDevModeEnabled from src.services.users.schemas.users import AnonymousUser, PublicUser -from src.services.users.users import security_get_user, security_verify_password +from src.services.users.users import security_verify_password from src.security.security import ALGORITHM, SECRET_KEY from fastapi_jwt_auth import AuthJWT @@ -45,10 +49,13 @@ class TokenData(BaseModel): #### Classes #################################################### - - -async def authenticate_user(request: Request, email: str, password: str): - user = await security_get_user(request, email) +async def authenticate_user( + request: Request, + email: str, + password: str, + db_session: Session, +) -> User | bool: + user = await security_get_user(request, db_session, email) if not user: return False if not await security_verify_password(password, user.password): @@ -67,7 +74,11 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): return encoded_jwt -async def get_current_user(request: Request, Authorize: AuthJWT = Depends()): +async def get_current_user( + request: Request, + Authorize: AuthJWT = Depends(), + db_session: Session = Depends(get_db_session), +): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", @@ -81,10 +92,10 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()): except JWTError: raise credentials_exception if username: - user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email + user = await security_get_user(request, db_session, email=token_data.username) # type: ignore # treated as an email if user is None: raise credentials_exception - return PublicUser(**user.dict()) + return UserRead(**user.dict()) else: return AnonymousUser() diff --git a/apps/api/src/services/dev/mocks/initial.py b/apps/api/src/services/dev/mocks/initial.py index 7d8552e9..d1005093 100644 --- a/apps/api/src/services/dev/mocks/initial.py +++ b/apps/api/src/services/dev/mocks/initial.py @@ -3,10 +3,11 @@ import requests from datetime import datetime from uuid import uuid4 from fastapi import Request +from src.services.users.schemas.users import UserInDB 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.users.users import PublicUser from src.services.orgs.orgs import Organization, create_org from src.services.roles.schemas.roles import Permission, Elements, RoleInDB diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index 9aba3b2c..49fc22db 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -1,215 +1,285 @@ from datetime import datetime -from typing import Literal from uuid import uuid4 from fastapi import HTTPException, Request, status -from src.security.rbac.rbac import ( - authorization_verify_based_on_roles, - authorization_verify_if_user_is_anon, -) -from src.security.security import security_hash_password, security_verify_password -from src.services.users.schemas.users import ( - PasswordChangeForm, - PublicUser, +from sqlmodel import Session, select +from src.db.organizations import Organization +from src.db.users import ( User, - UserOrganization, - UserRolesInOrganization, - UserWithPassword, - UserInDB, + UserCreate, + UserRead, + UserUpdate, + UserUpdatePassword, ) +from src.db.user_organizations import UserOrganization +from src.security.security import security_hash_password, security_verify_password +from src.services.users.schemas.users import PublicUser async def create_user( request: Request, + db_session: Session, current_user: PublicUser | None, - user_object: UserWithPassword, - 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}) - isEmailAvailable = await users.find_one({"email": user_object.email}) + # Complete the user object + user.user_uuid = f"user_{uuid4()}" + user.password = await security_hash_password(user_object.password) + user.email_verified = False + user.creation_date = str(datetime.now()) + user.update_date = str(datetime.now()) - if isUsernameAvailable: + # Verifications + + # Check if Organization exists + statement = select(Organization).where(Organization.id == org_id) + result = db_session.exec(statement) + + if not result.first(): raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Username already exists" + status_code=400, + detail="Organization does not exist", ) - if isEmailAvailable: + # Username + statement = select(User).where(User.username == user.username) + result = db_session.exec(statement) + + if result.first(): raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Email already exists" + status_code=400, + detail="Username already exists", ) - # Generate user_id with uuid4 - user_id = str(f"user_{uuid4()}") + # Email + statement = select(User).where(User.email == user.email) + result = db_session.exec(statement) - # Check if the requesting user is authenticated - if current_user is not None: - # Verify rights - await verify_user_rights_on_user(request, current_user, "create", user_id) - - # Set the username & hash the password - user_object.username = user_object.username.lower() - user_object.password = await security_hash_password(user_object.password) - - # Get org_id from org_slug - orgs = request.app.db["organizations"] - - # Check if the org exists - isOrgExists = await orgs.find_one({"slug": org_slug}) - - # If the org does not exist, raise an error - if not isOrgExists and (org_slug != "None"): + if result.first(): raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="You are trying to create a user in an organization that does not exist", + status_code=400, + detail="Email already exists", ) - - org_id = isOrgExists["org_id"] if org_slug != "None" else '' - # Create initial orgs list with the org_id passed in - orgs = ( - [UserOrganization(org_id=org_id, org_role="member")] - if org_slug != "None" - else [] - ) + # Exclude unset values + user_data = user.dict(exclude_unset=True) + for key, value in user_data.items(): + setattr(user, key, value) - # Give role - roles = ( - [UserRolesInOrganization(role_id="role_member", org_id=org_id)] - if org_slug != "None" - else [] - ) + # Add user to database + db_session.add(user) + db_session.commit() + db_session.refresh(user) - # Create the user - user = UserInDB( - user_id=user_id, + # Link user and organization + user_organization = UserOrganization( + user_id=user.id is not None, + org_id=int(org_id), + role_id=1, creation_date=str(datetime.now()), update_date=str(datetime.now()), - orgs=orgs, - roles=roles, - **user_object.dict(), ) - # Insert the user into the database - await users.insert_one(user.dict()) + db_session.add(user_organization) + db_session.commit() + db_session.refresh(user_organization) - return User(**user.dict()) + user = UserRead.from_orm(user) + + return user -async def read_user(request: Request, current_user: PublicUser, user_id: str): - users = request.app.db["users"] +async def create_user_without_org( + request: Request, + db_session: Session, + current_user: PublicUser | None, + user_object: UserCreate, +): + user = User.from_orm(user_object) - # Check if the user exists - isUserExists = await users.find_one({"user_id": user_id}) + # Complete the user object + user.user_uuid = f"user_{uuid4()}" + user.password = await security_hash_password(user_object.password) + user.email_verified = False + user.creation_date = str(datetime.now()) + user.update_date = str(datetime.now()) - # Verify rights - await verify_user_rights_on_user(request, current_user, "read", user_id) + # Verifications - # If the user does not exist, raise an error - if not isUserExists: + # Username + statement = select(User).where(User.username == user.username) + result = db_session.exec(statement) + + if result.first(): raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist" + status_code=400, + detail="Username already exists", ) - return User(**isUserExists) + # Email + statement = select(User).where(User.email == user.email) + result = db_session.exec(statement) + + if result.first(): + raise HTTPException( + status_code=400, + detail="Email already exists", + ) + + # Exclude unset values + user_data = user.dict(exclude_unset=True) + for key, value in user_data.items(): + setattr(user, key, value) + + # Add user to database + db_session.add(user) + db_session.commit() + db_session.refresh(user) + + user = UserRead.from_orm(user) + + return user async def update_user( - request: Request, user_id: str, user_object: User, current_user: PublicUser + request: Request, + db_session: Session, + current_user: PublicUser | None, + user_object: UserUpdate, ): - users = request.app.db["users"] + # Get user + statement = select(User).where(User.username == user_object.username) + user = db_session.exec(statement).first() - # Verify rights - await verify_user_rights_on_user(request, current_user, "update", user_id) - - isUserExists = await users.find_one({"user_id": user_id}) - isUsernameAvailable = await users.find_one({"username": user_object.username}) - isEmailAvailable = await users.find_one({"email": user_object.email}) - - if not isUserExists: + if not user: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist" + status_code=400, + detail="User does not exist", ) - # okay if username is not changed - if isUserExists["username"] == user_object.username: - user_object.username = user_object.username.lower() + # Update user + user_data = user_object.dict(exclude_unset=True) + for key, value in user_data.items(): + setattr(user, key, value) - else: - if isUsernameAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Username already used" - ) + # Update user in database + db_session.add(user) + db_session.commit() + db_session.refresh(user) - if isEmailAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Email already used" - ) + user = UserRead.from_orm(user) - updated_user = {"$set": user_object.dict()} - users.update_one({"user_id": user_id}, updated_user) - - return User(**user_object.dict()) + return user async def update_user_password( request: Request, - current_user: PublicUser, - user_id: str, - password_change_form: PasswordChangeForm, + db_session: Session, + current_user: PublicUser | None, + form: UserUpdatePassword, ): - users = request.app.db["users"] + # Get user + statement = select(User).where(User.username == form.user_id) + user = db_session.exec(statement).first() - isUserExists = await users.find_one({"user_id": user_id}) - - # Verify rights - await verify_user_rights_on_user(request, current_user, "update", user_id) - - if not isUserExists: + if not user: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist" + status_code=400, + detail="User does not exist", ) - if not await security_verify_password( - password_change_form.old_password, isUserExists["password"] - ): + if not await security_verify_password(form.old_password, user.password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password" ) - new_password = await security_hash_password(password_change_form.new_password) + # Update user + user.password = await security_hash_password(form.new_password) - updated_user = {"$set": {"password": new_password}} - await users.update_one({"user_id": user_id}, updated_user) + # Update user in database + db_session.add(user) + db_session.commit() + db_session.refresh(user) - return {"detail": "Password updated"} + user = UserRead.from_orm(user) + + return user -async def delete_user(request: Request, current_user: PublicUser, user_id: str): - users = request.app.db["users"] +async def read_user_by_id( + request: Request, + db_session: Session, + current_user: PublicUser | None, + user_id: int, +): + # Get user + statement = select(User).where(User.id == user_id) + user = db_session.exec(statement).first() - isUserExists = await users.find_one({"user_id": user_id}) - - # Verify is user has permission to delete the user - await verify_user_rights_on_user(request, current_user, "delete", user_id) - - if not isUserExists: + if not user: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist" + status_code=400, + detail="User does not exist", ) - await users.delete_one({"user_id": user_id}) + user = UserRead.from_orm(user) - return {"detail": "User deleted"} + return user + + +async def read_user_by_uuid( + request: Request, + db_session: Session, + current_user: PublicUser | None, + uuid: str, +): + # Get user + statement = select(User).where(User.user_uuid == uuid) + user = db_session.exec(statement).first() + + if not user: + raise HTTPException( + status_code=400, + detail="User does not exist", + ) + + user = UserRead.from_orm(user) + + return user + + +async def delete_user_by_id( + request: Request, + db_session: Session, + current_user: PublicUser | None, + 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", + ) + + # Delete user + db_session.delete(user) + db_session.commit() + + return "User deleted" # Utils & Security functions -async def security_get_user(request: Request, email: str): - users = request.app.db["users"] - - user = await users.find_one({"email": email}) +async def security_get_user(request: Request, db_session: Session, email: str) -> User: + # Check if user exists + statement = select(User).where(User.email == email) + user = db_session.exec(statement).first() if not user: raise HTTPException( @@ -217,105 +287,6 @@ async def security_get_user(request: Request, email: str): detail="User with Email does not exist", ) - return UserInDB(**user) + user = User(**user.dict()) - -async def get_userid_by_username(request: Request, username: str): - users = request.app.db["users"] - - user = await users.find_one({"username": username}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist" - ) - - return user["user_id"] - - -async def get_user_by_userid(request: Request, user_id: str): - users = request.app.db["users"] - - user = await users.find_one({"user_id": user_id}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist" - ) - - user = User(**user) return user - - -async def get_profile_metadata(request: Request, user): - users = request.app.db["users"] - request.app.db["roles"] - - user = await users.find_one({"user_id": user["user_id"]}) - - if not user: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User does not exist" - ) - - return {"user_object": PublicUser(**user), "roles": "random"} - - -# 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 - ) From aa0eda56829a7bec66b65acb5dd8e631e851afbf Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 13 Nov 2023 21:23:44 +0100 Subject: [PATCH 05/39] feat: roles init --- apps/api/src/db/roles.py | 18 ++- apps/api/src/routers/roles.py | 41 +++++-- apps/api/src/routers/users.py | 1 - apps/api/src/services/roles/roles.py | 158 +++++++++++---------------- apps/api/src/services/users/users.py | 4 + 5 files changed, 113 insertions(+), 109 deletions(-) diff --git a/apps/api/src/db/roles.py b/apps/api/src/db/roles.py index 0653a411..b211fdf7 100644 --- a/apps/api/src/db/roles.py +++ b/apps/api/src/db/roles.py @@ -11,7 +11,7 @@ class RoleTypeEnum(str, Enum): class RoleBase(SQLModel): - name: str + name: str description: Optional[str] = "" rights: dict = Field(default={}, sa_column=Column(JSON)) @@ -19,12 +19,18 @@ class RoleBase(SQLModel): 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.GLOBAL - role_uuid: str - creation_date: str - update_date: str + role_type: RoleTypeEnum = RoleTypeEnum.ORGANIZATION + role_uuid: str = "" + creation_date: str = "" + update_date: str = "" class RoleCreate(RoleBase): org_id: int = Field(default=None, foreign_key="organization.id") - pass + + +class RoleUpdate(SQLModel): + role_id: int = Field(default=None, foreign_key="role.id") + name: Optional[str] = "" + description: Optional[str] = "" + rights: Optional[dict] = Field(default={}, sa_column=Column(JSON)) diff --git a/apps/api/src/routers/roles.py b/apps/api/src/routers/roles.py index d65891bf..61573d81 100644 --- a/apps/api/src/routers/roles.py +++ b/apps/api/src/routers/roles.py @@ -1,7 +1,10 @@ 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, RoleUpdate from src.security.auth import get_current_user from src.services.roles.schemas.roles import Role -from src.services.roles.roles import create_role, delete_role, read_role, update_role +from src.services.roles.roles import create_role, delete_role, read_role, update_role from src.services.users.schemas.users import PublicUser @@ -9,33 +12,53 @@ router = APIRouter() @router.post("/") -async def api_create_role(request: Request, role_object: Role, current_user: PublicUser = Depends(get_current_user)): +async def api_create_role( + request: Request, + role_object: RoleCreate, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Create new role """ - return await create_role(request, role_object, current_user) + return await create_role(request, db_session, role_object, current_user) @router.get("/{role_id}") -async def api_get_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_role( + request: Request, + role_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Get single role by role_id """ - return await read_role(request, role_id, current_user) + return await read_role(request, db_session, role_id, current_user) @router.put("/{role_id}") -async def api_update_role(request: Request, role_object: Role, role_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_update_role( + request: Request, + role_object: RoleUpdate, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Update role by role_id """ - return await update_role(request, role_id, role_object, current_user) + return await update_role(request, db_session, role_object, current_user) @router.delete("/{role_id}") -async def api_delete_role(request: Request, role_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_delete_role( + request: Request, + role_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Delete role by ID """ - return await delete_role(request, role_id, current_user) + return await delete_role(request, db_session, role_id, current_user) diff --git a/apps/api/src/routers/users.py b/apps/api/src/routers/users.py index 1fb8ebc6..82542c56 100644 --- a/apps/api/src/routers/users.py +++ b/apps/api/src/routers/users.py @@ -52,7 +52,6 @@ async def api_create_user_without_org( request: Request, db_session: Session = Depends(get_db_session), user_object: UserCreate, - org_id: int, ) -> UserRead: """ Create User diff --git a/apps/api/src/services/roles/roles.py b/apps/api/src/services/roles/roles.py index cc61c452..8e3aff3a 100644 --- a/apps/api/src/services/roles/roles.py +++ b/apps/api/src/services/roles/roles.py @@ -1,127 +1,99 @@ -from typing import Literal from uuid import uuid4 +from sqlmodel import Session, select +from src.db.roles import Role, RoleCreate, RoleUpdate from src.security.rbac.rbac import authorization_verify_if_user_is_anon -from src.services.roles.schemas.roles import Role, RoleInDB from src.services.users.schemas.users import PublicUser from fastapi import HTTPException, status, Request from datetime import datetime -async def create_role(request: Request, role_object: Role, current_user: PublicUser): - roles = request.app.db["roles"] +async def create_role( + request: Request, + db_session: Session, + role_object: RoleCreate, + current_user: PublicUser, +): + role = Role.from_orm(role_object) - await verify_user_permissions_on_roles(request, current_user, "create", None) + # Complete the role object + role.role_uuid = f"role_{uuid4()}" + role.creation_date = str(datetime.now()) + role.update_date = str(datetime.now()) - # create the role object in the database and return the object - role_id = "role_" + str(uuid4()) - - role = RoleInDB( - role_id=role_id, - created_at=str(datetime.now()), - updated_at=str(datetime.now()), - **role_object.dict() - ) - - await roles.insert_one(role.dict()) + db_session.add(role) + db_session.commit() + db_session.refresh(role) return role -async def read_role(request: Request, role_id: str, current_user: PublicUser): - roles = request.app.db["roles"] +async def read_role( + request: Request, db_session: Session, role_id: str, current_user: PublicUser +): + statement = select(Role).where(Role.id == role_id) + result = db_session.exec(statement) - await verify_user_permissions_on_roles(request, current_user, "read", role_id) + role = result.first() - role = RoleInDB(**await roles.find_one({"role_id": role_id})) + if not role: + raise HTTPException( + status_code=404, + detail="Role not found", + ) return role async def update_role( - request: Request, role_id: str, role_object: Role, current_user: PublicUser -): - roles = request.app.db["roles"] - - await verify_user_permissions_on_roles(request, current_user, "update", role_id) - - role_object.updated_at = datetime.now() - - # Update the role object in the database and return the object - updated_role = RoleInDB( - **await roles.find_one_and_update( - {"role_id": role_id}, {"$set": role_object.dict()}, return_document=True - ) - ) - - return updated_role - - -async def delete_role(request: Request, role_id: str, current_user: PublicUser): - roles = request.app.db["roles"] - - await verify_user_permissions_on_roles(request, current_user, "delete", role_id) - - # Delete the role object in the database and return the object - deleted_role = RoleInDB(**await roles.find_one_and_delete({"role_id": role_id})) - - return deleted_role - - -#### Security #################################################### - - -async def verify_user_permissions_on_roles( request: Request, + db_session: Session, + role_object: RoleUpdate, current_user: PublicUser, - action: Literal["create", "read", "update", "delete"], - role_id: str | None, ): - request.app.db["users"] - roles = request.app.db["roles"] + statement = select(Role).where(Role.id == role_object.role_id) + result = db_session.exec(statement) - # If current user is not authenticated + role = result.first() - if not current_user: + if not role: raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Roles : Not authenticated" + status_code=404, + detail="Role not found", ) - await authorization_verify_if_user_is_anon(current_user.user_id) + # Complete the role object + role.update_date = str(datetime.now()) - if action == "create": - if "owner" in [org.org_role for org in current_user.orgs]: - return True + # Remove the role_id from the role_object + del role_object.role_id - if role_id is not None: - role = RoleInDB(**await roles.find_one({"role_id": 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) - if action == "read": - if "owner" in [org.org_role for org in current_user.orgs]: - return True + db_session.add(role) + db_session.commit() + db_session.refresh(role) - 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 + return role -#### Security #################################################### +async def delete_role( + request: Request, db_session: Session, role_id: str, current_user: PublicUser +): + statement = select(Role).where(Role.id == role_id) + result = db_session.exec(statement) + + 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" diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index 49fc22db..d237aa9a 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -164,6 +164,8 @@ async def update_user( for key, value in user_data.items(): setattr(user, key, value) + user.update_date = str(datetime.now()) + # Update user in database db_session.add(user) db_session.commit() @@ -197,6 +199,8 @@ async def update_user_password( # Update user user.password = await security_hash_password(form.new_password) + user.update_date = str(datetime.now()) + # Update user in database db_session.add(user) From afa8e4ea982242c42f4bf06c8ed53157280f5a9a Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 13 Nov 2023 22:37:40 +0100 Subject: [PATCH 06/39] feat: orgs init + changes --- apps/api/src/db/organizations.py | 8 +- apps/api/src/db/users.py | 2 + apps/api/src/routers/orgs.py | 93 ++++++-- apps/api/src/services/orgs/orgs.py | 318 +++++++++++++-------------- apps/api/src/services/trail/trail.py | 2 +- 5 files changed, 242 insertions(+), 181 deletions(-) diff --git a/apps/api/src/db/organizations.py b/apps/api/src/db/organizations.py index 3cd147e6..a1170dab 100644 --- a/apps/api/src/db/organizations.py +++ b/apps/api/src/db/organizations.py @@ -12,10 +12,12 @@ class OrganizationBase(SQLModel): class Organization(OrganizationBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) - org_uuid: str - creation_date: str - update_date: str + org_uuid: str = "" + creation_date: str = "" + update_date: str = "" +class OrganizationUpdate(OrganizationBase): + org_id: int class OrganizationCreate(OrganizationBase): pass diff --git a/apps/api/src/db/users.py b/apps/api/src/db/users.py index 726cedfc..e8cc99fc 100644 --- a/apps/api/src/db/users.py +++ b/apps/api/src/db/users.py @@ -33,6 +33,8 @@ class UserUpdatePassword(SQLModel): class UserRead(UserBase): id: int +class PublicUser(UserRead): + pass class User(UserBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) diff --git a/apps/api/src/routers/orgs.py b/apps/api/src/routers/orgs.py index f3e2928f..9f8c0aa1 100644 --- a/apps/api/src/routers/orgs.py +++ b/apps/api/src/routers/orgs.py @@ -1,63 +1,120 @@ - from fastapi import APIRouter, Depends, Request, UploadFile +from sqlmodel import Session +from src.db.users import PublicUser +from src.db.organizations import OrganizationCreate, OrganizationUpdate +from src.core.events.database import get_db_session from src.security.auth import get_current_user -from src.services.orgs.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org, update_org_logo -from src.services.users.users import PublicUser, User +from src.services.orgs.orgs import ( + create_org, + delete_org, + get_organization, + get_organization_by_slug, + get_orgs_by_user, + update_org, + update_org_logo, +) router = APIRouter() @router.post("/") -async def api_create_org(request: Request, org_object: Organization, current_user: PublicUser = Depends(get_current_user)): +async def api_create_org( + request: Request, + org_object: OrganizationCreate, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Create new organization """ - return await create_org(request, org_object, current_user) + return await create_org(request, org_object, current_user, db_session) @router.get("/{org_id}") -async def api_get_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_org( + request: Request, + org_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Get single Org by ID """ - return await get_organization(request, org_id) + return await get_organization(request, org_id, db_session) @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), +): """ 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) + @router.put("/{org_id}/logo") -async def api_update_org_logo(request: Request, org_id: str, logo_file:UploadFile, current_user: PublicUser = Depends(get_current_user)): +async def api_update_org_logo( + request: Request, + org_id: str, + logo_file: UploadFile, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Get single Org by Slug """ - return await update_org_logo(request=request,logo_file=logo_file, org_id=org_id, current_user=current_user) + return await update_org_logo( + request=request, + logo_file=logo_file, + org_id=org_id, + current_user=current_user, + db_session=db_session, + ) + @router.get("/user/page/{page}/limit/{limit}") -async def api_user_orgs(request: Request, page: int, limit: int, current_user: PublicUser = Depends(get_current_user)): +async def api_user_orgs( + request: Request, + page: int, + limit: int, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Get orgs by page and limit by user """ - return await get_orgs_by_user(request, current_user.user_id, page, limit) + return await get_orgs_by_user( + request, db_session, str(current_user.id), page, limit + ) -@router.put("/{org_id}") -async def api_update_org(request: Request, org_object: Organization, org_id: str, current_user: PublicUser = Depends(get_current_user)): +@router.put("/") +async def api_update_org( + request: Request, + org_object: OrganizationUpdate, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Update Org by ID """ - return await update_org(request, org_object, org_id, current_user) + return await update_org(request, org_object, current_user, db_session) @router.delete("/{org_id}") -async def api_delete_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_delete_org( + request: Request, + org_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Delete Org by ID """ - return await delete_org(request, org_id, current_user) + return await delete_org(request, org_id, current_user, db_session) diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index a65a4f63..a24737db 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -1,230 +1,230 @@ +from datetime import datetime import json +from operator import or_ from typing import Literal from uuid import uuid4 +from sqlmodel import Session, select +from src.db.users import UserRead, PublicUser +from src.db.user_organizations import UserOrganization +from src.db.organizations import ( + Organization, + OrganizationCreate, + OrganizationRead, + OrganizationUpdate, +) from src.security.rbac.rbac import ( authorization_verify_based_on_roles, authorization_verify_if_user_is_anon, ) from src.services.orgs.logos import upload_org_logo -from src.services.orgs.schemas.orgs import ( - Organization, - OrganizationInDB, - PublicOrganization, -) -from src.services.users.schemas.users import UserOrganization -from src.services.users.users import PublicUser from fastapi import HTTPException, UploadFile, status, Request -async def get_organization(request: Request, org_id: str): - orgs = request.app.db["organizations"] +async def get_organization(request: Request, org_id: str, db_session: Session): + statement = select(Organization).where(Organization.id == org_id) + result = db_session.exec(statement) - org = await orgs.find_one({"org_id": org_id}) + org = result.first() if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + status_code=404, + detail="Organization not found", ) - org = PublicOrganization(**org) return org -async def get_organization_by_slug(request: Request, org_slug: str): - orgs = request.app.db["organizations"] +async def get_organization_by_slug( + request: Request, org_slug: str, db_session: Session +): + statement = select(Organization).where(Organization.slug == org_slug) + result = db_session.exec(statement) - org = await orgs.find_one({"slug": org_slug}) + org = result.first() if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + status_code=404, + detail="Organization not found", ) - org = PublicOrganization(**org) return org async def create_org( - request: Request, org_object: Organization, current_user: PublicUser + request: Request, + org_object: OrganizationCreate, + current_user: PublicUser, + db_session: Session, ): - orgs = request.app.db["organizations"] - user = request.app.db["users"] + statement = select(Organization).where(Organization.slug == org_object.slug) + result = db_session.exec(statement) - # find if org already exists using name - isOrgAvailable = await orgs.find_one({"slug": org_object.slug}) + org = result.first() - if isOrgAvailable: + if org: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Organization already exists", + ) + + org = Organization.from_orm(org_object) + + # 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 is not None), + 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, + current_user: PublicUser, + db_session: Session, +): + statement = select(Organization).where(Organization.id == org_object.org_id) + result = db_session.exec(statement) + + org = result.first() + + if not org: + raise HTTPException( + status_code=404, + detail="Organization slug not found", + ) + + org = Organization.from_orm(org_object) + + # 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: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Organization slug already exists", ) - # generate org_id with uuid4 - org_id = str(f"org_{uuid4()}") + # Remove the org_id from the org_object + del org_object.org_id - # force lowercase slug - org_object.slug = org_object.slug.lower() + # Update only the fields that were passed in + for var, value in vars(org_object).items(): + if value is not None: + setattr(org, var, value) - org = OrganizationInDB( - org_id=org_id, **org_object.dict() - ) + # Complete the org object + org.update_date = str(datetime.now()) - org_in_db = await orgs.insert_one(org.dict()) + db_session.add(org) + db_session.commit() + db_session.refresh(org) - user_organization: UserOrganization = UserOrganization( - org_id=org_id, org_role="owner" - ) - - # add org to user - await user.update_one( - {"user_id": current_user.user_id}, - {"$addToSet": {"orgs": user_organization.dict()}}, - ) - - # add role admin to org - await user.update_one( - {"user_id": current_user.user_id}, - {"$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}}, - ) - - if not org_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) - - return org.dict() - - -async def update_org( - request: Request, org_object: Organization, org_id: str, current_user: PublicUser -): - # verify org rights - await verify_org_rights(request, org_id, current_user, "update") - - orgs = request.app.db["organizations"] - - await orgs.find_one({"org_id": org_id}) - - updated_org = OrganizationInDB(org_id=org_id, **org_object.dict()) - - # update org - await orgs.update_one({"org_id": org_id}, {"$set": updated_org.dict()}) - - return updated_org.dict() + return org async def update_org_logo( - request: Request, logo_file: UploadFile, org_id: str, current_user: PublicUser + request: Request, + logo_file: UploadFile, + org_id: str, + current_user: PublicUser, + db_session: Session, ): - # verify org rights - await verify_org_rights(request, org_id, current_user, "update") + statement = select(Organization).where(Organization.id == org_id) + result = db_session.exec(statement) - orgs = request.app.db["organizations"] + org = result.first() - await orgs.find_one({"org_id": org_id}) + if not org: + raise HTTPException( + status_code=404, + detail="Organization not found", + ) + # Upload logo name_in_disk = await upload_org_logo(logo_file, org_id) - # update org - await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}}) + # Update org + org.logo_image = name_in_disk + + # Complete the org object + org.update_date = str(datetime.now()) + + db_session.add(org) + db_session.commit() + db_session.refresh(org) return {"detail": "Logo updated"} -async def delete_org(request: Request, org_id: str, current_user: PublicUser): - await verify_org_rights(request, org_id, current_user, "delete") +async def delete_org( + request: Request, org_id: str, current_user: PublicUser, db_session: Session +): + statement = select(Organization).where(Organization.id == org_id) + result = db_session.exec(statement) - orgs = request.app.db["organizations"] - - org = await orgs.find_one({"org_id": org_id}) + org = result.first() if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + status_code=404, + detail="Organization not found", ) - isDeleted = await orgs.delete_one({"org_id": org_id}) + db_session.delete(org) + db_session.commit() - # remove org from all users - users = request.app.db["users"] - await users.update_many({}, {"$pull": {"orgs": {"org_id": org_id}}}) + # Delete links to org + statement = select(UserOrganization).where(UserOrganization.org_id == org_id) + result = db_session.exec(statement) - if isDeleted: - return {"detail": "Org deleted"} - else: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) + user_orgs = result.all() + + for user_org in user_orgs: + db_session.delete(user_org) + db_session.commit() + + db_session.refresh(org) + + return {"detail": "Organization deleted"} async def get_orgs_by_user( - request: Request, user_id: str, page: int = 1, limit: int = 10 -): - orgs = request.app.db["organizations"] - user = request.app.db["users"] - - if user_id == "anonymous": - # raise error - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User not logged in" - ) - - # 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) - ) - - return [ - json.loads(json.dumps(org, default=str)) - for org in await all_orgs.to_list(length=100) - ] - - -#### Security #################################################### - - -async def verify_org_rights( request: Request, - org_id: str, - current_user: PublicUser, - action: Literal["create", "read", "update", "delete"], + db_session: Session, + user_id: str, + page: int = 1, + limit: int = 10, ): - orgs = request.app.db["organizations"] - users = request.app.db["users"] - - user = await users.find_one({"user_id": current_user.user_id}) - - org = await orgs.find_one({"org_id": org_id}) - - if not org: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" - ) - - await authorization_verify_if_user_is_anon(current_user.user_id) - - await authorization_verify_based_on_roles( - request, current_user.user_id, action, user["roles"], org_id + statement = ( + select(Organization) + .join(UserOrganization) + .where(Organization.id == UserOrganization.org_id) ) + result = db_session.exec(statement) + orgs = result.all() -#### Security #################################################### + return orgs diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 08e38700..1a9e9d9d 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -3,8 +3,8 @@ from typing import List, Literal, Optional from uuid import uuid4 from fastapi import HTTPException, Request, status from pydantic import BaseModel +from src.services.orgs.schemas.orgs import PublicOrganization from src.services.courses.chapters import get_coursechapters_meta -from src.services.orgs.orgs import PublicOrganization from src.services.users.users import PublicUser From ee07f491399edf51c510779c87f24e2132668b27 Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 14 Nov 2023 17:48:10 +0100 Subject: [PATCH 07/39] wip: courses --- apps/api/requirements.txt | 2 +- apps/api/src/db/courses.py | 36 +- apps/api/src/db/roles.py | 6 +- apps/api/src/db/users.py | 4 + apps/api/src/routers/courses/courses.py | 109 +++++- apps/api/src/services/courses/courses.py | 407 +++++++-------------- apps/api/src/services/dev/mocks/initial.py | 59 ++- apps/api/src/services/install/install.py | 89 +++-- 8 files changed, 334 insertions(+), 378 deletions(-) diff --git a/apps/api/requirements.txt b/apps/api/requirements.txt index afec2b0a..b361beec 100644 --- a/apps/api/requirements.txt +++ b/apps/api/requirements.txt @@ -1,4 +1,4 @@ -fastapi==0.101.1 +fastapi==0.104.1 pydantic>=1.8.0,<2.0.0 sqlmodel==0.0.10 uvicorn==0.23.2 diff --git a/apps/api/src/db/courses.py b/apps/api/src/db/courses.py index faca51b0..0f2347ee 100644 --- a/apps/api/src/db/courses.py +++ b/apps/api/src/db/courses.py @@ -1,29 +1,43 @@ from typing import Optional from sqlmodel import Field, SQLModel + class CourseBase(SQLModel): name: str - description: Optional[str] = "" - about: Optional[str] = "" - course_slug: str - learnings: Optional[str] = "" - tags: Optional[str] = "" - thumbnail_image: Optional[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 - + 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): + course_id: int + name: str + description: Optional[str] + about: Optional[str] + learnings: Optional[str] + tags: Optional[str] + public: Optional[bool] + + class CourseRead(CourseBase): id: int course_uuid: str creation_date: str update_date: str - pass \ No newline at end of file + pass diff --git a/apps/api/src/db/roles.py b/apps/api/src/db/roles.py index b211fdf7..9e1227b9 100644 --- a/apps/api/src/db/roles.py +++ b/apps/api/src/db/roles.py @@ -12,7 +12,7 @@ class RoleTypeEnum(str, Enum): class RoleBase(SQLModel): name: str - description: Optional[str] = "" + description: Optional[str] rights: dict = Field(default={}, sa_column=Column(JSON)) @@ -31,6 +31,6 @@ class RoleCreate(RoleBase): class RoleUpdate(SQLModel): role_id: int = Field(default=None, foreign_key="role.id") - name: Optional[str] = "" - description: Optional[str] = "" + name: Optional[str] + description: Optional[str] rights: Optional[dict] = Field(default={}, sa_column=Column(JSON)) diff --git a/apps/api/src/db/users.py b/apps/api/src/db/users.py index e8cc99fc..99f19fd7 100644 --- a/apps/api/src/db/users.py +++ b/apps/api/src/db/users.py @@ -36,6 +36,10 @@ class UserRead(UserBase): class PublicUser(UserRead): pass +class AnonymousUser(UserRead): + id: str = "anonymous" + username: str = "anonymous" + class User(UserBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) password: str = "" diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index 4aed4c99..c91ab48a 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -1,66 +1,139 @@ 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 Course, CourseCreate, CourseUpdate from src.security.auth import get_current_user -from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses_orgslug, update_course, delete_course, update_course_thumbnail -from src.services.users.users import PublicUser +from src.services.courses.courses import ( + create_course, + get_course, + get_course_meta, + get_courses_orgslug, + update_course, + delete_course, + update_course_thumbnail, +) router = APIRouter() @router.post("/") -async def api_create_course(request: Request, org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None): +async def api_create_course( + request: Request, + org_id: int, + name: str = Form(), + description: str = Form(), + public: bool = Form(), + learnings: str = Form(), + tags: str = Form(), + about: str = Form(), + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), + thumbnail: UploadFile | None = None, +): """ Create new Course """ - course = Course(name=name, mini_description=mini_description, description=description, - org_id=org_id, public=public, thumbnail="", chapters=[], chapters_content=[], learnings=[]) - return await create_course(request, course, org_id, current_user, thumbnail) + course = CourseCreate( + name=name, + description=description, + org_id=org_id, + public=public, + thumbnail_image="", + about=about, + learnings=learnings, + tags=tags, + ) + return await create_course( + request, course, org_id, current_user, db_session, thumbnail + ) @router.put("/thumbnail/{course_id}") -async def api_create_course_thumbnail(request: Request, course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)): +async def api_create_course_thumbnail( + request: Request, + course_id: str, + thumbnail: UploadFile | None = None, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Update new Course Thumbnail """ - return await update_course_thumbnail(request, course_id, current_user, thumbnail) + return await update_course_thumbnail( + request, course_id, current_user, db_session, thumbnail + ) @router.get("/{course_id}") -async def api_get_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_course( + request: Request, + course_id: str, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Get single Course by course_id """ - return await get_course(request, course_id, current_user=current_user) + return await get_course( + request, course_id, current_user=current_user, db_session=db_session + ) @router.get("/meta/{course_id}") -async def api_get_course_meta(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_course_meta( + request: Request, + course_id: str, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Get single Course Metadata (chapters, activities) by course_id """ - return await get_course_meta(request, course_id, current_user=current_user) + return await get_course_meta( + request, course_id, current_user=current_user, db_session=db_session + ) + @router.get("/org_slug/{org_slug}/page/{page}/limit/{limit}") -async def api_get_course_by_orgslug(request: Request, page: int, limit: int, org_slug: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_course_by_orgslug( + request: Request, + page: int, + limit: int, + org_slug: str, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Get houses by page and limit """ return await get_courses_orgslug(request, current_user, page, limit, org_slug) -@router.put("/{course_id}") -async def api_update_course(request: Request, course_object: Course, course_id: str, current_user: PublicUser = Depends(get_current_user)): +@router.put("/") +async def api_update_course( + request: Request, + course_object: CourseUpdate, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Update Course by course_id """ - return await update_course(request, course_object, course_id, current_user) + return await update_course(request, course_object, current_user, db_session) @router.delete("/{course_id}") -async def api_delete_course(request: Request, course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_delete_course( + request: Request, + course_id: str, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Delete Course by ID """ - return await delete_course(request, course_id, current_user) + return await delete_course(request, course_id, current_user, db_session) diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 86841d26..26fd2dd6 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -2,6 +2,10 @@ import json from typing import List, Literal, Optional from uuid import uuid4 from pydantic import BaseModel +from sqlmodel import Session, select +from src.db.course_authors import CourseAuthor, CourseAuthorshipEnum +from src.db.users import PublicUser, AnonymousUser +from src.db.courses import Course, CourseCreate, CourseRead, CourseUpdate from src.security.rbac.rbac import ( authorization_verify_based_on_roles, authorization_verify_based_on_roles_and_authorship, @@ -10,320 +14,183 @@ from src.security.rbac.rbac import ( ) from src.services.courses.activities.activities import ActivityInDB from src.services.courses.thumbnails import upload_thumbnail -from src.services.users.schemas.users import AnonymousUser -from src.services.users.users import PublicUser from fastapi import HTTPException, Request, status, UploadFile from datetime import datetime -#### Classes #################################################### - -class Course(BaseModel): - name: str - mini_description: str - description: str - learnings: List[str] - thumbnail: str - public: bool - chapters: List[str] - chapters_content: Optional[List] - org_id: str - - -class CourseInDB(Course): - course_id: str - creationDate: str - updateDate: str - authors: List[str] - - -# TODO : wow terrible, fix this -# those models need to be available only in the chapters service -class CourseChapter(BaseModel): - name: str - description: str - activities: list - - -class CourseChapterInDB(CourseChapter): - coursechapter_id: str - course_id: str - creationDate: str - updateDate: str - - -#### Classes #################################################### - -# TODO : Add courses photo & cover upload and delete - - -#################################################### -# CRUD -#################################################### - - -async def get_course(request: Request, course_id: str, current_user: PublicUser): - courses = request.app.db["courses"] - - course = await courses.find_one({"course_id": course_id}) - - # verify course rights - await verify_rights(request, course_id, current_user, "read") +async def get_course( + request: Request, course_id: str, current_user: PublicUser, db_session: Session +): + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=404, + detail="Course not found", ) - course = Course(**course) return course -async def get_course_meta(request: Request, course_id: str, current_user: PublicUser): - courses = request.app.db["courses"] - trails = request.app.db["trails"] - - course = await courses.find_one({"course_id": course_id}) - activities = request.app.db["activities"] - - # verify course rights - await verify_rights(request, course_id, current_user, "read") +async def get_course_meta( + request: Request, course_id: str, current_user: PublicUser, db_session: Session +): + course_statement = select(Course).where(Course.id == course_id) + course = db_session.exec(course_statement).first() if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=404, + detail="Course not found", ) - coursechapters = await courses.find_one( - {"course_id": course_id}, {"chapters_content": 1, "_id": 0} - ) + # todo : get course chapters + # todo : get course activities + # todo : get trail - # 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, - } - - 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, - } + return course async def create_course( request: Request, - course_object: Course, - org_id: str, + course_object: CourseCreate, + org_id: int, current_user: PublicUser, + db_session: Session, thumbnail_file: UploadFile | None = None, ): - courses = request.app.db["courses"] - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) - - # generate course_id with uuid4 - course_id = str(f"course_{uuid4()}") - - # TODO(fix) : the implementation here is clearly not the best one (this entire function) - course_object.org_id = org_id - course_object.chapters_content = [] - - await authorization_verify_based_on_roles( - request, - current_user.user_id, - "create", - user["roles"], - course_id, - ) + course = Course.from_orm(course_object) + # Complete course object + course.org_id = org_id + course.course_uuid = str(uuid4()) + course.creation_date = str(datetime.now()) + course.update_date = str(datetime.now()) + # Upload thumbnail if thumbnail_file and thumbnail_file.filename: - name_in_disk = ( - f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" - ) - await upload_thumbnail( - thumbnail_file, name_in_disk, course_object.org_id, course_id - ) + name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" + await upload_thumbnail(thumbnail_file, name_in_disk, org_id, course.course_uuid) course_object.thumbnail = name_in_disk - course = CourseInDB( - course_id=course_id, - authors=[current_user.user_id], - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), - **course_object.dict(), + # Insert course + db_session.add(course) + db_session.commit() + db_session.refresh(course) + + # Make the user the creator of the course + course_author = CourseAuthor( + course_id=course.id is not None, + user_id=current_user.id, + authorship=CourseAuthorshipEnum.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(course_author) + db_session.commit() + db_session.refresh(course_author) - if not course_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) - - return course.dict() + return CourseRead.from_orm(course) async def update_course_thumbnail( request: Request, course_id: str, current_user: PublicUser, + db_session: Session, thumbnail_file: UploadFile | None = None, ): - courses = request.app.db["courses"] + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() - course = await courses.find_one({"course_id": course_id}) - - # verify course rights - await verify_rights(request, course_id, current_user, "update") - - # TODO(fix) : the implementation here is clearly not the best one - if course: - creationDate = course["creationDate"] - authors = course["authors"] - if thumbnail_file and thumbnail_file.filename: - name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" - course = Course(**course).copy(update={"thumbnail": name_in_disk}) - await upload_thumbnail( - thumbnail_file, name_in_disk, course.org_id, course_id - ) - - updated_course = CourseInDB( - course_id=course_id, - creationDate=creationDate, - authors=authors, - updateDate=str(datetime.now()), - **course.dict(), - ) - - await courses.update_one( - {"course_id": course_id}, {"$set": updated_course.dict()} - ) - - return CourseInDB(**updated_course.dict()) - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" - ) - - -async def update_course( - request: Request, course_object: Course, course_id: str, current_user: PublicUser -): - courses = request.app.db["courses"] - - course = await courses.find_one({"course_id": course_id}) - - # verify course rights - await verify_rights(request, course_id, current_user, "update") - - if course: - creationDate = course["creationDate"] - authors = course["authors"] - - # get today's date - datetime_object = datetime.now() - - updated_course = CourseInDB( - course_id=course_id, - creationDate=creationDate, - authors=authors, - updateDate=str(datetime_object), - **course_object.dict(), - ) - - await courses.update_one( - {"course_id": course_id}, {"$set": updated_course.dict()} - ) - - return CourseInDB(**updated_course.dict()) - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" - ) - - -async def delete_course(request: Request, course_id: str, current_user: PublicUser): - courses = request.app.db["courses"] - - course = await courses.find_one({"course_id": course_id}) - - # verify course rights - await verify_rights(request, course_id, current_user, "delete") + name_in_disk = None if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=404, + detail="Course not found", ) - isDeleted = await courses.delete_one({"course_id": course_id}) + # Upload thumbnail + if thumbnail_file and thumbnail_file.filename: + name_in_disk = ( + f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" + ) + await upload_thumbnail( + thumbnail_file, name_in_disk, course.org_id, course.course_uuid + ) - if isDeleted: - return {"detail": "Course deleted"} + # Update course + if name_in_disk: + course.thumbnail_image = name_in_disk else: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", + status_code=500, + detail="Issue with thumbnail upload", ) + # Complete the course object + course.update_date = str(datetime.now()) + + db_session.add(course) + db_session.commit() + db_session.refresh(course) + + return course + + +async def update_course( + request: Request, + course_object: CourseUpdate, + current_user: PublicUser, + db_session: Session, +): + statement = select(Course).where(Course.id == course_object.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + del course_object.course_id + + # 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) + + return course + + +async def delete_course( + request: Request, course_id: str, current_user: PublicUser, db_session: Session +): + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + db_session.delete(course) + db_session.commit() + + return {"detail": "Course deleted"} + #################################################### # Misc @@ -349,7 +216,7 @@ async def get_courses_orgslug( ) # show only public courses if user is not logged in - if current_user.user_id == "anonymous": + if current_user.id == "anonymous": all_courses = ( courses.find({"org_id": org["org_id"], "public": True}) .sort("name", 1) @@ -380,30 +247,30 @@ async def verify_rights( action: Literal["create", "read", "update", "delete"], ): if action == "read": - if current_user.user_id == "anonymous": + if current_user.id == "anonymous": await authorization_verify_if_element_is_public( - request, course_id, current_user.user_id, action + request, course_id, str(current_user.id), action ) else: users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) + user = await users.find_one({"user_id": str(current_user.id)}) await authorization_verify_based_on_roles_and_authorship( request, - current_user.user_id, + str(current_user.id), action, user["roles"], course_id, ) else: users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) + user = await users.find_one({"user_id": str(current_user.id)}) - await authorization_verify_if_user_is_anon(current_user.user_id) + await authorization_verify_if_user_is_anon(str(current_user.id)) await authorization_verify_based_on_roles_and_authorship( request, - current_user.user_id, + str(current_user.id), action, user["roles"], course_id, diff --git a/apps/api/src/services/dev/mocks/initial.py b/apps/api/src/services/dev/mocks/initial.py index d1005093..5bc4ac35 100644 --- a/apps/api/src/services/dev/mocks/initial.py +++ b/apps/api/src/services/dev/mocks/initial.py @@ -11,7 +11,6 @@ from src.services.users.users import PublicUser 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 @@ -128,16 +127,16 @@ async def create_initial_data(request: Request): 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) + # 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 @@ -160,22 +159,22 @@ async def create_initial_data(request: Request): 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=[], - ) + # 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" @@ -191,10 +190,10 @@ async def create_initial_data(request: Request): with open(f"content/uploads/img/{name_in_disk}", "wb") as f: f.write(image.content) - course.thumbnail = name_in_disk + # course.thumbnail = name_in_disk - course = CourseInDB(**course.dict()) - await courses.insert_one(course.dict()) + # course = CourseInDB(**course.dict()) + # await courses.insert_one(course.dict()) # create chapters for i in range(0, 5): diff --git a/apps/api/src/services/install/install.py b/apps/api/src/services/install/install.py index 55a6e362..33c2a31a 100644 --- a/apps/api/src/services/install/install.py +++ b/apps/api/src/services/install/install.py @@ -7,7 +7,6 @@ from config.config import get_learnhouse_config from src.security.security import security_hash_password from src.services.courses.activities.activities import Activity, create_activity from src.services.courses.chapters import create_coursechapter, CourseChapter -from src.services.courses.courses import CourseInDB from src.services.orgs.schemas.orgs import Organization, OrganizationInDB from faker import Faker @@ -370,50 +369,50 @@ async def create_sample_data(org_slug: str, username: str, request: Request): 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=[], - ) + # 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"] + # courses = request.app.db["courses"] - course = CourseInDB(**course.dict()) - await courses.insert_one(course.dict()) + # 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, - ) + # # 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, + # ) From 727f17ba7cfa1aa0298287a522b30eac9ce72d29 Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 14 Nov 2023 22:30:39 +0100 Subject: [PATCH 08/39] feat: init activities --- apps/api/src/db/activities.py | 52 ++-- apps/api/src/db/organizations.py | 4 +- apps/api/src/db/roles.py | 16 +- apps/api/src/routers/courses/activities.py | 52 ++-- .../services/courses/activities/activities.py | 294 +++++++----------- .../src/services/courses/activities/pdf.py | 111 ++++--- .../src/services/courses/activities/video.py | 184 +++++------ apps/api/src/services/courses/chapters.py | 14 +- apps/api/src/services/courses/courses.py | 1 - 9 files changed, 358 insertions(+), 370 deletions(-) diff --git a/apps/api/src/db/activities.py b/apps/api/src/db/activities.py index ca05a622..943c5a16 100644 --- a/apps/api/src/db/activities.py +++ b/apps/api/src/db/activities.py @@ -1,55 +1,69 @@ from typing import Literal, Optional +from click import Option from sqlalchemy import JSON, Column from sqlmodel import Field, Session, SQLModel, create_engine, select from enum import Enum class ActivityTypeEnum(str, Enum): - VIDEO = "VIDEO" - DOCUMENT = "DOCUMENT" - DYNAMIC = "DYNAMIC" - ASSESSMENT = "ASSESSMENT" - CUSTOM = "CUSTOM" + 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 - DYNAMIC_PAGE = "DYNAMIC_PAGE" + SUBTYPE_DYNAMIC_PAGE = "SUBTYPE_DYNAMIC_PAGE" # Video - VIDEO_YOUTUBE = "VIDEO_YOUTUBE" - VIDEO_HOSTED = "VIDEO_HOSTED" + SUBTYPE_VIDEO_YOUTUBE = "SUBTYPE_VIDEO_YOUTUBE" + SUBTYPE_VIDEO_HOSTED = "SUBTYPE_VIDEO_HOSTED" # Document - DOCUMENT_PDF = "DOCUMENT_PDF" - DOCUMENT_DOC = "DOCUMENT_GDOC" + SUBTYPE_DOCUMENT_PDF = "SUBTYPE_DOCUMENT_PDF" + SUBTYPE_DOCUMENT_DOC = "SUBTYPE_DOCUMENT_DOC" # Assessment - ASSESSMENT_QUIZ = "ASSESSMENT_QUIZ" + SUBTYPE_ASSESSMENT_QUIZ = "SUBTYPE_ASSESSMENT_QUIZ" # Custom - CUSTOM = "CUSTOM" + SUBTYPE_CUSTOM = "SUBTYPE_CUSTOM" class ActivityBase(SQLModel): name: str - activity_type: ActivityTypeEnum = ActivityTypeEnum.CUSTOM - activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.CUSTOM - slug: 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 - org_id: int = Field(default=None, foreign_key="organization.id") course_id: int = Field(default=None, foreign_key="course.id") class Activity(ActivityBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) - activity_uuid: str - creation_date: str - update_date: str + org_id: int = Field(default=None, foreign_key="organization.id") + activity_uuid: str = "" + creation_date: str = "" + update_date: str = "" class ActivityCreate(ActivityBase): + order: int + org_id: int = Field(default=None, foreign_key="organization.id") + course_id: int = Field(default=None, foreign_key="course.id") + chapter_id : int pass +class ActivityUpdate(ActivityBase): + activity_id: int + 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 diff --git a/apps/api/src/db/organizations.py b/apps/api/src/db/organizations.py index a1170dab..6c8c2a1b 100644 --- a/apps/api/src/db/organizations.py +++ b/apps/api/src/db/organizations.py @@ -4,10 +4,10 @@ from sqlmodel import Field, SQLModel class OrganizationBase(SQLModel): name: str - description: Optional[str] = "" + description: Optional[str] slug: str email: str - logo_image: Optional[str] = "" + logo_image: Optional[str] class Organization(OrganizationBase, table=True): diff --git a/apps/api/src/db/roles.py b/apps/api/src/db/roles.py index 9e1227b9..10f1e917 100644 --- a/apps/api/src/db/roles.py +++ b/apps/api/src/db/roles.py @@ -5,21 +5,21 @@ from sqlmodel import Field, SQLModel class RoleTypeEnum(str, Enum): - ORGANIZATION = "ORGANIZATION" - ORGANIZATION_API_TOKEN = "ORGANIZATION_API_TOKEN" - GLOBAL = "GLOBAL" + TYPE_ORGANIZATION = "TYPE_ORGANIZATION" + TYPE_ORGANIZATION_API_TOKEN = "TYPE_ORGANIZATION_API_TOKEN" + TYPE_GLOBAL = "TYPE_GLOBAL" class RoleBase(SQLModel): - name: str - description: Optional[str] + name: str + description: Optional[str] 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.ORGANIZATION + role_type: RoleTypeEnum = RoleTypeEnum.TYPE_GLOBAL role_uuid: str = "" creation_date: str = "" update_date: str = "" @@ -31,6 +31,6 @@ class RoleCreate(RoleBase): class RoleUpdate(SQLModel): role_id: int = Field(default=None, foreign_key="role.id") - name: Optional[str] - description: Optional[str] + name: Optional[str] + description: Optional[str] rights: Optional[dict] = Field(default={}, sa_column=Column(JSON)) diff --git a/apps/api/src/routers/courses/activities.py b/apps/api/src/routers/courses/activities.py index f04af1d7..6b62128c 100644 --- a/apps/api/src/routers/courses/activities.py +++ b/apps/api/src/routers/courses/activities.py @@ -1,4 +1,7 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request +from src.db.activities import ActivityCreate, ActivityUpdate +from src.db.users import PublicUser +from src.core.events.database import get_db_session from src.services.courses.activities.activities import ( Activity, create_activity, @@ -14,7 +17,6 @@ from src.services.courses.activities.video import ( create_external_video_activity, create_video_activity, ) -from src.services.users.schemas.users import PublicUser router = APIRouter() @@ -22,17 +24,14 @@ router = APIRouter() @router.post("/") async def api_create_activity( request: Request, - activity_object: Activity, - org_id: str, - coursechapter_id: str, + activity_object: ActivityCreate, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Create new activity """ - return await create_activity( - request, activity_object, org_id, coursechapter_id, current_user - ) + return await create_activity(request, activity_object, current_user, db_session) @router.get("/{activity_id}") @@ -40,11 +39,14 @@ async def api_get_activity( request: Request, activity_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ 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}") @@ -52,24 +54,25 @@ async def api_get_activities( request: Request, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Get CourseChapter activities """ - return await get_activities(request, coursechapter_id, current_user) + return await get_activities(request, coursechapter_id, current_user, db_session) -@router.put("/{activity_id}") +@router.put("/") async def api_update_activity( request: Request, - activity_object: Activity, - activity_id: str, + activity_object: ActivityUpdate, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Update activity by activity_id """ - return await update_activity(request, activity_object, activity_id, current_user) + return await update_activity(request, activity_object, current_user, db_session) @router.delete("/{activity_id}") @@ -77,11 +80,12 @@ async def api_delete_activity( request: Request, activity_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Delete activity by activity_id """ - return await delete_activity(request, activity_id, current_user) + return await delete_activity(request, activity_id, current_user, db_session) # Video activity @@ -91,15 +95,21 @@ async def api_delete_activity( async def api_create_video_activity( request: Request, name: str = Form(), - coursechapter_id: str = Form(), + chapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), video_file: UploadFile | None = None, + db_session=Depends(get_db_session), ): """ Create new activity """ return await create_video_activity( - request, name, coursechapter_id, current_user, video_file + request, + name, + chapter_id, + current_user, + db_session, + video_file, ) @@ -108,24 +118,28 @@ async def api_create_external_video_activity( request: Request, external_video: ExternalVideo, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Create new activity """ - return await create_external_video_activity(request, current_user, external_video) + return await create_external_video_activity( + request, current_user, external_video, db_session + ) @router.post("/documentpdf") async def api_create_documentpdf_activity( request: Request, name: str = Form(), - coursechapter_id: str = Form(), + chapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), pdf_file: UploadFile | None = None, + db_session=Depends(get_db_session), ): """ Create new activity """ return await create_documentpdf_activity( - request, name, coursechapter_id, current_user, pdf_file + request, name, chapter_id, current_user, db_session, pdf_file ) diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index 44ee1779..363b8884 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -1,35 +1,21 @@ +import stat from typing import Literal from pydantic import BaseModel +from sqlmodel import Session, select +from src.db.organizations import Organization +from src import db +from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate +from src.db.chapter_activities import ChapterActivity from src.security.rbac.rbac import ( authorization_verify_based_on_roles, authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) -from src.services.users.schemas.users import AnonymousUser, PublicUser +from src.db.users import AnonymousUser, PublicUser from fastapi import HTTPException, status, Request from uuid import uuid4 from datetime import datetime -#### Classes #################################################### - - -class Activity(BaseModel): - name: str - type: str - content: object - - -class ActivityInDB(Activity): - activity_id: str - course_id: str - coursechapter_id: str - org_id: str - creationDate: str - updateDate: str - - -#### Classes #################################################### - #################################################### # CRUD @@ -38,148 +24,130 @@ class ActivityInDB(Activity): async def create_activity( request: Request, - activity_object: Activity, - org_id: str, - coursechapter_id: str, + activity_object: ActivityCreate, current_user: PublicUser, + db_session: Session, ): - activities = request.app.db["activities"] - courses = request.app.db["courses"] - users = request.app.db["users"] + activity = Activity.from_orm(activity_object) - # get user - user = await users.find_one({"user_id": current_user.user_id}) + # CHeck if org exists + statement = select(Organization).where(Organization.id == activity_object.org_id) + org = db_session.exec(statement).first() - # generate activity_id - activity_id = str(f"activity_{uuid4()}") + if not org: + raise HTTPException( + status_code=404, + detail="Organization not found", + ) - # verify activity rights - await authorization_verify_based_on_roles( - request, - current_user.user_id, - "create", - user["roles"], - activity_id, + activity.activity_uuid = str(f"activity_{uuid4()}") + activity.creation_date = str(datetime.now()) + activity.update_date = str(datetime.now()) + + # Insert Activity in DB + db_session.add(activity) + db_session.commit() + db_session.refresh(activity) + + # Add activity to chapter + activity_chapter = ChapterActivity( + chapter_id=activity_object.chapter_id, + activity_id=activity.id is not None, + course_id=activity_object.course_id, + org_id=activity_object.org_id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + order=activity_object.order, ) - # get course_id from activity - course = await courses.find_one({"chapters": coursechapter_id}) + # Insert ChapterActivity link in DB + db_session.add(activity_chapter) + db_session.commit() + db_session.refresh(activity_chapter) - # create activity - activity = ActivityInDB( - **activity_object.dict(), - creationDate=str(datetime.now()), - coursechapter_id=coursechapter_id, - updateDate=str(datetime.now()), - activity_id=activity_id, - org_id=org_id, - course_id=course["course_id"], - ) - 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 + return ActivityRead.from_orm(activity) -async def get_activity(request: Request, activity_id: str, current_user: PublicUser): - activities = request.app.db["activities"] - courses = request.app.db["courses"] - - activity = await activities.find_one({"activity_id": activity_id}) - - # get course_id from activity - coursechapter_id = activity["coursechapter_id"] - await courses.find_one({"chapters": coursechapter_id}) - - # verify course rights - await verify_rights(request, activity["course_id"], current_user, "read") +async def get_activity( + request: Request, + activity_id: str, + current_user: PublicUser, + db_session: Session, +): + statement = select(Activity).where(Activity.id == activity_id) + activity = db_session.exec(statement).first() if not activity: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=404, + detail="Activity not found", ) - activity = ActivityInDB(**activity) return activity async def update_activity( request: Request, - activity_object: Activity, - activity_id: str, + activity_object: ActivityUpdate, current_user: PublicUser, + db_session: Session, ): - activities = request.app.db["activities"] - - activity = await activities.find_one({"activity_id": activity_id}) - - # verify course rights - await verify_rights(request, activity_id, current_user, "update") - - if activity: - creationDate = activity["creationDate"] - - # get today's date - datetime_object = datetime.now() - - updated_course = ActivityInDB( - activity_id=activity_id, - coursechapter_id=activity["coursechapter_id"], - creationDate=creationDate, - updateDate=str(datetime_object), - course_id=activity["course_id"], - org_id=activity["org_id"], - **activity_object.dict(), - ) - - await activities.update_one( - {"activity_id": activity_id}, {"$set": updated_course.dict()} - ) - - return ActivityInDB(**updated_course.dict()) - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="activity does not exist" - ) - - -async def delete_activity(request: Request, activity_id: str, current_user: PublicUser): - activities = request.app.db["activities"] - - activity = await activities.find_one({"activity_id": activity_id}) - - # verify course rights - await verify_rights(request, activity_id, current_user, "delete") + statement = select(Activity).where(Activity.id == activity_object.activity_id) + activity = db_session.exec(statement).first() if not activity: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="activity does not exist" + status_code=404, + detail="Activity not found", ) - # Remove Activity - isDeleted = await activities.delete_one({"activity_id": activity_id}) + del activity_object.activity_id - # Remove Activity from chapter - courses = request.app.db["courses"] - isDeletedFromChapter = await courses.update_one( - {"chapters_content.activities": activity_id}, - {"$pull": {"chapters_content.$.activities": activity_id}}, - ) + # Update only the fields that were passed in + for var, value in vars(activity_object).items(): + if value is not None: + setattr(activity, var, value) - if isDeleted and isDeletedFromChapter: - return {"detail": "Activity deleted"} - else: + db_session.add(activity) + db_session.commit() + db_session.refresh(activity) + + return activity + + +async def delete_activity( + request: Request, + activity_id: str, + current_user: PublicUser, + db_session: Session, +): + statement = select(Activity).where(Activity.id == activity_id) + activity = db_session.exec(statement).first() + + if not activity: raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", + status_code=404, + detail="Activity not found", ) + # Delete activity from chapter + statement = select(ChapterActivity).where( + ChapterActivity.activity_id == activity_id + ) + activity_chapter = db_session.exec(statement).first() + + if not activity_chapter: + raise HTTPException( + status_code=404, + detail="Activity not found in chapter", + ) + + db_session.delete(activity_chapter) + db_session.delete(activity) + db_session.commit() + + return {"detail": "Activity deleted"} + #################################################### # Misc @@ -187,64 +155,20 @@ async def delete_activity(request: Request, activity_id: str, current_user: Publ async def get_activities( - request: Request, coursechapter_id: str, current_user: PublicUser + request: Request, + coursechapter_id: str, + current_user: PublicUser, + db_session: Session, ): - activities = request.app.db["activities"] - - activities = activities.find({"coursechapter_id": coursechapter_id}) + statement = select(ChapterActivity).where( + ChapterActivity.chapter_id == coursechapter_id + ) + activities = db_session.exec(statement).all() if not activities: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=404, + detail="No activities found", ) - activities = [ - ActivityInDB(**activity) for activity in await activities.to_list(length=100) - ] - return activities - - -#### Security #################################################### - - -async def verify_rights( - request: Request, - activity_id: str, # course_id in case of read - current_user: PublicUser | AnonymousUser, - action: Literal["create", "read", "update", "delete"], -): - if action == "read": - if current_user.user_id == "anonymous": - await authorization_verify_if_element_is_public( - request, activity_id, current_user.user_id, action - ) - else: - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) - - await authorization_verify_if_user_is_anon(current_user.user_id) - - await authorization_verify_based_on_roles( - request, - current_user.user_id, - action, - user["roles"], - activity_id, - ) - else: - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) - - await authorization_verify_if_user_is_anon(current_user.user_id) - - await authorization_verify_based_on_roles( - request, - current_user.user_id, - action, - user["roles"], - activity_id, - ) - - -#### Security #################################################### diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index 8919639b..8442d387 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -1,7 +1,16 @@ +from sqlmodel import Session, select +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 PublicUser from src.security.rbac.rbac import authorization_verify_based_on_roles from src.services.courses.activities.uploads.pdfs import upload_pdf -from src.services.users.users import PublicUser -from src.services.courses.activities.activities import ActivityInDB from fastapi import HTTPException, status, UploadFile, Request from uuid import uuid4 from datetime import datetime @@ -10,26 +19,35 @@ from datetime import datetime async def create_documentpdf_activity( request: Request, name: str, - coursechapter_id: str, + chapter_id: str, current_user: PublicUser, + db_session: Session, pdf_file: UploadFile | None = None, ): - activities = request.app.db["activities"] - courses = request.app.db["courses"] - users = request.app.db["users"] + # get chapter_id + statement = select(Chapter).where(Chapter.id == chapter_id) + chapter = db_session.exec(statement).first() - # get user - user = await users.find_one({"user_id": current_user.user_id}) + if not chapter: + raise HTTPException( + status_code=404, + detail="Chapter not found", + ) - # generate activity_id - activity_id = str(f"activity_{uuid4()}") + statement = select(CourseChapter).where(CourseChapter.chapter_id == chapter_id) + coursechapter = db_session.exec(statement).first() - # get org_id from course - coursechapter = await courses.find_one( - {"chapters_content.coursechapter_id": coursechapter_id} - ) + if not coursechapter: + raise HTTPException( + status_code=404, + detail="CourseChapter not found", + ) - org_id = coursechapter["org_id"] + # get org_id + org_id = coursechapter.id + + # create activity uuid + activity_uuid = f"activity_{uuid4()}" # check if pdf_file is not None if not pdf_file: @@ -51,45 +69,48 @@ async def create_documentpdf_activity( status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided" ) - activity_object = ActivityInDB( - org_id=org_id, - activity_id=activity_id, - coursechapter_id=coursechapter_id, + # Create activity + activity = Activity( name=name, - type="documentpdf", - course_id=coursechapter["course_id"], + activity_type=ActivityTypeEnum.TYPE_DOCUMENT, + activity_sub_type=ActivitySubTypeEnum.SUBTYPE_DOCUMENT_PDF, content={ - "documentpdf": { - "filename": "documentpdf." + pdf_format, - "activity_id": activity_id, - } + "filename": "documentpdf." + pdf_format, + "activity_uuid": activity_uuid, }, - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), + published_version=1, + version=1, + org_id=org_id is not None, + course_id=coursechapter.course_id, + activity_uuid=activity_uuid, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) - await authorization_verify_based_on_roles( - request, - current_user.user_id, - "create", - user["roles"], - activity_id, - ) + # Insert Activity in DB + db_session.add(activity) + db_session.commit() + db_session.refresh(activity) - # create activity - activity = ActivityInDB(**activity_object.dict()) - await activities.insert_one(activity.dict()) + # Add activity to chapter + activity_chapter = ChapterActivity( + chapter_id=(int(chapter_id)), + activity_id=activity.id is not None, + course_id=coursechapter.course_id, + org_id=coursechapter.org_id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + order=1, + ) # upload pdf if pdf_file: # get pdffile format - await upload_pdf(pdf_file, activity_id, org_id, coursechapter["course_id"]) + await upload_pdf(pdf_file, activity.id, org_id, coursechapter.course_id) - # todo : choose whether to update the chapter or not - # update chapter - await courses.update_one( - {"chapters_content.coursechapter_id": coursechapter_id}, - {"$addToSet": {"chapters_content.$.activities": activity_id}}, - ) + # Insert ChapterActivity link in DB + db_session.add(activity_chapter) + db_session.commit() + db_session.refresh(activity_chapter) - return activity + return ActivityRead.from_orm(activity) diff --git a/apps/api/src/services/courses/activities/video.py b/apps/api/src/services/courses/activities/video.py index 470e4ddc..924a68ad 100644 --- a/apps/api/src/services/courses/activities/video.py +++ b/apps/api/src/services/courses/activities/video.py @@ -1,12 +1,16 @@ from typing import Literal from pydantic import BaseModel +from sqlmodel import Session, select +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 PublicUser from src.security.rbac.rbac import ( authorization_verify_based_on_roles, ) from src.services.courses.activities.uploads.videos import upload_video -from src.services.users.users import PublicUser -from src.services.courses.activities.activities import ActivityInDB from fastapi import HTTPException, status, UploadFile, Request from uuid import uuid4 from datetime import datetime @@ -15,32 +19,32 @@ from datetime import datetime async def create_video_activity( request: Request, name: str, - coursechapter_id: str, + chapter_id: str, current_user: PublicUser, + db_session: Session, video_file: UploadFile | None = None, ): - activities = request.app.db["activities"] - courses = request.app.db["courses"] - users = request.app.db["users"] + # get chapter_id + statement = select(Chapter).where(Chapter.id == chapter_id) + chapter = db_session.exec(statement).first() - # get user - user = await users.find_one({"user_id": current_user.user_id}) + if not chapter: + raise HTTPException( + status_code=404, + detail="Chapter not found", + ) - # generate activity_id - activity_id = str(f"activity_{uuid4()}") - - # get org_id from course - coursechapter = await courses.find_one( - {"chapters_content.coursechapter_id": coursechapter_id} - ) + statement = select(CourseChapter).where(CourseChapter.chapter_id == chapter_id) + coursechapter = db_session.exec(statement).first() if not coursechapter: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="CourseChapter : No coursechapter found", + status_code=404, + detail="CourseChapter not found", ) - org_id = coursechapter["org_id"] + # generate activity_uuid + activity_uuid = str(f"activity_{uuid4()}") # check if video_file is not None if not video_file: @@ -64,55 +68,57 @@ async def create_video_activity( detail="Video : No video file provided", ) - activity_object = ActivityInDB( - org_id=org_id, - activity_id=activity_id, - coursechapter_id=coursechapter_id, - course_id=coursechapter["course_id"], + activity_object = Activity( name=name, - type="video", + activity_type=ActivityTypeEnum.TYPE_VIDEO, + activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_HOSTED, + activity_uuid=activity_uuid, + published_version=1, content={ - "video": { - "filename": "video." + video_format, - "activity_id": activity_id, - } + "filename": "video." + video_format, + "activity_uuid": activity_uuid, }, - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), - ) - - await authorization_verify_based_on_roles( - request, - current_user.user_id, - "create", - user["roles"], - activity_id, + version=1, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) # create activity - activity = ActivityInDB(**activity_object.dict()) - await activities.insert_one(activity.dict()) + activity = Activity.from_orm(activity_object) + db_session.add(activity) + db_session.commit() + db_session.refresh(activity) # upload video if video_file: # get videofile format - await upload_video(video_file, activity_id, org_id, coursechapter["course_id"]) + await upload_video( + video_file, activity.id, coursechapter.org_id, coursechapter.course_id + ) - # todo : choose whether to update the chapter or not # update chapter - await courses.update_one( - {"chapters_content.coursechapter_id": coursechapter_id}, - {"$addToSet": {"chapters_content.$.activities": activity_id}}, + chapter_activity_object = ChapterActivity( + chapter_id=coursechapter.id is not None, + activity_id=activity.id is not None, + 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): name: str uri: str type: Literal["youtube", "vimeo"] - coursechapter_id: str + chapter_id: str class ExternalVideoInDB(BaseModel): @@ -123,65 +129,63 @@ async def create_external_video_activity( request: Request, current_user: PublicUser, data: ExternalVideo, + db_session: Session, ): - activities = request.app.db["activities"] - courses = request.app.db["courses"] - users = request.app.db["users"] + # get chapter_id + statement = select(Chapter).where(Chapter.id == data.chapter_id) + chapter = db_session.exec(statement).first() - # get user - user = await users.find_one({"user_id": current_user.user_id}) + if not chapter: + raise HTTPException( + status_code=404, + detail="Chapter not found", + ) - # generate activity_id - activity_id = str(f"activity_{uuid4()}") - - # get org_id from course - coursechapter = await courses.find_one( - {"chapters_content.coursechapter_id": data.coursechapter_id} - ) + statement = select(CourseChapter).where(CourseChapter.chapter_id == data.chapter_id) + coursechapter = db_session.exec(statement).first() if not coursechapter: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="CourseChapter : No coursechapter found", + status_code=404, + detail="CourseChapter not found", ) - org_id = coursechapter["org_id"] + # generate activity_uuid + activity_uuid = str(f"activity_{uuid4()}") - activity_object = ActivityInDB( - org_id=org_id, - activity_id=activity_id, - coursechapter_id=data.coursechapter_id, + activity_object = Activity( name=data.name, - type="video", + activity_type=ActivityTypeEnum.TYPE_VIDEO, + activity_sub_type=ActivitySubTypeEnum.SUBTYPE_VIDEO_YOUTUBE, + activity_uuid=activity_uuid, + published_version=1, content={ - "external_video": { - "uri": data.uri, - "activity_id": activity_id, - "type": data.type, - } + "uri": data.uri, + "type": data.type, + "activity_uuid": activity_uuid, }, - course_id=coursechapter["course_id"], - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), - ) - - await authorization_verify_based_on_roles( - request, - current_user.user_id, - "create", - user["roles"], - activity_id, + version=1, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) # create activity - activity = ActivityInDB(**activity_object.dict()) - await activities.insert_one(activity.dict()) + activity = Activity.from_orm(activity_object) + db_session.add(activity) + db_session.commit() + db_session.refresh(activity) - # todo : choose whether to update the chapter or not # update chapter - await courses.update_one( - {"chapters_content.coursechapter_id": data.coursechapter_id}, - {"$addToSet": {"chapters_content.$.activities": activity_id}}, + chapter_activity_object = ChapterActivity( + chapter_id=coursechapter.id is not None, + activity_id=activity.id is not None, + 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) diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 97184ecd..596b825a 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -10,11 +10,23 @@ from src.security.rbac.rbac import ( authorization_verify_if_user_is_anon, ) from src.services.courses.courses import Course -from src.services.courses.activities.activities import ActivityInDB from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request +class 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 + class CourseChapter(BaseModel): name: str description: str diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 26fd2dd6..e12d3d06 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -12,7 +12,6 @@ from src.security.rbac.rbac import ( authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) -from src.services.courses.activities.activities import ActivityInDB from src.services.courses.thumbnails import upload_thumbnail from fastapi import HTTPException, Request, status, UploadFile from datetime import datetime From e6adbca5627620d263fcb197b9f0fa42a43821f3 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 16 Nov 2023 21:30:01 +0100 Subject: [PATCH 09/39] feat: init collections --- apps/api/src/db/collections.py | 39 ++- apps/api/src/db/collections_courses.py | 11 + apps/api/src/routers/courses/collections.py | 24 +- apps/api/src/services/courses/collections.py | 323 +++++++++---------- 4 files changed, 219 insertions(+), 178 deletions(-) create mode 100644 apps/api/src/db/collections_courses.py diff --git a/apps/api/src/db/collections.py b/apps/api/src/db/collections.py index 63960f70..26085f2e 100644 --- a/apps/api/src/db/collections.py +++ b/apps/api/src/db/collections.py @@ -1,3 +1,40 @@ from typing import Optional from sqlmodel import Field, SQLModel -from enum import Enum \ No newline at end of file + + +class CollectionBase(SQLModel): + name: str + public: bool + description: Optional[str] = "" + + +class Collection(CollectionBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + org_id: int = Field(default=None, foreign_key="organization.id") + collection_uuid: str = "" + creation_date: str = "" + update_date: str = "" + + +class CollectionCreate(CollectionBase): + courses: list + org_id: int = Field(default=None, foreign_key="organization.id") + + pass + + +class CollectionUpdate(CollectionBase): + collection_id: int + courses: Optional[list] + name: Optional[str] + public: Optional[bool] + description: Optional[str] + + +class CollectionRead(CollectionBase): + id: int + courses: list + collection_uuid: str + creation_date: str + update_date: str + pass diff --git a/apps/api/src/db/collections_courses.py b/apps/api/src/db/collections_courses.py new file mode 100644 index 00000000..7ec5ff1b --- /dev/null +++ b/apps/api/src/db/collections_courses.py @@ -0,0 +1,11 @@ +from typing import Optional +from sqlmodel import Field, SQLModel + + +class CollectionCourse(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + collection_id: int = Field(default=None, foreign_key="collection.id") + course_id: int = Field(default=None, foreign_key="course.id") + org_id: int = Field(default=None, foreign_key="organization.id") + creation_date: str + update_date: str diff --git a/apps/api/src/routers/courses/collections.py b/apps/api/src/routers/courses/collections.py index 87dfe81c..d691401f 100644 --- a/apps/api/src/routers/courses/collections.py +++ b/apps/api/src/routers/courses/collections.py @@ -1,4 +1,6 @@ from fastapi import APIRouter, Depends, Request +from src.core.events.database import get_db_session +from src.db.collections import CollectionCreate, CollectionUpdate from src.security.auth import get_current_user from src.services.users.users import PublicUser from src.services.courses.collections import ( @@ -17,13 +19,14 @@ router = APIRouter() @router.post("/") async def api_create_collection( request: Request, - collection_object: Collection, + collection_object: CollectionCreate, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ 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}") @@ -31,11 +34,12 @@ async def api_get_collection( request: Request, collection_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Get single collection by ID """ - return await get_collection(request, collection_id, current_user) + return await get_collection(request, collection_id, current_user, db_session) @router.get("/org_id/{org_id}/page/{page}/limit/{limit}") @@ -45,26 +49,25 @@ async def api_get_collections_by( limit: int, org_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ 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}") async def api_update_collection( request: Request, - collection_object: Collection, - collection_id: str, + collection_object: CollectionUpdate, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Update collection by ID """ - return await update_collection( - request, collection_object, collection_id, current_user - ) + return await update_collection(request, collection_object, current_user, db_session) @router.delete("/{collection_id}") @@ -72,9 +75,10 @@ async def api_delete_collection( request: Request, collection_id: str, current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), ): """ Delete collection by ID """ - return await delete_collection(request, collection_id, current_user) + return await delete_collection(request, collection_id, current_user, db_session) diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index 05e133bf..38269e53 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -1,27 +1,31 @@ +from datetime import datetime +from gc import collect from typing import List, Literal from uuid import uuid4 from pydantic import BaseModel -from src.security.rbac.rbac import authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon +from sqlmodel import Session, select +from src.db.collections import ( + Collection, + CollectionCreate, + CollectionRead, + CollectionUpdate, +) +from src.db.collections_courses import CollectionCourse +from src.db.courses import Course +from src.security.rbac.rbac import ( + authorization_verify_based_on_roles_and_authorship, + authorization_verify_if_user_is_anon, +) from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request +from typing import List +from fastapi import HTTPException, Request +from sqlmodel import Session, select +from src.db.collections import Collection +from src.db.courses import Course +from src.db.collections_courses import CollectionCourse +from src.services.users.users import PublicUser -#### Classes #################################################### - - -class Collection(BaseModel): - name: str - description: str - courses: List[str] # course_id - public: bool - org_id: str # org_id - - -class CollectionInDB(Collection): - collection_id: str - authors: List[str] # user_id - - -#### Classes #################################################### #################################################### # CRUD @@ -29,134 +33,164 @@ class CollectionInDB(Collection): async def get_collection( - request: Request, collection_id: str, current_user: PublicUser -): - collections = request.app.db["collections"] - - collection = await collections.find_one({"collection_id": collection_id}) - - # verify collection rights - await verify_collection_rights( - request, collection_id, current_user, "read", collection["org_id"] - ) + request: Request, collection_id: str, current_user: PublicUser, db_session: Session +) -> CollectionRead: + statement = select(Collection).where(Collection.id == collection_id) + collection = db_session.exec(statement).first() if not collection: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" ) - collection = Collection(**collection) + # get courses in collection + statement = ( + select(Course) + .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .distinct(Course.id) + ) + courses = db_session.exec(statement).all() - # add courses to collection - courses = request.app.db["courses"] - courseids = [course for course in collection.courses] - - collection.courses = [] - collection.courses = courses.find({"course_id": {"$in": courseids}}, {"_id": 0}) - - collection.courses = [ - course for course in await collection.courses.to_list(length=100) - ] + collection = CollectionRead(**collection.dict(), courses=courses) return collection async def create_collection( - request: Request, collection_object: Collection, current_user: PublicUser -): - collections = request.app.db["collections"] + request: Request, + collection_object: CollectionCreate, + current_user: PublicUser, + db_session: Session, +) -> CollectionRead: + collection = Collection.from_orm(collection_object) - # find if collection already exists using name - isCollectionNameAvailable = await collections.find_one( - {"name": collection_object.name} - ) + # Complete the collection object + collection.collection_uuid = f"collection_{uuid4()}" + collection.creation_date = str(datetime.now()) + collection.update_date = str(datetime.now()) - # TODO - # await verify_collection_rights("*", current_user, "create") + # Add collection to database + db_session.add(collection) + db_session.commit() - if isCollectionNameAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Collection name already exists", + db_session.refresh(collection) + + # Link courses to collection + for course in collection_object.courses: + collection_course = CollectionCourse( + collection_id=int(collection.id is not None), + course_id=int(course), + org_id=int(collection_object.org_id), + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) + # Add collection_course to database + db_session.add(collection_course) - # generate collection_id with uuid4 - collection_id = str(f"collection_{uuid4()}") + db_session.commit() + db_session.refresh(collection) - collection = CollectionInDB( - collection_id=collection_id, - authors=[current_user.user_id], - **collection_object.dict(), + # Get courses once again + statement = ( + select(Course) + .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .distinct(Course.id) ) + courses = db_session.exec(statement).all() - collection_in_db = await collections.insert_one(collection.dict()) + collection = CollectionRead(**collection.dict(), courses=courses) - if not collection_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) - - return collection.dict() + return CollectionRead.from_orm(collection) async def update_collection( request: Request, - collection_object: Collection, - collection_id: str, + collection_object: CollectionUpdate, current_user: PublicUser, -): - # verify collection rights - - collections = request.app.db["collections"] - - collection = await collections.find_one({"collection_id": collection_id}) - - await verify_collection_rights( - request, collection_id, current_user, "update", collection["org_id"] + db_session: Session, +) -> CollectionRead: + statement = select(Collection).where( + Collection.id == collection_object.collection_id ) + collection = db_session.exec(statement).first() if not collection: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" ) - updated_collection = CollectionInDB( - collection_id=collection_id, **collection_object.dict() + courses = collection_object.courses + + del collection_object.collection_id + del collection_object.courses + + # Update only the fields that were passed in + for var, value in vars(collection_object).items(): + if value is not None: + setattr(collection, var, value) + + collection.update_date = str(datetime.now()) + + # Update only the fields that were passed in + for var, value in vars(collection_object).items(): + if value is not None: + setattr(collection, var, value) + + statement = select(CollectionCourse).where( + CollectionCourse.collection_id == collection.id + ) + collection_courses = db_session.exec(statement).all() + + # Delete all collection_courses + for collection_course in collection_courses: + db_session.delete(collection_course) + + # Add new collection_courses + for course in courses or []: + collection_course = CollectionCourse( + collection_id=int(collection.id is not None), + course_id=int(course), + org_id=int(collection.org_id), + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + # Add collection_course to database + db_session.add(collection_course) + + db_session.commit() + db_session.refresh(collection) + + # Get courses once again + statement = ( + select(Course) + .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .distinct(Course.id) ) - await collections.update_one( - {"collection_id": collection_id}, {"$set": updated_collection.dict()} - ) + courses = db_session.exec(statement).all() - return Collection(**updated_collection.dict()) + collection = CollectionRead(**collection.dict(), courses=courses) + + return collection async def delete_collection( - request: Request, collection_id: str, current_user: PublicUser + request: Request, collection_id: str, current_user: PublicUser, db_session: Session ): - collections = request.app.db["collections"] - - collection = await collections.find_one({"collection_id": collection_id}) - - await verify_collection_rights( - request, collection_id, current_user, "delete", collection["org_id"] - ) + statement = select(Collection).where(Collection.id == collection_id) + collection = db_session.exec(statement).first() if not collection: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" + status_code=404, + detail="Collection not found", ) - isDeleted = await collections.delete_one({"collection_id": collection_id}) + # delete collection from database + db_session.delete(collection) + db_session.commit() - if isDeleted: - return {"detail": "collection deleted"} - else: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) + return {"detail": "Collection deleted"} #################################################### @@ -168,75 +202,30 @@ async def get_collections( request: Request, org_id: str, current_user: PublicUser, + db_session: Session, page: int = 1, limit: int = 10, -): - collections = request.app.db["collections"] - - - if current_user.user_id == "anonymous": - all_collections = collections.find( - {"org_id": org_id, "public": True}, {"_id": 0} - ) - else: - # get all collections from database without ObjectId - all_collections = ( - collections.find({"org_id": org_id}) - .sort("name", 1) - .skip(10 * (page - 1)) - .limit(limit) - ) - - # create list of collections and include courses in each collection - collections_list = [] - for collection in await all_collections.to_list(length=100): - collection = CollectionInDB(**collection) - collections_list.append(collection) - - collection_courses = [course for course in collection.courses] - # add courses to collection - courses = request.app.db["courses"] - collection.courses = [] - collection.courses = courses.find( - {"course_id": {"$in": collection_courses}}, {"_id": 0} - ) - - collection.courses = [ - course for course in await collection.courses.to_list(length=100) - ] - - return collections_list - - -#### Security #################################################### - - -async def verify_collection_rights( - request: Request, - collection_id: str, - current_user: PublicUser, - action: Literal["create", "read", "update", "delete"], - org_id: str, -): - collections = request.app.db["collections"] - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) - collection = await collections.find_one({"collection_id": collection_id}) - - if not collection and action != "create" and collection_id != "*": - raise HTTPException( - 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( - request, current_user.user_id, action, user["roles"], collection_id +) -> List[CollectionRead]: + statement = ( + select(Collection).where(Collection.org_id == org_id).distinct(Collection.id) ) + collections = db_session.exec(statement).all() + if not collections: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="No collections found" + ) -#### Security #################################################### + collections_with_courses = [] + for collection in collections: + statement = ( + select(Course) + .join(CollectionCourse, Course.id == CollectionCourse.course_id) + .distinct(Course.id) + ) + courses = db_session.exec(statement).all() + + collection = CollectionRead(**collection.dict(), courses=courses) + collections_with_courses.append(collection) + + return collections_with_courses From b04fd64c92b64369e4fe277b80186fd6087044cc Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 16 Nov 2023 22:37:09 +0100 Subject: [PATCH 10/39] feat: inot blocks + schemas updates --- apps/api/src/core/events/database.py | 2 +- apps/api/src/db/blocks.py | 16 +-- apps/api/src/routers/blocks.py | 108 +++++++++++------- .../block_types/imageBlock/imageBlock.py | 82 +++++++++++++ .../blocks/block_types/imageBlock/images.py | 72 ------------ .../blocks/block_types/pdfBlock/pdfBlock.py | 82 +++++++------ .../blocks/block_types/quizBlock/quizBlock.py | 70 ------------ .../block_types/videoBlock/videoBlock.py | 82 +++++++------ 8 files changed, 250 insertions(+), 264 deletions(-) create mode 100644 apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py delete mode 100644 apps/api/src/services/blocks/block_types/imageBlock/images.py delete mode 100644 apps/api/src/services/blocks/block_types/quizBlock/quizBlock.py diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index 8c16e65b..04bc0e3d 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -15,8 +15,8 @@ from src.db import ( activities, course_chapters, chapter_activities, - blocks, collections, + blocks, ) engine = create_engine( diff --git a/apps/api/src/db/blocks.py b/apps/api/src/db/blocks.py index 24f67c2d..7fb2adb4 100644 --- a/apps/api/src/db/blocks.py +++ b/apps/api/src/db/blocks.py @@ -5,16 +5,16 @@ from enum import Enum class BlockTypeEnum(str, Enum): - QUIZ_BLOCK = "QUIZ_BLOCK" - VIDEO_BLOCK = "VIDEO_BLOCK" - DOCUMENT_PDF_BLOCK = "DOCUMENT_PDF_BLOCK" - IMAGE_BLOCK = "IMAGE_BLOCK" - CUSTOM = "CUSTOM" + 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.CUSTOM + block_type: BlockTypeEnum = BlockTypeEnum.BLOCK_CUSTOM content: dict = Field(default={}, sa_column=Column(JSON)) @@ -29,9 +29,11 @@ class Block(BlockBase, table=True): 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") @@ -41,4 +43,4 @@ class BlockRead(BlockBase): block_uuid: str creation_date: str update_date: str - pass \ No newline at end of file + pass diff --git a/apps/api/src/routers/blocks.py b/apps/api/src/routers/blocks.py index 3d2c4929..f598e2c6 100644 --- a/apps/api/src/routers/blocks.py +++ b/apps/api/src/routers/blocks.py @@ -1,9 +1,19 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request +from src.core.events.database import get_db_session from src.security.auth import get_current_user -from src.services.blocks.block_types.imageBlock.images import create_image_block, get_image_block -from src.services.blocks.block_types.videoBlock.videoBlock import create_video_block, get_video_block -from src.services.blocks.block_types.pdfBlock.pdfBlock import create_pdf_block, get_pdf_block -from src.services.blocks.block_types.quizBlock.quizBlock import create_quiz_block, get_quiz_block_answers, get_quiz_block_options, quizBlock +from src.services.blocks.block_types.imageBlock.imageBlock import ( + create_image_block, + get_image_block, +) +from src.services.blocks.block_types.videoBlock.videoBlock import ( + create_video_block, + get_video_block, +) +from src.services.blocks.block_types.pdfBlock.pdfBlock import ( + create_pdf_block, + get_pdf_block, +) + from src.services.users.users import PublicUser router = APIRouter() @@ -12,83 +22,93 @@ router = APIRouter() # Image Block #################### + @router.post("/image") -async def api_create_image_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)): +async def api_create_image_file_block( + request: Request, + file_object: UploadFile, + activity_id: str = Form(), + db_session=Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Create new image file """ - return await create_image_block(request, file_object, activity_id) + return await create_image_block(request, file_object, activity_id, db_session) @router.get("/image") -async def api_get_image_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_image_file_block( + request: Request, + block_uuid: str, + db_session=Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Get image file """ - return await get_image_block(request, file_id, current_user) + return await get_image_block(request, block_uuid, current_user, db_session) + #################### # Video Block #################### + @router.post("/video") -async def api_create_video_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)): +async def api_create_video_file_block( + request: Request, + file_object: UploadFile, + activity_id: str = Form(), + db_session=Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Create new video file """ - return await create_video_block(request, file_object, activity_id) + return await create_video_block(request, file_object, activity_id, db_session) @router.get("/video") -async def api_get_video_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_video_file_block( + request: Request, + block_uuid: str, + db_session=Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Get video file """ - return await get_video_block(request, file_id, current_user) + return await get_video_block(request, block_uuid, current_user, db_session) + #################### # PDF Block #################### + @router.post("/pdf") -async def api_create_pdf_file_block(request: Request, file_object: UploadFile, activity_id: str = Form(), current_user: PublicUser = Depends(get_current_user)): +async def api_create_pdf_file_block( + request: Request, + file_object: UploadFile, + activity_id: str = Form(), + db_session=Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Create new pdf file """ - return await create_pdf_block(request, file_object, activity_id) + return await create_pdf_block(request, file_object, activity_id, db_session) @router.get("/pdf") -async def api_get_pdf_file_block(request: Request, file_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_pdf_file_block( + request: Request, + block_uuid: str, + db_session=Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +): """ Get pdf file """ - return await get_pdf_block(request, file_id, current_user) - - -#################### -# Quiz Block -#################### - -@router.post("/quiz/{activity_id}") -async def api_create_quiz_block(request: Request, quiz_block: quizBlock, activity_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Create new document file - """ - return await create_quiz_block(request, quiz_block, activity_id, current_user) - - -@router.get("/quiz/options") -async def api_get_quiz_options(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Get quiz options - """ - return await get_quiz_block_options(request, block_id, current_user) - - -@router.get("/quiz/answers") -async def api_get_quiz_answers(request: Request, block_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Get quiz answers - """ - return await get_quiz_block_answers(request, block_id, current_user) + return await get_pdf_block(request, block_uuid, current_user, db_session) diff --git a/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py b/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py new file mode 100644 index 00000000..26ec7609 --- /dev/null +++ b/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py @@ -0,0 +1,82 @@ +from datetime import datetime +from uuid import uuid4 +from fastapi import HTTPException, status, UploadFile, Request +from sqlmodel import Session, select +from src.db.activities import Activity +from src.db.blocks import Block, 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_id: str, db_session: Session +): + statement = select(Activity).where(Activity.id == activity_id) + 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_id from activity + 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 + block_uuid = str(f"block_{uuid4()}") + + block_data = await upload_file_and_return_file_object( + request, + image_file, + activity_id, + block_uuid, + ["jpg", "jpeg", "png", "gif"], + block_type, + str(org_id), + str(course.id), + ) + + # create block + block = Block( + activity_id=activity.id is not None, + block_type=BlockTypeEnum.BLOCK_IMAGE, + content=block_data.dict(), + org_id=org_id, + course_id=course.id is not None, + 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) + + 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: + return block + else: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist" + ) diff --git a/apps/api/src/services/blocks/block_types/imageBlock/images.py b/apps/api/src/services/blocks/block_types/imageBlock/images.py deleted file mode 100644 index 0aa16e92..00000000 --- a/apps/api/src/services/blocks/block_types/imageBlock/images.py +++ /dev/null @@ -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" - ) diff --git a/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py b/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py index 00de83b1..e9bd5941 100644 --- a/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py +++ b/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py @@ -1,69 +1,83 @@ +from datetime import datetime from uuid import uuid4 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, BlockTypeEnum +from src.db.courses import Course from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.users.users import PublicUser -async def create_pdf_block(request: Request, pdf_file: UploadFile, activity_id: str): - blocks = request.app.db["blocks"] - activity = request.app.db["activities"] - courses = request.app.db["courses"] +async def create_pdf_block( + request: Request, pdf_file: UploadFile, activity_id: str, db_session: Session +): + statement = select(Activity).where(Activity.id == activity_id) + activity = db_session.exec(statement).first() + + if not activity: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found" + ) block_type = "pdfBlock" # get org_id from activity - activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0}) - org_id = activity["org_id"] + 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 - block_id = str(f"block_{uuid4()}") - - coursechapter_id = activity["coursechapter_id"] - - # get course_id from coursechapter - course = await courses.find_one( - {"chapters": coursechapter_id}, - {"_id": 0}, - ) + block_uuid = str(f"block_{uuid4()}") block_data = await upload_file_and_return_file_object( request, pdf_file, activity_id, - block_id, + block_uuid, ["pdf"], block_type, - org_id, - course["course_id"], + str(org_id), + str(course.id), ) # create block block = Block( - block_id=block_id, - activity_id=activity_id, - block_type=block_type, - block_data=block_data, + activity_id=activity.id is not None, + block_type=BlockTypeEnum.BLOCK_DOCUMENT_PDF, + content=block_data.dict(), org_id=org_id, - course_id=course["course_id"], + course_id=course.id is not None, + block_uuid=block_uuid, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) # insert block - await blocks.insert_one(block.dict()) + db_session.add(block) + db_session.commit() + db_session.refresh(block) return block -async def get_pdf_block(request: Request, file_id: str, current_user: PublicUser): - blocks = request.app.db["blocks"] +async def get_pdf_block( + request: Request, block_uuid: str, current_user: PublicUser, db_session: Session +): + statement = select(Block).where(Block.block_uuid == block_uuid) + block = db_session.exec(statement).first() - pdf_block = await blocks.find_one({"block_id": file_id}) - - if pdf_block: - return Block(**pdf_block) - - else: + if not block: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist" + status_code=status.HTTP_404_NOT_FOUND, detail="Video file does not exist" ) + + return block diff --git a/apps/api/src/services/blocks/block_types/quizBlock/quizBlock.py b/apps/api/src/services/blocks/block_types/quizBlock/quizBlock.py deleted file mode 100644 index f8758516..00000000 --- a/apps/api/src/services/blocks/block_types/quizBlock/quizBlock.py +++ /dev/null @@ -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 diff --git a/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py b/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py index 0c7c2282..b18ff94b 100644 --- a/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py +++ b/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py @@ -1,73 +1,83 @@ +from datetime import datetime from uuid import uuid4 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, BlockTypeEnum +from src.db.courses import Course from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.users.users import PublicUser async def create_video_block( - request: Request, video_file: UploadFile, activity_id: str + request: Request, video_file: UploadFile, activity_id: str, db_session: Session ): - blocks = request.app.db["blocks"] - activity = request.app.db["activities"] - courses = request.app.db["courses"] + statement = select(Activity).where(Activity.id == activity_id) + activity = db_session.exec(statement).first() + + if not activity: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found" + ) block_type = "videoBlock" # get org_id from activity - activity = await activity.find_one( - {"activity_id": activity_id}, {"_id": 0} - ) - org_id = activity["org_id"] + 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 - block_id = str(f"block_{uuid4()}") - - coursechapter_id = activity["coursechapter_id"] - - # get course_id from coursechapter - course = await courses.find_one( - {"chapters": coursechapter_id}, - {"_id": 0}, - ) + block_uuid = str(f"block_{uuid4()}") block_data = await upload_file_and_return_file_object( request, video_file, activity_id, - block_id, + block_uuid, ["mp4", "webm", "ogg"], block_type, - org_id, - course["course_id"], + str(org_id), + str(course.id), ) # create block block = Block( - block_id=block_id, - activity_id=activity_id, - block_type=block_type, - block_data=block_data, + activity_id=activity.id is not None, + block_type=BlockTypeEnum.BLOCK_VIDEO, + content=block_data.dict(), org_id=org_id, - course_id=course["course_id"], + course_id=course.id is not None, + block_uuid=block_uuid, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) # insert block - await blocks.insert_one(block.dict()) + db_session.add(block) + db_session.commit() + db_session.refresh(block) return block -async def get_video_block(request: Request, file_id: str, current_user: PublicUser): - blocks = request.app.db["blocks"] +async def get_video_block( + request: Request, block_uuid: str, current_user: PublicUser, db_session: Session +): + statement = select(Block).where(Block.block_uuid == block_uuid) + block = db_session.exec(statement).first() - video_block = await blocks.find_one({"block_id": file_id}) - - if video_block: - return Block(**video_block) - - else: + if not block: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist" + status_code=status.HTTP_404_NOT_FOUND, detail="Video file does not exist" ) + + return block From eca819b896cbf7bbfddedac742aefac2243d709a Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 18 Nov 2023 12:22:00 +0100 Subject: [PATCH 11/39] feat: init trails --- apps/api/src/db/trail_runs.py | 53 +++ apps/api/src/db/trail_steps.py | 32 ++ apps/api/src/db/trails.py | 34 ++ apps/api/src/routers/trail.py | 85 ++++- apps/api/src/services/trail/trail.py | 544 +++++++++++++++------------ 5 files changed, 495 insertions(+), 253 deletions(-) create mode 100644 apps/api/src/db/trail_runs.py create mode 100644 apps/api/src/db/trail_steps.py create mode 100644 apps/api/src/db/trails.py diff --git a/apps/api/src/db/trail_runs.py b/apps/api/src/db/trail_runs.py new file mode 100644 index 00000000..ce177589 --- /dev/null +++ b/apps/api/src/db/trail_runs.py @@ -0,0 +1,53 @@ +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") + # timestamps + creation_date: str + update_date: str + steps: list[TrailStep] + pass diff --git a/apps/api/src/db/trail_steps.py b/apps/api/src/db/trail_steps.py new file mode 100644 index 00000000..3afba947 --- /dev/null +++ b/apps/api/src/db/trail_steps.py @@ -0,0 +1,32 @@ +from enum import Enum +from typing import Optional +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel + + +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(default=None, foreign_key="trailrun.id") + 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 diff --git a/apps/api/src/db/trails.py b/apps/api/src/db/trails.py new file mode 100644 index 00000000..94c19f31 --- /dev/null +++ b/apps/api/src/db/trails.py @@ -0,0 +1,34 @@ +from typing import Optional +from pydantic import BaseModel +from sqlmodel import Field, SQLModel +from enum import Enum +from src.db.trail_runs import TrailRun, TrailRunRead + +from src.db.trail_steps import TrailStep + + +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] diff --git a/apps/api/src/routers/trail.py b/apps/api/src/routers/trail.py index f2eb38b5..0cbcaeff 100644 --- a/apps/api/src/routers/trail.py +++ b/apps/api/src/routers/trail.py @@ -1,56 +1,103 @@ from fastapi import APIRouter, Depends, Request +from src.core.events.database import get_db_session +from src.db.trails import TrailCreate, TrailRead from src.security.auth import get_current_user -from src.services.trail.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail +from src.services.trail.trail import ( + Trail, + add_activity_to_trail, + add_course_to_trail, + create_user_trail, + get_user_trails, + get_user_trail_with_orgid, + remove_course_from_trail, +) router = APIRouter() @router.post("/start") -async def api_start_trail(request: Request, trail_object: Trail, org_id: str, user=Depends(get_current_user)) -> Trail: +async def api_start_trail( + request: Request, + trail_object: TrailCreate, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> Trail: """ - Start trail + Start trail """ - return await create_trail(request, user, org_id, trail_object) + return await create_user_trail(request, user, trail_object, db_session) -@router.get("/org_id/{org_id}/trail") -async def api_get_trail_by_orgid(request: Request, org_slug: str, user=Depends(get_current_user)): +@router.get("/") +async def api_get_user_trail( + request: Request, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Get a user trails """ - return await get_user_trail(request, user=user, org_slug=org_slug) + return await get_user_trails( + request, user=user, db_session=db_session + ) -@router.get("/org_slug/{org_slug}/trail") -async def api_get_trail_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)): +@router.get("/org_slug/{org_id}/trail") +async def api_get_trail_by_org_id( + request: Request, + org_id: int, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Get a user trails using org slug """ - return await get_user_trail_with_orgslug(request, user, org_slug=org_slug) + 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}") -async def api_add_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)): +@router.post("/add_course/{course_id}") +async def api_add_course_to_trail( + request: Request, + course_id: str, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Add Course to trail """ - return await add_course_to_trail(request, user, org_slug, course_id) + return await add_course_to_trail(request, user, course_id, db_session) -@router.post("/org_slug/{org_slug}/remove_course/{course_id}") -async def api_remove_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)): +@router.post("/remove_course/{course_id}") +async def api_remove_course_to_trail( + request: Request, + course_id: str, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Remove Course from trail """ - return await remove_course_from_trail(request, user, org_slug, course_id) + return await remove_course_from_trail(request, user, course_id, db_session) -@router.post("/org_slug/{org_slug}/add_activity/course_id/{course_id}/activity_id/{activity_id}") -async def api_add_activity_to_trail(request: Request, activity_id: str, course_id: str, org_slug: str, user=Depends(get_current_user)): +@router.post("/add_activity/course_id/{course_id}/activity_id/{activity_id}") +async def api_add_activity_to_trail( + request: Request, + activity_id: int, + course_id: int, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Add Course to trail """ - return await add_activity_to_trail(request, user, course_id, org_slug, activity_id) + return await add_activity_to_trail( + request, user, course_id, activity_id, db_session + ) diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 1a9e9d9d..ff7a8f56 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -1,286 +1,362 @@ from datetime import datetime +import stat from typing import List, Literal, Optional from uuid import uuid4 from fastapi import HTTPException, Request, status from pydantic import BaseModel +from sqlmodel import Session, select +from src.db.courses import Course +from src.db.trail_runs import TrailRun, TrailRunCreate, TrailRunRead +from src.db.trail_steps import TrailStep +from src.db.trails import Trail, TrailCreate, TrailRead +from src.db.users import PublicUser from src.services.orgs.schemas.orgs import PublicOrganization from src.services.courses.chapters import get_coursechapters_meta -from src.services.users.users import PublicUser -#### Classes #################################################### - - -class ActivityData(BaseModel): - activity_id: str - activity_type: str - data: Optional[dict] - - -class TrailCourse(BaseModel): - course_id: str - elements_type: Optional[Literal["course"]] = "course" - status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing" - course_object: dict - masked: Optional[bool] = False - activities_marked_complete: Optional[List[str]] - activities_data: Optional[List[ActivityData]] - progress: Optional[int] - - -class Trail(BaseModel): - status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing" - masked: Optional[bool] = False - courses: Optional[List[TrailCourse]] - - -class TrailInDB(Trail): - trail_id: str - org_id: str - user_id: str - creationDate: str = datetime.now().isoformat() - updateDate: str = datetime.now().isoformat() - - -#### Classes #################################################### - - -async def create_trail( - request: Request, user: PublicUser, org_id: str, trail_object: Trail +async def create_user_trail( + request: Request, + user: PublicUser, + trail_object: TrailCreate, + db_session: Session, ) -> Trail: - trails = request.app.db["trails"] + statement = select(Trail).where(Trail.org_id == trail_object.org_id) + trail = db_session.exec(statement).first() - # get list of courses - if trail_object.courses: - courses = trail_object.courses - # get course ids - course_ids = [course.course_id for course in courses] - - # find if the user has already started the course - existing_trail = await trails.find_one( - {"user_id": user.user_id, "courses.course_id": {"$in": course_ids}} + if trail: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Trail already exists", ) - 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_id = f"trail_{uuid4()}" + trail = Trail.from_orm(trail_object) + + trail.creation_date = str(datetime.now()) + trail.update_date = str(datetime.now()) + trail.org_id = trail_object.org_id + trail.trail_uuid = str(f"trail_{uuid4()}") # create trail - trail = TrailInDB( - **trail_object.dict(), trail_id=trail_id, user_id=user.user_id, org_id=org_id - ) - - await trails.insert_one(trail.dict()) + db_session.add(trail) + db_session.commit() + db_session.refresh(trail) return trail -async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> Trail: - trails = request.app.db["trails"] - trail = await trails.find_one({"user_id": user.user_id}) +async def get_user_trails( + request: Request, + user: PublicUser, + db_session: Session, +) -> TrailRead: + statement = select(Trail).where(Trail.user_id == user.id) + trail = db_session.exec(statement).first() + if not trail: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" ) - for element in trail["courses"]: - course_id = element["course_id"] - chapters_meta = await get_coursechapters_meta(request, course_id, user) - activities = chapters_meta["activities"] - num_activities = len(activities) - num_completed_activities = len(element.get("activities_marked_complete", [])) - element["progress"] = ( - round((num_completed_activities / num_activities) * 100, 2) - if num_activities > 0 - else 0 - ) + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() - return Trail(**trail) + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) 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() -async def get_user_trail_with_orgslug( - request: Request, user: PublicUser, org_slug: str -) -> Trail: - trails = request.app.db["trails"] - orgs = request.app.db["organizations"] - courses_mongo = request.app.db["courses"] + trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] + trail_run.steps = trail_steps - # get org_id from orgslug - org = await orgs.find_one({"slug": org_slug}) + 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 = 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 + trail_read = TrailRead( + **trail.dict(), + runs=trail_runs, ) - for course in trail["courses"]: - course_id = course["course_id"] + return trail_read - 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) - activities = chapters_meta["activities"] +async def get_user_trail_with_orgid( + request: Request, user: PublicUser, org_id: int, db_session: Session +) -> TrailRead: + 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 - course_object = await courses_mongo.find_one( - {"course_id": course_id}, {"_id": 0} + if not trail: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" ) - course["course_object"] = course_object - num_activities = len(activities) + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() - num_completed_activities = len(course.get("activities_marked_complete", [])) - course["progress"] = ( - round((num_completed_activities / num_activities) * 100, 2) - if num_activities > 0 - else 0 - ) + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + ] - 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( - request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str -) -> Trail: - trails = request.app.db["trails"] - orgs = request.app.db["organizations"] - courseid = "course_" + course_id - activityid = "activity_" + activity_id + request: Request, + user: PublicUser, + course_id: int, + activity_id: int, + db_session: Session, +) -> TrailRead: + + # check if run already exists + statement = select(TrailRun).where(TrailRun.course_id == course_id) + trailrun = db_session.exec(statement).first() - # get org_id from orgslug - org = await orgs.find_one({"slug": org_slug}) - org_id = org["org_id"] + if trailrun: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" + ) - # 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} + statement = select(Course).where(Course.id == course_id) + 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 ) - - if user.user_id == "anonymous": - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Anonymous users cannot add activity to trail", - ) - - if not trail: - return Trail(masked=False, courses=[]) - - # if a trail has course_id in the courses array, then add the activity_id to the activities_marked_complete array - for element in trail["courses"]: - if element["course_id"] == courseid: - if "activities_marked_complete" in element: - # check if activity_id is already in the array - if activityid not in element["activities_marked_complete"]: - element["activities_marked_complete"].append(activityid) - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Activity already marked complete", - ) - else: - element["activities_marked_complete"] = [activity_id] - - # modify trail object - await trails.replace_one({"trail_id": trail["trail_id"]}, trail) - - return Trail(**trail) - - -async def add_course_to_trail( - request: Request, user: PublicUser, orgslug: str, course_id: str -) -> Trail: - trails = request.app.db["trails"] - orgs = request.app.db["organizations"] - - 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: - trail_to_insert = TrailInDB( - trail_id=f"trail_{uuid4()}", - user_id=user.user_id, - org_id=org["org_id"], - courses=[], - ) - trail_to_insert = await trails.insert_one(trail_to_insert.dict()) - - trail = await trails.find_one({"_id": trail_to_insert.inserted_id}) - - # check if course is already present in the trail - for element in trail["courses"]: - if element["course_id"] == course_id: - 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()) - await trails.replace_one({"trail_id": trail["trail_id"]}, trail) - return Trail(**trail) - - -async def remove_course_from_trail( - request: Request, user: PublicUser, orgslug: str, course_id: str -) -> Trail: - trails = request.app.db["trails"] - orgs = request.app.db["organizations"] - - if user.user_id == "anonymous": - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Anonymous users cannot add activity to trail", - ) - - org = await orgs.find_one({"slug": orgslug}) - - org = PublicOrganization(**org) - trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]}) + trail = db_session.exec(statement).first() if not trail: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" ) - # check if course is already present in the trail + statement = select(TrailRun).where( + TrailRun.trail_id == trail.id, TrailRun.course_id == course.id + ) + trailrun = db_session.exec(statement).first() - for element in trail["courses"]: - if element["course_id"] == course_id: - trail["courses"].remove(element) - break + if not trailrun: + trailrun = TrailRun( + trail_id=trail.id is not None, + course_id=course.id is not None, + 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) - await trails.replace_one({"trail_id": trail["trail_id"]}, trail) - return Trail(**trail) + 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 is not None, + activity_id=activity_id, + course_id=course.id is not None, + 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__, steps=[]) for trail_run in trail_runs + ] + + for trail_run in trail_runs: + statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) + trail_steps = db_session.exec(statement).all() + + trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] + trail_run.steps = trail_steps + + for trail_step in trail_steps: + statement = select(Course).where(Course.id == trail_step.course_id) + course = db_session.exec(statement).first() + trail_step.data = dict(course=course) + + trail_read = TrailRead( + **trail.dict(), + runs=trail_runs, + ) + + return trail_read + + +async def add_course_to_trail( + request: Request, + user: PublicUser, + course_id: str, + db_session: Session, +) -> TrailRead: + + # check if run already exists + statement = select(TrailRun).where(TrailRun.course_id == course_id) + trailrun = db_session.exec(statement).first() + + if trailrun: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" + ) + + statement = select(Course).where(Course.id == course_id) + 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 not trail_run: + trail_run = TrailRun( + trail_id=trail.id is not None, + course_id=course.id is not None, + 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) + + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() + + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) 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_id: str, + db_session: Session, +) -> TrailRead: + statement = select(Course).where(Course.id == course_id) + 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() + + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() + + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) 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 From 4fcc8aa77c16eed2989dbb14b74389efe76c92c4 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 20 Nov 2023 19:53:13 +0100 Subject: [PATCH 12/39] feat: init chapters, coursechapters. fix courses --- apps/api/src/db/activities.py | 2 +- apps/api/src/db/chapter_activities.py | 2 +- apps/api/src/db/chapters.py | 41 ++++++- apps/api/src/db/courses.py | 14 ++- apps/api/src/routers/courses/chapters.py | 103 ++++++++++++++---- apps/api/src/routers/courses/courses.py | 2 +- .../services/courses/activities/activities.py | 16 ++- apps/api/src/services/courses/courses.py | 7 +- 8 files changed, 151 insertions(+), 36 deletions(-) diff --git a/apps/api/src/db/activities.py b/apps/api/src/db/activities.py index 943c5a16..29d460cc 100644 --- a/apps/api/src/db/activities.py +++ b/apps/api/src/db/activities.py @@ -50,7 +50,7 @@ class ActivityCreate(ActivityBase): order: int org_id: int = Field(default=None, foreign_key="organization.id") course_id: int = Field(default=None, foreign_key="course.id") - chapter_id : int + chapter_id: int pass diff --git a/apps/api/src/db/chapter_activities.py b/apps/api/src/db/chapter_activities.py index 73d20598..1e31b7c4 100644 --- a/apps/api/src/db/chapter_activities.py +++ b/apps/api/src/db/chapter_activities.py @@ -5,7 +5,7 @@ from enum import Enum class ChapterActivity(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) order: int - chapter_id: int = Field(default=None, foreign_key="chapter.id") + chapter_id: int = Field(default=None, foreign_key="chapter.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") diff --git a/apps/api/src/db/chapters.py b/apps/api/src/db/chapters.py index c7490c4a..7b80f564 100644 --- a/apps/api/src/db/chapters.py +++ b/apps/api/src/db/chapters.py @@ -1,5 +1,7 @@ -from typing import Optional +from typing import List, Optional +from pydantic import BaseModel from sqlmodel import Field, SQLModel +from src.db.activities import Activity, ActivityRead, ActivityUpdate class ChapterBase(SQLModel): @@ -7,24 +9,55 @@ class ChapterBase(SQLModel): description: Optional[str] = "" thumbnail_image: Optional[str] = "" org_id: int = Field(default=None, foreign_key="organization.id") + course_id: int = Field(default=None, foreign_key="course.id") creation_date: str update_date: str class Chapter(ChapterBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) - chapter_uuid: str - creation_date: str - update_date: str + 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): + chapter_id: int + name: Optional[str] + description: Optional[str] + thumbnail_image: Optional[str] + + 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): + chapter_order: list[str] + chapters: List[ChapterRead] + activities: List[ActivityRead] + pass diff --git a/apps/api/src/db/courses.py b/apps/api/src/db/courses.py index 0f2347ee..5774b28a 100644 --- a/apps/api/src/db/courses.py +++ b/apps/api/src/db/courses.py @@ -1,6 +1,8 @@ -from typing import Optional +from typing import List, Optional from sqlmodel import Field, SQLModel +from src.db.chapters import ChapterRead + class CourseBase(SQLModel): name: str @@ -41,3 +43,13 @@ class CourseRead(CourseBase): 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] + pass diff --git a/apps/api/src/routers/courses/chapters.py b/apps/api/src/routers/courses/chapters.py index 213dcc55..263a93d7 100644 --- a/apps/api/src/routers/courses/chapters.py +++ b/apps/api/src/routers/courses/chapters.py @@ -1,6 +1,23 @@ +from typing import List from fastapi import APIRouter, Depends, Request +from src.core.events.database import get_db_session +from src.db.chapters import ( + ChapterCreate, + ChapterRead, + ChapterUpdate, + ChapterUpdateOrder, + DepreceatedChaptersRead, +) +from src.services.courses.chapters import ( + create_chapter, + delete_chapter, + get_chapter, + get_course_chapters, + get_depreceated_course_chapters, + reorder_chapters_and_activities, + update_chapter, +) -from src.services.courses.chapters import CourseChapter, CourseChapterMetaData, create_coursechapter, delete_coursechapter, get_coursechapter, get_coursechapters, get_coursechapters_meta, update_coursechapter, update_coursechapters_meta from src.services.users.users import PublicUser from src.security.auth import get_current_user @@ -8,57 +25,99 @@ router = APIRouter() @router.post("/") -async def api_create_coursechapter(request: Request,coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_create_coursechapter( + request: Request, + coursechapter_object: ChapterCreate, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +) -> ChapterRead: """ - Create new 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}") -async def api_get_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): +@router.get("/{chapter_id}") +async def api_get_coursechapter( + request: Request, + chapter_id: int, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +) -> ChapterRead: """ - Get single CourseChapter by coursechapter_id + Get single CourseChapter by chapter_id """ - return await get_coursechapter(request, coursechapter_id, current_user=current_user) + return await get_chapter(request, chapter_id, current_user, db_session) @router.get("/meta/{course_id}") -async def api_get_coursechapter_meta(request: Request,course_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_chapter_meta( + request: Request, + course_id: int, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +) -> DepreceatedChaptersRead: """ - Get coursechapter metadata + Get Chapters metadata """ - return await get_coursechapters_meta(request, course_id, current_user=current_user) + return await get_depreceated_course_chapters(request, course_id, current_user, db_session) -@router.put("/meta/{course_id}") -async def api_update_coursechapter_meta(request: Request,course_id: str, coursechapters_metadata: CourseChapterMetaData, current_user: PublicUser = Depends(get_current_user)): +@router.put("/order/{course_id}") +async def api_update_chapter_meta( + request: Request, + course_id: int, + 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_id, order, current_user, db_session + ) @router.get("/{course_id}/page/{page}/limit/{limit}") -async def api_get_coursechapter_by(request: Request,course_id: str, page: int, limit: int): +async def api_get_chapter_by( + request: Request, + course_id: int, + page: int, + limit: int, + 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, page, limit) @router.put("/{coursechapter_id}") -async def api_update_coursechapter(request: Request,coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_update_coursechapter( + request: Request, + coursechapter_object: ChapterUpdate, + coursechapter_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +) -> ChapterRead: """ Update CourseChapters by course_id """ - return await update_coursechapter(request, coursechapter_object, coursechapter_id, current_user) + return await update_chapter(request, coursechapter_object, current_user, db_session) @router.delete("/{coursechapter_id}") -async def api_delete_coursechapter(request: Request,coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_delete_coursechapter( + request: Request, + coursechapter_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): """ Delete CourseChapters by ID """ - return await delete_coursechapter(request,coursechapter_id, current_user) + return await delete_chapter( + request, coursechapter_id, current_user, db_session + ) diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index c91ab48a..b496f376 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -47,7 +47,7 @@ async def api_create_course( tags=tags, ) return await create_course( - request, course, org_id, current_user, db_session, thumbnail + request, course, current_user, db_session, thumbnail ) diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index 363b8884..3035d84b 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -2,6 +2,7 @@ import stat from typing import Literal from pydantic import BaseModel from sqlmodel import Session, select +from src.db.chapters import Chapter from src.db.organizations import Organization from src import db from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate @@ -49,15 +50,26 @@ async def create_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 is not None, + activity_id=activity.id if activity.id else 0, course_id=activity_object.course_id, org_id=activity_object.org_id, creation_date=str(datetime.now()), update_date=str(datetime.now()), - order=activity_object.order, + order=to_be_used_order, ) # Insert ChapterActivity link in DB diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index e12d3d06..95a90426 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -54,7 +54,6 @@ async def get_course_meta( async def create_course( request: Request, course_object: CourseCreate, - org_id: int, current_user: PublicUser, db_session: Session, thumbnail_file: UploadFile | None = None, @@ -62,7 +61,7 @@ async def create_course( course = Course.from_orm(course_object) # Complete course object - course.org_id = org_id + course.org_id = course.org_id course.course_uuid = str(uuid4()) course.creation_date = str(datetime.now()) course.update_date = str(datetime.now()) @@ -70,7 +69,7 @@ async def create_course( # Upload thumbnail if thumbnail_file and thumbnail_file.filename: name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" - await upload_thumbnail(thumbnail_file, name_in_disk, org_id, course.course_uuid) + await upload_thumbnail(thumbnail_file, name_in_disk, course_object.org_id, course.course_uuid) course_object.thumbnail = name_in_disk # Insert course @@ -80,7 +79,7 @@ async def create_course( # Make the user the creator of the course course_author = CourseAuthor( - course_id=course.id is not None, + course_id=course.id if course.id else 0, user_id=current_user.id, authorship=CourseAuthorshipEnum.CREATOR, creation_date=str(datetime.now()), From d95497e804214140e98a15ff3d82c48af343caef Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 20 Nov 2023 19:53:39 +0100 Subject: [PATCH 13/39] init : more chapters --- apps/api/src/services/courses/chapters.py | 706 ++++++++++++---------- 1 file changed, 403 insertions(+), 303 deletions(-) diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 596b825a..09403480 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -1,7 +1,19 @@ from datetime import datetime -from typing import List, Literal +from typing import List from uuid import uuid4 -from pydantic import BaseModel +from sqlmodel import Session, select +from src import db +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, + DepreceatedChaptersRead, +) from src.security.auth import non_public_endpoint from src.security.rbac.rbac import ( authorization_verify_based_on_roles, @@ -14,366 +26,454 @@ from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request -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 - -class CourseChapter(BaseModel): - name: str - description: str - activities: list - - -class CourseChapterInDB(CourseChapter): - coursechapter_id: str - course_id: str - creationDate: str - updateDate: str - - -# Frontend -class CourseChapterMetaData(BaseModel): - chapterOrder: List[str] - chapters: dict - activities: object - - -#### Classes #################################################### - #################################################### # CRUD #################################################### -async def create_coursechapter( +async def create_chapter( request: Request, - coursechapter_object: CourseChapter, - course_id: str, + chapter_object: ChapterCreate, current_user: PublicUser, -): - courses = request.app.db["courses"] - users = request.app.db["users"] - # get course org_id and verify rights - await courses.find_one({"course_id": course_id}) - user = await users.find_one({"user_id": current_user.user_id}) + db_session: Session, +) -> ChapterRead: + chapter = Chapter.from_orm(chapter_object) - # generate coursechapter_id with uuid4 - coursechapter_id = str(f"coursechapter_{uuid4()}") + # 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()) - hasRoleRights = await authorization_verify_based_on_roles( - request, current_user.user_id, "create", user["roles"], course_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: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Roles : Insufficient rights to perform this action", + # get last chapter order + last_order = course_chapters[-1].order if course_chapters else 0 + to_be_used_order = last_order + 1 + + # Add chapter to database + db_session.add(chapter) + db_session.commit() + db_session.refresh(chapter) + + chapter = ChapterRead(**chapter.dict(), activities=[]) + + # Check if COurseChapter link exists + + statement = ( + select(CourseChapter) + .where(CourseChapter.chapter_id == chapter.id) + .where(CourseChapter.course_id == chapter.course_id) + .where(CourseChapter.order == to_be_used_order) + ) + course_chapter = db_session.exec(statement).first() + + if not course_chapter: + # Add CourseChapter link + course_chapter = CourseChapter( + course_id=chapter.course_id, + chapter_id=chapter.id, + org_id=chapter.org_id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + order=to_be_used_order, ) - coursechapter = CourseChapterInDB( - coursechapter_id=coursechapter_id, - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), - course_id=course_id, - **coursechapter_object.dict(), - ) + # Insert CourseChapter link in DB + db_session.add(course_chapter) + db_session.commit() - courses.update_one( - {"course_id": course_id}, - { - "$addToSet": { - "chapters": coursechapter_id, - "chapters_content": coursechapter.dict(), - } - }, - ) - - return coursechapter.dict() + return chapter -async def get_coursechapter( - request: Request, coursechapter_id: str, current_user: PublicUser -): - courses = request.app.db["courses"] - - coursechapter = await courses.find_one( - {"chapters_content.coursechapter_id": coursechapter_id} - ) - - if coursechapter: - # verify course rights - await verify_rights(request, coursechapter["course_id"], current_user, "read") - coursechapter = CourseChapter(**coursechapter) - - return coursechapter - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="CourseChapter does not exist" - ) - - -async def update_coursechapter( +async def get_chapter( request: Request, - coursechapter_object: CourseChapter, - coursechapter_id: str, + chapter_id: int, current_user: PublicUser, -): - courses = request.app.db["courses"] + db_session: Session, +) -> ChapterRead: + statement = select(Chapter).where(Chapter.id == chapter_id) + chapter = db_session.exec(statement).first() - coursechapter = await courses.find_one( - {"chapters_content.coursechapter_id": coursechapter_id} + if not chapter: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist" + ) + + # 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: - # verify course rights - await verify_rights(request, coursechapter["course_id"], current_user, "update") + activities = db_session.exec(statement).all() - coursechapter = CourseChapterInDB( - coursechapter_id=coursechapter_id, - creationDate=str(datetime.now()), - updateDate=str(datetime.now()), - course_id=coursechapter["course_id"], - **coursechapter_object.dict(), - ) - - courses.update_one( - {"chapters_content.coursechapter_id": coursechapter_id}, - {"$set": {"chapters_content.$": coursechapter.dict()}}, - ) - - return coursechapter - - else: - 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} + chapter = ChapterRead( + **chapter.dict(), + activities=[ActivityRead(**activity.dict()) for activity in activities], ) - if course: - # verify course rights - await verify_rights(request, course["course_id"], current_user, "delete") + return chapter - # 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}}}, - ) +async def update_chapter( + request: Request, + chapter_object: ChapterUpdate, + current_user: PublicUser, + db_session: Session, +) -> ChapterRead: + statement = select(Chapter).where(Chapter.id == chapter_object.chapter_id) + chapter = db_session.exec(statement).first() - return {"message": "Coursechapter deleted"} - - else: + if not chapter: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist" ) + # Update only the fields that were passed in + for var, value in vars(chapter_object).items(): + if value is not None: + setattr(chapter, var, value) + + chapter.update_date = str(datetime.now()) + + db_session.commit() + db_session.refresh(chapter) + + chapter = ChapterRead(**chapter.dict()) + + return chapter + + +async def delete_chapter( + request: Request, + chapter_id: str, + current_user: PublicUser, + db_session: Session, +): + statement = select(Chapter).where(Chapter.id == chapter_id) + chapter = db_session.exec(statement).first() + + if not chapter: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist" + ) + + db_session.delete(chapter) + db_session.commit() + + # Remove all linked activities + statement = select(ChapterActivity).where(ChapterActivity.chapter_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"} + #################################################### # Misc #################################################### -async def get_coursechapters( - request: Request, course_id: str, page: int = 1, limit: int = 10 -): - courses = request.app.db["courses"] +async def get_course_chapters( + request: Request, + course_id: int, + db_session: Session, + page: int = 1, + limit: int = 10, +) -> List[ChapterRead]: + statement = select(Chapter).where(Chapter.course_id == course_id) + chapters = db_session.exec(statement).all() - 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: + if not chapters: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" + status_code=status.HTTP_409_CONFLICT, detail="Course do not have chapters" ) - # activities - coursechapter_activityIds_global = [] + chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] - # chapters - chapters = {} - if coursechapters["chapters_content"]: - for coursechapter in coursechapters["chapters_content"]: - coursechapter = CourseChapterInDB(**coursechapter) - coursechapter_activityIds = [] + # 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 activity in coursechapter.activities: - coursechapter_activityIds.append(activity) - coursechapter_activityIds_global.append(activity) + 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() - chapters[coursechapter.coursechapter_id] = { - "id": coursechapter.coursechapter_id, - "name": coursechapter.name, - "activityIds": coursechapter_activityIds, - } + if activity: + chapter.activities.append(ActivityRead(**activity.dict())) - # 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 + return chapters -async def update_coursechapters_meta( +async def get_depreceated_course_chapters( request: Request, - course_id: str, - coursechapters_metadata: CourseChapterMetaData, + course_id: int, current_user: PublicUser, -): - courses = request.app.db["courses"] - - await verify_rights(request, course_id, current_user, "update") - - # update chapters in course - await courses.update_one( - {"course_id": course_id}, - {"$set": {"chapters": coursechapters_metadata.chapterOrder}}, - ) - - if coursechapters_metadata.chapters is not None: - for ( - coursechapter_id, - chapter_metadata, - ) in coursechapters_metadata.chapters.items(): - filter_query = {"chapters_content.coursechapter_id": coursechapter_id} - update_query = { - "$set": { - "chapters_content.$.activities": chapter_metadata["activityIds"] - } - } - result = await courses.update_one(filter_query, update_query) - if result.matched_count == 0: - # handle error when no documents are matched by the filter query - print(f"No documents found for course chapter ID {coursechapter_id}") - - # update activities in coursechapters - activity = request.app.db["activities"] - if coursechapters_metadata.chapters is not None: - for ( - coursechapter_id, - chapter_metadata, - ) in coursechapters_metadata.chapters.items(): - # Update coursechapter_id in activities - filter_query = {"activity_id": {"$in": chapter_metadata["activityIds"]}} - update_query = {"$set": {"coursechapter_id": coursechapter_id}} - - result = await activity.update_many(filter_query, update_query) - if result.matched_count == 0: - # handle error when no documents are matched by the filter query - print(f"No documents found for course chapter ID {coursechapter_id}") - - return {"detail": "coursechapters metadata updated"} - - -#### Security #################################################### - - -async def verify_rights( - request: Request, - course_id: str, - current_user: PublicUser, - action: Literal["read", "update", "delete"], -): - courses = request.app.db["courses"] - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) - course = await courses.find_one({"course_id": course_id}) + db_session: Session, +) -> DepreceatedChaptersRead: + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() if not course: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" ) - if action == "read": - if current_user.user_id == "anonymous": - await authorization_verify_if_element_is_public( - request, course_id, current_user.user_id, action + # Get chapters that are linked to his course and order them by order, using the order field in the CourseChapter table + statement = ( + select(Chapter) + .join(CourseChapter, Chapter.id == CourseChapter.chapter_id) + .where(CourseChapter.course_id == course_id) + .order_by(CourseChapter.order) + .group_by(Chapter.id, CourseChapter.order) + ) + print("ded", statement) + chapters = db_session.exec(statement).all() + + chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] + + # Get activities for each chapter + for chapter in chapters: + statement = ( + select(Activity) + .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) + .where(ChapterActivity.chapter_id == chapter.id) + .order_by(ChapterActivity.order) + .distinct(Activity.id, ChapterActivity.order) + ) + chapter_activities = db_session.exec(statement).all() + + for chapter_activity in chapter_activities: + statement = ( + select(Activity) + .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) + .where(Activity.id == chapter_activity.id) + .distinct(Activity.id, ChapterActivity.order) + .order_by(ChapterActivity.order) ) - else: - users = request.app.db["users"] - user = await users.find_one({"user_id": current_user.user_id}) + activity = db_session.exec(statement).first() - await authorization_verify_if_user_is_anon(current_user.user_id) + if activity: + chapter.activities.append(ActivityRead(**activity.dict())) - await authorization_verify_based_on_roles_and_authorship( - 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}) + # Get a list of chapter ids + chapter_order: List[str] = [str(chapter.id) for chapter in chapters] - await authorization_verify_if_user_is_anon(current_user.user_id) + # Get activities for each chapter + activities = [] + for chapter_id in chapter_order: + # order by activity order + statement = ( + select(Activity) + .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) + .where(ChapterActivity.chapter_id == chapter_id) + .order_by(ChapterActivity.order) + .distinct(Activity.id, ChapterActivity.order) + ) + chapter_activities = db_session.exec(statement).all() - await authorization_verify_based_on_roles_and_authorship( - request, - current_user.user_id, - action, - user["roles"], - course_id, + activities.extend(chapter_activities) + + result = DepreceatedChaptersRead( + chapter_order=chapter_order, chapters=chapters, activities=activities + ) + + return result + + +async def reorder_chapters_and_activities( + request: Request, + course_id: int, + chapters_order: ChapterUpdateOrder, + current_user: PublicUser, + db_session: Session, +): + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" ) + ########### + # Chapters + ########### -#### Security #################################################### + # 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, + 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 not linked to chapter_id and activity_id and org_id and course_id + 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_keep = [ + activity_order.activity_id + for chapter_order in chapters_order.chapter_order_by_ids + for activity_order in chapter_order.activities_order_by_ids + ] + + for chapter_activity in chapter_activities: + if chapter_activity.activity_id not in activity_ids_to_keep: + db_session.delete(chapter_activity) + db_session.commit() + + # If links do not exists, create them + 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 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, + 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() + + # 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"} From 2485285a068e59a60645f47c73a3eb83297209d9 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 20 Nov 2023 19:53:56 +0100 Subject: [PATCH 14/39] generic fixes --- .../block_types/imageBlock/imageBlock.py | 4 +-- .../blocks/block_types/pdfBlock/pdfBlock.py | 4 +-- .../block_types/videoBlock/videoBlock.py | 4 +-- .../src/services/courses/activities/pdf.py | 2 +- apps/api/src/services/dev/mocks/initial.py | 33 +++++++++---------- apps/api/src/services/install/install.py | 1 - apps/api/src/services/orgs/orgs.py | 2 +- apps/api/src/services/trail/trail.py | 14 ++++---- apps/api/src/services/users/users.py | 3 +- 9 files changed, 31 insertions(+), 36 deletions(-) diff --git a/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py b/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py index 26ec7609..f9f3eabf 100644 --- a/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py +++ b/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py @@ -50,11 +50,11 @@ async def create_image_block( # create block block = Block( - activity_id=activity.id is not None, + activity_id=activity.id if activity.id else 0, block_type=BlockTypeEnum.BLOCK_IMAGE, content=block_data.dict(), org_id=org_id, - course_id=course.id is not None, + course_id=course.id if course.id else 0, block_uuid=block_uuid, creation_date=str(datetime.now()), update_date=str(datetime.now()), diff --git a/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py b/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py index e9bd5941..07fea4ca 100644 --- a/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py +++ b/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py @@ -51,11 +51,11 @@ async def create_pdf_block( # create block block = Block( - activity_id=activity.id is not None, + activity_id=activity.id if activity.id else 0, block_type=BlockTypeEnum.BLOCK_DOCUMENT_PDF, content=block_data.dict(), org_id=org_id, - course_id=course.id is not None, + course_id=course.id if course.id else 0, block_uuid=block_uuid, creation_date=str(datetime.now()), update_date=str(datetime.now()), diff --git a/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py b/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py index b18ff94b..2af8c79b 100644 --- a/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py +++ b/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py @@ -51,11 +51,11 @@ async def create_video_block( # create block block = Block( - activity_id=activity.id is not None, + activity_id=activity.id if activity.id else 0, block_type=BlockTypeEnum.BLOCK_VIDEO, content=block_data.dict(), org_id=org_id, - course_id=course.id is not None, + course_id=course.id if course.id else 0, block_uuid=block_uuid, creation_date=str(datetime.now()), update_date=str(datetime.now()), diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index 8442d387..9011c8a5 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -80,7 +80,7 @@ async def create_documentpdf_activity( }, published_version=1, version=1, - org_id=org_id is not None, + org_id=org_id if org_id else 0, course_id=coursechapter.course_id, activity_uuid=activity_uuid, creation_date=str(datetime.now()), diff --git a/apps/api/src/services/dev/mocks/initial.py b/apps/api/src/services/dev/mocks/initial.py index 5bc4ac35..86973158 100644 --- a/apps/api/src/services/dev/mocks/initial.py +++ b/apps/api/src/services/dev/mocks/initial.py @@ -5,7 +5,6 @@ from uuid import uuid4 from fastapi import Request from src.services.users.schemas.users import UserInDB 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 @@ -196,19 +195,19 @@ async def create_initial_data(request: Request): # 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) + # 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) diff --git a/apps/api/src/services/install/install.py b/apps/api/src/services/install/install.py index 33c2a31a..152ef745 100644 --- a/apps/api/src/services/install/install.py +++ b/apps/api/src/services/install/install.py @@ -6,7 +6,6 @@ import requests from config.config import get_learnhouse_config from src.security.security import security_hash_password from src.services.courses.activities.activities import Activity, create_activity -from src.services.courses.chapters import create_coursechapter, CourseChapter from src.services.orgs.schemas.orgs import Organization, OrganizationInDB from faker import Faker diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index a24737db..87f4e9f7 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -83,7 +83,7 @@ async def create_org( # Link user to org user_org = UserOrganization( user_id=int(current_user.id), - org_id=int(org.id is not None), + org_id=int(org.id if org.id else 0), role_id=1, creation_date=str(datetime.now()), update_date=str(datetime.now()), diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index ff7a8f56..92a5ae03 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -10,8 +10,6 @@ from src.db.trail_runs import TrailRun, TrailRunCreate, TrailRunRead from src.db.trail_steps import TrailStep from src.db.trails import Trail, TrailCreate, TrailRead from src.db.users import PublicUser -from src.services.orgs.schemas.orgs import PublicOrganization -from src.services.courses.chapters import get_coursechapters_meta async def create_user_trail( @@ -164,8 +162,8 @@ async def add_activity_to_trail( if not trailrun: trailrun = TrailRun( - trail_id=trail.id is not None, - course_id=course.id is not None, + 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()), @@ -182,9 +180,9 @@ async def add_activity_to_trail( if not trailstep: trailstep = TrailStep( - trailrun_id=trailrun.id is not None, + trailrun_id=trailrun.id if trailrun.id is not None else 0 , activity_id=activity_id, - course_id=course.id is not None, + course_id=course.id if course.id is not None else 0, org_id=course.org_id, complete=False, teacher_verified=False, @@ -265,8 +263,8 @@ async def add_course_to_trail( if not trail_run: trail_run = TrailRun( - trail_id=trail.id is not None, - course_id=course.id is not None, + 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()), diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index d237aa9a..553a632a 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -75,7 +75,7 @@ async def create_user( # Link user and organization user_organization = UserOrganization( - user_id=user.id is not None, + user_id=user.id if user.id else 0, org_id=int(org_id), role_id=1, creation_date=str(datetime.now()), @@ -201,7 +201,6 @@ async def update_user_password( user.password = await security_hash_password(form.new_password) user.update_date = str(datetime.now()) - # Update user in database db_session.add(user) db_session.commit() From 38288e8a57e410f26efd13303d6d56007652353e Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 20 Nov 2023 22:38:49 +0100 Subject: [PATCH 15/39] feat: init install + cleanup code --- apps/api/src/core/events/database.py | 17 +- apps/api/src/db/activities.py | 5 +- apps/api/src/db/chapter_activities.py | 1 - apps/api/src/db/chapters.py | 2 +- apps/api/src/db/course_chapters.py | 1 - apps/api/src/db/install.py | 31 ++ apps/api/src/db/roles.py | 42 +- apps/api/src/db/trails.py | 4 +- apps/api/src/routers/auth.py | 1 - apps/api/src/routers/courses/activities.py | 1 - apps/api/src/routers/courses/collections.py | 1 - apps/api/src/routers/courses/courses.py | 2 +- apps/api/src/routers/dev.py | 9 +- apps/api/src/routers/install/install.py | 47 +- apps/api/src/routers/roles.py | 1 - .../services/courses/activities/activities.py | 14 +- .../src/services/courses/activities/pdf.py | 1 - .../src/services/courses/activities/video.py | 3 - apps/api/src/services/courses/chapters.py | 8 - apps/api/src/services/courses/collections.py | 8 +- apps/api/src/services/courses/courses.py | 4 +- apps/api/src/services/dev/mocks/__init__.py | 0 apps/api/src/services/dev/mocks/initial.py | 213 -------- apps/api/src/services/install/install.py | 458 ++++++++---------- apps/api/src/services/orgs/orgs.py | 9 +- apps/api/src/services/roles/roles.py | 3 +- apps/api/src/services/trail/trail.py | 5 +- apps/api/src/services/users/users.py | 2 +- 28 files changed, 310 insertions(+), 583 deletions(-) create mode 100644 apps/api/src/db/install.py delete mode 100644 apps/api/src/services/dev/mocks/__init__.py delete mode 100644 apps/api/src/services/dev/mocks/initial.py diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index 04bc0e3d..deb88120 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -1,23 +1,8 @@ import logging from fastapi import FastAPI import motor.motor_asyncio -from sqlmodel import Field, SQLModel, Session, create_engine +from sqlmodel import SQLModel, Session, create_engine -from src.db import ( - user_organizations, - users, - roles, - organization_settings, - organizations, - courses, - course_authors, - chapters, - activities, - course_chapters, - chapter_activities, - collections, - blocks, -) engine = create_engine( "postgresql://learnhouse:learnhouse@db:5432/learnhouse", echo=True diff --git a/apps/api/src/db/activities.py b/apps/api/src/db/activities.py index 29d460cc..27c06bac 100644 --- a/apps/api/src/db/activities.py +++ b/apps/api/src/db/activities.py @@ -1,7 +1,6 @@ -from typing import Literal, Optional -from click import Option +from typing import Optional from sqlalchemy import JSON, Column -from sqlmodel import Field, Session, SQLModel, create_engine, select +from sqlmodel import Field, SQLModel from enum import Enum diff --git a/apps/api/src/db/chapter_activities.py b/apps/api/src/db/chapter_activities.py index 1e31b7c4..b567e0de 100644 --- a/apps/api/src/db/chapter_activities.py +++ b/apps/api/src/db/chapter_activities.py @@ -1,6 +1,5 @@ from typing import Optional from sqlmodel import Field, SQLModel -from enum import Enum class ChapterActivity(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) diff --git a/apps/api/src/db/chapters.py b/apps/api/src/db/chapters.py index 7b80f564..ce5c19d1 100644 --- a/apps/api/src/db/chapters.py +++ b/apps/api/src/db/chapters.py @@ -1,7 +1,7 @@ from typing import List, Optional from pydantic import BaseModel from sqlmodel import Field, SQLModel -from src.db.activities import Activity, ActivityRead, ActivityUpdate +from src.db.activities import ActivityRead class ChapterBase(SQLModel): diff --git a/apps/api/src/db/course_chapters.py b/apps/api/src/db/course_chapters.py index 600eef0c..1d9f0990 100644 --- a/apps/api/src/db/course_chapters.py +++ b/apps/api/src/db/course_chapters.py @@ -1,6 +1,5 @@ from typing import Optional from sqlmodel import Field, SQLModel -from enum import Enum class CourseChapter(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) diff --git a/apps/api/src/db/install.py b/apps/api/src/db/install.py new file mode 100644 index 00000000..92624b17 --- /dev/null +++ b/apps/api/src/db/install.py @@ -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 diff --git a/apps/api/src/db/roles.py b/apps/api/src/db/roles.py index 10f1e917..70d52637 100644 --- a/apps/api/src/db/roles.py +++ b/apps/api/src/db/roles.py @@ -1,19 +1,46 @@ from enum import Enum -from typing import Optional +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" - TYPE_ORGANIZATION_API_TOKEN = "TYPE_ORGANIZATION_API_TOKEN" - TYPE_GLOBAL = "TYPE_GLOBAL" + 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: dict = Field(default={}, sa_column=Column(JSON)) + rights: Optional[Union[Rights,dict]] = Field(default={}, sa_column=Column(JSON)) class Role(RoleBase, table=True): @@ -26,11 +53,12 @@ class Role(RoleBase, table=True): class RoleCreate(RoleBase): - org_id: int = Field(default=None, foreign_key="organization.id") + 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[dict] = Field(default={}, sa_column=Column(JSON)) + rights: Optional[Union[Rights,dict]] = Field(default={}, sa_column=Column(JSON)) diff --git a/apps/api/src/db/trails.py b/apps/api/src/db/trails.py index 94c19f31..c830bbd2 100644 --- a/apps/api/src/db/trails.py +++ b/apps/api/src/db/trails.py @@ -1,10 +1,8 @@ from typing import Optional from pydantic import BaseModel from sqlmodel import Field, SQLModel -from enum import Enum -from src.db.trail_runs import TrailRun, TrailRunRead +from src.db.trail_runs import TrailRunRead -from src.db.trail_steps import TrailStep class TrailBase(SQLModel): diff --git a/apps/api/src/routers/auth.py b/apps/api/src/routers/auth.py index 589782e0..307ad70b 100644 --- a/apps/api/src/routers/auth.py +++ b/apps/api/src/routers/auth.py @@ -5,7 +5,6 @@ from src.db.users import UserRead from src.core.events.database import get_db_session from config.config import get_learnhouse_config from src.security.auth import AuthJWT, authenticate_user -from src.services.users.users import PublicUser router = APIRouter() diff --git a/apps/api/src/routers/courses/activities.py b/apps/api/src/routers/courses/activities.py index 6b62128c..963d14f9 100644 --- a/apps/api/src/routers/courses/activities.py +++ b/apps/api/src/routers/courses/activities.py @@ -3,7 +3,6 @@ from src.db.activities import ActivityCreate, ActivityUpdate from src.db.users import PublicUser from src.core.events.database import get_db_session from src.services.courses.activities.activities import ( - Activity, create_activity, get_activity, get_activities, diff --git a/apps/api/src/routers/courses/collections.py b/apps/api/src/routers/courses/collections.py index d691401f..99ab1d2d 100644 --- a/apps/api/src/routers/courses/collections.py +++ b/apps/api/src/routers/courses/collections.py @@ -4,7 +4,6 @@ from src.db.collections import CollectionCreate, CollectionUpdate from src.security.auth import get_current_user from src.services.users.users import PublicUser from src.services.courses.collections import ( - Collection, create_collection, get_collection, get_collections, diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index b496f376..f9bc33ea 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -2,7 +2,7 @@ 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 Course, CourseCreate, CourseUpdate +from src.db.courses import CourseCreate, CourseUpdate from src.security.auth import get_current_user from src.services.courses.courses import ( diff --git a/apps/api/src/routers/dev.py b/apps/api/src/routers/dev.py index 318154e8..4fd3d2d5 100644 --- a/apps/api/src/routers/dev.py +++ b/apps/api/src/routers/dev.py @@ -1,6 +1,5 @@ -from fastapi import APIRouter, Request +from fastapi import APIRouter from config.config import get_learnhouse_config -from src.services.dev.mocks.initial import create_initial_data router = APIRouter() @@ -10,9 +9,3 @@ router = APIRouter() async def config(): config = get_learnhouse_config() return config.dict() - - -@router.get("/mock/initial") -async def initial_data(request: Request): - await create_initial_data(request) - return {"Message": "Initial data created 🤖"} diff --git a/apps/api/src/routers/install/install.py b/apps/api/src/routers/install/install.py index 52f6d5f2..dae19f3a 100644 --- a/apps/api/src/routers/install/install.py +++ b/apps/api/src/routers/install/install.py @@ -1,70 +1,71 @@ -from fastapi import APIRouter, Request +from fastapi import APIRouter, Depends, Request +from src.core.events.database import get_db_session +from src.db.organizations import OrganizationCreate +from src.db.users import UserCreate from src.services.install.install import ( create_install_instance, - create_sample_data, get_latest_install_instance, install_create_organization, install_create_organization_user, install_default_elements, update_install_instance, ) -from src.services.orgs.schemas.orgs import Organization -from src.services.users.schemas.users import UserWithPassword router = APIRouter() @router.post("/start") -async def api_create_install_instance(request: Request, data: dict): +async def api_create_install_instance( + request: Request, data: dict, db_session=Depends(get_db_session), +): # create install - install = await create_install_instance(request, data) + install = await create_install_instance(request, data, db_session) return install @router.get("/latest") -async def api_get_latest_install_instance(request: Request): +async def api_get_latest_install_instance(request: Request, db_session=Depends(get_db_session),): # get latest created install - install = await get_latest_install_instance(request) + install = await get_latest_install_instance(request, db_session=db_session) return install @router.post("/default_elements") -async def api_install_def_elements(request: Request): - elements = await install_default_elements(request, {}) +async def api_install_def_elements(request: Request, db_session=Depends(get_db_session),): + elements = await install_default_elements(request, {}, db_session) return elements @router.post("/org") -async def api_install_org(request: Request, org: Organization): - organization = await install_create_organization(request, org) +async def api_install_org( + request: Request, org: OrganizationCreate, db_session=Depends(get_db_session), +): + organization = await install_create_organization(request, org, db_session) return organization @router.post("/user") -async def api_install_user(request: Request, data: UserWithPassword, org_slug: str): - user = await install_create_organization_user(request, data, org_slug) +async def api_install_user( + request: Request, data: UserCreate, org_slug: str, db_session=Depends(get_db_session), +): + user = await install_create_organization_user(request, data, org_slug, db_session) return user -@router.post("/sample") -async def api_install_user_sample(request: Request, username: str, org_slug: str): - sample = await create_sample_data(org_slug, username, request) - - return sample - - @router.post("/update") -async def api_update_install_instance(request: Request, data: dict, step: int): +async def api_update_install_instance( + request: Request, data: dict, step: int, db_session=Depends(get_db_session), +): request.app.db["installs"] # get latest created install - install = await update_install_instance(request, data, step) + install = await update_install_instance(request, data, step, db_session) return install diff --git a/apps/api/src/routers/roles.py b/apps/api/src/routers/roles.py index 61573d81..114cc255 100644 --- a/apps/api/src/routers/roles.py +++ b/apps/api/src/routers/roles.py @@ -3,7 +3,6 @@ from sqlmodel import Session from src.core.events.database import get_db_session from src.db.roles import RoleCreate, RoleUpdate from src.security.auth import get_current_user -from src.services.roles.schemas.roles import Role from src.services.roles.roles import create_role, delete_role, read_role, update_role from src.services.users.schemas.users import PublicUser diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index 3035d84b..9531ca4c 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -1,19 +1,9 @@ -import stat -from typing import Literal -from pydantic import BaseModel from sqlmodel import Session, select -from src.db.chapters import Chapter from src.db.organizations import Organization -from src import db from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate from src.db.chapter_activities import ChapterActivity -from src.security.rbac.rbac import ( - authorization_verify_based_on_roles, - authorization_verify_if_element_is_public, - authorization_verify_if_user_is_anon, -) -from src.db.users import AnonymousUser, PublicUser -from fastapi import HTTPException, status, Request +from src.db.users import PublicUser +from fastapi import HTTPException, Request from uuid import uuid4 from datetime import datetime diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index 9011c8a5..5dae7d02 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -9,7 +9,6 @@ from src.db.activities import ( from src.db.chapter_activities import ChapterActivity from src.db.course_chapters import CourseChapter from src.db.users import PublicUser -from src.security.rbac.rbac import authorization_verify_based_on_roles from src.services.courses.activities.uploads.pdfs import upload_pdf from fastapi import HTTPException, status, UploadFile, Request from uuid import uuid4 diff --git a/apps/api/src/services/courses/activities/video.py b/apps/api/src/services/courses/activities/video.py index 924a68ad..70babb80 100644 --- a/apps/api/src/services/courses/activities/video.py +++ b/apps/api/src/services/courses/activities/video.py @@ -7,9 +7,6 @@ from src.db.activities import Activity, ActivityRead, ActivitySubTypeEnum, Activ from src.db.chapter_activities import ChapterActivity from src.db.course_chapters import CourseChapter from src.db.users import PublicUser -from src.security.rbac.rbac import ( - authorization_verify_based_on_roles, -) from src.services.courses.activities.uploads.videos import upload_video from fastapi import HTTPException, status, UploadFile, Request from uuid import uuid4 diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 09403480..f8ad98d4 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -2,7 +2,6 @@ from datetime import datetime from typing import List from uuid import uuid4 from sqlmodel import Session, select -from src import db from src.db.course_chapters import CourseChapter from src.db.activities import Activity, ActivityRead from src.db.chapter_activities import ChapterActivity @@ -14,13 +13,6 @@ from src.db.chapters import ( ChapterUpdateOrder, DepreceatedChaptersRead, ) -from src.security.auth import non_public_endpoint -from src.security.rbac.rbac import ( - authorization_verify_based_on_roles, - authorization_verify_based_on_roles_and_authorship, - authorization_verify_if_element_is_public, - authorization_verify_if_user_is_anon, -) from src.services.courses.courses import Course from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index 38269e53..73e911b0 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -1,8 +1,6 @@ from datetime import datetime -from gc import collect -from typing import List, Literal +from typing import List from uuid import uuid4 -from pydantic import BaseModel from sqlmodel import Session, select from src.db.collections import ( Collection, @@ -12,10 +10,6 @@ from src.db.collections import ( ) from src.db.collections_courses import CollectionCourse from src.db.courses import Course -from src.security.rbac.rbac import ( - authorization_verify_based_on_roles_and_authorship, - authorization_verify_if_user_is_anon, -) from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request from typing import List diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 95a90426..cc69524f 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -1,13 +1,11 @@ import json -from typing import List, Literal, Optional +from typing import Literal from uuid import uuid4 -from pydantic import BaseModel from sqlmodel import Session, select from src.db.course_authors import CourseAuthor, CourseAuthorshipEnum from src.db.users import PublicUser, AnonymousUser from src.db.courses import Course, CourseCreate, CourseRead, CourseUpdate 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, diff --git a/apps/api/src/services/dev/mocks/__init__.py b/apps/api/src/services/dev/mocks/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/src/services/dev/mocks/initial.py b/apps/api/src/services/dev/mocks/initial.py deleted file mode 100644 index 86973158..00000000 --- a/apps/api/src/services/dev/mocks/initial.py +++ /dev/null @@ -1,213 +0,0 @@ -import os -import requests -from datetime import datetime -from uuid import uuid4 -from fastapi import Request -from src.services.users.schemas.users import UserInDB -from src.security.security import security_hash_password -from src.services.courses.activities.activities import Activity, create_activity -from src.services.users.users import PublicUser - -from src.services.orgs.orgs import Organization, create_org -from src.services.roles.schemas.roles import Permission, Elements, RoleInDB -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) diff --git a/apps/api/src/services/install/install.py b/apps/api/src/services/install/install.py index 152ef745..9c08c3fb 100644 --- a/apps/api/src/services/install/install.py +++ b/apps/api/src/services/install/install.py @@ -1,33 +1,15 @@ from datetime import datetime from uuid import uuid4 -from fastapi import HTTPException, Request, status -from pydantic import BaseModel -import requests +from fastapi import HTTPException, Request +from sqlalchemy import desc +from sqlmodel import Session, select +from src.db.install import Install +from src.db.organizations import Organization, OrganizationCreate +from src.db.roles import Permission, Rights, Role, RoleTypeEnum +from src.db.user_organizations import UserOrganization +from src.db.users import User, UserCreate, UserRead from config.config import get_learnhouse_config from src.security.security import security_hash_password -from src.services.courses.activities.activities import Activity, create_activity - -from src.services.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(): @@ -42,37 +24,29 @@ async def isInstallModeEnabled(): ) -async def create_install_instance(request: Request, data: dict): - installs = request.app.db["installs"] +async def create_install_instance(request: Request, data: dict, db_session: Session): + install = Install.from_orm(data) - # get install_id - install_id = str(f"install_{uuid4()}") - created_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - updated_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - step = 1 + # complete install instance + install.install_uuid = str(f"install_{uuid4()}") + install.update_date = str(datetime.now()) + install.creation_date = str(datetime.now()) - # create install - install = InstallInstance( - install_id=install_id, - created_date=created_date, - updated_date=updated_date, - step=step, - data=data, - ) + # insert install instance + db_session.add(install) - # insert install - installs.insert_one(install.dict()) + # commit changes + db_session.commit() + + # refresh install instance + db_session.refresh(install) return install -async def get_latest_install_instance(request: Request): - installs = request.app.db["installs"] - - # get latest created install instance using find_one - install = await installs.find_one( - sort=[("created_date", -1)], limit=1, projection={"_id": 0} - ) +async def get_latest_install_instance(request: Request, db_session: Session): + statement = select(Install).order_by(desc(Install.creation_date)).limit(1) + install = db_session.exec(statement).first() if install is None: raise HTTPException( @@ -80,37 +54,31 @@ async def get_latest_install_instance(request: Request): detail="No install instance found", ) - else: - install = InstallInstance(**install) - - return install + return install -async def update_install_instance(request: Request, data: dict, step: int): - installs = request.app.db["installs"] - - # get latest created install - install = await installs.find_one( - sort=[("created_date", -1)], limit=1, projection={"_id": 0} - ) +async def update_install_instance( + request: Request, data: dict, step: int, db_session: Session +): + statement = select(Install).order_by(desc(Install.creation_date)).limit(1) + install = db_session.exec(statement).first() if install is None: - return None - - else: - # update install - install["data"] = data - install["step"] = step - install["updated_date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # update install - await installs.update_one( - {"install_id": install["install_id"]}, {"$set": install} + raise HTTPException( + status_code=404, + detail="No install instance found", ) - install = InstallInstance(**install) + install.step = step + install.data = data - return install + # commit changes + db_session.commit() + + # refresh install instance + db_session.refresh(install) + + return install ############################################################################################################ @@ -119,24 +87,34 @@ async def update_install_instance(request: Request, data: dict, step: int): # Install Default roles -async def install_default_elements(request: Request, data: dict): - roles = request.app.db["roles"] +async def install_default_elements(request: Request, data: dict, db_session: Session): + # remove all default roles + statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL) + roles = db_session.exec(statement).all() - # check if default roles ADMIN_ROLE and USER_ROLE already exist - admin_role = await roles.find_one({"role_id": "role_admin"}) - user_role = await roles.find_one({"role_id": "role_member"}) + for role in roles: + db_session.delete(role) - if admin_role is not None or user_role is not None: + db_session.commit() + + # Check if default roles already exist + statement = select(Role).where(Role.role_type == RoleTypeEnum.TYPE_GLOBAL) + roles = db_session.exec(statement).all() + + if roles and len(roles) == 3: raise HTTPException( - status_code=400, + status_code=409, detail="Default roles already exist", ) - # get default roles - ADMIN_ROLE = RoleInDB( - name="Admin Role", - description="This role grants all permissions to the user", - elements=Elements( + # Create default roles + role_global_admin = Role( + name="Admin", + description="Standard Admin Role", + id=1, + role_type=RoleTypeEnum.TYPE_GLOBAL, + role_uuid="role_global_admin", + rights=Rights( courses=Permission( action_create=True, action_read=True, @@ -149,12 +127,6 @@ async def install_default_elements(request: Request, data: dict): action_update=True, action_delete=True, ), - houses=Permission( - action_create=True, - action_read=True, - action_update=True, - action_delete=True, - ), collections=Permission( action_create=True, action_read=True, @@ -180,16 +152,65 @@ async def install_default_elements(request: Request, data: dict): action_delete=True, ), ), - org_id="*", - role_id="role_admin", - created_at=str(datetime.now()), - updated_at=str(datetime.now()), + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) - USER_ROLE = RoleInDB( - name="Member Role", - description="This role grants read-only permissions to the user", - elements=Elements( + role_global_maintainer = Role( + name="Maintainer", + description="Standard Maintainer Role", + id=2, + role_type=RoleTypeEnum.TYPE_GLOBAL, + role_uuid="role_global_maintainer", + rights=Rights( + courses=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + users=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + collections=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + organizations=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + coursechapters=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + activities=Permission( + action_create=True, + action_read=True, + action_update=True, + action_delete=True, + ), + ), + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + + role_global_user = Role( + name="User", + description="Standard User Role", + role_type=RoleTypeEnum.TYPE_GLOBAL, + role_uuid="role_global_user", + id=3, + rights=Rights( courses=Permission( action_create=False, action_read=True, @@ -197,13 +218,7 @@ async def install_default_elements(request: Request, data: dict): action_delete=False, ), users=Permission( - action_create=False, - action_read=True, - action_update=False, - action_delete=False, - ), - houses=Permission( - action_create=False, + action_create=True, action_read=True, action_update=False, action_delete=False, @@ -233,185 +248,122 @@ async def install_default_elements(request: Request, data: dict): action_delete=False, ), ), - org_id="*", - role_id="role_member", - created_at=str(datetime.now()), - updated_at=str(datetime.now()), + creation_date=str(datetime.now()), + update_date=str(datetime.now()), ) - try: - # insert default roles - await roles.insert_many([USER_ROLE.dict(), ADMIN_ROLE.dict()]) - return True + # Serialize rights to JSON + role_global_admin.rights = role_global_admin.rights.dict() # type: ignore + role_global_maintainer.rights = role_global_maintainer.rights.dict() # type: ignore + role_global_user.rights = role_global_user.rights.dict() # type: ignore - except Exception: - raise HTTPException( - status_code=400, - detail="Error while inserting default roles", - ) + # Insert roles in DB + db_session.add(role_global_admin) + db_session.add(role_global_maintainer) + db_session.add(role_global_user) + + # commit changes + db_session.commit() + + # refresh roles + db_session.refresh(role_global_admin) + + return True # Organization creation async def install_create_organization( - request: Request, - org_object: Organization, + request: Request, org_object: OrganizationCreate, db_session: Session ): - orgs = request.app.db["organizations"] - request.app.db["users"] + org = Organization.from_orm(org_object) - # find if org already exists using name + # Complete the org object + org.org_uuid = f"org_{uuid4()}" + org.creation_date = str(datetime.now()) + org.update_date = str(datetime.now()) - isOrgAvailable = await orgs.find_one({"slug": org_object.slug.lower()}) + db_session.add(org) + db_session.commit() + db_session.refresh(org) - if isOrgAvailable: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="Organization slug already exists", - ) - - # generate org_id with uuid4 - org_id = str(f"org_{uuid4()}") - - org = OrganizationInDB(org_id=org_id, **org_object.dict()) - - org_in_db = await orgs.insert_one(org.dict()) - - if not org_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) - - return org.dict() + return org async def install_create_organization_user( - request: Request, user_object: UserWithPassword, org_slug: str + request: Request, user_object: UserCreate, org_slug: str, db_session: Session ): - users = request.app.db["users"] + user = User.from_orm(user_object) - isUsernameAvailable = await users.find_one({"username": user_object.username}) - isEmailAvailable = await users.find_one({"email": user_object.email}) + # Complete the user object + user.user_uuid = f"user_{uuid4()}" + user.password = await security_hash_password(user_object.password) + user.email_verified = False + user.creation_date = str(datetime.now()) + user.update_date = str(datetime.now()) - if isUsernameAvailable: + # Verifications + + # Check if Organization exists + statement = select(Organization).where(Organization.slug == org_slug) + org = db_session.exec(statement) + + if not org.first(): raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Username already exists" + status_code=400, + detail="Organization does not exist", ) - if isEmailAvailable: + # Username + statement = select(User).where(User.username == user.username) + result = db_session.exec(statement) + + if result.first(): raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Email already exists" + status_code=400, + detail="Username already exists", ) - # Generate user_id with uuid4 - user_id = str(f"user_{uuid4()}") + # Email + statement = select(User).where(User.email == user.email) + result = db_session.exec(statement) - # Set the username & hash the password - user_object.username = user_object.username.lower() - user_object.password = await security_hash_password(user_object.password) - - # Get org_id from org_slug - orgs = request.app.db["organizations"] - - # Check if the org exists - isOrgExists = await orgs.find_one({"slug": org_slug}) - - # If the org does not exist, raise an error - if not isOrgExists: + if result.first(): raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail="You are trying to create a user in an organization that does not exist", + status_code=400, + detail="Email already exists", ) - org_id = isOrgExists["org_id"] + # Exclude unset values + user_data = user.dict(exclude_unset=True) + for key, value in user_data.items(): + setattr(user, key, value) - # Create initial orgs list with the org_id passed in - orgs = [UserOrganization(org_id=org_id, org_role="owner")] + # Add user to database + db_session.add(user) + db_session.commit() + db_session.refresh(user) - # Give role - roles = [UserRolesInOrganization(role_id="role_admin", org_id=org_id)] + - # Create the user - user = UserInDB( - user_id=user_id, + # get org id + statement = select(Organization).where(Organization.slug == org_slug) + org = db_session.exec(statement) + org = org.first() + org_id = org.id if org else 0 + + # Link user and organization + user_organization = UserOrganization( + user_id=user.id if user.id else 0, + org_id=org_id or 0, + role_id=1, creation_date=str(datetime.now()), update_date=str(datetime.now()), - orgs=orgs, - roles=roles, - **user_object.dict(), ) - # Insert the user into the database - await users.insert_one(user.dict()) + db_session.add(user_organization) + db_session.commit() + db_session.refresh(user_organization) - return User(**user.dict()) + user = UserRead.from_orm(user) - -async def create_sample_data(org_slug: str, username: str, request: Request): - Faker(["en_US"]) - fake_multilang = Faker( - ["en_US", "de_DE", "ja_JP", "es_ES", "it_IT", "pt_BR", "ar_PS"] - ) - - users = request.app.db["users"] - orgs = request.app.db["organizations"] - user = await users.find_one({"username": username}) - org = await orgs.find_one({"slug": org_slug.lower()}) - user_id = user["user_id"] - org_id = org["org_id"] - - current_user = PublicUser(**user) - - for i in range(0, 5): - # get image in BinaryIO format from unsplash and save it to disk - image = requests.get("https://source.unsplash.com/random/800x600") - with open("thumbnail.jpg", "wb") as f: - f.write(image.content) - - # course_id = f"course_{uuid4()}" - # course = CourseInDB( - # name=fake_multilang.unique.sentence(), - # description=fake_multilang.unique.text(), - # mini_description=fake_multilang.unique.text(), - # thumbnail="thumbnail", - # org_id=org_id, - # learnings=[fake_multilang.unique.sentence() for i in range(0, 5)], - # public=True, - # chapters=[], - # course_id=course_id, - # creationDate=str(datetime.now()), - # updateDate=str(datetime.now()), - # authors=[user_id], - # chapters_content=[], - # ) - - # courses = request.app.db["courses"] - - # course = CourseInDB(**course.dict()) - # await courses.insert_one(course.dict()) - - # # create chapters - # for i in range(0, 5): - # coursechapter = CourseChapter( - # name=fake_multilang.unique.sentence(), - # description=fake_multilang.unique.text(), - # activities=[], - # ) - # coursechapter = await create_coursechapter( - # request, coursechapter, course_id, current_user - # ) - # if coursechapter: - # # create activities - # for i in range(0, 5): - # activity = Activity( - # name=fake_multilang.unique.sentence(), - # type="dynamic", - # content={}, - # ) - # activity = await create_activity( - # request, - # activity, - # org_id, - # coursechapter["coursechapter_id"], - # current_user, - # ) + return user diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index 87f4e9f7..54182f0c 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -1,10 +1,7 @@ from datetime import datetime -import json -from operator import or_ -from typing import Literal from uuid import uuid4 from sqlmodel import Session, select -from src.db.users import UserRead, PublicUser +from src.db.users import PublicUser from src.db.user_organizations import UserOrganization from src.db.organizations import ( Organization, @@ -12,10 +9,6 @@ from src.db.organizations import ( OrganizationRead, OrganizationUpdate, ) -from src.security.rbac.rbac import ( - authorization_verify_based_on_roles, - authorization_verify_if_user_is_anon, -) from src.services.orgs.logos import upload_org_logo from fastapi import HTTPException, UploadFile, status, Request diff --git a/apps/api/src/services/roles/roles.py b/apps/api/src/services/roles/roles.py index 8e3aff3a..3da8f089 100644 --- a/apps/api/src/services/roles/roles.py +++ b/apps/api/src/services/roles/roles.py @@ -1,9 +1,8 @@ from uuid import uuid4 from sqlmodel import Session, select from src.db.roles import Role, RoleCreate, RoleUpdate -from src.security.rbac.rbac import authorization_verify_if_user_is_anon from src.services.users.schemas.users import PublicUser -from fastapi import HTTPException, status, Request +from fastapi import HTTPException, Request from datetime import datetime diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 92a5ae03..7c4d5b58 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -1,12 +1,9 @@ from datetime import datetime -import stat -from typing import List, Literal, Optional from uuid import uuid4 from fastapi import HTTPException, Request, status -from pydantic import BaseModel from sqlmodel import Session, select from src.db.courses import Course -from src.db.trail_runs import TrailRun, TrailRunCreate, TrailRunRead +from src.db.trail_runs import TrailRun, TrailRunRead from src.db.trail_steps import TrailStep from src.db.trails import Trail, TrailCreate, TrailRead from src.db.users import PublicUser diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index 553a632a..55bbab6d 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -77,7 +77,7 @@ async def create_user( user_organization = UserOrganization( user_id=user.id if user.id else 0, org_id=int(org_id), - role_id=1, + role_id=3, creation_date=str(datetime.now()), update_date=str(datetime.now()), ) From 0595bfdb3f7958ef130948e3441c72e0a24008a2 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 27 Nov 2023 22:16:22 +0100 Subject: [PATCH 16/39] feat: init roles + authorship detection --- apps/api/src/db/course_authors.py | 18 --- apps/api/src/db/resource_authors.py | 20 +++ apps/api/src/db/users.py | 2 +- apps/api/src/routers/roles.py | 2 +- apps/api/src/security/auth.py | 5 +- apps/api/src/security/rbac/rbac.py | 123 ++++++++++-------- apps/api/src/services/courses/courses.py | 27 ++-- apps/api/src/services/orgs/orgs.py | 5 +- .../api/src/services/orgs/schemas/__init__.py | 0 apps/api/src/services/orgs/schemas/orgs.py | 28 ---- apps/api/src/services/roles/roles.py | 2 +- .../src/services/roles/schemas/__init__.py | 0 apps/api/src/services/roles/schemas/roles.py | 41 ------ .../src/services/users/schemas/__init__.py | 0 apps/api/src/services/users/schemas/users.py | 70 ---------- apps/api/src/services/users/users.py | 2 +- 16 files changed, 109 insertions(+), 236 deletions(-) delete mode 100644 apps/api/src/db/course_authors.py create mode 100644 apps/api/src/db/resource_authors.py delete mode 100644 apps/api/src/services/orgs/schemas/__init__.py delete mode 100644 apps/api/src/services/orgs/schemas/orgs.py delete mode 100644 apps/api/src/services/roles/schemas/__init__.py delete mode 100644 apps/api/src/services/roles/schemas/roles.py delete mode 100644 apps/api/src/services/users/schemas/__init__.py delete mode 100644 apps/api/src/services/users/schemas/users.py diff --git a/apps/api/src/db/course_authors.py b/apps/api/src/db/course_authors.py deleted file mode 100644 index 1edaa47f..00000000 --- a/apps/api/src/db/course_authors.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Optional -from sqlmodel import Field, SQLModel -from enum import Enum - - -class CourseAuthorshipEnum(str, Enum): - CREATOR = "CREATOR" - MAINTAINER = "MAINTAINER" - REPORTER = "REPORTER" - - -class CourseAuthor(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - course_id: int = Field(default=None, foreign_key="course.id") - user_id: int = Field(default=None, foreign_key="user.id") - authorship: CourseAuthorshipEnum = CourseAuthorshipEnum.CREATOR - creation_date: str - update_date: str diff --git a/apps/api/src/db/resource_authors.py b/apps/api/src/db/resource_authors.py new file mode 100644 index 00000000..3befb846 --- /dev/null +++ b/apps/api/src/db/resource_authors.py @@ -0,0 +1,20 @@ +from enum import Enum +from typing import Optional, Union +from pydantic import BaseModel +from sqlalchemy import JSON, Column +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 = "" diff --git a/apps/api/src/db/users.py b/apps/api/src/db/users.py index 99f19fd7..8042ad3f 100644 --- a/apps/api/src/db/users.py +++ b/apps/api/src/db/users.py @@ -36,7 +36,7 @@ class UserRead(UserBase): class PublicUser(UserRead): pass -class AnonymousUser(UserRead): +class AnonymousUser(SQLModel): id: str = "anonymous" username: str = "anonymous" diff --git a/apps/api/src/routers/roles.py b/apps/api/src/routers/roles.py index 114cc255..6fcb600d 100644 --- a/apps/api/src/routers/roles.py +++ b/apps/api/src/routers/roles.py @@ -4,7 +4,7 @@ from src.core.events.database import get_db_session from src.db.roles import RoleCreate, RoleUpdate from src.security.auth import get_current_user from src.services.roles.roles import create_role, delete_role, read_role, update_role -from src.services.users.schemas.users import PublicUser +from src.db.users import PublicUser router = APIRouter() diff --git a/apps/api/src/security/auth.py b/apps/api/src/security/auth.py index d6691864..3ddad99b 100644 --- a/apps/api/src/security/auth.py +++ b/apps/api/src/security/auth.py @@ -1,6 +1,6 @@ from sqlmodel import Session from src.core.events.database import get_db_session -from src.db.users import User, UserRead +from src.db.users import AnonymousUser, User, UserRead from src.services.users.users import security_get_user from config.config import get_learnhouse_config from pydantic import BaseModel @@ -9,7 +9,6 @@ from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from datetime import datetime, timedelta from src.services.dev.dev import isDevModeEnabled -from src.services.users.schemas.users import AnonymousUser, PublicUser from src.services.users.users import security_verify_password from src.security.security import ALGORITHM, SECRET_KEY from fastapi_jwt_auth import AuthJWT @@ -100,6 +99,6 @@ async def get_current_user( return AnonymousUser() -async def non_public_endpoint(current_user: PublicUser): +async def non_public_endpoint(current_user: UserRead | AnonymousUser): if isinstance(current_user, AnonymousUser): raise HTTPException(status_code=401, detail="Not authenticated") diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index ea2e1838..898ea594 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -1,17 +1,24 @@ +from math import e from typing import Literal from fastapi import HTTPException, status, Request -from src.security.rbac.utils import check_element_type, get_id_identifier_of_element -from src.services.roles.schemas.roles import RoleInDB -from src.services.users.schemas.users import UserRolesInOrganization +from sqlalchemy import func, null, or_ +from sqlmodel import Session, select +from src.db.collections import Collection +from src.db.courses import Course +from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum +from src.db.roles import Role +from src.db.user_organizations import UserOrganization +from src.security.rbac.utils import check_element_type async def authorization_verify_if_element_is_public( request, - element_id: str, + element_uuid: str, user_id: str, action: Literal["read"], + db_session: Session, ): - element_nature = await check_element_type(element_id) + element_nature = await check_element_type(element_uuid) # Verifies if the element is public if ( @@ -20,10 +27,12 @@ async def authorization_verify_if_element_is_public( and user_id == "anonymous" ): if element_nature == "courses": - courses = request.app.db["courses"] - course = await courses.find_one({"course_id": element_id}) + statement = select(Course).where( + Course.public == True, Course.course_uuid == element_uuid + ) + course = db_session.exec(statement).first() - if course["public"]: + if course: return True else: raise HTTPException( @@ -32,10 +41,12 @@ async def authorization_verify_if_element_is_public( ) if element_nature == "collections": - collections = request.app.db["collections"] - collection = await collections.find_one({"collection_id": element_id}) + statement = select(Collection).where( + Collection.public == True, Collection.collection_uuid == element_uuid + ) + collection = db_session.exec(statement).first() - if collection["public"]: + if collection: return True else: raise HTTPException( @@ -53,67 +64,65 @@ async def authorization_verify_if_user_is_author( request, user_id: str, action: Literal["read", "update", "delete", "create"], - element_id: str, + element_uuid: str, + db_session: Session, ): if action == "update" or "delete" or "read": - element_nature = await check_element_type(element_id) - elements = request.app.db[element_nature] - element_identifier = await get_id_identifier_of_element(element_id) - element = await elements.find_one({element_identifier: element_id}) - if user_id in element["authors"]: - return True + statement = select(ResourceAuthor).where( + ResourceAuthor.resource_uuid == element_uuid + ) + resource_author = db_session.exec(statement).first() + + if resource_author: + if resource_author.user_id == user_id: + if (resource_author.authorship == ResourceAuthorshipEnum.CREATOR) or ( + resource_author.authorship == ResourceAuthorshipEnum.MAINTAINER + ): + return True + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="User rights (authorship) : You don't have the right to perform this action", + ) else: - return False - else: - return False + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Wrong action (create)", + ) async def authorization_verify_based_on_roles( request: Request, user_id: str, action: Literal["read", "update", "delete", "create"], - roles_list: list[UserRolesInOrganization], - element_id: str, + element_uuid: str, + db_session: Session, ): - element_type = await check_element_type(element_id) - element = request.app.db[element_type] - roles = request.app.db["roles"] + element_type = await check_element_type(element_uuid) - # Get the element - element_identifier = await get_id_identifier_of_element(element_id) - element = await element.find_one({element_identifier: element_id}) + # Get user roles bound to an organization and standard roles + statement = ( + select(Role) + .join(UserOrganization) + .where(UserOrganization.user_id == user_id) + .where((UserOrganization.id == Role.org_id) | (UserOrganization.id == null)) + ) - # 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) + user_roles_in_organization_and_standard_roles = db_session.exec(statement).all() - async def checkRoles(): - # Check Roles - for role in roles: - role = RoleInDB(**role) - if role.elements[element_type][f"action_{action}"] is True: + # 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 - - 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", + detail="User rights (roles) : You don't have the right to perform this action", ) @@ -121,15 +130,15 @@ async def authorization_verify_based_on_roles_and_authorship( request: Request, user_id: str, action: Literal["read", "update", "delete", "create"], - roles_list: list[UserRolesInOrganization], - element_id: str, + element_uuid: str, + db_session: Session, ): isAuthor = await authorization_verify_if_user_is_author( - request, user_id, action, element_id + request, user_id, action, element_uuid, db_session ) isRole = await authorization_verify_based_on_roles( - request, user_id, action, roles_list, element_id + request, user_id, action, element_uuid, db_session ) if isAuthor or isRole: diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index cc69524f..afe4b614 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -1,8 +1,10 @@ import json +import resource from typing import Literal from uuid import uuid4 from sqlmodel import Session, select -from src.db.course_authors import CourseAuthor, CourseAuthorshipEnum +from src import db +from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum from src.db.users import PublicUser, AnonymousUser from src.db.courses import Course, CourseCreate, CourseRead, CourseUpdate from src.security.rbac.rbac import ( @@ -60,7 +62,7 @@ async def create_course( # Complete course object course.org_id = course.org_id - course.course_uuid = str(uuid4()) + course.course_uuid = str(f"course_{uuid4()}") course.creation_date = str(datetime.now()) course.update_date = str(datetime.now()) @@ -76,18 +78,18 @@ async def create_course( db_session.refresh(course) # Make the user the creator of the course - course_author = CourseAuthor( - course_id=course.id if course.id else 0, + resource_author = ResourceAuthor( + resource_uuid=course.course_uuid, user_id=current_user.id, - authorship=CourseAuthorshipEnum.CREATOR, + authorship=ResourceAuthorshipEnum.CREATOR, creation_date=str(datetime.now()), update_date=str(datetime.now()), ) # Insert course author - db_session.add(course_author) + db_session.add(resource_author) db_session.commit() - db_session.refresh(course_author) + db_session.refresh(resource_author) return CourseRead.from_orm(course) @@ -241,26 +243,23 @@ async def verify_rights( course_id: str, current_user: PublicUser | AnonymousUser, action: Literal["create", "read", "update", "delete"], + db_session: Session, ): if action == "read": if current_user.id == "anonymous": await authorization_verify_if_element_is_public( - request, course_id, str(current_user.id), action + request, course_id, str(current_user.id), action, db_session ) else: - users = request.app.db["users"] - user = await users.find_one({"user_id": str(current_user.id)}) await authorization_verify_based_on_roles_and_authorship( request, str(current_user.id), action, - user["roles"], course_id, + db_session, ) else: - users = request.app.db["users"] - user = await users.find_one({"user_id": str(current_user.id)}) await authorization_verify_if_user_is_anon(str(current_user.id)) @@ -268,8 +267,8 @@ async def verify_rights( request, str(current_user.id), action, - user["roles"], course_id, + db_session, ) diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index 54182f0c..40cfdccc 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -214,7 +214,10 @@ async def get_orgs_by_user( statement = ( select(Organization) .join(UserOrganization) - .where(Organization.id == UserOrganization.org_id) + .where( + Organization.id == UserOrganization.org_id, + UserOrganization.user_id == user_id, + ) ) result = db_session.exec(statement) diff --git a/apps/api/src/services/orgs/schemas/__init__.py b/apps/api/src/services/orgs/schemas/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/src/services/orgs/schemas/orgs.py b/apps/api/src/services/orgs/schemas/orgs.py deleted file mode 100644 index 56531357..00000000 --- a/apps/api/src/services/orgs/schemas/orgs.py +++ /dev/null @@ -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) diff --git a/apps/api/src/services/roles/roles.py b/apps/api/src/services/roles/roles.py index 3da8f089..d7ac88e8 100644 --- a/apps/api/src/services/roles/roles.py +++ b/apps/api/src/services/roles/roles.py @@ -1,7 +1,7 @@ from uuid import uuid4 from sqlmodel import Session, select +from src.db.users import PublicUser from src.db.roles import Role, RoleCreate, RoleUpdate -from src.services.users.schemas.users import PublicUser from fastapi import HTTPException, Request from datetime import datetime diff --git a/apps/api/src/services/roles/schemas/__init__.py b/apps/api/src/services/roles/schemas/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/src/services/roles/schemas/roles.py b/apps/api/src/services/roles/schemas/roles.py deleted file mode 100644 index 18b0c34c..00000000 --- a/apps/api/src/services/roles/schemas/roles.py +++ /dev/null @@ -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 - diff --git a/apps/api/src/services/users/schemas/__init__.py b/apps/api/src/services/users/schemas/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/api/src/services/users/schemas/users.py b/apps/api/src/services/users/schemas/users.py deleted file mode 100644 index 2dea0e97..00000000 --- a/apps/api/src/services/users/schemas/users.py +++ /dev/null @@ -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 diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index 55bbab6d..29979fab 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -4,6 +4,7 @@ from fastapi import HTTPException, Request, status from sqlmodel import Session, select from src.db.organizations import Organization from src.db.users import ( + PublicUser, User, UserCreate, UserRead, @@ -12,7 +13,6 @@ from src.db.users import ( ) from src.db.user_organizations import UserOrganization from src.security.security import security_hash_password, security_verify_password -from src.services.users.schemas.users import PublicUser async def create_user( From 7738316200daea51b254733b77f2e2fd86cbbdd9 Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 28 Nov 2023 20:25:14 +0100 Subject: [PATCH 17/39] feat: implement authorization with roles --- apps/api/src/db/activities.py | 1 - apps/api/src/db/collections.py | 2 +- apps/api/src/db/courses.py | 15 +- apps/api/src/db/users.py | 4 +- apps/api/src/routers/courses/courses.py | 11 +- apps/api/src/routers/orgs.py | 6 +- apps/api/src/routers/users.py | 23 ++- apps/api/src/security/auth.py | 4 +- apps/api/src/security/rbac/rbac.py | 44 ++--- .../services/courses/activities/activities.py | 58 +++++- .../src/services/courses/activities/pdf.py | 38 +++- .../src/services/courses/activities/video.py | 51 +++++- apps/api/src/services/courses/chapters.py | 65 ++++++- apps/api/src/services/courses/collections.py | 77 ++++++-- apps/api/src/services/courses/courses.py | 169 +++++++++++------- apps/api/src/services/orgs/orgs.py | 73 +++++++- apps/api/src/services/roles/roles.py | 40 ++++- apps/api/src/services/trail/trail.py | 4 +- apps/api/src/services/users/users.py | 81 ++++++++- 19 files changed, 596 insertions(+), 170 deletions(-) diff --git a/apps/api/src/db/activities.py b/apps/api/src/db/activities.py index 27c06bac..e5ae9a53 100644 --- a/apps/api/src/db/activities.py +++ b/apps/api/src/db/activities.py @@ -46,7 +46,6 @@ class Activity(ActivityBase, table=True): class ActivityCreate(ActivityBase): - order: int org_id: int = Field(default=None, foreign_key="organization.id") course_id: int = Field(default=None, foreign_key="course.id") chapter_id: int diff --git a/apps/api/src/db/collections.py b/apps/api/src/db/collections.py index 26085f2e..f3cbd850 100644 --- a/apps/api/src/db/collections.py +++ b/apps/api/src/db/collections.py @@ -17,7 +17,7 @@ class Collection(CollectionBase, table=True): class CollectionCreate(CollectionBase): - courses: list + courses: list[int] org_id: int = Field(default=None, foreign_key="organization.id") pass diff --git a/apps/api/src/db/courses.py b/apps/api/src/db/courses.py index 5774b28a..4d29251c 100644 --- a/apps/api/src/db/courses.py +++ b/apps/api/src/db/courses.py @@ -1,6 +1,6 @@ from typing import List, Optional from sqlmodel import Field, SQLModel - +from src.db.trails import TrailRead from src.db.chapters import ChapterRead @@ -39,6 +39,7 @@ class CourseUpdate(CourseBase): class CourseRead(CourseBase): id: int + org_id: int = Field(default=None, foreign_key="organization.id") course_uuid: str creation_date: str update_date: str @@ -53,3 +54,15 @@ class FullCourseRead(CourseBase): # Chapters, Activities chapters: List[ChapterRead] pass + + +class FullCourseReadWithTrail(CourseBase): + id: int + course_uuid: str + creation_date: str + update_date: str + # Chapters, Activities + chapters: List[ChapterRead] + # Trail + trail: TrailRead + pass diff --git a/apps/api/src/db/users.py b/apps/api/src/db/users.py index 8042ad3f..0c29799f 100644 --- a/apps/api/src/db/users.py +++ b/apps/api/src/db/users.py @@ -32,12 +32,14 @@ class UserUpdatePassword(SQLModel): class UserRead(UserBase): id: int + user_uuid: str class PublicUser(UserRead): pass class AnonymousUser(SQLModel): - id: str = "anonymous" + id: int = 0 + user_uuid: str = "user_anonymous" username: str = "anonymous" class User(UserBase, table=True): diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index f9bc33ea..335f3665 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -4,7 +4,6 @@ from src.core.events.database import get_db_session from src.db.users import PublicUser from src.db.courses import CourseCreate, CourseUpdate from src.security.auth import get_current_user - from src.services.courses.courses import ( create_course, get_course, @@ -46,9 +45,7 @@ async def api_create_course( learnings=learnings, tags=tags, ) - return await create_course( - request, course, current_user, db_session, thumbnail - ) + return await create_course(request, course, current_user, db_session, thumbnail) @router.put("/thumbnail/{course_id}") @@ -85,7 +82,7 @@ async def api_get_course( @router.get("/meta/{course_id}") async def api_get_course_meta( request: Request, - course_id: str, + course_id: int, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), ): @@ -109,7 +106,9 @@ async def api_get_course_by_orgslug( """ Get houses 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("/") diff --git a/apps/api/src/routers/orgs.py b/apps/api/src/routers/orgs.py index 9f8c0aa1..6406dfc4 100644 --- a/apps/api/src/routers/orgs.py +++ b/apps/api/src/routers/orgs.py @@ -41,7 +41,7 @@ async def api_get_org( """ Get single Org by ID """ - return await get_organization(request, org_id, db_session) + return await get_organization(request, org_id, db_session, current_user) @router.get("/slug/{org_slug}") @@ -54,7 +54,7 @@ async def api_get_org_by_slug( """ Get single Org by Slug """ - return await get_organization_by_slug(request, org_slug, db_session) + return await get_organization_by_slug(request, org_slug, db_session, current_user) @router.put("/{org_id}/logo") @@ -109,7 +109,7 @@ async def api_update_org( @router.delete("/{org_id}") async def api_delete_org( request: Request, - org_id: str, + org_id: int, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), ): diff --git a/apps/api/src/routers/users.py b/apps/api/src/routers/users.py index 82542c56..d82972da 100644 --- a/apps/api/src/routers/users.py +++ b/apps/api/src/routers/users.py @@ -1,9 +1,11 @@ from fastapi import APIRouter, Depends, Request from sqlmodel import Session +from src.security.rbac.rbac import authorization_verify_based_on_roles, authorization_verify_if_element_is_public, authorization_verify_if_user_is_author from src.security.auth import get_current_user from src.core.events.database import get_db_session from src.db.users import ( + PublicUser, User, UserCreate, UserRead, @@ -37,13 +39,14 @@ async def api_create_user_with_orgid( *, request: Request, db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), user_object: UserCreate, org_id: int, ) -> UserRead: """ Create User with Org ID """ - return await create_user(request, db_session, None, user_object, org_id) + return await create_user(request, db_session, current_user, user_object, org_id) @router.post("/", response_model=UserRead, tags=["users"]) @@ -51,12 +54,13 @@ 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 User """ - return await create_user_without_org(request, db_session, None, user_object) + return await create_user_without_org(request, db_session, current_user, user_object) @router.get("/user_id/{user_id}", response_model=UserRead, tags=["users"]) @@ -64,12 +68,13 @@ 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: """ Get User by ID """ - return await read_user_by_id(request, db_session, None, user_id) + return await read_user_by_id(request, db_session, current_user, user_id) @router.get("/user_uuid/{user_uuid}", response_model=UserRead, tags=["users"]) @@ -77,12 +82,13 @@ 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: """ Get User by UUID """ - return await read_user_by_uuid(request, db_session, None, user_uuid) + return await read_user_by_uuid(request, db_session, current_user, user_uuid) @router.put("/", response_model=UserRead, tags=["users"]) @@ -90,12 +96,13 @@ async def api_update_user( *, request: Request, db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), user_object: UserUpdate, ) -> UserRead: """ Update User """ - return await update_user(request, db_session, None, user_object) + return await update_user(request, db_session, current_user, user_object) @router.put("/change_password/", response_model=UserRead, tags=["users"]) @@ -103,12 +110,13 @@ async def api_update_user_password( *, request: Request, db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), form: UserUpdatePassword, ) -> UserRead: """ Update User Password """ - return await update_user_password(request, db_session, None, form) + return await update_user_password(request, db_session, current_user, form) @router.delete("/user_id/{user_id}", tags=["users"]) @@ -116,9 +124,10 @@ 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, None, user_id) + return await delete_user_by_id(request, db_session, current_user, user_id) diff --git a/apps/api/src/security/auth.py b/apps/api/src/security/auth.py index 3ddad99b..4d6d290a 100644 --- a/apps/api/src/security/auth.py +++ b/apps/api/src/security/auth.py @@ -1,6 +1,6 @@ from sqlmodel import Session from src.core.events.database import get_db_session -from src.db.users import AnonymousUser, User, UserRead +from src.db.users import AnonymousUser, PublicUser, User, UserRead from src.services.users.users import security_get_user from config.config import get_learnhouse_config from pydantic import BaseModel @@ -94,7 +94,7 @@ async def get_current_user( user = await security_get_user(request, db_session, email=token_data.username) # type: ignore # treated as an email if user is None: raise credentials_exception - return UserRead(**user.dict()) + return PublicUser(**user.dict()) else: return AnonymousUser() diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index 898ea594..0dba06d9 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -11,27 +11,21 @@ from src.db.user_organizations import UserOrganization from src.security.rbac.utils import check_element_type +# Tested and working async def authorization_verify_if_element_is_public( request, element_uuid: str, - user_id: str, action: Literal["read"], db_session: Session, ): element_nature = await check_element_type(element_uuid) - # Verifies if the element is public - if ( - element_nature == ("courses" or "collections") - and action == "read" - and user_id == "anonymous" - ): + if element_nature == ("courses" or "collections") and action == "read": if element_nature == "courses": statement = select(Course).where( Course.public == True, Course.course_uuid == element_uuid ) course = db_session.exec(statement).first() - if course: return True else: @@ -60,9 +54,10 @@ async def authorization_verify_if_element_is_public( ) +# Tested and working async def authorization_verify_if_user_is_author( request, - user_id: str, + user_id: int, action: Literal["read", "update", "delete", "create"], element_uuid: str, db_session: Session, @@ -74,26 +69,23 @@ async def authorization_verify_if_user_is_author( resource_author = db_session.exec(statement).first() if resource_author: - if resource_author.user_id == user_id: + 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: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="User rights (authorship) : You don't have the right to perform this action", - ) + return False else: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Wrong action (create)", - ) + return False +# Tested and working async def authorization_verify_based_on_roles( request: Request, - user_id: str, + user_id: int, action: Literal["read", "update", "delete", "create"], element_uuid: str, db_session: Session, @@ -104,8 +96,8 @@ async def authorization_verify_based_on_roles( statement = ( select(Role) .join(UserOrganization) + .where((UserOrganization.org_id == Role.org_id) | (Role.org_id == null())) .where(UserOrganization.user_id == user_id) - .where((UserOrganization.id == Role.org_id) | (UserOrganization.id == null)) ) user_roles_in_organization_and_standard_roles = db_session.exec(statement).all() @@ -120,15 +112,13 @@ async def authorization_verify_based_on_roles( else: return False else: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="User rights (roles) : You don't have the right to perform this action", - ) + return False +# Tested and working async def authorization_verify_based_on_roles_and_authorship( request: Request, - user_id: str, + user_id: int, action: Literal["read", "update", "delete", "create"], element_uuid: str, db_session: Session, @@ -150,8 +140,8 @@ async def authorization_verify_based_on_roles_and_authorship( ) -async def authorization_verify_if_user_is_anon(user_id: str): - if user_id == "anonymous": +async def authorization_verify_if_user_is_anon(user_id: int): + if user_id == 0: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You should be logged in to perform this action", diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index 9531ca4c..d2355b2f 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -1,8 +1,13 @@ +from typing import Literal 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.organizations import Organization from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate from src.db.chapter_activities import ChapterActivity -from src.db.users import PublicUser +from src.db.users import AnonymousUser, PublicUser from fastapi import HTTPException, Request from uuid import uuid4 from datetime import datetime @@ -16,7 +21,7 @@ from datetime import datetime async def create_activity( request: Request, activity_object: ActivityCreate, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): activity = Activity.from_orm(activity_object) @@ -31,6 +36,9 @@ async def create_activity( detail="Organization not found", ) + # RBAC check + await rbac_check(request, org.org_uuid, current_user, "create", db_session) + activity.activity_uuid = str(f"activity_{uuid4()}") activity.creation_date = str(datetime.now()) activity.update_date = str(datetime.now()) @@ -85,13 +93,16 @@ async def get_activity( detail="Activity not found", ) + # RBAC check + await rbac_check(request, activity.activity_uuid, current_user, "read", db_session) + return activity async def update_activity( request: Request, activity_object: ActivityUpdate, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): statement = select(Activity).where(Activity.id == activity_object.activity_id) @@ -103,6 +114,11 @@ async def update_activity( detail="Activity not found", ) + # RBAC check + await rbac_check( + request, activity.activity_uuid, current_user, "update", db_session + ) + del activity_object.activity_id # Update only the fields that were passed in @@ -120,7 +136,7 @@ async def update_activity( async def delete_activity( request: Request, activity_id: str, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): statement = select(Activity).where(Activity.id == activity_id) @@ -132,6 +148,11 @@ async def delete_activity( 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 @@ -159,7 +180,7 @@ async def delete_activity( async def get_activities( request: Request, coursechapter_id: str, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): statement = select(ChapterActivity).where( @@ -173,4 +194,31 @@ async def get_activities( detail="No activities found", ) + # RBAC check + await rbac_check(request, "activity_x", current_user, "read", db_session) + return activities + + +## 🔒 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 ## diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index 5dae7d02..5d728709 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -1,4 +1,9 @@ +from typing import Literal 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, @@ -8,7 +13,7 @@ from src.db.activities import ( ) from src.db.chapter_activities import ChapterActivity from src.db.course_chapters import CourseChapter -from src.db.users import PublicUser +from src.db.users import AnonymousUser, PublicUser from src.services.courses.activities.uploads.pdfs import upload_pdf from fastapi import HTTPException, status, UploadFile, Request from uuid import uuid4 @@ -19,10 +24,13 @@ async def create_documentpdf_activity( request: Request, name: str, chapter_id: str, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, pdf_file: UploadFile | None = None, ): + # RBAC check + await rbac_check(request, "activity_x", current_user, "create", db_session) + # get chapter_id statement = select(Chapter).where(Chapter.id == chapter_id) chapter = db_session.exec(statement).first() @@ -94,7 +102,7 @@ async def create_documentpdf_activity( # Add activity to chapter activity_chapter = ChapterActivity( chapter_id=(int(chapter_id)), - activity_id=activity.id is not None, + activity_id=activity.id, # type: ignore course_id=coursechapter.course_id, org_id=coursechapter.org_id, creation_date=str(datetime.now()), @@ -113,3 +121,27 @@ async def create_documentpdf_activity( db_session.refresh(activity_chapter) return ActivityRead.from_orm(activity) + + +## 🔒 RBAC Utils ## + + +async def rbac_check( + request: Request, + course_id: str, + current_user: PublicUser | AnonymousUser, + action: Literal["create", "read", "update", "delete"], + db_session: Session, +): + await authorization_verify_if_user_is_anon(current_user.id) + + await authorization_verify_based_on_roles_and_authorship( + request, + current_user.id, + action, + course_id, + db_session, + ) + + +## 🔒 RBAC Utils ## diff --git a/apps/api/src/services/courses/activities/video.py b/apps/api/src/services/courses/activities/video.py index 70babb80..a1d3beda 100644 --- a/apps/api/src/services/courses/activities/video.py +++ b/apps/api/src/services/courses/activities/video.py @@ -2,11 +2,20 @@ from typing import Literal 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.chapters import Chapter -from src.db.activities import Activity, ActivityRead, ActivitySubTypeEnum, ActivityTypeEnum +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 PublicUser +from src.db.users import AnonymousUser, PublicUser from src.services.courses.activities.uploads.videos import upload_video from fastapi import HTTPException, status, UploadFile, Request from uuid import uuid4 @@ -21,6 +30,9 @@ async def create_video_activity( db_session: Session, video_file: UploadFile | None = None, ): + # RBAC check + await rbac_check(request, "activity_x", current_user, "create", db_session) + # get chapter_id statement = select(Chapter).where(Chapter.id == chapter_id) chapter = db_session.exec(statement).first() @@ -95,8 +107,8 @@ async def create_video_activity( # update chapter chapter_activity_object = ChapterActivity( - chapter_id=coursechapter.id is not None, - activity_id=activity.id is not None, + chapter_id=chapter.id, # type: ignore + activity_id=activity.id, # type: ignore course_id=coursechapter.course_id, org_id=coursechapter.org_id, creation_date=str(datetime.now()), @@ -111,6 +123,7 @@ async def create_video_activity( return ActivityRead.from_orm(activity) + class ExternalVideo(BaseModel): name: str uri: str @@ -124,10 +137,13 @@ class ExternalVideoInDB(BaseModel): async def create_external_video_activity( request: Request, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, data: ExternalVideo, db_session: Session, ): + # RBAC check + await rbac_check(request, "activity_x", current_user, "create", db_session) + # get chapter_id statement = select(Chapter).where(Chapter.id == data.chapter_id) chapter = db_session.exec(statement).first() @@ -174,8 +190,8 @@ async def create_external_video_activity( # update chapter chapter_activity_object = ChapterActivity( - chapter_id=coursechapter.id is not None, - activity_id=activity.id is not None, + chapter_id=coursechapter.id, # type: ignore + activity_id=activity.id, # type: ignore creation_date=str(datetime.now()), update_date=str(datetime.now()), order=1, @@ -186,3 +202,24 @@ async def create_external_video_activity( 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 ## diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index f8ad98d4..1fa3aae9 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -1,7 +1,12 @@ from datetime import datetime -from typing import List +from typing import List, Literal from uuid import uuid4 from sqlmodel import Session, select +from src.db.users import AnonymousUser +from src.security.rbac.rbac import ( + authorization_verify_based_on_roles_and_authorship, + authorization_verify_if_user_is_anon, +) from src.db.course_chapters import CourseChapter from src.db.activities import Activity, ActivityRead from src.db.chapter_activities import ChapterActivity @@ -26,11 +31,14 @@ from fastapi import HTTPException, status, Request async def create_chapter( request: Request, chapter_object: ChapterCreate, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ) -> ChapterRead: chapter = Chapter.from_orm(chapter_object) + # 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()}" @@ -87,7 +95,7 @@ async def create_chapter( async def get_chapter( request: Request, chapter_id: int, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ) -> ChapterRead: statement = select(Chapter).where(Chapter.id == chapter_id) @@ -98,6 +106,9 @@ async def get_chapter( 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) @@ -119,7 +130,7 @@ async def get_chapter( async def update_chapter( request: Request, chapter_object: ChapterUpdate, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ) -> ChapterRead: statement = select(Chapter).where(Chapter.id == chapter_object.chapter_id) @@ -130,6 +141,9 @@ async def update_chapter( status_code=status.HTTP_409_CONFLICT, detail="Chapter does not exist" ) + # RBAC check + await rbac_check(request, chapter.chapter_uuid, current_user, "update", db_session) + # Update only the fields that were passed in for var, value in vars(chapter_object).items(): if value is not None: @@ -148,7 +162,7 @@ async def update_chapter( async def delete_chapter( request: Request, chapter_id: str, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): statement = select(Chapter).where(Chapter.id == chapter_id) @@ -159,6 +173,9 @@ async def delete_chapter( 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() @@ -173,15 +190,12 @@ async def delete_chapter( return {"detail": "chapter deleted"} -#################################################### -# Misc -#################################################### - 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]: @@ -195,6 +209,9 @@ async def get_course_chapters( 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 = ( @@ -233,6 +250,9 @@ async def get_depreceated_course_chapters( status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" ) + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + # Get chapters that are linked to his course and order them by order, using the order field in the CourseChapter table statement = ( select(Chapter) @@ -310,6 +330,9 @@ async def reorder_chapters_and_activities( status_code=status.HTTP_409_CONFLICT, detail="Course does not exist" ) + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + ########### # Chapters ########### @@ -469,3 +492,27 @@ async def reorder_chapters_and_activities( 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 ## diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index 73e911b0..73855956 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -1,7 +1,12 @@ from datetime import datetime -from typing import List +from typing import List, Literal from uuid import uuid4 from sqlmodel import Session, select +from src.db.users import AnonymousUser +from src.security.rbac.rbac import ( + authorization_verify_based_on_roles_and_authorship, + authorization_verify_if_user_is_anon, +) from src.db.collections import ( Collection, CollectionCreate, @@ -37,6 +42,11 @@ async def get_collection( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" ) + # RBAC check + await rbac_check( + request, collection.collection_uuid, current_user, "read", db_session + ) + # get courses in collection statement = ( select(Course) @@ -58,6 +68,9 @@ async def create_collection( ) -> CollectionRead: collection = Collection.from_orm(collection_object) + # RBAC check + await rbac_check(request, "collection_x", current_user, "create", db_session) + # Complete the collection object collection.collection_uuid = f"collection_{uuid4()}" collection.creation_date = str(datetime.now()) @@ -70,16 +83,17 @@ async def create_collection( db_session.refresh(collection) # Link courses to collection - for course in collection_object.courses: - collection_course = CollectionCourse( - collection_id=int(collection.id is not None), - course_id=int(course), - org_id=int(collection_object.org_id), - creation_date=str(datetime.now()), - update_date=str(datetime.now()), - ) - # Add collection_course to database - db_session.add(collection_course) + 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) @@ -113,6 +127,11 @@ async def update_collection( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist" ) + # RBAC check + await rbac_check( + request, collection.collection_uuid, current_user, "update", db_session + ) + courses = collection_object.courses del collection_object.collection_id @@ -142,7 +161,7 @@ async def update_collection( # Add new collection_courses for course in courses or []: collection_course = CollectionCourse( - collection_id=int(collection.id is not None), + collection_id=int(collection.id), # type: ignore course_id=int(course), org_id=int(collection.org_id), creation_date=str(datetime.now()), @@ -180,6 +199,11 @@ async def delete_collection( detail="Collection not found", ) + # RBAC check + await rbac_check( + request, collection.collection_uuid, current_user, "delete", db_session + ) + # delete collection from database db_session.delete(collection) db_session.commit() @@ -195,11 +219,14 @@ async def delete_collection( async def get_collections( request: Request, org_id: str, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, page: int = 1, limit: int = 10, ) -> List[CollectionRead]: + # 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) ) @@ -223,3 +250,27 @@ async def get_collections( collections_with_courses.append(collection) return collections_with_courses + + +## 🔒 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 ## diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index afe4b614..4da62ba5 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -1,12 +1,28 @@ +from calendar import c import json +from queue import Full import resource from typing import Literal from uuid import uuid4 from sqlmodel import Session, select +from src.db import chapters +from src.db.activities import Activity, ActivityRead +from src.db.chapter_activities import ChapterActivity +from src.db.chapters import Chapter, ChapterRead +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 import db from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum from src.db.users import PublicUser, AnonymousUser -from src.db.courses import Course, CourseCreate, CourseRead, CourseUpdate +from src.db.courses import ( + Course, + CourseCreate, + CourseRead, + CourseUpdate, + FullCourseReadWithTrail, +) from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship, authorization_verify_if_element_is_public, @@ -18,7 +34,10 @@ from datetime import datetime async def get_course( - request: Request, course_id: str, current_user: PublicUser, db_session: Session + request: Request, + course_id: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, ): statement = select(Course).where(Course.id == course_id) course = db_session.exec(statement).first() @@ -29,12 +48,21 @@ async def get_course( detail="Course not found", ) + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + return course async def get_course_meta( - request: Request, course_id: str, current_user: PublicUser, db_session: Session -): + request: Request, + course_id: int, + current_user: PublicUser | AnonymousUser, + db_session: Session, +) -> FullCourseReadWithTrail: + # Avoid circular import + from src.services.courses.chapters import get_course_chapters + course_statement = select(Course).where(Course.id == course_id) course = db_session.exec(course_statement).first() @@ -44,22 +72,40 @@ async def get_course_meta( detail="Course not found", ) - # todo : get course chapters - # todo : get course activities - # todo : get trail + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) - return course + course = CourseRead.from_orm(course) + + # 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 + ) + + trail = TrailRead.from_orm(trail) + + return FullCourseReadWithTrail( + **course.dict(), + chapters=chapters, + trail=trail, + ) async def create_course( request: Request, course_object: CourseCreate, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, thumbnail_file: UploadFile | None = None, ): course = Course.from_orm(course_object) + # RBAC check + await rbac_check(request, "course_x", current_user, "create", db_session) + # Complete course object course.org_id = course.org_id course.course_uuid = str(f"course_{uuid4()}") @@ -69,7 +115,9 @@ async def create_course( # Upload thumbnail if thumbnail_file and thumbnail_file.filename: name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" - await upload_thumbnail(thumbnail_file, name_in_disk, course_object.org_id, course.course_uuid) + await upload_thumbnail( + thumbnail_file, name_in_disk, course_object.org_id, course.course_uuid + ) course_object.thumbnail = name_in_disk # Insert course @@ -97,7 +145,7 @@ async def create_course( async def update_course_thumbnail( request: Request, course_id: str, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, thumbnail_file: UploadFile | None = None, ): @@ -112,6 +160,9 @@ async def update_course_thumbnail( detail="Course not found", ) + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + # Upload thumbnail if thumbnail_file and thumbnail_file.filename: name_in_disk = ( @@ -143,7 +194,7 @@ async def update_course_thumbnail( async def update_course( request: Request, course_object: CourseUpdate, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): statement = select(Course).where(Course.id == course_object.course_id) @@ -154,7 +205,10 @@ async def update_course( status_code=404, detail="Course not found", ) - + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + del course_object.course_id # Update only the fields that were passed in @@ -173,7 +227,10 @@ async def update_course( async def delete_course( - request: Request, course_id: str, current_user: PublicUser, db_session: Session + request: Request, + course_id: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, ): statement = select(Course).where(Course.id == course_id) course = db_session.exec(statement).first() @@ -184,92 +241,74 @@ async def delete_course( 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"} -#################################################### -# Misc -#################################################### - - async def get_courses_orgslug( request: Request, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, + org_slug: str, + db_session: Session, page: int = 1, limit: int = 10, - org_slug: str | None = None, ): - courses = request.app.db["courses"] - orgs = request.app.db["organizations"] + statement_public = ( + select(Course) + .join(Organization) + .where(Organization.slug == org_slug, Course.public == True) + ) + statement_all = ( + select(Course).join(Organization).where(Organization.slug == org_slug) + ) - # get org_id from slug - org = await orgs.find_one({"slug": org_slug}) - - if not org: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" - ) - - # show only public courses if user is not logged in - if current_user.id == "anonymous": - all_courses = ( - courses.find({"org_id": org["org_id"], "public": True}) - .sort("name", 1) - .skip(10 * (page - 1)) - .limit(limit) - ) + if current_user.id == 0: + statement = statement_public else: - all_courses = ( - courses.find({"org_id": org["org_id"]}) - .sort("name", 1) - .skip(10 * (page - 1)) - .limit(limit) - ) + # RBAC check + await authorization_verify_if_user_is_anon(current_user.id) - return [ - json.loads(json.dumps(course, default=str)) - for course in await all_courses.to_list(length=100) - ] + statement = statement_all + + courses = db_session.exec(statement) + + return courses -#### Security #################################################### +## 🔒 RBAC Utils ## -async def verify_rights( +async def rbac_check( request: Request, - course_id: str, + course_uuid: str, current_user: PublicUser | AnonymousUser, action: Literal["create", "read", "update", "delete"], db_session: Session, ): if action == "read": - if current_user.id == "anonymous": + if current_user.id == 0: # Anonymous user await authorization_verify_if_element_is_public( - request, course_id, str(current_user.id), action, db_session + request, course_uuid, action, db_session ) else: - await authorization_verify_based_on_roles_and_authorship( - request, - str(current_user.id), - action, - course_id, - db_session, + request, current_user.id, action, course_uuid, db_session ) else: - - await authorization_verify_if_user_is_anon(str(current_user.id)) + await authorization_verify_if_user_is_anon(current_user.id) await authorization_verify_based_on_roles_and_authorship( request, - str(current_user.id), + current_user.id, action, - course_id, + course_uuid, db_session, ) -#### Security #################################################### +## 🔒 RBAC Utils ## diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index 40cfdccc..6db9c550 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -1,7 +1,12 @@ from datetime import datetime +from typing import Literal from uuid import uuid4 from sqlmodel import Session, select -from src.db.users import PublicUser +from src.security.rbac.rbac import ( + authorization_verify_based_on_roles_and_authorship, + authorization_verify_if_user_is_anon, +) +from src.db.users import AnonymousUser, PublicUser from src.db.user_organizations import UserOrganization from src.db.organizations import ( Organization, @@ -13,7 +18,12 @@ from src.services.orgs.logos import upload_org_logo from fastapi import HTTPException, UploadFile, status, Request -async def get_organization(request: Request, org_id: str, db_session: Session): +async def get_organization( + request: Request, + org_id: str, + db_session: Session, + current_user: PublicUser | AnonymousUser, +): statement = select(Organization).where(Organization.id == org_id) result = db_session.exec(statement) @@ -25,11 +35,17 @@ async def get_organization(request: Request, org_id: str, db_session: Session): detail="Organization not found", ) + # RBAC check + await rbac_check(request, org.org_uuid, current_user, "read", db_session) + return org async def get_organization_by_slug( - request: Request, org_slug: str, db_session: Session + 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) @@ -42,13 +58,16 @@ async def get_organization_by_slug( detail="Organization not found", ) + # RBAC check + await rbac_check(request, org.org_uuid, current_user, "read", db_session) + return org async def create_org( request: Request, org_object: OrganizationCreate, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): statement = select(Organization).where(Organization.slug == org_object.slug) @@ -64,6 +83,9 @@ async def create_org( 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()) @@ -92,7 +114,7 @@ async def create_org( async def update_org( request: Request, org_object: OrganizationUpdate, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): statement = select(Organization).where(Organization.id == org_object.org_id) @@ -106,6 +128,9 @@ async def update_org( detail="Organization slug not found", ) + # RBAC check + await rbac_check(request, org.org_uuid, current_user, "update", db_session) + org = Organization.from_orm(org_object) # Verify if the new slug is already in use @@ -142,7 +167,7 @@ async def update_org_logo( request: Request, logo_file: UploadFile, org_id: str, - current_user: PublicUser, + current_user: PublicUser | AnonymousUser, db_session: Session, ): statement = select(Organization).where(Organization.id == org_id) @@ -156,6 +181,9 @@ async def update_org_logo( detail="Organization not found", ) + # RBAC check + await rbac_check(request, org.org_uuid, current_user, "update", db_session) + # Upload logo name_in_disk = await upload_org_logo(logo_file, org_id) @@ -173,7 +201,10 @@ async def update_org_logo( async def delete_org( - request: Request, org_id: str, current_user: PublicUser, db_session: Session + 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) @@ -186,6 +217,9 @@ async def delete_org( detail="Organization not found", ) + # RBAC check + await rbac_check(request, org.org_uuid, current_user, "delete", db_session) + db_session.delete(org) db_session.commit() @@ -224,3 +258,28 @@ async def get_orgs_by_user( orgs = result.all() return orgs + + +## 🔒 RBAC Utils ## + + +async def rbac_check( + request: Request, + org_id: str, + current_user: PublicUser | AnonymousUser, + action: Literal["create", "read", "update", "delete"], + db_session: Session, +): + # Organizations are readable by anyone + if action == "read": + return True + + else: + await authorization_verify_if_user_is_anon(current_user.id) + + await authorization_verify_based_on_roles_and_authorship( + request, current_user.id, action, org_id, db_session + ) + + +## 🔒 RBAC Utils ## diff --git a/apps/api/src/services/roles/roles.py b/apps/api/src/services/roles/roles.py index d7ac88e8..982d76f8 100644 --- a/apps/api/src/services/roles/roles.py +++ b/apps/api/src/services/roles/roles.py @@ -1,6 +1,12 @@ +from typing import Literal from uuid import uuid4 from sqlmodel import Session, select -from src.db.users import PublicUser +from src.security.rbac.rbac import ( + authorization_verify_based_on_roles_and_authorship, + authorization_verify_if_user_is_anon, + authorization_verify_if_user_is_author, +) +from src.db.users import AnonymousUser, PublicUser from src.db.roles import Role, RoleCreate, RoleUpdate from fastapi import HTTPException, Request from datetime import datetime @@ -14,6 +20,9 @@ async def create_role( ): role = Role.from_orm(role_object) + # RBAC check + await rbac_check(request, current_user, "create", "role_xxx", db_session) + # Complete the role object role.role_uuid = f"role_{uuid4()}" role.creation_date = str(datetime.now()) @@ -40,6 +49,9 @@ async def read_role( detail="Role not found", ) + # RBAC check + await rbac_check(request, current_user, "read", role.role_uuid, db_session) + return role @@ -60,6 +72,9 @@ async def update_role( detail="Role not found", ) + # RBAC check + await rbac_check(request, current_user, "update", role.role_uuid, db_session) + # Complete the role object role.update_date = str(datetime.now()) @@ -81,6 +96,9 @@ async def update_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) @@ -96,3 +114,23 @@ async def delete_role( db_session.commit() return "Role deleted" + + +## 🔒 RBAC Utils ## + + +async def rbac_check( + request: Request, + current_user: PublicUser | AnonymousUser, + action: Literal["create", "read", "update", "delete"], + role_uuid: str, + db_session: Session, +): + await authorization_verify_if_user_is_anon(current_user.id) + + await authorization_verify_based_on_roles_and_authorship( + request, current_user.id, action, role_uuid, db_session + ) + + +## 🔒 RBAC Utils ## diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 7c4d5b58..c79d4d0d 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -6,7 +6,7 @@ from src.db.courses import Course from src.db.trail_runs import TrailRun, TrailRunRead from src.db.trail_steps import TrailStep from src.db.trails import Trail, TrailCreate, TrailRead -from src.db.users import PublicUser +from src.db.users import AnonymousUser, PublicUser async def create_user_trail( @@ -80,7 +80,7 @@ async def get_user_trails( async def get_user_trail_with_orgid( - request: Request, user: PublicUser, org_id: int, db_session: Session + request: Request, user: PublicUser | AnonymousUser, org_id: int, db_session: Session ) -> TrailRead: statement = select(Trail).where(Trail.org_id == org_id, Trail.user_id == user.id) trail = db_session.exec(statement).first() diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index 29979fab..94fc594b 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -1,9 +1,17 @@ from datetime import datetime +from typing import Literal from uuid import uuid4 from fastapi import HTTPException, Request, status from sqlmodel import Session, select +from src import db +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.organizations import Organization from src.db.users import ( + AnonymousUser, PublicUser, User, UserCreate, @@ -18,12 +26,15 @@ from src.security.security import security_hash_password, security_verify_passwo async def create_user( request: Request, db_session: Session, - current_user: PublicUser | None, + current_user: PublicUser | AnonymousUser, user_object: UserCreate, org_id: int, ): user = User.from_orm(user_object) + # RBAC check + await rbac_check(request, current_user, "create", "user_x", db_session) + # Complete the user object user.user_uuid = f"user_{uuid4()}" user.password = await security_hash_password(user_object.password) @@ -94,11 +105,14 @@ async def create_user( async def create_user_without_org( request: Request, db_session: Session, - current_user: PublicUser | None, + current_user: PublicUser | AnonymousUser, user_object: UserCreate, ): user = User.from_orm(user_object) + # RBAC check + await rbac_check(request, current_user, "create", "user_x", db_session) + # Complete the user object user.user_uuid = f"user_{uuid4()}" user.password = await security_hash_password(user_object.password) @@ -146,7 +160,7 @@ async def create_user_without_org( async def update_user( request: Request, db_session: Session, - current_user: PublicUser | None, + current_user: PublicUser | AnonymousUser, user_object: UserUpdate, ): # Get user @@ -158,6 +172,9 @@ async def update_user( status_code=400, detail="User does not exist", ) + + # RBAC check + await rbac_check(request, current_user, "update", user.user_uuid, db_session) # Update user user_data = user_object.dict(exclude_unset=True) @@ -179,7 +196,7 @@ async def update_user( async def update_user_password( request: Request, db_session: Session, - current_user: PublicUser | None, + current_user: PublicUser | AnonymousUser, form: UserUpdatePassword, ): # Get user @@ -191,6 +208,9 @@ async def update_user_password( status_code=400, detail="User does not exist", ) + + # RBAC check + await rbac_check(request, current_user, "update", user.user_uuid, db_session) if not await security_verify_password(form.old_password, user.password): raise HTTPException( @@ -214,7 +234,7 @@ async def update_user_password( async def read_user_by_id( request: Request, db_session: Session, - current_user: PublicUser | None, + current_user: PublicUser | AnonymousUser, user_id: int, ): # Get user @@ -227,6 +247,9 @@ async def read_user_by_id( 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 @@ -235,11 +258,11 @@ async def read_user_by_id( async def read_user_by_uuid( request: Request, db_session: Session, - current_user: PublicUser | None, - uuid: str, + current_user: PublicUser | AnonymousUser, + user_uuid: str, ): # Get user - statement = select(User).where(User.user_uuid == uuid) + statement = select(User).where(User.user_uuid == user_uuid) user = db_session.exec(statement).first() if not user: @@ -248,6 +271,9 @@ async def read_user_by_uuid( 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 @@ -256,7 +282,7 @@ async def read_user_by_uuid( async def delete_user_by_id( request: Request, db_session: Session, - current_user: PublicUser | None, + current_user: PublicUser | AnonymousUser, user_id: int, ): # Get user @@ -269,6 +295,9 @@ async def delete_user_by_id( 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() @@ -293,3 +322,37 @@ async def security_get_user(request: Request, db_session: Session, email: str) - user = User(**user.dict()) return user + + +## 🔒 RBAC Utils ## + + +async def rbac_check( + request: Request, + 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: + res = 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, "read", action, db_session + ) + + +## 🔒 RBAC Utils ## From 71279a1294878dacdd49bad836abd0db56521a0e Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 28 Nov 2023 20:49:20 +0100 Subject: [PATCH 18/39] feat: add response models to endpoints --- apps/api/src/db/organizations.py | 2 ++ apps/api/src/db/roles.py | 14 ++++++-- apps/api/src/routers/blocks.py | 13 ++++---- apps/api/src/routers/courses/activities.py | 17 +++++----- apps/api/src/routers/courses/chapters.py | 13 +++++--- apps/api/src/routers/courses/collections.py | 11 ++++--- apps/api/src/routers/courses/courses.py | 17 +++++----- apps/api/src/routers/install/install.py | 32 ++++++++++++++----- apps/api/src/routers/orgs.py | 18 +++++++---- apps/api/src/routers/roles.py | 8 ++--- apps/api/src/routers/trail.py | 7 +--- apps/api/src/routers/users.py | 2 +- .../block_types/imageBlock/imageBlock.py | 7 +++- .../blocks/block_types/pdfBlock/pdfBlock.py | 6 +++- .../block_types/videoBlock/videoBlock.py | 8 +++-- .../services/courses/activities/activities.py | 8 ++++- apps/api/src/services/courses/courses.py | 8 +++++ apps/api/src/services/install/install.py | 8 ++++- apps/api/src/services/orgs/orgs.py | 9 +++++- apps/api/src/services/roles/roles.py | 8 ++++- 20 files changed, 148 insertions(+), 68 deletions(-) diff --git a/apps/api/src/db/organizations.py b/apps/api/src/db/organizations.py index 6c8c2a1b..9d3d1412 100644 --- a/apps/api/src/db/organizations.py +++ b/apps/api/src/db/organizations.py @@ -26,3 +26,5 @@ class OrganizationCreate(OrganizationBase): class OrganizationRead(OrganizationBase): id: int org_uuid: str + creation_date: str + update_date: str diff --git a/apps/api/src/db/roles.py b/apps/api/src/db/roles.py index 70d52637..09ff4206 100644 --- a/apps/api/src/db/roles.py +++ b/apps/api/src/db/roles.py @@ -40,7 +40,7 @@ class RoleTypeEnum(str, Enum): class RoleBase(SQLModel): name: str description: Optional[str] - rights: Optional[Union[Rights,dict]] = Field(default={}, sa_column=Column(JSON)) + rights: Optional[Union[Rights, dict]] = Field(default={}, sa_column=Column(JSON)) class Role(RoleBase, table=True): @@ -52,13 +52,21 @@ class Role(RoleBase, table=True): 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)) + rights: Optional[Union[Rights, dict]] = Field(default={}, sa_column=Column(JSON)) diff --git a/apps/api/src/routers/blocks.py b/apps/api/src/routers/blocks.py index f598e2c6..a30c6cd4 100644 --- a/apps/api/src/routers/blocks.py +++ b/apps/api/src/routers/blocks.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request +from src.db.blocks import BlockRead from src.core.events.database import get_db_session from src.security.auth import get_current_user from src.services.blocks.block_types.imageBlock.imageBlock import ( @@ -30,7 +31,7 @@ async def api_create_image_file_block( activity_id: str = Form(), db_session=Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> BlockRead: """ Create new image file """ @@ -43,7 +44,7 @@ async def api_get_image_file_block( block_uuid: str, db_session=Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> BlockRead: """ Get image file """ @@ -62,7 +63,7 @@ async def api_create_video_file_block( activity_id: str = Form(), db_session=Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> BlockRead: """ Create new video file """ @@ -75,7 +76,7 @@ async def api_get_video_file_block( block_uuid: str, db_session=Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> BlockRead: """ Get video file """ @@ -94,7 +95,7 @@ async def api_create_pdf_file_block( activity_id: str = Form(), db_session=Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> BlockRead: """ Create new pdf file """ @@ -107,7 +108,7 @@ async def api_get_pdf_file_block( block_uuid: str, db_session=Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> BlockRead: """ Get pdf file """ diff --git a/apps/api/src/routers/courses/activities.py b/apps/api/src/routers/courses/activities.py index 963d14f9..3b609b2d 100644 --- a/apps/api/src/routers/courses/activities.py +++ b/apps/api/src/routers/courses/activities.py @@ -1,5 +1,6 @@ +from typing import List from fastapi import APIRouter, Depends, UploadFile, Form, Request -from src.db.activities import ActivityCreate, ActivityUpdate +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 ( @@ -26,7 +27,7 @@ async def api_create_activity( activity_object: ActivityCreate, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> ActivityRead: """ Create new activity """ @@ -39,7 +40,7 @@ async def api_get_activity( activity_id: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> ActivityRead: """ Get single activity by activity_id """ @@ -54,7 +55,7 @@ async def api_get_activities( coursechapter_id: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> List[ActivityRead]: """ Get CourseChapter activities """ @@ -67,7 +68,7 @@ async def api_update_activity( activity_object: ActivityUpdate, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> ActivityRead: """ Update activity by activity_id """ @@ -98,7 +99,7 @@ async def api_create_video_activity( current_user: PublicUser = Depends(get_current_user), video_file: UploadFile | None = None, db_session=Depends(get_db_session), -): +) -> ActivityRead: """ Create new activity """ @@ -118,7 +119,7 @@ async def api_create_external_video_activity( external_video: ExternalVideo, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> ActivityRead: """ Create new activity """ @@ -135,7 +136,7 @@ async def api_create_documentpdf_activity( current_user: PublicUser = Depends(get_current_user), pdf_file: UploadFile | None = None, db_session=Depends(get_db_session), -): +) -> ActivityRead: """ Create new activity """ diff --git a/apps/api/src/routers/courses/chapters.py b/apps/api/src/routers/courses/chapters.py index 263a93d7..b4e4574b 100644 --- a/apps/api/src/routers/courses/chapters.py +++ b/apps/api/src/routers/courses/chapters.py @@ -60,7 +60,9 @@ async def api_get_chapter_meta( """ Get Chapters metadata """ - return await get_depreceated_course_chapters(request, course_id, current_user, db_session) + return await get_depreceated_course_chapters( + request, course_id, current_user, db_session + ) @router.put("/order/{course_id}") @@ -85,12 +87,15 @@ async def api_get_chapter_by( course_id: int, page: int, limit: int, + current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ) -> List[ChapterRead]: """ Get Course Chapters by page and limit """ - return await get_course_chapters(request, course_id, db_session, page, limit) + return await get_course_chapters( + request, course_id, db_session, current_user, page, limit + ) @router.put("/{coursechapter_id}") @@ -118,6 +123,4 @@ async def api_delete_coursechapter( Delete CourseChapters by ID """ - return await delete_chapter( - request, coursechapter_id, current_user, db_session - ) + return await delete_chapter(request, coursechapter_id, current_user, db_session) diff --git a/apps/api/src/routers/courses/collections.py b/apps/api/src/routers/courses/collections.py index 99ab1d2d..2b0fd572 100644 --- a/apps/api/src/routers/courses/collections.py +++ b/apps/api/src/routers/courses/collections.py @@ -1,6 +1,7 @@ +from typing import List from fastapi import APIRouter, Depends, Request from src.core.events.database import get_db_session -from src.db.collections import CollectionCreate, CollectionUpdate +from src.db.collections import CollectionCreate, CollectionRead, CollectionUpdate from src.security.auth import get_current_user from src.services.users.users import PublicUser from src.services.courses.collections import ( @@ -21,7 +22,7 @@ async def api_create_collection( collection_object: CollectionCreate, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> CollectionRead: """ Create new Collection """ @@ -34,7 +35,7 @@ async def api_get_collection( collection_id: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> CollectionRead: """ Get single collection by ID """ @@ -49,7 +50,7 @@ async def api_get_collections_by( org_id: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> List[CollectionRead]: """ Get collections by page and limit """ @@ -62,7 +63,7 @@ async def api_update_collection( collection_object: CollectionUpdate, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -): +) -> CollectionRead: """ Update collection by ID """ diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index 335f3665..5914647d 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -1,8 +1,9 @@ +from typing import List from fastapi import APIRouter, Depends, UploadFile, Form, Request from sqlmodel import Session from src.core.events.database import get_db_session from src.db.users import PublicUser -from src.db.courses import CourseCreate, CourseUpdate +from src.db.courses import CourseCreate, CourseRead, CourseUpdate, FullCourseReadWithTrail from src.security.auth import get_current_user from src.services.courses.courses import ( create_course, @@ -31,7 +32,7 @@ async def api_create_course( current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), thumbnail: UploadFile | None = None, -): +) -> CourseRead: """ Create new Course """ @@ -55,7 +56,7 @@ async def api_create_course_thumbnail( thumbnail: UploadFile | None = None, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> CourseRead: """ Update new Course Thumbnail """ @@ -70,7 +71,7 @@ async def api_get_course( course_id: str, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> CourseRead: """ Get single Course by course_id """ @@ -85,7 +86,7 @@ async def api_get_course_meta( course_id: int, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> FullCourseReadWithTrail: """ Get single Course Metadata (chapters, activities) by course_id """ @@ -102,9 +103,9 @@ async def api_get_course_by_orgslug( 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, org_slug, db_session, page, limit @@ -117,7 +118,7 @@ async def api_update_course( course_object: CourseUpdate, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), -): +) -> CourseRead: """ Update Course by course_id """ diff --git a/apps/api/src/routers/install/install.py b/apps/api/src/routers/install/install.py index dae19f3a..38bebb69 100644 --- a/apps/api/src/routers/install/install.py +++ b/apps/api/src/routers/install/install.py @@ -1,4 +1,5 @@ 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 @@ -18,8 +19,10 @@ router = APIRouter() @router.post("/start") async def api_create_install_instance( - request: Request, data: dict, db_session=Depends(get_db_session), -): + request: Request, + data: dict, + db_session=Depends(get_db_session), +) -> InstallRead: # create install install = await create_install_instance(request, data, db_session) @@ -27,7 +30,9 @@ async def api_create_install_instance( @router.get("/latest") -async def api_get_latest_install_instance(request: Request, db_session=Depends(get_db_session),): +async def api_get_latest_install_instance( + request: Request, db_session=Depends(get_db_session) +) -> InstallRead: # get latest created install install = await get_latest_install_instance(request, db_session=db_session) @@ -35,7 +40,10 @@ async def api_get_latest_install_instance(request: Request, db_session=Depends(g @router.post("/default_elements") -async def api_install_def_elements(request: Request, db_session=Depends(get_db_session),): +async def api_install_def_elements( + request: Request, + db_session=Depends(get_db_session), +): elements = await install_default_elements(request, {}, db_session) return elements @@ -43,7 +51,9 @@ async def api_install_def_elements(request: Request, db_session=Depends(get_db_s @router.post("/org") async def api_install_org( - request: Request, org: OrganizationCreate, db_session=Depends(get_db_session), + request: Request, + org: OrganizationCreate, + db_session=Depends(get_db_session), ): organization = await install_create_organization(request, org, db_session) @@ -52,7 +62,10 @@ async def api_install_org( @router.post("/user") async def api_install_user( - request: Request, data: UserCreate, org_slug: str, db_session=Depends(get_db_session), + 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) @@ -61,8 +74,11 @@ async def api_install_user( @router.post("/update") async def api_update_install_instance( - request: Request, data: dict, step: int, db_session=Depends(get_db_session), -): + request: Request, + data: dict, + step: int, + db_session=Depends(get_db_session), +) -> InstallRead: request.app.db["installs"] # get latest created install diff --git a/apps/api/src/routers/orgs.py b/apps/api/src/routers/orgs.py index 6406dfc4..0f29efb1 100644 --- a/apps/api/src/routers/orgs.py +++ b/apps/api/src/routers/orgs.py @@ -1,7 +1,13 @@ +from typing import List from fastapi import APIRouter, Depends, Request, UploadFile from sqlmodel import Session from src.db.users import PublicUser -from src.db.organizations import OrganizationCreate, OrganizationUpdate +from src.db.organizations import ( + Organization, + OrganizationCreate, + OrganizationRead, + OrganizationUpdate, +) from src.core.events.database import get_db_session from src.security.auth import get_current_user from src.services.orgs.orgs import ( @@ -24,7 +30,7 @@ async def api_create_org( org_object: OrganizationCreate, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +) -> OrganizationRead: """ Create new organization """ @@ -37,7 +43,7 @@ async def api_get_org( org_id: str, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +) -> OrganizationRead: """ Get single Org by ID """ @@ -50,7 +56,7 @@ async def api_get_org_by_slug( org_slug: str, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +) -> OrganizationRead: """ Get single Org by Slug """ @@ -84,7 +90,7 @@ async def api_user_orgs( 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 """ @@ -99,7 +105,7 @@ async def api_update_org( org_object: OrganizationUpdate, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +) -> OrganizationRead: """ Update Org by ID """ diff --git a/apps/api/src/routers/roles.py b/apps/api/src/routers/roles.py index 6fcb600d..ef9350e0 100644 --- a/apps/api/src/routers/roles.py +++ b/apps/api/src/routers/roles.py @@ -1,7 +1,7 @@ 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, RoleUpdate +from src.db.roles import RoleCreate, RoleRead, RoleUpdate from src.security.auth import get_current_user from src.services.roles.roles import create_role, delete_role, read_role, update_role from src.db.users import PublicUser @@ -16,7 +16,7 @@ async def api_create_role( role_object: RoleCreate, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +)-> RoleRead: """ Create new role """ @@ -29,7 +29,7 @@ async def api_get_role( role_id: str, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +)-> RoleRead: """ Get single role by role_id """ @@ -42,7 +42,7 @@ async def api_update_role( role_object: RoleUpdate, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), -): +)-> RoleRead: """ Update role by role_id """ diff --git a/apps/api/src/routers/trail.py b/apps/api/src/routers/trail.py index 0cbcaeff..7e935486 100644 --- a/apps/api/src/routers/trail.py +++ b/apps/api/src/routers/trail.py @@ -38,9 +38,7 @@ async def api_get_user_trail( """ Get a user trails """ - return await get_user_trails( - request, user=user, db_session=db_session - ) + return await get_user_trails(request, user=user, db_session=db_session) @router.get("/org_slug/{org_id}/trail") @@ -58,9 +56,6 @@ async def api_get_trail_by_org_id( ) -# Courses in trail - - @router.post("/add_course/{course_id}") async def api_add_course_to_trail( request: Request, diff --git a/apps/api/src/routers/users.py b/apps/api/src/routers/users.py index d82972da..181898a2 100644 --- a/apps/api/src/routers/users.py +++ b/apps/api/src/routers/users.py @@ -126,7 +126,7 @@ async def api_delete_user( db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), user_id: int, -): +) : """ Delete User """ diff --git a/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py b/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py index f9f3eabf..ebabade3 100644 --- a/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py +++ b/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py @@ -3,7 +3,7 @@ from uuid import uuid4 from fastapi import HTTPException, status, UploadFile, Request from sqlmodel import Session, select from src.db.activities import Activity -from src.db.blocks import Block, BlockTypeEnum +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 @@ -65,6 +65,8 @@ async def create_image_block( db_session.commit() db_session.refresh(block) + block = BlockRead.from_orm(block) + return block @@ -75,6 +77,9 @@ async def get_image_block( block = db_session.exec(statement).first() if block: + + block = BlockRead.from_orm(block) + return block else: raise HTTPException( diff --git a/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py b/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py index 07fea4ca..708497e2 100644 --- a/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py +++ b/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py @@ -3,7 +3,7 @@ from uuid import uuid4 from fastapi import HTTPException, status, UploadFile, Request from sqlmodel import Session, select from src.db.activities import Activity -from src.db.blocks import Block, BlockTypeEnum +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 @@ -66,6 +66,8 @@ async def create_pdf_block( db_session.commit() db_session.refresh(block) + block = BlockRead.from_orm(block) + return block @@ -80,4 +82,6 @@ async def get_pdf_block( status_code=status.HTTP_404_NOT_FOUND, detail="Video file does not exist" ) + block = BlockRead.from_orm(block) + return block diff --git a/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py b/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py index 2af8c79b..53006497 100644 --- a/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py +++ b/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py @@ -3,7 +3,7 @@ from uuid import uuid4 from fastapi import HTTPException, status, UploadFile, Request from sqlmodel import Session, select from src.db.activities import Activity -from src.db.blocks import Block, BlockTypeEnum +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 @@ -66,6 +66,8 @@ async def create_video_block( db_session.commit() db_session.refresh(block) + block = BlockRead.from_orm(block) + return block @@ -79,5 +81,7 @@ async def get_video_block( raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Video file does not exist" ) - + + block = BlockRead.from_orm(block) + return block diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index d2355b2f..5eeb7656 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -96,6 +96,8 @@ async def get_activity( # RBAC check await rbac_check(request, activity.activity_uuid, current_user, "read", db_session) + activity = ActivityRead.from_orm(activity) + return activity @@ -130,6 +132,8 @@ async def update_activity( db_session.commit() db_session.refresh(activity) + activity = ActivityRead.from_orm(activity) + return activity @@ -182,7 +186,7 @@ async def get_activities( coursechapter_id: str, current_user: PublicUser | AnonymousUser, db_session: Session, -): +) -> list[ActivityRead]: statement = select(ChapterActivity).where( ChapterActivity.chapter_id == coursechapter_id ) @@ -197,6 +201,8 @@ async def get_activities( # RBAC check await rbac_check(request, "activity_x", current_user, "read", db_session) + activities = [ActivityRead.from_orm(activity) for activity in activities] + return activities diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 4da62ba5..19c8e63e 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -51,6 +51,8 @@ async def get_course( # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) + course = CourseRead.from_orm(course) + return course @@ -188,6 +190,8 @@ async def update_course_thumbnail( db_session.commit() db_session.refresh(course) + course = CourseRead.from_orm(course) + return course @@ -223,6 +227,8 @@ async def update_course( db_session.commit() db_session.refresh(course) + course = CourseRead.from_orm(course) + return course @@ -277,6 +283,8 @@ async def get_courses_orgslug( courses = db_session.exec(statement) + courses = [CourseRead.from_orm(course) for course in courses] + return courses diff --git a/apps/api/src/services/install/install.py b/apps/api/src/services/install/install.py index 9c08c3fb..b4538b28 100644 --- a/apps/api/src/services/install/install.py +++ b/apps/api/src/services/install/install.py @@ -3,7 +3,7 @@ from uuid import uuid4 from fastapi import HTTPException, Request from sqlalchemy import desc from sqlmodel import Session, select -from src.db.install import Install +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 @@ -41,6 +41,8 @@ async def create_install_instance(request: Request, data: dict, db_session: Sess # refresh install instance db_session.refresh(install) + install = InstallRead.from_orm(install) + return install @@ -53,6 +55,8 @@ async def get_latest_install_instance(request: Request, db_session: Session): status_code=404, detail="No install instance found", ) + + install = InstallRead.from_orm(install) return install @@ -78,6 +82,8 @@ async def update_install_instance( # refresh install instance db_session.refresh(install) + install = InstallRead.from_orm(install) + return install diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index 6db9c550..b1701802 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -38,6 +38,8 @@ async def get_organization( # RBAC check await rbac_check(request, org.org_uuid, current_user, "read", db_session) + org = OrganizationRead.from_orm(org) + return org @@ -61,6 +63,8 @@ async def get_organization_by_slug( # RBAC check await rbac_check(request, org.org_uuid, current_user, "read", db_session) + org = OrganizationRead.from_orm(org) + return org @@ -160,6 +164,8 @@ async def update_org( db_session.commit() db_session.refresh(org) + org = OrganizationRead.from_orm(org) + return org @@ -197,6 +203,7 @@ async def update_org_logo( db_session.commit() db_session.refresh(org) + return {"detail": "Logo updated"} @@ -244,7 +251,7 @@ async def get_orgs_by_user( user_id: str, page: int = 1, limit: int = 10, -): +) -> list[Organization]: statement = ( select(Organization) .join(UserOrganization) diff --git a/apps/api/src/services/roles/roles.py b/apps/api/src/services/roles/roles.py index 982d76f8..8eceda95 100644 --- a/apps/api/src/services/roles/roles.py +++ b/apps/api/src/services/roles/roles.py @@ -7,7 +7,7 @@ from src.security.rbac.rbac import ( authorization_verify_if_user_is_author, ) from src.db.users import AnonymousUser, PublicUser -from src.db.roles import Role, RoleCreate, RoleUpdate +from src.db.roles import Role, RoleCreate, RoleRead, RoleUpdate from fastapi import HTTPException, Request from datetime import datetime @@ -32,6 +32,8 @@ async def create_role( db_session.commit() db_session.refresh(role) + role = RoleRead(**role.dict()) + return role @@ -52,6 +54,8 @@ async def read_role( # RBAC check await rbac_check(request, current_user, "read", role.role_uuid, db_session) + role = RoleRead(**role.dict()) + return role @@ -90,6 +94,8 @@ async def update_role( db_session.commit() db_session.refresh(role) + role = RoleRead(**role.dict()) + return role From 7daf6df5a0162293acc151bff807c1e1128ff4aa Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 28 Nov 2023 20:53:10 +0100 Subject: [PATCH 19/39] fix: ruff issues --- apps/api/src/db/resource_authors.py | 4 +--- apps/api/src/routers/users.py | 1 - apps/api/src/security/rbac/rbac.py | 7 +++---- apps/api/src/services/courses/collections.py | 7 ------- apps/api/src/services/courses/courses.py | 13 ++----------- apps/api/src/services/roles/roles.py | 1 - apps/api/src/services/users/users.py | 4 +--- 7 files changed, 7 insertions(+), 30 deletions(-) diff --git a/apps/api/src/db/resource_authors.py b/apps/api/src/db/resource_authors.py index 3befb846..758a59c3 100644 --- a/apps/api/src/db/resource_authors.py +++ b/apps/api/src/db/resource_authors.py @@ -1,7 +1,5 @@ from enum import Enum -from typing import Optional, Union -from pydantic import BaseModel -from sqlalchemy import JSON, Column +from typing import Optional from sqlmodel import Field, SQLModel diff --git a/apps/api/src/routers/users.py b/apps/api/src/routers/users.py index 181898a2..efab5dda 100644 --- a/apps/api/src/routers/users.py +++ b/apps/api/src/routers/users.py @@ -1,6 +1,5 @@ from fastapi import APIRouter, Depends, Request from sqlmodel import Session -from src.security.rbac.rbac import authorization_verify_based_on_roles, authorization_verify_if_element_is_public, authorization_verify_if_user_is_author from src.security.auth import get_current_user from src.core.events.database import get_db_session diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index 0dba06d9..66e3d81f 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -1,7 +1,6 @@ -from math import e from typing import Literal from fastapi import HTTPException, status, Request -from sqlalchemy import func, null, or_ +from sqlalchemy import null from sqlmodel import Session, select from src.db.collections import Collection from src.db.courses import Course @@ -23,7 +22,7 @@ async def authorization_verify_if_element_is_public( if element_nature == ("courses" or "collections") and action == "read": if element_nature == "courses": statement = select(Course).where( - Course.public == True, Course.course_uuid == element_uuid + Course.public is True, Course.course_uuid == element_uuid ) course = db_session.exec(statement).first() if course: @@ -36,7 +35,7 @@ async def authorization_verify_if_element_is_public( if element_nature == "collections": statement = select(Collection).where( - Collection.public == True, Collection.collection_uuid == element_uuid + Collection.public is True, Collection.collection_uuid == element_uuid ) collection = db_session.exec(statement).first() diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index 73855956..e19ef879 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -17,13 +17,6 @@ from src.db.collections_courses import CollectionCourse from src.db.courses import Course from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request -from typing import List -from fastapi import HTTPException, Request -from sqlmodel import Session, select -from src.db.collections import Collection -from src.db.courses import Course -from src.db.collections_courses import CollectionCourse -from src.services.users.users import PublicUser #################################################### diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 19c8e63e..6f7fd613 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -1,19 +1,10 @@ -from calendar import c -import json -from queue import Full -import resource from typing import Literal from uuid import uuid4 from sqlmodel import Session, select -from src.db import chapters -from src.db.activities import Activity, ActivityRead -from src.db.chapter_activities import ChapterActivity -from src.db.chapters import Chapter, ChapterRead 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 import db from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum from src.db.users import PublicUser, AnonymousUser from src.db.courses import ( @@ -29,7 +20,7 @@ from src.security.rbac.rbac import ( authorization_verify_if_user_is_anon, ) from src.services.courses.thumbnails import upload_thumbnail -from fastapi import HTTPException, Request, status, UploadFile +from fastapi import HTTPException, Request, UploadFile from datetime import datetime @@ -267,7 +258,7 @@ async def get_courses_orgslug( statement_public = ( select(Course) .join(Organization) - .where(Organization.slug == org_slug, Course.public == True) + .where(Organization.slug == org_slug, Course.public is True) ) statement_all = ( select(Course).join(Organization).where(Organization.slug == org_slug) diff --git a/apps/api/src/services/roles/roles.py b/apps/api/src/services/roles/roles.py index 8eceda95..a5d46253 100644 --- a/apps/api/src/services/roles/roles.py +++ b/apps/api/src/services/roles/roles.py @@ -4,7 +4,6 @@ from sqlmodel import Session, select from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, - authorization_verify_if_user_is_author, ) from src.db.users import AnonymousUser, PublicUser from src.db.roles import Role, RoleCreate, RoleRead, RoleUpdate diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index 94fc594b..bc137af7 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -3,9 +3,7 @@ from typing import Literal from uuid import uuid4 from fastapi import HTTPException, Request, status from sqlmodel import Session, select -from src import db from src.security.rbac.rbac import ( - authorization_verify_based_on_roles, authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, ) @@ -338,7 +336,7 @@ async def rbac_check( if current_user.id == 0: # if user is anonymous return True else: - res = await authorization_verify_based_on_roles_and_authorship( + await authorization_verify_based_on_roles_and_authorship( request, current_user.id, "create", "user_x", db_session ) From 2bf80030d7f01fc4af3d9d1e529b803fb1a13f45 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 29 Nov 2023 21:59:35 +0100 Subject: [PATCH 20/39] fix: various bugs + improve api readability --- apps/api/src/db/activities.py | 1 - apps/api/src/db/chapters.py | 2 +- apps/api/src/db/courses.py | 2 +- apps/api/src/db/organizations.py | 2 +- apps/api/src/db/trails.py | 6 +++-- apps/api/src/db/users.py | 1 - apps/api/src/routers/courses/activities.py | 17 ++++++++------ apps/api/src/routers/courses/chapters.py | 20 ++++++++-------- apps/api/src/routers/courses/collections.py | 2 +- apps/api/src/routers/courses/courses.py | 18 +++++++++++---- apps/api/src/routers/orgs.py | 7 +++--- apps/api/src/routers/trail.py | 9 ++++---- apps/api/src/routers/users.py | 18 ++++++++------- apps/api/src/security/rbac/rbac.py | 2 +- apps/api/src/security/rbac/utils.py | 5 +++- .../services/courses/activities/activities.py | 7 +++--- apps/api/src/services/courses/chapters.py | 6 ++--- apps/api/src/services/courses/courses.py | 7 +++--- apps/api/src/services/orgs/orgs.py | 7 ++---- apps/api/src/services/trail/trail.py | 23 ++++++++++++------- apps/api/src/services/users/users.py | 11 +++++---- 21 files changed, 98 insertions(+), 75 deletions(-) diff --git a/apps/api/src/db/activities.py b/apps/api/src/db/activities.py index e5ae9a53..bae65eb4 100644 --- a/apps/api/src/db/activities.py +++ b/apps/api/src/db/activities.py @@ -53,7 +53,6 @@ class ActivityCreate(ActivityBase): class ActivityUpdate(ActivityBase): - activity_id: int name: Optional[str] activity_type: Optional[ActivityTypeEnum] activity_sub_type: Optional[ActivitySubTypeEnum] diff --git a/apps/api/src/db/chapters.py b/apps/api/src/db/chapters.py index ce5c19d1..d44efe67 100644 --- a/apps/api/src/db/chapters.py +++ b/apps/api/src/db/chapters.py @@ -21,6 +21,7 @@ class Chapter(ChapterBase, table=True): update_date: str = "" + class ChapterCreate(ChapterBase): # referenced order here will be ignored and just used for validation # used order will be the next available. @@ -28,7 +29,6 @@ class ChapterCreate(ChapterBase): class ChapterUpdate(ChapterBase): - chapter_id: int name: Optional[str] description: Optional[str] thumbnail_image: Optional[str] diff --git a/apps/api/src/db/courses.py b/apps/api/src/db/courses.py index 4d29251c..bb726803 100644 --- a/apps/api/src/db/courses.py +++ b/apps/api/src/db/courses.py @@ -22,13 +22,13 @@ class Course(CourseBase, table=True): update_date: str = "" + class CourseCreate(CourseBase): org_id: int = Field(default=None, foreign_key="organization.id") pass class CourseUpdate(CourseBase): - course_id: int name: str description: Optional[str] about: Optional[str] diff --git a/apps/api/src/db/organizations.py b/apps/api/src/db/organizations.py index 9d3d1412..c16d3809 100644 --- a/apps/api/src/db/organizations.py +++ b/apps/api/src/db/organizations.py @@ -17,7 +17,7 @@ class Organization(OrganizationBase, table=True): update_date: str = "" class OrganizationUpdate(OrganizationBase): - org_id: int + pass class OrganizationCreate(OrganizationBase): pass diff --git a/apps/api/src/db/trails.py b/apps/api/src/db/trails.py index c830bbd2..c59697ef 100644 --- a/apps/api/src/db/trails.py +++ b/apps/api/src/db/trails.py @@ -4,7 +4,6 @@ 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") @@ -14,7 +13,7 @@ class Trail(TrailBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) trail_uuid: str = "" creation_date: str = "" - update_date: str = "" + update_date: str = "" class TrailCreate(TrailBase): @@ -30,3 +29,6 @@ class TrailRead(BaseModel): creation_date: str update_date: str runs: list[TrailRunRead] + + class Config: + orm_mode = True diff --git a/apps/api/src/db/users.py b/apps/api/src/db/users.py index 0c29799f..79ee9788 100644 --- a/apps/api/src/db/users.py +++ b/apps/api/src/db/users.py @@ -25,7 +25,6 @@ class UserUpdate(UserBase): class UserUpdatePassword(SQLModel): - user_id: int old_password: str new_password: str diff --git a/apps/api/src/routers/courses/activities.py b/apps/api/src/routers/courses/activities.py index 3b609b2d..2ac43897 100644 --- a/apps/api/src/routers/courses/activities.py +++ b/apps/api/src/routers/courses/activities.py @@ -49,30 +49,33 @@ async def api_get_activity( ) -@router.get("/coursechapter/{coursechapter_id}") -async def api_get_activities( +@router.get("/chapter/{chapter_id}") +async def api_get_chapter_activities( request: Request, - coursechapter_id: str, + chapter_id: int, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ) -> List[ActivityRead]: """ - Get CourseChapter activities + Get Activities for a chapter """ - return await get_activities(request, coursechapter_id, current_user, db_session) + return await get_activities(request, chapter_id, current_user, db_session) -@router.put("/") +@router.put("/{activity_id}") async def api_update_activity( request: Request, activity_object: ActivityUpdate, + activity_id: int, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ) -> ActivityRead: """ Update activity by activity_id """ - return await update_activity(request, activity_object, current_user, db_session) + return await update_activity( + request, activity_object, activity_id, current_user, db_session + ) @router.delete("/{activity_id}") diff --git a/apps/api/src/routers/courses/chapters.py b/apps/api/src/routers/courses/chapters.py index b4e4574b..bcd3c34c 100644 --- a/apps/api/src/routers/courses/chapters.py +++ b/apps/api/src/routers/courses/chapters.py @@ -50,7 +50,7 @@ async def api_get_coursechapter( return await get_chapter(request, chapter_id, current_user, db_session) -@router.get("/meta/{course_id}") +@router.get("/course/{course_id}/meta") async def api_get_chapter_meta( request: Request, course_id: int, @@ -65,7 +65,7 @@ async def api_get_chapter_meta( ) -@router.put("/order/{course_id}") +@router.put("/course/{course_id}/order") async def api_update_chapter_meta( request: Request, course_id: int, @@ -81,7 +81,7 @@ async def api_update_chapter_meta( ) -@router.get("/{course_id}/page/{page}/limit/{limit}") +@router.get("/course/{course_id}/page/{page}/limit/{limit}") async def api_get_chapter_by( request: Request, course_id: int, @@ -98,24 +98,26 @@ async def api_get_chapter_by( ) -@router.put("/{coursechapter_id}") +@router.put("/{chapter_id}") async def api_update_coursechapter( request: Request, coursechapter_object: ChapterUpdate, - coursechapter_id: str, + chapter_id: int, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ) -> ChapterRead: """ Update CourseChapters by course_id """ - return await update_chapter(request, coursechapter_object, current_user, db_session) + 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, + chapter_id: int, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ): @@ -123,4 +125,4 @@ async def api_delete_coursechapter( Delete CourseChapters by ID """ - return await delete_chapter(request, coursechapter_id, current_user, db_session) + return await delete_chapter(request, chapter_id, current_user, db_session) diff --git a/apps/api/src/routers/courses/collections.py b/apps/api/src/routers/courses/collections.py index 2b0fd572..ba960848 100644 --- a/apps/api/src/routers/courses/collections.py +++ b/apps/api/src/routers/courses/collections.py @@ -42,7 +42,7 @@ async def api_get_collection( return await get_collection(request, collection_id, current_user, db_session) -@router.get("/org_id/{org_id}/page/{page}/limit/{limit}") +@router.get("/org/{org_id}/page/{page}/limit/{limit}") async def api_get_collections_by( request: Request, page: int, diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index 5914647d..e6669738 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -3,7 +3,12 @@ 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.db.courses import ( + CourseCreate, + CourseRead, + CourseUpdate, + FullCourseReadWithTrail, +) from src.security.auth import get_current_user from src.services.courses.courses import ( create_course, @@ -49,7 +54,7 @@ async def api_create_course( return await create_course(request, course, current_user, db_session, thumbnail) -@router.put("/thumbnail/{course_id}") +@router.put("/{course_id}/thumbnail") async def api_create_course_thumbnail( request: Request, course_id: str, @@ -80,7 +85,7 @@ async def api_get_course( ) -@router.get("/meta/{course_id}") +@router.get("/{course_id}/meta") async def api_get_course_meta( request: Request, course_id: int, @@ -112,17 +117,20 @@ async def api_get_course_by_orgslug( ) -@router.put("/") +@router.put("/{course_id}") async def api_update_course( request: Request, course_object: CourseUpdate, + course_id: int, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), ) -> CourseRead: """ Update Course by course_id """ - return await update_course(request, course_object, current_user, db_session) + return await update_course( + request, course_object, course_id, current_user, db_session + ) @router.delete("/{course_id}") diff --git a/apps/api/src/routers/orgs.py b/apps/api/src/routers/orgs.py index 0f29efb1..1b39c4e8 100644 --- a/apps/api/src/routers/orgs.py +++ b/apps/api/src/routers/orgs.py @@ -92,24 +92,25 @@ async def api_user_orgs( 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, db_session, str(current_user.id), page, limit ) -@router.put("/") +@router.put("/{org_id}") async def api_update_org( request: Request, org_object: OrganizationUpdate, + org_id: int, current_user: PublicUser = Depends(get_current_user), db_session: Session = Depends(get_db_session), ) -> OrganizationRead: """ Update Org by ID """ - return await update_org(request, org_object, current_user, db_session) + return await update_org(request, org_object,org_id, current_user, db_session) @router.delete("/{org_id}") diff --git a/apps/api/src/routers/trail.py b/apps/api/src/routers/trail.py index 7e935486..0c6455e5 100644 --- a/apps/api/src/routers/trail.py +++ b/apps/api/src/routers/trail.py @@ -41,7 +41,7 @@ async def api_get_user_trail( return await get_user_trails(request, user=user, db_session=db_session) -@router.get("/org_slug/{org_id}/trail") +@router.get("/org/{org_id}/trail") async def api_get_trail_by_org_id( request: Request, org_id: int, @@ -69,7 +69,7 @@ async def api_add_course_to_trail( return await add_course_to_trail(request, user, course_id, db_session) -@router.post("/remove_course/{course_id}") +@router.delete("/remove_course/{course_id}") async def api_remove_course_to_trail( request: Request, course_id: str, @@ -82,11 +82,10 @@ async def api_remove_course_to_trail( return await remove_course_from_trail(request, user, course_id, db_session) -@router.post("/add_activity/course_id/{course_id}/activity_id/{activity_id}") +@router.post("/add_activity/{activity_id}") async def api_add_activity_to_trail( request: Request, activity_id: int, - course_id: int, user=Depends(get_current_user), db_session=Depends(get_db_session), ) -> TrailRead: @@ -94,5 +93,5 @@ async def api_add_activity_to_trail( Add Course to trail """ return await add_activity_to_trail( - request, user, course_id, activity_id, db_session + request, user, activity_id, db_session ) diff --git a/apps/api/src/routers/users.py b/apps/api/src/routers/users.py index efab5dda..cf6e81f9 100644 --- a/apps/api/src/routers/users.py +++ b/apps/api/src/routers/users.py @@ -33,7 +33,7 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)): return current_user.dict() -@router.post("/org_id/{org_id}", response_model=UserRead, tags=["users"]) +@router.post("/{org_id}", response_model=UserRead, tags=["users"]) async def api_create_user_with_orgid( *, request: Request, @@ -62,7 +62,7 @@ async def api_create_user_without_org( return await create_user_without_org(request, db_session, current_user, user_object) -@router.get("/user_id/{user_id}", response_model=UserRead, tags=["users"]) +@router.get("/id/{user_id}", response_model=UserRead, tags=["users"]) async def api_get_user_by_id( *, request: Request, @@ -76,7 +76,7 @@ async def api_get_user_by_id( return await read_user_by_id(request, db_session, current_user, user_id) -@router.get("/user_uuid/{user_uuid}", response_model=UserRead, tags=["users"]) +@router.get("/uuid/{user_uuid}", response_model=UserRead, tags=["users"]) async def api_get_user_by_uuid( *, request: Request, @@ -90,32 +90,34 @@ async def api_get_user_by_uuid( return await read_user_by_uuid(request, db_session, current_user, user_uuid) -@router.put("/", response_model=UserRead, tags=["users"]) +@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 """ - return await update_user(request, db_session, current_user, user_object) + return await update_user(request, db_session, user_id, current_user, user_object) -@router.put("/change_password/", response_model=UserRead, tags=["users"]) +@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, form) + return await update_user_password(request, db_session, current_user, user_id, form) @router.delete("/user_id/{user_id}", tags=["users"]) @@ -125,7 +127,7 @@ async def api_delete_user( db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), user_id: int, -) : +): """ Delete User """ diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index 66e3d81f..3d6872b5 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -16,7 +16,7 @@ async def authorization_verify_if_element_is_public( element_uuid: str, action: Literal["read"], db_session: Session, -): +): element_nature = await check_element_type(element_uuid) # Verifies if the element is public if element_nature == ("courses" or "collections") and action == "read": diff --git a/apps/api/src/security/rbac/utils.py b/apps/api/src/security/rbac/utils.py index 3ef48a09..51835ee9 100644 --- a/apps/api/src/security/rbac/utils.py +++ b/apps/api/src/security/rbac/utils.py @@ -5,6 +5,7 @@ async def check_element_type(element_id): """ Check if the element is a course, a user, a house or a collection, by checking its prefix """ + print("element_id", element_id) if element_id.startswith("course_"): return "courses" elif element_id.startswith("user_"): @@ -13,12 +14,14 @@ async def check_element_type(element_id): return "houses" elif element_id.startswith("org_"): return "organizations" - elif element_id.startswith("coursechapter_"): + elif element_id.startswith("chapter_"): return "coursechapters" elif element_id.startswith("collection_"): return "collections" elif element_id.startswith("activity_"): return "activities" + elif element_id.startswith("role_"): + return "roles" else: raise HTTPException( status_code=status.HTTP_409_CONFLICT, diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index 5eeb7656..dc4dfda5 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -104,10 +104,11 @@ async def get_activity( async def update_activity( request: Request, activity_object: ActivityUpdate, + activity_id: int, current_user: PublicUser | AnonymousUser, db_session: Session, ): - statement = select(Activity).where(Activity.id == activity_object.activity_id) + statement = select(Activity).where(Activity.id == activity_id) activity = db_session.exec(statement).first() if not activity: @@ -121,8 +122,6 @@ async def update_activity( request, activity.activity_uuid, current_user, "update", db_session ) - del activity_object.activity_id - # Update only the fields that were passed in for var, value in vars(activity_object).items(): if value is not None: @@ -183,7 +182,7 @@ async def delete_activity( async def get_activities( request: Request, - coursechapter_id: str, + coursechapter_id: int, current_user: PublicUser | AnonymousUser, db_session: Session, ) -> list[ActivityRead]: diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 1fa3aae9..51830815 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -130,10 +130,11 @@ async def get_chapter( async def update_chapter( request: Request, chapter_object: ChapterUpdate, + chapter_id: int, current_user: PublicUser | AnonymousUser, db_session: Session, ) -> ChapterRead: - statement = select(Chapter).where(Chapter.id == chapter_object.chapter_id) + statement = select(Chapter).where(Chapter.id == chapter_id) chapter = db_session.exec(statement).first() if not chapter: @@ -161,7 +162,7 @@ async def update_chapter( async def delete_chapter( request: Request, - chapter_id: str, + chapter_id: int, current_user: PublicUser | AnonymousUser, db_session: Session, ): @@ -190,7 +191,6 @@ async def delete_chapter( return {"detail": "chapter deleted"} - async def get_course_chapters( request: Request, course_id: int, diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 6f7fd613..e36eb93e 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -65,6 +65,8 @@ async def get_course_meta( detail="Course not found", ) + print('cd',course.course_uuid) + # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) @@ -189,10 +191,11 @@ async def update_course_thumbnail( async def update_course( request: Request, course_object: CourseUpdate, + course_id: int, current_user: PublicUser | AnonymousUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_object.course_id) + statement = select(Course).where(Course.id == course_id) course = db_session.exec(statement).first() if not course: @@ -204,8 +207,6 @@ async def update_course( # RBAC check await rbac_check(request, course.course_uuid, current_user, "update", db_session) - del course_object.course_id - # Update only the fields that were passed in for var, value in vars(course_object).items(): if value is not None: diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index b1701802..0f67d08c 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -118,10 +118,11 @@ async def create_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_object.org_id) + statement = select(Organization).where(Organization.id == org_id) result = db_session.exec(statement) org = result.first() @@ -149,9 +150,6 @@ async def update_org( detail="Organization slug already exists", ) - # Remove the org_id from the org_object - del org_object.org_id - # Update only the fields that were passed in for var, value in vars(org_object).items(): if value is not None: @@ -203,7 +201,6 @@ async def update_org_logo( db_session.commit() db_session.refresh(org) - return {"detail": "Logo updated"} diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index c79d4d0d..019520a2 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -2,6 +2,7 @@ from datetime import datetime from uuid import uuid4 from fastapi import HTTPException, Request, status from sqlmodel import Session, select +from src.db.activities import Activity from src.db.courses import Course from src.db.trail_runs import TrailRun, TrailRunRead from src.db.trail_steps import TrailStep @@ -120,13 +121,20 @@ async def get_user_trail_with_orgid( async def add_activity_to_trail( request: Request, user: PublicUser, - course_id: int, activity_id: int, db_session: Session, ) -> TrailRead: - + # Look for the activity + statement = select(Activity).where(Activity.id == activity_id) + activity = db_session.exec(statement).first() + + if not activity: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found" + ) + # check if run already exists - statement = select(TrailRun).where(TrailRun.course_id == course_id) + statement = select(TrailRun).where(TrailRun.course_id == activity.course_id) trailrun = db_session.exec(statement).first() if trailrun: @@ -134,7 +142,7 @@ async def add_activity_to_trail( status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" ) - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.id == activity.course_id) course = db_session.exec(statement).first() if not course: @@ -160,7 +168,7 @@ async def add_activity_to_trail( 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 , + 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()), @@ -177,7 +185,7 @@ async def add_activity_to_trail( if not trailstep: trailstep = TrailStep( - trailrun_id=trailrun.id if trailrun.id is not None else 0 , + trailrun_id=trailrun.id if trailrun.id is not None else 0, activity_id=activity_id, course_id=course.id if course.id is not None else 0, org_id=course.org_id, @@ -225,7 +233,6 @@ async def add_course_to_trail( course_id: str, db_session: Session, ) -> TrailRead: - # check if run already exists statement = select(TrailRun).where(TrailRun.course_id == course_id) trailrun = db_session.exec(statement).first() @@ -234,7 +241,7 @@ async def add_course_to_trail( raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" ) - + statement = select(Course).where(Course.id == course_id) course = db_session.exec(statement).first() diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index bc137af7..e36b7040 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -158,11 +158,12 @@ async def create_user_without_org( async def update_user( request: Request, db_session: Session, + user_id: int, current_user: PublicUser | AnonymousUser, user_object: UserUpdate, ): # Get user - statement = select(User).where(User.username == user_object.username) + statement = select(User).where(User.id == user_id) user = db_session.exec(statement).first() if not user: @@ -170,7 +171,7 @@ async def update_user( status_code=400, detail="User does not exist", ) - + # RBAC check await rbac_check(request, current_user, "update", user.user_uuid, db_session) @@ -195,10 +196,11 @@ async def update_user_password( request: Request, db_session: Session, current_user: PublicUser | AnonymousUser, + user_id: int, form: UserUpdatePassword, ): # Get user - statement = select(User).where(User.username == form.user_id) + statement = select(User).where(User.id == user_id) user = db_session.exec(statement).first() if not user: @@ -206,7 +208,7 @@ async def update_user_password( status_code=400, detail="User does not exist", ) - + # RBAC check await rbac_check(request, current_user, "update", user.user_uuid, db_session) @@ -339,7 +341,6 @@ async def rbac_check( 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) From 187f75e58377b2a5f7839e85b0737b99c9fb54da Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 29 Nov 2023 22:29:48 +0100 Subject: [PATCH 21/39] feat: various improvements wip: frontend feat: enable cascade on foreign keys wip1 wip2 fix chapters issues wip4 --- apps/api/src/core/events/database.py | 2 +- apps/api/src/db/activities.py | 20 ++- apps/api/src/db/blocks.py | 8 +- apps/api/src/db/chapter_activities.py | 7 +- apps/api/src/db/chapters.py | 21 ++- apps/api/src/db/collections.py | 1 - apps/api/src/db/collections_courses.py | 5 +- apps/api/src/db/course_chapters.py | 14 +- apps/api/src/db/courses.py | 7 +- apps/api/src/db/organization_settings.py | 6 +- apps/api/src/db/user_organizations.py | 5 +- apps/api/src/routers/courses/chapters.py | 20 +-- apps/api/src/routers/courses/collections.py | 19 +- apps/api/src/routers/courses/courses.py | 38 ++-- apps/api/src/routers/trail.py | 14 +- apps/api/src/routers/users.py | 18 ++ .../services/courses/activities/activities.py | 14 +- .../src/services/courses/activities/pdf.py | 2 +- .../src/services/courses/activities/video.py | 6 + apps/api/src/services/courses/chapters.py | 165 ++++++++++-------- apps/api/src/services/courses/collections.py | 19 +- apps/api/src/services/courses/courses.py | 118 ++++++++++--- apps/api/src/services/orgs/orgs.py | 2 - apps/api/src/services/trail/trail.py | 26 +-- apps/api/src/services/users/users.py | 33 +++- .../collection/[collectionid]/page.tsx | 6 +- .../(withmenu)/collections/new/page.tsx | 45 ++--- .../[orgslug]/(withmenu)/collections/page.tsx | 14 +- .../activity/[activityid]/activity.tsx | 4 +- .../activity/[activityid]/error.tsx | 0 .../activity/[activityid]/loading.tsx | 0 .../activity/[activityid]/page.tsx | 0 .../{[courseid] => [courseuuid]}/course.tsx | 80 +++++---- .../edit/[[...subpage]]/edit.tsx | 65 +++++-- .../edit/[[...subpage]]/page.tsx | 15 +- .../edit/subpages/CourseContentEdition.tsx | 80 ++++----- .../edit/subpages/CourseEdition.tsx | 20 +-- .../{[courseid] => [courseuuid]}/error.tsx | 0 .../{[courseid] => [courseuuid]}/loading.tsx | 0 .../{[courseid] => [courseuuid]}/page.tsx | 24 +-- .../[orgslug]/(withmenu)/courses/courses.tsx | 12 +- .../app/orgs/[orgslug]/(withmenu)/page.tsx | 18 +- .../app/orgs/[orgslug]/settings/layout.tsx | 12 +- .../Activities/DocumentPdf/DocumentPdf.tsx | 2 +- .../Objects/Activities/Video/Video.tsx | 2 +- apps/web/components/Objects/Editor/Editor.tsx | 10 +- .../Extensions/Image/ImageBlockComponent.tsx | 2 +- .../Extensions/PDF/PDFBlockComponent.tsx | 2 +- .../Extensions/Video/VideoBlockComponent.tsx | 2 +- .../Modals/Activities/Create/NewActivity.tsx | 8 +- .../Create/NewActivityModal/DocumentPdf.tsx | 10 +- .../Create/NewActivityModal/DynamicCanva.tsx | 11 +- .../Create/NewActivityModal/Video.tsx | 23 ++- .../Objects/Modals/Chapters/NewChapter.tsx | 20 ++- .../Modals/Course/Create/CreateCourse.tsx | 44 ++++- .../Objects/Other/CollectionThumbnail.tsx | 15 +- .../Objects/Other/CourseThumbnail.tsx | 21 ++- .../Pages/CourseEdit/Draggables/Activity.tsx | 8 +- .../Pages/CourseEdit/Draggables/Chapter.tsx | 8 +- .../Pages/Courses/ActivityIndicators.tsx | 26 +-- .../Pages/Trail/TrailCourseElement.tsx | 10 +- .../Security/AuthenticatedClientElement.tsx | 57 +++--- .../components/Security/HeaderProfileBox.tsx | 4 +- apps/web/package-lock.json | 96 +++++----- apps/web/package.json | 6 +- apps/web/services/auth/auth.ts | 2 +- apps/web/services/courses/activities.ts | 9 +- apps/web/services/courses/activity.ts | 12 +- apps/web/services/courses/chapters.ts | 14 +- apps/web/services/courses/collections.ts | 14 +- apps/web/services/courses/courses.ts | 24 +-- 71 files changed, 879 insertions(+), 568 deletions(-) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/activity/[activityid]/activity.tsx (96%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/activity/[activityid]/error.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/activity/[activityid]/loading.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/activity/[activityid]/page.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/course.tsx (77%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/edit/[[...subpage]]/edit.tsx (66%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/edit/[[...subpage]]/page.tsx (64%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/edit/subpages/CourseContentEdition.tsx (82%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/edit/subpages/CourseEdition.tsx (82%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/error.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/loading.tsx (100%) rename apps/web/app/orgs/[orgslug]/(withmenu)/course/{[courseid] => [courseuuid]}/page.tsx (68%) diff --git a/apps/api/src/core/events/database.py b/apps/api/src/core/events/database.py index deb88120..43f6e1af 100644 --- a/apps/api/src/core/events/database.py +++ b/apps/api/src/core/events/database.py @@ -5,7 +5,7 @@ from sqlmodel import SQLModel, Session, create_engine engine = create_engine( - "postgresql://learnhouse:learnhouse@db:5432/learnhouse", echo=True + "postgresql://learnhouse:learnhouse@db:5432/learnhouse", echo=False ) SQLModel.metadata.create_all(engine) diff --git a/apps/api/src/db/activities.py b/apps/api/src/db/activities.py index bae65eb4..ada0d56d 100644 --- a/apps/api/src/db/activities.py +++ b/apps/api/src/db/activities.py @@ -1,5 +1,5 @@ from typing import Optional -from sqlalchemy import JSON, Column +from sqlalchemy import JSON, BigInteger, Column, ForeignKey from sqlmodel import Field, SQLModel from enum import Enum @@ -34,20 +34,32 @@ class ActivityBase(SQLModel): content: dict = Field(default={}, sa_column=Column(JSON)) published_version: int version: int - course_id: int = Field(default=None, foreign_key="course.id") + course_id: int = Field( + default=None, + sa_column=Column( + BigInteger, ForeignKey("course.id", ondelete="CASCADE") + ), + ) 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): - org_id: int = Field(default=None, foreign_key="organization.id") - course_id: int = Field(default=None, foreign_key="course.id") + course_id: int = Field( + sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) + ) chapter_id: int pass diff --git a/apps/api/src/db/blocks.py b/apps/api/src/db/blocks.py index 7fb2adb4..59972a04 100644 --- a/apps/api/src/db/blocks.py +++ b/apps/api/src/db/blocks.py @@ -1,5 +1,5 @@ from typing import Optional -from sqlalchemy import JSON, Column +from sqlalchemy import JSON, Column, ForeignKey from sqlmodel import Field, SQLModel from enum import Enum @@ -22,9 +22,9 @@ 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(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") + 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 diff --git a/apps/api/src/db/chapter_activities.py b/apps/api/src/db/chapter_activities.py index b567e0de..936078d9 100644 --- a/apps/api/src/db/chapter_activities.py +++ b/apps/api/src/db/chapter_activities.py @@ -1,12 +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(default=None, foreign_key="chapter.id", ) - activity_id: int = Field(default=None, foreign_key="activity.id") - course_id : int = Field(default=None, foreign_key="course.id") + 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 \ No newline at end of file diff --git a/apps/api/src/db/chapters.py b/apps/api/src/db/chapters.py index d44efe67..4e94dc62 100644 --- a/apps/api/src/db/chapters.py +++ b/apps/api/src/db/chapters.py @@ -1,5 +1,6 @@ -from typing import List, Optional +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 @@ -9,19 +10,21 @@ class ChapterBase(SQLModel): description: Optional[str] = "" thumbnail_image: Optional[str] = "" org_id: int = Field(default=None, foreign_key="organization.id") - course_id: int = Field(default=None, foreign_key="course.id") - creation_date: str - update_date: str + 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. @@ -32,6 +35,8 @@ class ChapterUpdate(ChapterBase): name: Optional[str] description: Optional[str] thumbnail_image: Optional[str] + course_id: Optional[int] + org_id: Optional[int] class ChapterRead(ChapterBase): @@ -57,7 +62,7 @@ class ChapterUpdateOrder(BaseModel): class DepreceatedChaptersRead(BaseModel): - chapter_order: list[str] - chapters: List[ChapterRead] - activities: List[ActivityRead] + chapterOrder: Any + chapters: Any + activities: Any pass diff --git a/apps/api/src/db/collections.py b/apps/api/src/db/collections.py index f3cbd850..9b191c8d 100644 --- a/apps/api/src/db/collections.py +++ b/apps/api/src/db/collections.py @@ -24,7 +24,6 @@ class CollectionCreate(CollectionBase): class CollectionUpdate(CollectionBase): - collection_id: int courses: Optional[list] name: Optional[str] public: Optional[bool] diff --git a/apps/api/src/db/collections_courses.py b/apps/api/src/db/collections_courses.py index 7ec5ff1b..6b30c2ee 100644 --- a/apps/api/src/db/collections_courses.py +++ b/apps/api/src/db/collections_courses.py @@ -1,11 +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(default=None, foreign_key="collection.id") - course_id: int = Field(default=None, foreign_key="course.id") + 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 diff --git a/apps/api/src/db/course_chapters.py b/apps/api/src/db/course_chapters.py index 1d9f0990..dec820c6 100644 --- a/apps/api/src/db/course_chapters.py +++ b/apps/api/src/db/course_chapters.py @@ -1,11 +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(default=None, foreign_key="course.id") - chapter_id: int = Field(default=None, foreign_key="chapter.id") - org_id : int = Field(default=None, foreign_key="organization.id") + 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 \ No newline at end of file + update_date: str diff --git a/apps/api/src/db/courses.py b/apps/api/src/db/courses.py index bb726803..686538a2 100644 --- a/apps/api/src/db/courses.py +++ b/apps/api/src/db/courses.py @@ -1,5 +1,6 @@ from typing import List, Optional from sqlmodel import Field, SQLModel +from src.db.users import User, UserRead from src.db.trails import TrailRead from src.db.chapters import ChapterRead @@ -22,7 +23,6 @@ class Course(CourseBase, table=True): update_date: str = "" - class CourseCreate(CourseBase): org_id: int = Field(default=None, foreign_key="organization.id") pass @@ -40,6 +40,7 @@ class CourseUpdate(CourseBase): 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 @@ -53,6 +54,7 @@ class FullCourseRead(CourseBase): update_date: str # Chapters, Activities chapters: List[ChapterRead] + authors: List[UserRead] pass @@ -61,8 +63,9 @@ class FullCourseReadWithTrail(CourseBase): course_uuid: str creation_date: str update_date: str + authors: List[UserRead] # Chapters, Activities chapters: List[ChapterRead] # Trail - trail: TrailRead + trail: TrailRead | None pass diff --git a/apps/api/src/db/organization_settings.py b/apps/api/src/db/organization_settings.py index 4c464b5d..babdef08 100644 --- a/apps/api/src/db/organization_settings.py +++ b/apps/api/src/db/organization_settings.py @@ -1,7 +1,9 @@ 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" @@ -9,7 +11,9 @@ class HeaderTypeEnum(str, Enum): class OrganizationSettings(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) - org_id: int = Field(default=None, foreign_key="organization.id") + 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 = "" diff --git a/apps/api/src/db/user_organizations.py b/apps/api/src/db/user_organizations.py index bb70a5fd..c842d41c 100644 --- a/apps/api/src/db/user_organizations.py +++ b/apps/api/src/db/user_organizations.py @@ -1,11 +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(default=None, foreign_key="organization.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 diff --git a/apps/api/src/routers/courses/chapters.py b/apps/api/src/routers/courses/chapters.py index bcd3c34c..be87247d 100644 --- a/apps/api/src/routers/courses/chapters.py +++ b/apps/api/src/routers/courses/chapters.py @@ -9,11 +9,11 @@ from src.db.chapters import ( DepreceatedChaptersRead, ) from src.services.courses.chapters import ( + DEPRECEATED_get_course_chapters, create_chapter, delete_chapter, get_chapter, get_course_chapters, - get_depreceated_course_chapters, reorder_chapters_and_activities, update_chapter, ) @@ -50,25 +50,25 @@ async def api_get_coursechapter( return await get_chapter(request, chapter_id, current_user, db_session) -@router.get("/course/{course_id}/meta") +@router.get("/course/{course_uuid}/meta", deprecated=True) async def api_get_chapter_meta( request: Request, - course_id: int, + course_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), -) -> DepreceatedChaptersRead: +): """ Get Chapters metadata """ - return await get_depreceated_course_chapters( - request, course_id, current_user, db_session + return await DEPRECEATED_get_course_chapters( + request, course_uuid, current_user, db_session ) -@router.put("/course/{course_id}/order") +@router.put("/course/{course_uuid}/order") async def api_update_chapter_meta( request: Request, - course_id: int, + course_uuid: str, order: ChapterUpdateOrder, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), @@ -77,7 +77,7 @@ async def api_update_chapter_meta( Update Chapter metadata """ return await reorder_chapters_and_activities( - request, course_id, order, current_user, db_session + request, course_uuid, order, current_user, db_session ) @@ -117,7 +117,7 @@ async def api_update_coursechapter( @router.delete("/{chapter_id}") async def api_delete_coursechapter( request: Request, - chapter_id: int, + chapter_id: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ): diff --git a/apps/api/src/routers/courses/collections.py b/apps/api/src/routers/courses/collections.py index ba960848..12fcfb66 100644 --- a/apps/api/src/routers/courses/collections.py +++ b/apps/api/src/routers/courses/collections.py @@ -29,17 +29,17 @@ async def api_create_collection( return await create_collection(request, collection_object, current_user, db_session) -@router.get("/{collection_id}") +@router.get("/{collection_uuid}") async def api_get_collection( request: Request, - collection_id: str, + collection_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ) -> CollectionRead: """ Get single collection by ID """ - return await get_collection(request, collection_id, current_user, db_session) + return await get_collection(request, collection_uuid, current_user, db_session) @router.get("/org/{org_id}/page/{page}/limit/{limit}") @@ -57,23 +57,26 @@ async def api_get_collections_by( return await get_collections(request, org_id, current_user, db_session, page, limit) -@router.put("/{collection_id}") +@router.put("/{collection_uuid}") async def api_update_collection( request: Request, collection_object: CollectionUpdate, + collection_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ) -> CollectionRead: """ Update collection by ID """ - return await update_collection(request, collection_object, current_user, db_session) + return await update_collection( + request, collection_object, collection_uuid, current_user, db_session + ) -@router.delete("/{collection_id}") +@router.delete("/{collection_uuid}") async def api_delete_collection( request: Request, - collection_id: str, + collection_uuid: str, current_user: PublicUser = Depends(get_current_user), db_session=Depends(get_db_session), ): @@ -81,4 +84,4 @@ async def api_delete_collection( Delete collection by ID """ - return await delete_collection(request, collection_id, current_user, db_session) + return await delete_collection(request, collection_uuid, current_user, db_session) diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index e6669738..f9485e80 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -51,13 +51,13 @@ async def api_create_course( learnings=learnings, tags=tags, ) - return await create_course(request, course, current_user, db_session, thumbnail) + return await create_course(request, org_id, course, current_user, db_session, thumbnail) -@router.put("/{course_id}/thumbnail") +@router.put("/{course_uuid}/thumbnail") async def api_create_course_thumbnail( request: Request, - course_id: str, + course_uuid: str, thumbnail: UploadFile | None = None, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), @@ -66,37 +66,37 @@ async def api_create_course_thumbnail( Update new Course Thumbnail """ return await update_course_thumbnail( - request, course_id, current_user, db_session, 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, + 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, db_session=db_session + request, course_uuid, current_user=current_user, db_session=db_session ) -@router.get("/{course_id}/meta") +@router.get("/{course_uuid}/meta") async def api_get_course_meta( request: Request, - course_id: int, + 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, db_session=db_session + request, course_uuid, current_user=current_user, db_session=db_session ) @@ -117,26 +117,26 @@ async def api_get_course_by_orgslug( ) -@router.put("/{course_id}") +@router.put("/{course_uuid}") async def api_update_course( request: Request, course_object: CourseUpdate, - course_id: int, + 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, db_session + 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, + course_uuid: str, db_session: Session = Depends(get_db_session), current_user: PublicUser = Depends(get_current_user), ): @@ -144,4 +144,4 @@ async def api_delete_course( Delete Course by ID """ - return await delete_course(request, course_id, current_user, db_session) + return await delete_course(request, course_uuid, current_user, db_session) diff --git a/apps/api/src/routers/trail.py b/apps/api/src/routers/trail.py index 0c6455e5..1c1c2fa5 100644 --- a/apps/api/src/routers/trail.py +++ b/apps/api/src/routers/trail.py @@ -56,33 +56,33 @@ async def api_get_trail_by_org_id( ) -@router.post("/add_course/{course_id}") +@router.post("/add_course/{course_uuid}") async def api_add_course_to_trail( request: Request, - course_id: str, + course_uuid: str, user=Depends(get_current_user), db_session=Depends(get_db_session), ) -> TrailRead: """ Add Course to trail """ - return await add_course_to_trail(request, user, course_id, db_session) + return await add_course_to_trail(request, user, course_uuid, db_session) -@router.delete("/remove_course/{course_id}") +@router.delete("/remove_course/{course_uuid}") async def api_remove_course_to_trail( request: Request, - course_id: str, + course_uuid: str, user=Depends(get_current_user), db_session=Depends(get_db_session), ) -> TrailRead: """ Remove Course from trail """ - return await remove_course_from_trail(request, user, course_id, db_session) + return await remove_course_from_trail(request, user, course_uuid, db_session) -@router.post("/add_activity/{activity_id}") +@router.post("/add_activity/{activity_uuid}") async def api_add_activity_to_trail( request: Request, activity_id: int, diff --git a/apps/api/src/routers/users.py b/apps/api/src/routers/users.py index cf6e81f9..71ec2741 100644 --- a/apps/api/src/routers/users.py +++ b/apps/api/src/routers/users.py @@ -1,3 +1,4 @@ +from typing import Literal from fastapi import APIRouter, Depends, Request from sqlmodel import Session from src.security.auth import get_current_user @@ -12,6 +13,7 @@ from src.db.users import ( UserUpdatePassword, ) from src.services.users.users import ( + authorize_user_action, create_user, create_user_without_org, delete_user_by_id, @@ -33,6 +35,22 @@ async def api_get_current_user(current_user: User = Depends(get_current_user)): return current_user.dict() +@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 authorization status + """ + return await authorize_user_action( + request, db_session, current_user, ressource_uuid, action + ) + + @router.post("/{org_id}", response_model=UserRead, tags=["users"]) async def api_create_user_with_orgid( *, diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index dc4dfda5..0912937d 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -1,5 +1,6 @@ from typing import Literal from sqlmodel import Session, select +from src.db.chapters import Chapter from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship, authorization_verify_if_user_is_anon, @@ -27,21 +28,22 @@ async def create_activity( activity = Activity.from_orm(activity_object) # CHeck if org exists - statement = select(Organization).where(Organization.id == activity_object.org_id) - org = db_session.exec(statement).first() + statement = select(Chapter).where(Chapter.id == activity_object.chapter_id) + chapter = db_session.exec(statement).first() - if not org: + if not chapter: raise HTTPException( status_code=404, - detail="Organization not found", + detail="Chapter not found", ) # RBAC check - await rbac_check(request, org.org_uuid, current_user, "create", db_session) + await rbac_check(request, chapter.chapter_uuid, current_user, "create", db_session) activity.activity_uuid = str(f"activity_{uuid4()}") activity.creation_date = str(datetime.now()) activity.update_date = str(datetime.now()) + activity.org_id = chapter.org_id # Insert Activity in DB db_session.add(activity) @@ -64,7 +66,7 @@ async def create_activity( chapter_id=activity_object.chapter_id, activity_id=activity.id if activity.id else 0, course_id=activity_object.course_id, - org_id=activity_object.org_id, + org_id=chapter.org_id, creation_date=str(datetime.now()), update_date=str(datetime.now()), order=to_be_used_order, diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index 5d728709..cc9c9a8f 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -51,7 +51,7 @@ async def create_documentpdf_activity( ) # get org_id - org_id = coursechapter.id + org_id = coursechapter.org_id # create activity uuid activity_uuid = f"activity_{uuid4()}" diff --git a/apps/api/src/services/courses/activities/video.py b/apps/api/src/services/courses/activities/video.py index a1d3beda..8e25b9de 100644 --- a/apps/api/src/services/courses/activities/video.py +++ b/apps/api/src/services/courses/activities/video.py @@ -82,6 +82,8 @@ async def create_video_activity( 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={ "filename": "video." + video_format, @@ -171,6 +173,8 @@ async def create_external_video_activity( 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={ "uri": data.uri, @@ -192,6 +196,8 @@ async def create_external_video_activity( chapter_activity_object = ChapterActivity( chapter_id=coursechapter.id, # type: ignore activity_id=activity.id, # type: ignore + course_id=coursechapter.course_id, + org_id=coursechapter.org_id, creation_date=str(datetime.now()), update_date=str(datetime.now()), order=1, diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 51830815..f610d6fe 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -36,6 +36,11 @@ async def create_chapter( ) -> ChapterRead: chapter = Chapter.from_orm(chapter_object) + # Get COurse + statement = select(Course).where(Course.id == chapter_object.course_id) + + course = db_session.exec(statement).one() + # RBAC check await rbac_check(request, "chapter_x", current_user, "create", db_session) @@ -44,6 +49,7 @@ async def create_chapter( 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 = ( @@ -155,14 +161,17 @@ async def update_chapter( db_session.commit() db_session.refresh(chapter) - chapter = ChapterRead(**chapter.dict()) + if chapter: + chapter = await get_chapter( + request, chapter.id, current_user, db_session # type: ignore + ) return chapter async def delete_chapter( request: Request, - chapter_id: int, + chapter_id: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): @@ -181,7 +190,7 @@ async def delete_chapter( db_session.commit() # Remove all linked activities - statement = select(ChapterActivity).where(ChapterActivity.chapter_id == chapter_id) + statement = select(ChapterActivity).where(ChapterActivity.id == chapter.id) chapter_activities = db_session.exec(statement).all() for chapter_activity in chapter_activities: @@ -199,14 +208,16 @@ async def get_course_chapters( page: int = 1, limit: int = 10, ) -> List[ChapterRead]: - statement = select(Chapter).where(Chapter.course_id == course_id) + 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() - if not chapters: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course do not have chapters" - ) - chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] # RBAC check @@ -236,13 +247,16 @@ async def get_course_chapters( return chapters -async def get_depreceated_course_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_id: int, + course_uuid: str, current_user: PublicUser, db_session: Session, -) -> DepreceatedChaptersRead: - statement = select(Course).where(Course.id == course_id) +): + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -253,76 +267,79 @@ async def get_depreceated_course_chapters( # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) - # Get chapters that are linked to his course and order them by order, using the order field in the CourseChapter table + chapters_in_db = await get_course_chapters(request, course.id, db_session, current_user) # type: ignore + + # activities + chapter_activityIdsGlobal = [] + + # chapters + chapters = {} + + for chapter in chapters_in_db: + chapter_activityIds = [] + + for activity in chapter.activities: + print("test", activity) + chapter_activityIds.append(activity.activity_uuid) + + chapters[chapter.chapter_uuid] = { + "uuid": chapter.chapter_uuid, + "id": chapter.id, + "name": chapter.name, + "activityIds": chapter_activityIds, + } + + # activities + activities_list = {} + statement = ( + select(Activity) + .join(ChapterActivity, ChapterActivity.activity_id == Activity.id) + .where(ChapterActivity.activity_id == Activity.id) + .group_by(Activity.id) + ) + activities_in_db = db_session.exec(statement).all() + + for activity in activities_in_db: + activities_list[activity.activity_uuid] = { + "uuid": activity.activity_uuid, + "id": activity.id, + "name": activity.name, + "type": activity.activity_type, + "content": activity.content, + } + + # get chapter order statement = ( select(Chapter) - .join(CourseChapter, Chapter.id == CourseChapter.chapter_id) - .where(CourseChapter.course_id == course_id) - .order_by(CourseChapter.order) + .join(CourseChapter, CourseChapter.chapter_id == Chapter.id) + .where(CourseChapter.chapter_id == Chapter.id) .group_by(Chapter.id, CourseChapter.order) + .order_by(CourseChapter.order) ) - print("ded", statement) - chapters = db_session.exec(statement).all() + chapters_in_db = db_session.exec(statement).all() - chapters = [ChapterRead(**chapter.dict(), activities=[]) for chapter in chapters] + chapterOrder = [] - # Get activities for each chapter - for chapter in chapters: - statement = ( - select(Activity) - .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) - .where(ChapterActivity.chapter_id == chapter.id) - .order_by(ChapterActivity.order) - .distinct(Activity.id, ChapterActivity.order) - ) - chapter_activities = db_session.exec(statement).all() + for chapter in chapters_in_db: + chapterOrder.append(chapter.chapter_uuid) - for chapter_activity in chapter_activities: - statement = ( - select(Activity) - .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) - .where(Activity.id == chapter_activity.id) - .distinct(Activity.id, ChapterActivity.order) - .order_by(ChapterActivity.order) - ) - activity = db_session.exec(statement).first() + final = { + "chapters": chapters, + "chapterOrder": chapterOrder, + "activities": activities_list, + } - if activity: - chapter.activities.append(ActivityRead(**activity.dict())) - - # Get a list of chapter ids - chapter_order: List[str] = [str(chapter.id) for chapter in chapters] - - # Get activities for each chapter - activities = [] - for chapter_id in chapter_order: - # order by activity order - statement = ( - select(Activity) - .join(ChapterActivity, Activity.id == ChapterActivity.activity_id) - .where(ChapterActivity.chapter_id == chapter_id) - .order_by(ChapterActivity.order) - .distinct(Activity.id, ChapterActivity.order) - ) - chapter_activities = db_session.exec(statement).all() - - activities.extend(chapter_activities) - - result = DepreceatedChaptersRead( - chapter_order=chapter_order, chapters=chapters, activities=activities - ) - - return result + return final async def reorder_chapters_and_activities( request: Request, - course_id: int, + course_uuid: str, chapters_order: ChapterUpdateOrder, current_user: PublicUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -341,7 +358,7 @@ async def reorder_chapters_and_activities( statement = ( select(CourseChapter) .where( - CourseChapter.course_id == course_id, CourseChapter.org_id == course.org_id + CourseChapter.course_id == course.id, CourseChapter.org_id == course.org_id ) .order_by(CourseChapter.order) ) @@ -357,7 +374,7 @@ async def reorder_chapters_and_activities( db_session.commit() # Delete Chapters that are not in the list of chapters_order - statement = select(Chapter).where(Chapter.course_id == course_id) + statement = select(Chapter).where(Chapter.course_id == course.id) chapters = db_session.exec(statement).all() chapter_ids_to_keep = [ @@ -376,7 +393,7 @@ async def reorder_chapters_and_activities( select(CourseChapter) .where( CourseChapter.chapter_id == chapter_order.chapter_id, - CourseChapter.course_id == course_id, + CourseChapter.course_id == course.id, ) .order_by(CourseChapter.order) ) @@ -386,7 +403,7 @@ async def reorder_chapters_and_activities( # Add CourseChapter link course_chapter = CourseChapter( chapter_id=chapter_order.chapter_id, - course_id=course_id, + course_id=course.id, # type: ignore org_id=course.org_id, creation_date=str(datetime.now()), update_date=str(datetime.now()), @@ -403,7 +420,7 @@ async def reorder_chapters_and_activities( select(CourseChapter) .where( CourseChapter.chapter_id == chapter_order.chapter_id, - CourseChapter.course_id == course_id, + CourseChapter.course_id == course.id, ) .order_by(CourseChapter.order) ) @@ -424,7 +441,7 @@ async def reorder_chapters_and_activities( statement = ( select(ChapterActivity) .where( - ChapterActivity.course_id == course_id, + ChapterActivity.course_id == course.id, ChapterActivity.org_id == course.org_id, ) .order_by(ChapterActivity.order) @@ -461,7 +478,7 @@ async def reorder_chapters_and_activities( chapter_id=chapter_order.chapter_id, activity_id=activity_order.activity_id, org_id=course.org_id, - course_id=course_id, + course_id=course.id, # type: ignore creation_date=str(datetime.now()), update_date=str(datetime.now()), order=activity_order.activity_id, diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index e19ef879..3a8c8810 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -25,9 +25,9 @@ from fastapi import HTTPException, status, Request async def get_collection( - request: Request, collection_id: str, current_user: PublicUser, db_session: Session + request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session ) -> CollectionRead: - statement = select(Collection).where(Collection.id == collection_id) + statement = select(Collection).where(Collection.collection_uuid == collection_uuid) collection = db_session.exec(statement).first() if not collection: @@ -107,12 +107,11 @@ async def create_collection( async def update_collection( request: Request, collection_object: CollectionUpdate, + collection_uuid: str, current_user: PublicUser, db_session: Session, ) -> CollectionRead: - statement = select(Collection).where( - Collection.id == collection_object.collection_id - ) + statement = select(Collection).where(Collection.collection_uuid == collection_uuid) collection = db_session.exec(statement).first() if not collection: @@ -127,7 +126,6 @@ async def update_collection( courses = collection_object.courses - del collection_object.collection_id del collection_object.courses # Update only the fields that were passed in @@ -181,9 +179,9 @@ async def update_collection( async def delete_collection( - request: Request, collection_id: str, current_user: PublicUser, db_session: Session + request: Request, collection_uuid: str, current_user: PublicUser, db_session: Session ): - statement = select(Collection).where(Collection.id == collection_id) + statement = select(Collection).where(Collection.collection_uuid == collection_uuid) collection = db_session.exec(statement).first() if not collection: @@ -225,10 +223,7 @@ async def get_collections( ) collections = db_session.exec(statement).all() - if not collections: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="No collections found" - ) + collections_with_courses = [] for collection in collections: diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index e36eb93e..3cc05dff 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -6,7 +6,7 @@ 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 +from src.db.users import PublicUser, AnonymousUser, User, UserRead from src.db.courses import ( Course, CourseCreate, @@ -26,11 +26,11 @@ from datetime import datetime async def get_course( request: Request, - course_id: str, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -42,21 +42,32 @@ async def get_course( # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) - course = CourseRead.from_orm(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 get_course_meta( request: Request, - course_id: int, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ) -> FullCourseReadWithTrail: # Avoid circular import from src.services.courses.chapters import get_course_chapters - course_statement = select(Course).where(Course.id == course_id) + course_statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(course_statement).first() if not course: @@ -65,12 +76,21 @@ async def get_course_meta( detail="Course not found", ) - print('cd',course.course_uuid) - # RBAC check await rbac_check(request, course.course_uuid, current_user, "read", db_session) - course = CourseRead.from_orm(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) # Get course chapters chapters = await get_course_chapters(request, course.id, db_session, current_user) @@ -85,12 +105,13 @@ async def get_course_meta( return FullCourseReadWithTrail( **course.dict(), chapters=chapters, - trail=trail, + trail=trail if trail else None, ) async def create_course( request: Request, + org_id: int, course_object: CourseCreate, current_user: PublicUser | AnonymousUser, db_session: Session, @@ -111,9 +132,9 @@ async def create_course( if thumbnail_file and thumbnail_file.filename: name_in_disk = f"{course.course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" await upload_thumbnail( - thumbnail_file, name_in_disk, course_object.org_id, course.course_uuid + thumbnail_file, name_in_disk, org_id, course.course_uuid ) - course_object.thumbnail = name_in_disk + course_object.thumbnail_image = name_in_disk # Insert course db_session.add(course) @@ -134,17 +155,30 @@ async def create_course( db_session.commit() db_session.refresh(resource_author) + # 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 CourseRead.from_orm(course) async def update_course_thumbnail( request: Request, - course_id: str, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, thumbnail_file: UploadFile | None = None, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() name_in_disk = None @@ -160,9 +194,7 @@ async def update_course_thumbnail( # Upload thumbnail if thumbnail_file and thumbnail_file.filename: - name_in_disk = ( - f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" - ) + name_in_disk = f"{course_uuid}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" await upload_thumbnail( thumbnail_file, name_in_disk, course.org_id, course.course_uuid ) @@ -183,7 +215,20 @@ async def update_course_thumbnail( db_session.commit() db_session.refresh(course) - course = CourseRead.from_orm(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 @@ -191,11 +236,11 @@ async def update_course_thumbnail( async def update_course( request: Request, course_object: CourseUpdate, - course_id: int, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -219,18 +264,29 @@ async def update_course( db_session.commit() db_session.refresh(course) - course = CourseRead.from_orm(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_id: str, + course_uuid: str, current_user: PublicUser | AnonymousUser, db_session: Session, ): - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -275,7 +331,21 @@ async def get_courses_orgslug( courses = db_session.exec(statement) - courses = [CourseRead.from_orm(course) for course in courses] + 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() + + # convert from User to UserRead + authors = [UserRead.from_orm(author) for author in authors] + + course.authors = authors return courses diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index 0f67d08c..c151b1b9 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -136,8 +136,6 @@ async def update_org( # RBAC check await rbac_check(request, org.org_uuid, current_user, "update", db_session) - org = Organization.from_orm(org_object) - # Verify if the new slug is already in use statement = select(Organization).where(Organization.slug == org_object.slug) result = db_session.exec(statement) diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 019520a2..51a81384 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -230,19 +230,10 @@ async def add_activity_to_trail( async def add_course_to_trail( request: Request, user: PublicUser, - course_id: str, + course_uuid: str, db_session: Session, ) -> TrailRead: - # check if run already exists - statement = select(TrailRun).where(TrailRun.course_id == course_id) - trailrun = db_session.exec(statement).first() - - if trailrun: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" - ) - - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: @@ -250,6 +241,15 @@ async def add_course_to_trail( status_code=status.HTTP_404_NOT_FOUND, detail="Course not found" ) + # check if run already exists + statement = select(TrailRun).where(TrailRun.course_id == course.id) + trailrun = db_session.exec(statement).first() + + if trailrun: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" + ) + statement = select(Trail).where( Trail.org_id == course.org_id, Trail.user_id == user.id ) @@ -308,10 +308,10 @@ async def add_course_to_trail( async def remove_course_from_trail( request: Request, user: PublicUser, - course_id: str, + course_uuid: str, db_session: Session, ) -> TrailRead: - statement = select(Course).where(Course.id == course_id) + statement = select(Course).where(Course.course_uuid == course_uuid) course = db_session.exec(statement).first() if not course: diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index e36b7040..4a9c5ee6 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -279,6 +279,37 @@ async def read_user_by_uuid( 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, @@ -350,7 +381,7 @@ async def rbac_check( return True await authorization_verify_based_on_roles_and_authorship( - request, current_user.id, "read", action, db_session + request, current_user.id, action, user_uuid, db_session ) diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx index ad1e8569..c51601ba 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx @@ -62,9 +62,9 @@ const CollectionPage = async (params: any) => {
{col.courses.map((course: any) => ( -
- -
+
+ +

{course.name}

diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx index e338f86e..85922e14 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx @@ -41,7 +41,7 @@ function NewCollection(params: any) { description: description, courses: selectedCourses, public: true, - org_id: org.org_id, + org_id: org.id, }; await createCollection(collection); await revalidateTags(["collections"], orgslug); @@ -69,26 +69,29 @@ function NewCollection(params: any) { ) : (
{courses.map((course: any) => ( -
- { - const courseId = e.target.value; - setSelectedCourses((prevSelectedCourses: string[]) => { - if (e.target.checked) { - return [...prevSelectedCourses, courseId]; - } else { - return prevSelectedCourses.filter((selectedCourse) => selectedCourse !== courseId); - } - }); - }} - className="mr-2 focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - +
+ + { + if (e.target.checked) { + setSelectedCourses([...selectedCourses, course.id]); + } + else { + setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid)); + } + } + } + className="mr-2" +/> + +
))} diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx index 864450fd..86ae0018 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx @@ -49,14 +49,17 @@ const CollectionsPage = async (params: any) => { const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const orgslug = params.params.orgslug; const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); - const org_id = org.org_id; + const org_id = org.id; const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] }); return (
- + @@ -64,7 +67,7 @@ const CollectionsPage = async (params: any) => {
{collections.map((collection: any) => ( -
+
))} @@ -81,7 +84,10 @@ const CollectionsPage = async (params: any) => {

No collections yet

Create a collection to group courses together

- + diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx similarity index 96% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index 596dcf6f..c076e09c 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -47,7 +47,7 @@ function ActivityClient(props: ActivityClientProps) {
- +
@@ -55,7 +55,7 @@ function ActivityClient(props: ActivityClientProps) {

{course.course.name}

- +
diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/error.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/error.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/error.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/error.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/loading.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/loading.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/loading.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/loading.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/page.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/page.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/page.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx similarity index 77% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx index f4ceb215..a73654d4 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx @@ -15,23 +15,24 @@ import { getUser } from "@services/users/users"; const CourseClient = (props: any) => { const [user, setUser] = useState({}); - const courseid = props.courseid; + const [learnings, setLearnings] = useState([]); + const courseuuid = props.courseuuid; const orgslug = props.orgslug; const course = props.course; 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); } + console.log(course); + async function startCourseUI() { // Create activity - await startCourse("course_" + courseid, orgslug); + await startCourse("course_" + courseuuid, orgslug); await revalidateTags(['courses'], orgslug); router.refresh(); @@ -39,17 +40,23 @@ const CourseClient = (props: any) => { // window.location.reload(); } + function isCourseStarted() { + const runs = course.trail.runs; + // checks if one of the obejcts in the array has the property "STATUS_IN_PROGRESS" + return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS"); + } + async function quitCourse() { // Close activity - let activity = await removeCourse("course_" + courseid, orgslug); + let activity = await removeCourse("course_" + courseuuid, orgslug); // Mutate course await revalidateTags(['courses'], orgslug); router.refresh(); } - useEffect(() => { - getUserUI(); - } + useEffect(() => { + + } , []); return ( @@ -61,26 +68,26 @@ const CourseClient = (props: any) => {

Course

- {course.course.name} + {course.name}

-
+
- +

Description

-

{course.course.description}

+

{course.description}

What you will learn

- {course.course.learnings.map((learning: any) => { + {learnings.map((learning: any) => { return (
@@ -118,48 +125,48 @@ const CourseClient = (props: any) => {

- {activity.type === "dynamic" && + {activity.activity_type === "TYPE_DYNAMIC" &&
} - {activity.type === "video" && + {activity.activity_type === "TYPE_VIDEO" &&
} - {activity.type === "documentpdf" && + {activity.activity_type === "TYPE_DOCUMENT" &&
}
- +

{activity.name}

- {activity.type === "dynamic" && + {activity.activity_type === "TYPE_DYNAMIC" && <> - +

Page

} - {activity.type === "video" && + {activity.activity_type === "TYPE_VIDEO" && <> - +

Video

} - {activity.type === "documentpdf" && + {activity.activity_type === "TYPE_DOCUMENT" && <> - +

Document

@@ -178,19 +185,20 @@ const CourseClient = (props: any) => {
- { user && + {user &&
-
- +
+ +
+
+
Author
+
{course.authors[0].first_name} {course.authors[0].last_name}
+
-
-
Author
-
{user.full_name}
-
-
} + {console.log(course)} - {course.trail.status == "ongoing" ? ( + {isCourseStarted() ? ( diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/edit.tsx similarity index 66% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/edit.tsx index ed292f48..294400c6 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/edit.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/edit.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { FC, use, useEffect, useReducer } from 'react' +import React, { FC, useEffect, useReducer } from 'react' import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; import { getAPIUrl, getUriWithOrg } from '@services/config/config'; import useSWR, { mutate } from 'swr'; @@ -14,15 +14,40 @@ 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); +function CourseEditClient({ courseuuid, courseid, subpage, params }: { courseid: any, courseuuid: string, subpage: string, params: any }) { + const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`, swrFetcher); + const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseuuid}/meta`, swrFetcher); const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {}); const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {}); const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true); const router = useRouter(); + // This function is a quick fix to transform the payload object from what was used before to the new and improved format + // The entire course edition frontend code will be remade in the future in a proper way. + const ConvertToNewAPIOrderUpdatePayload = (courseChaptersMetadata: any) => { + const old_format = courseChaptersMetadata + console.log() + + // Convert originalObject to the desired format + const convertedObject = { + "chapter_order_by_ids": old_format.chapterOrder.map((chapterId: string | number, chapterIndex: any) => { + const chapter = old_format.chapters[chapterId]; + return { + "chapter_id": chapter.id, + "activities_order_by_ids": chapter.activityIds.map((activityId: any, activityIndex: any) => { + return { + "activity_id": activityIndex + }; + }) + }; + }) + }; + + return convertedObject + } + + function courseChaptersReducer(state: any, action: any) { switch (action.type) { @@ -57,22 +82,25 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub async function saveCourse() { if (subpage.toString() === 'content') { - await updateChaptersMetadata(courseid, courseChaptersMetadata) + let payload = ConvertToNewAPIOrderUpdatePayload(courseChaptersMetadata) + await updateChaptersMetadata(courseuuid, payload) dispatchSavedContent({ type: 'saved_content' }) - await mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`) + await mutate(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`) await revalidateTags(['courses'], params.params.orgslug) router.refresh() } else if (subpage.toString() === 'general') { - await updateCourse(courseid, courseState) + await updateCourse(courseuuid, courseState) dispatchSavedContent({ type: 'saved_content' }) - await mutate(`${getAPIUrl()}courses/course_${courseid}`) + await mutate(`${getAPIUrl()}courses/course_${courseuuid}`) + await mutate(`${getAPIUrl()}chapters/course/course_${courseuuid}/meta`) await revalidateTags(['courses'], params.params.orgslug) router.refresh() } } useEffect(() => { + if (chapters_meta) { dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta }) dispatchSavedContent({ type: 'saved_content' }) @@ -91,8 +119,8 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub {course && <>
- - + +
@@ -118,27 +146,28 @@ function CourseEditClient({ courseid, subpage, params }: { courseid: string, sub
}
- +
General
- +
Content
- + ) } -const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch, dispatchCourseMetadata: React.Dispatch, dispatchSavedContent: React.Dispatch, courseChaptersMetadata: any, courseState: any }) => { - if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) { - return +const CoursePageViewer = ({ subpage, course, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseuuid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch, dispatchCourseMetadata: React.Dispatch, dispatchSavedContent: React.Dispatch, courseChaptersMetadata: any, courseState: any, course: any }) => { + + if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0 && course) { + return } - else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) { - return + else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0 && course) { + return } else if (subpage.toString() === 'content' || subpage.toString() === 'general') { return diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/page.tsx similarity index 64% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/page.tsx index 28e4a8d4..bc2fb786 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/[[...subpage]]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/[[...subpage]]/page.tsx @@ -6,7 +6,7 @@ import { Metadata } from 'next'; import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from "@services/auth/auth"; type MetadataProps = { - params: { orgslug: string, courseid: string }; + params: { orgslug: string, courseuuid: string }; searchParams: { [key: string]: string | string[] | undefined }; }; @@ -19,20 +19,23 @@ export async function generateMetadata( // Get Org context information const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); - const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) + const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) return { - title: `Edit Course - ` + course_meta.course.name, - description: course_meta.course.mini_description, + title: `Edit Course - ` + course_meta.name, + description: course_meta.mini_description, }; } -function CourseEdit(params: any) { +async function CourseEdit(params: any) { + const cookieStore = cookies(); + const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) let subpage = params.params.subpage ? params.params.subpage : 'general'; + const course_meta = await getCourseMetadataWithAuthHeader(params.params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) return ( <> - + ); } diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseContentEdition.tsx similarity index 82% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseContentEdition.tsx index b3634d5f..a5269054 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseContentEdition.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseContentEdition.tsx @@ -14,40 +14,43 @@ 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"; +import { mutate } from "swr"; function CourseContentEdition(props: any) { const router = useRouter(); - // Initial Course State - const data = props.data; + // Initial Course Chapters State + const course_chapters_with_orders_and_activities = props.course_chapters_with_orders_and_activities; // 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; + const [selectedChapterToAddActivityTo, setSelectedChapterToAddActivityTo] = useState("") as any; // Check window availability const [winReady, setwinReady] = useState(false); - const courseid = props.courseid; + const course = props.course; + const course_uuid = props.course ? props.course.course_uuid : '' const orgslug = props.orgslug; + // + useEffect(() => { setwinReady(true); - }, [courseid, orgslug]); + }, [course_uuid, orgslug]); // get a list of chapters order by chapter order const getChapters = () => { - const chapterOrder = data.chapterOrder ? data.chapterOrder : []; + const chapterOrder = course_chapters_with_orders_and_activities.chapterOrder ? course_chapters_with_orders_and_activities.chapterOrder : []; return chapterOrder.map((chapterId: any) => { - const chapter = data.chapters[chapterId]; + const chapter = course_chapters_with_orders_and_activities.chapters[chapterId]; let activities = []; - if (data.activities) { - activities = chapter.activityIds.map((activityId: any) => data.activities[activityId]) - ? chapter.activityIds.map((activityId: any) => data.activities[activityId]) + if (course_chapters_with_orders_and_activities.activities) { + activities = chapter.activityIds.map((activityId: any) => course_chapters_with_orders_and_activities.activities[activityId]) + ? chapter.activityIds.map((activityId: any) => course_chapters_with_orders_and_activities.activities[activityId]) : []; } return { @@ -61,9 +64,9 @@ function CourseContentEdition(props: any) { // Submit new chapter const submitChapter = async (chapter: any) => { - await createChapter(chapter, courseid); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); - // await getCourseChapters(); + await createChapter(chapter); + + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`,true); await revalidateTags(['courses'], orgslug); router.refresh(); setNewChapterModal(false); @@ -72,22 +75,21 @@ function CourseContentEdition(props: any) { // 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}`); + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`); // 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 updateChaptersMetadata(course_uuid, course_chapters_with_orders_and_activities); await createFileActivity(file, type, activity, chapterId); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`); // await getCourseChapters(); setNewActivityModal(false); await revalidateTags(['courses'], orgslug); @@ -96,9 +98,9 @@ function CourseContentEdition(props: any) { // Submit YouTube Video Upload const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => { - await updateChaptersMetadata(courseid, data); + //await updateChaptersMetadata(course_uuid, course_chapters_with_orders_and_activities); await createExternalVideoActivity(external_video_data, activity, chapterId); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`); // await getCourseChapters(); setNewActivityModal(false); await revalidateTags(['courses'], orgslug); @@ -106,19 +108,15 @@ function CourseContentEdition(props: any) { }; const deleteChapterUI = async (chapterId: any) => { - await deleteChapter(chapterId); - mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`); + + mutate(`${getAPIUrl()}chapters/course/${course_uuid}/meta`,true); // await getCourseChapters(); await revalidateTags(['courses'], orgslug); router.refresh(); }; - const updateChapters = () => { - updateChaptersMetadata(courseid, data); - revalidateTags(['courses'], orgslug); - router.refresh(); - }; + /* Modals @@ -126,7 +124,7 @@ function CourseContentEdition(props: any) { const openNewActivityModal = async (chapterId: any) => { setNewActivityModal(true); - setNewActivityModalData(chapterId); + setSelectedChapterToAddActivityTo(chapterId); }; // Close new chapter modal @@ -157,12 +155,12 @@ function CourseContentEdition(props: any) { } //////////////////////////// CHAPTERS //////////////////////////// if (type === "chapter") { - const newChapterOrder = Array.from(data.chapterOrder); + const newChapterOrder = Array.from(course_chapters_with_orders_and_activities.chapterOrder); newChapterOrder.splice(source.index, 1); newChapterOrder.splice(destination.index, 0, draggableId); const newState = { - ...data, + ...course_chapters_with_orders_and_activities, chapterOrder: newChapterOrder, }; @@ -174,13 +172,13 @@ function CourseContentEdition(props: any) { //////////////////////// 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]; + const start = course_chapters_with_orders_and_activities.chapters[source.droppableId]; + const finish = course_chapters_with_orders_and_activities.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 chapter = course_chapters_with_orders_and_activities.chapters[source.droppableId]; const newActivityIds = Array.from(chapter.activityIds); // remove the activity from the old position @@ -195,9 +193,9 @@ function CourseContentEdition(props: any) { }; const newState = { - ...data, + ...course_chapters_with_orders_and_activities, chapters: { - ...data.chapters, + ...course_chapters_with_orders_and_activities.chapters, [newChapter.id]: newChapter, }, }; @@ -229,9 +227,9 @@ function CourseContentEdition(props: any) { }; const newState = { - ...data, + ...course_chapters_with_orders_and_activities, chapters: { - ...data.chapters, + ...course_chapters_with_orders_and_activities.chapters, [newStart.id]: newStart, [newFinish.id]: newFinish, }, @@ -259,7 +257,8 @@ function CourseContentEdition(props: any) { submitFileActivity={submitFileActivity} submitExternalVideo={submitExternalVideo} submitActivity={submitActivity} - chapterId={newActivityModalData} + chapterId={selectedChapterToAddActivityTo} + course={course} >} dialogTitle="Create Activity" dialogDescription="Choose between types of activities to add to the course" @@ -276,7 +275,7 @@ function CourseContentEdition(props: any) { <> } diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseEdition.tsx similarity index 82% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseEdition.tsx index 23b77f85..3684daab 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/subpages/CourseEdition.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/edit/subpages/CourseEdition.tsx @@ -45,10 +45,10 @@ 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), + name: String(props.course_chapters_with_orders_and_activities.name), + mini_description: String(props.course_chapters_with_orders_and_activities.mini_description), + description: String(props.course_chapters_with_orders_and_activities.description), + learnings: String(props.course_chapters_with_orders_and_activities.learnings), }, validate, onSubmit: async values => { @@ -61,11 +61,10 @@ function CourseEdition(props: any) { if (formik.values !== formik.initialValues) { props.dispatchSavedContent({ type: 'unsaved_content' }); const updatedCourse = { - ...props.data, + ...props.course_chapters_with_orders_and_activities, name: formik.values.name, - mini_description: formik.values.mini_description, description: formik.values.description, - learnings: formik.values.learnings.split(", "), + learnings: formik.values.learnings, }; props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse }); } @@ -88,12 +87,7 @@ function CourseEdition(props: any) { - - - - - - + diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/error.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/error.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/error.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/error.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/loading.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/loading.tsx similarity index 100% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/loading.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/loading.tsx diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx similarity index 68% rename from apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx rename to apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx index dac4a9ba..efae450b 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/page.tsx @@ -7,7 +7,7 @@ import { Metadata } from 'next'; import { getAccessTokenFromRefreshTokenCookie, getNewAccessTokenUsingRefreshTokenServer } from '@services/auth/auth'; type MetadataProps = { - params: { orgslug: string, courseid: string }; + params: { orgslug: string, courseuuid: string }; searchParams: { [key: string]: string | string[] | undefined }; }; @@ -19,14 +19,14 @@ export async function generateMetadata( // Get Org context information const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); - const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) + const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) // SEO return { - title: course_meta.course.name + ` — ${org.name}`, - description: course_meta.course.mini_description, - keywords: course_meta.course.learnings, + title: course_meta.name + ` — ${org.name}`, + description: course_meta.description, + keywords: course_meta.learnings, robots: { index: true, follow: true, @@ -38,11 +38,11 @@ export async function generateMetadata( } }, openGraph: { - title: course_meta.course.name + ` — ${org.name}`, - description: course_meta.course.mini_description, + title: course_meta.name + ` — ${org.name}`, + description: course_meta.description, type: 'article', - publishedTime: course_meta.course.creationDate, - tags: course_meta.course.learnings, + publishedTime: course_meta.creation_date, + tags: course_meta.learnings, }, }; } @@ -50,14 +50,14 @@ export async function generateMetadata( const CoursePage = async (params: any) => { const cookieStore = cookies(); - const courseid = params.params.courseid + const courseuuid = params.params.courseuuid const orgslug = params.params.orgslug; const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) - const course_meta = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) + const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null) return (
- +
) } diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx index d115dc0d..3b786301 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx @@ -32,7 +32,10 @@ function Courses(props: CourseProps) {
- + {courses.map((course: any) => ( -
+
))} @@ -73,7 +76,10 @@ function Courses(props: CourseProps) {

No courses yet

Create a course to add content

- + { const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore) const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null); const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); - const org_id = org.org_id; - const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] }); + const org_id = org.id; + const collections = await getOrgCollectionsWithAuthHeader(org.id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] }); return (
@@ -67,7 +67,11 @@ const OrgHomePage = async (params: any) => {
- + @@ -105,7 +109,11 @@ const OrgHomePage = async (params: any) => {
- + @@ -113,7 +121,7 @@ const OrgHomePage = async (params: any) => {
{courses.map((course: any) => ( -
+
))} diff --git a/apps/web/app/orgs/[orgslug]/settings/layout.tsx b/apps/web/app/orgs/[orgslug]/settings/layout.tsx index 3e2ff305..b5e4a401 100644 --- a/apps/web/app/orgs/[orgslug]/settings/layout.tsx +++ b/apps/web/app/orgs/[orgslug]/settings/layout.tsx @@ -8,12 +8,15 @@ import Avvvatars from 'avvvatars-react'; import Image from 'next/image'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import { getOrganizationContextInfo } from '@services/organizations/orgs'; +import useSWR, { mutate } from "swr"; +import { getAPIUrl } from '@services/config/config'; +import { swrFetcher } from '@services/utils/ts/requests'; -async function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) { +function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) { const auth: any = React.useContext(AuthContext); const orgslug = params.orgslug; - let org = await getOrganizationContextInfo(orgslug, {}); + const { data: org, error: error } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher); return ( <> @@ -33,7 +36,10 @@ async function SettingsLayout({ children, params }: { children: React.ReactNode,
  • Profile
  • Passwords
  • - + Organization
    • General
    • diff --git a/apps/web/components/Objects/Activities/DocumentPdf/DocumentPdf.tsx b/apps/web/components/Objects/Activities/DocumentPdf/DocumentPdf.tsx index 78bd4f4b..e3859254 100644 --- a/apps/web/components/Objects/Activities/DocumentPdf/DocumentPdf.tsx +++ b/apps/web/components/Objects/Activities/DocumentPdf/DocumentPdf.tsx @@ -8,7 +8,7 @@ function DocumentPdfActivity({ activity, course }: { activity: any; course: any