From 0a2c5526bc2db34b8d96c81f07269ca01dcc5b31 Mon Sep 17 00:00:00 2001 From: swve Date: Thu, 21 Dec 2023 15:07:22 +0100 Subject: [PATCH 1/8] wip --- apps/api/src/db/courses.py | 18 +++++----- apps/api/src/db/trail_runs.py | 6 ++-- apps/api/src/db/trails.py | 6 ++-- apps/api/src/services/courses/chapters.py | 10 ++++-- apps/api/src/services/courses/courses.py | 13 +++++--- apps/api/src/services/trail/trail.py | 10 ++++-- apps/web/app/orgs/[orgslug]/signup/signup.tsx | 33 +++++++++---------- 7 files changed, 55 insertions(+), 41 deletions(-) diff --git a/apps/api/src/db/courses.py b/apps/api/src/db/courses.py index 7cc950b1..fade9b48 100644 --- a/apps/api/src/db/courses.py +++ b/apps/api/src/db/courses.py @@ -40,7 +40,7 @@ class CourseUpdate(CourseBase): class CourseRead(CourseBase): id: int org_id: int = Field(default=None, foreign_key="organization.id") - authors: List[UserRead] + authors: List[UserRead] course_uuid: str creation_date: str update_date: str @@ -49,22 +49,22 @@ class CourseRead(CourseBase): class FullCourseRead(CourseBase): id: int - course_uuid: str - creation_date: str - update_date: str + course_uuid: Optional[str] + creation_date: Optional[str] + update_date: Optional[str] # Chapters, Activities chapters: List[ChapterRead] - authors: List[UserRead] + authors: List[UserRead] pass class FullCourseReadWithTrail(CourseBase): id: int - course_uuid: str - creation_date: str - update_date: str + course_uuid: Optional[str] + creation_date: Optional[str] + update_date: Optional[str] org_id: int = Field(default=None, foreign_key="organization.id") - authors: List[UserRead] + authors: List[UserRead] # Chapters, Activities chapters: List[ChapterRead] # Trail diff --git a/apps/api/src/db/trail_runs.py b/apps/api/src/db/trail_runs.py index e160a790..26bfec8f 100644 --- a/apps/api/src/db/trail_runs.py +++ b/apps/api/src/db/trail_runs.py @@ -47,10 +47,10 @@ class TrailRunRead(BaseModel): org_id: int = Field(default=None, foreign_key="organization.id") user_id: int = Field(default=None, foreign_key="user.id") # course object - course: dict + course: Optional[dict] # timestamps - creation_date: str - update_date: str + creation_date: Optional[str] + update_date: Optional[str] # number of activities in course course_total_steps: int steps: list[TrailStep] diff --git a/apps/api/src/db/trails.py b/apps/api/src/db/trails.py index c59697ef..e29f241f 100644 --- a/apps/api/src/db/trails.py +++ b/apps/api/src/db/trails.py @@ -23,11 +23,11 @@ class TrailCreate(TrailBase): # 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 + trail_uuid: Optional[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 + creation_date: Optional[str] + update_date: Optional[str] runs: list[TrailRunRead] class Config: diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index 1e166d74..bb128c44 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -207,6 +207,10 @@ async def get_course_chapters( page: int = 1, limit: int = 10, ) -> List[ChapterRead]: + + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() + statement = ( select(Chapter) .join(CourseChapter, Chapter.id == CourseChapter.chapter_id) @@ -220,7 +224,7 @@ 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) + await rbac_check(request, course.course_uuid, current_user, "read", db_session) # Get activities for each chapter for chapter in chapters: @@ -532,7 +536,7 @@ async def reorder_chapters_and_activities( 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, @@ -543,7 +547,7 @@ async def rbac_check( request, current_user.id, action, - course_id, + course_uuid, db_session, ) diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index d7366503..eabc85c2 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -96,11 +96,16 @@ async def get_course_meta( 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 = None + + if isinstance(current_user, AnonymousUser): + trail = None + else: + trail = await get_user_trail_with_orgid( + request, current_user, course.org_id, db_session + ) + trail = TrailRead.from_orm(trail) - trail = TrailRead.from_orm(trail) return FullCourseReadWithTrail( **course.dict(), diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 973575a7..ca9f666c 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -8,7 +8,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( @@ -122,9 +122,15 @@ async def check_trail_presence( 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: + if isinstance(user, AnonymousUser): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Anonymous users cannot access this endpoint", + ) + trail = await check_trail_presence( org_id=org_id, user_id=user.id, diff --git a/apps/web/app/orgs/[orgslug]/signup/signup.tsx b/apps/web/app/orgs/[orgslug]/signup/signup.tsx index 102af86c..b22454f0 100644 --- a/apps/web/app/orgs/[orgslug]/signup/signup.tsx +++ b/apps/web/app/orgs/[orgslug]/signup/signup.tsx @@ -7,7 +7,7 @@ import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, For import Image from 'next/image'; import * as Form from '@radix-ui/react-form'; import { getOrgLogoMediaDirectory } from '@services/media/media'; -import { AlertTriangle } from 'lucide-react'; +import { AlertTriangle, Check, User } from 'lucide-react'; import Link from 'next/link'; import { signup } from '@services/auth/auth'; import { getUriWithOrg } from '@services/config/config'; @@ -44,10 +44,6 @@ const validate = (values: any) => { errors.username = 'Username must be at least 4 characters'; } - if (!values.full_name) { - errors.full_name = 'Required'; - } - if (!values.bio) { errors.bio = 'Required'; } @@ -61,6 +57,7 @@ function SignUpClient(props: SignUpClientProps) { const [isSubmitting, setIsSubmitting] = React.useState(false); const router = useRouter(); const [error, setError] = React.useState(''); + const [message, setMessage] = React.useState(''); const formik = useFormik({ initialValues: { org_slug: props.org?.slug, @@ -68,7 +65,8 @@ function SignUpClient(props: SignUpClientProps) { password: '', username: '', bio: '', - full_name: '', + first_name: '', + last_name: '', }, validate, onSubmit: async values => { @@ -76,7 +74,8 @@ function SignUpClient(props: SignUpClientProps) { let res = await signup(values); let message = await res.json(); if (res.status == 200) { - router.push(`/`); + //router.push(`/login`); + setMessage('Your account was successfully created') setIsSubmitting(false); } else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) { @@ -126,6 +125,16 @@ function SignUpClient(props: SignUpClientProps) {
{error}
)} + {message && ( +
+
+ +
{message}
+
+
+
Login
+
+ )} @@ -150,16 +159,6 @@ function SignUpClient(props: SignUpClientProps) { - {/* for full name */} - - - - - - - - - {/* for bio */} From 1c86a829b0666d68a758297a13ab3f0e2dc3c495 Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 22 Dec 2023 16:27:36 +0100 Subject: [PATCH 2/8] feat: init unit-testing --- apps/api/requirements.txt | 2 + apps/api/src/services/install/install.py | 6 +- apps/api/src/tests/test_main.py | 50 ++++++++++++++++ apps/api/src/tests/test_rbac.py | 0 apps/api/src/tests/utils/__init__.py | 0 .../src/tests/utils/init_data_for_tests.py | 57 +++++++++++++++++++ 6 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/tests/test_main.py create mode 100644 apps/api/src/tests/test_rbac.py create mode 100644 apps/api/src/tests/utils/__init__.py create mode 100644 apps/api/src/tests/utils/init_data_for_tests.py diff --git a/apps/api/requirements.txt b/apps/api/requirements.txt index b361beec..4c38a422 100644 --- a/apps/api/requirements.txt +++ b/apps/api/requirements.txt @@ -11,6 +11,8 @@ botocore python-jose passlib fastapi-jwt-auth +pytest +httpx faker requests pyyaml diff --git a/apps/api/src/services/install/install.py b/apps/api/src/services/install/install.py index b4538b28..bc079957 100644 --- a/apps/api/src/services/install/install.py +++ b/apps/api/src/services/install/install.py @@ -93,7 +93,7 @@ async def update_install_instance( # Install Default roles -async def install_default_elements(request: Request, data: dict, db_session: Session): +async def install_default_elements( 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() @@ -279,7 +279,7 @@ async def install_default_elements(request: Request, data: dict, db_session: Ses # Organization creation async def install_create_organization( - request: Request, org_object: OrganizationCreate, db_session: Session + org_object: OrganizationCreate, db_session: Session ): org = Organization.from_orm(org_object) @@ -296,7 +296,7 @@ async def install_create_organization( async def install_create_organization_user( - request: Request, user_object: UserCreate, org_slug: str, db_session: Session + user_object: UserCreate, org_slug: str, db_session: Session ): user = User.from_orm(user_object) diff --git a/apps/api/src/tests/test_main.py b/apps/api/src/tests/test_main.py new file mode 100644 index 00000000..22a02f5f --- /dev/null +++ b/apps/api/src/tests/test_main.py @@ -0,0 +1,50 @@ +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.pool import StaticPool +from sqlmodel import SQLModel, Session +from src.tests.utils.init_data_for_tests import create_initial_data_for_tests +from src.core.events.database import get_db_session +import pytest +import asyncio +from app import app + +client = TestClient(app) + +# TODO : fix this later https://stackoverflow.com/questions/10253826/path-issue-with-pytest-importerror-no-module-named + + +@pytest.fixture(name="session", scope="session") +def session_fixture(): + engine = create_engine( + "sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool + ) + SQLModel.metadata.create_all(engine) + with Session(engine) as session: + yield session + + +@pytest.fixture(name="client") +def client_fixture(session: Session): + def get_session_override(): + return session + + app.dependency_overrides[get_db_session] = get_session_override + + client = TestClient(app) + yield client + app.dependency_overrides.clear() + + +@pytest.fixture(scope="session", autouse=True) +def execute_before_all_tests(session: Session): + # This function will run once before all tests. + asyncio.run(create_initial_data_for_tests(session)) + + +def test_create_default_elements(client: TestClient, session: Session): + + response = client.get( + "/api/v1/orgs/slug/wayne", + ) + + assert response.status_code == 200 diff --git a/apps/api/src/tests/test_rbac.py b/apps/api/src/tests/test_rbac.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/tests/utils/__init__.py b/apps/api/src/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/api/src/tests/utils/init_data_for_tests.py b/apps/api/src/tests/utils/init_data_for_tests.py new file mode 100644 index 00000000..8229cfe2 --- /dev/null +++ b/apps/api/src/tests/utils/init_data_for_tests.py @@ -0,0 +1,57 @@ +from sqlmodel import Session, select +from src.db.user_organizations import UserOrganization +from src.db.organizations import OrganizationCreate +from src.db.users import User, UserCreate +from src.services.install.install import ( + install_create_organization, + install_create_organization_user, + install_default_elements, +) + + +async def create_initial_data_for_tests(db_session: Session): + # Install default elements + await install_default_elements({}, db_session) + + # Initiate test Organization + test_org = OrganizationCreate( + name="Wayne Enterprises", + description=None, + slug="wayne", + email="hello@wayne.dev", + logo_image=None, + ) + + # Create test organization + await install_create_organization(test_org, db_session) + + users = [ + UserCreate( + username="batman", + first_name="Bruce", + last_name="Wayne", + email="bruce@wayne.com", + password="imbatman", + ), + UserCreate( + username="robin", + first_name="Richard John", + last_name="Grayson", + email="robin@wayne.com", + password="secret", + ), + ] + + # Create 2 users in that Organization + for user in users: + await install_create_organization_user(user, "wayne", db_session) + + # Make robin a normal user + statement = select(UserOrganization).join(User).where(User.username == "robin") + user_org = db_session.exec(statement).first() + + user_org.role_id = 3 # type: ignore + db_session.add(user_org) + db_session.commit() + + return True From 2108763b6d0572790beaece30482bec5585fd35c Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 22 Dec 2023 16:36:18 +0100 Subject: [PATCH 3/8] fix: course start button bug --- .../[orgslug]/(withmenu)/course/[courseuuid]/course.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx index f23c9404..365404da 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/course.tsx @@ -43,8 +43,7 @@ const CourseClient = (props: any) => { 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"); + return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS" && run.course_id === course.id); } async function quitCourse() { @@ -193,11 +192,10 @@ const CourseClient = (props: any) => {
Author
-
{course.authors[0].first_name} {course.authors[0].last_name} { (course.authors[0].first_name && course.authors[0].last_name) ? course.authors[0].first_name + ' ' + course.authors[0].last_name : course.authors[0].username }
+
{course.authors[0].first_name} {course.authors[0].last_name} {(course.authors[0].first_name && course.authors[0].last_name) ? course.authors[0].first_name + ' ' + course.authors[0].last_name : course.authors[0].username}
} - {console.log(course)} {isCourseStarted() ? (