From 056365dac983247ca94c53c46b5e0cd71b6c1671 Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 10 Jul 2023 15:05:11 +0100 Subject: [PATCH] feat: init install mode --- .gitignore | 10 +- front/app/install/install.tsx | 80 +++ front/app/install/page.tsx | 18 + front/app/install/steps/account_creation.tsx | 132 +++++ front/app/install/steps/default_elements.tsx | 45 ++ .../install/steps/disable_install_mode.tsx | 19 + front/app/install/steps/finish.tsx | 39 ++ front/app/install/steps/get_started.tsx | 67 +++ front/app/install/steps/org_creation.tsx | 138 ++++++ front/app/install/steps/sample_data.tsx | 43 ++ front/app/install/steps/steps.tsx | 53 ++ front/components/StyledElements/Form/Form.tsx | 8 + front/middleware.ts | 5 + front/services/install/install.ts | 32 ++ src/router.py | 4 + src/routers/install/__init__.py | 0 src/routers/install/install.py | 70 +++ src/security/security.py | 2 +- src/services/install/__init__.py | 0 src/services/install/install.py | 469 ++++++++++++++++++ src/services/trail.py | 2 +- 21 files changed, 1230 insertions(+), 6 deletions(-) create mode 100644 front/app/install/install.tsx create mode 100644 front/app/install/page.tsx create mode 100644 front/app/install/steps/account_creation.tsx create mode 100644 front/app/install/steps/default_elements.tsx create mode 100644 front/app/install/steps/disable_install_mode.tsx create mode 100644 front/app/install/steps/finish.tsx create mode 100644 front/app/install/steps/get_started.tsx create mode 100644 front/app/install/steps/org_creation.tsx create mode 100644 front/app/install/steps/sample_data.tsx create mode 100644 front/app/install/steps/steps.tsx create mode 100644 front/services/install/install.ts create mode 100644 src/routers/install/__init__.py create mode 100644 src/routers/install/install.py create mode 100644 src/services/install/__init__.py create mode 100644 src/services/install/install.py diff --git a/.gitignore b/.gitignore index 3eeb18b3..b28b9fe6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -10,9 +9,12 @@ __pycache__/ # Visual Studio Code .vscode/ -# Learnhouse +# Learnhouse content/* +# Flyio +fly.toml + # Distribution / packaging .Python build/ @@ -88,7 +90,7 @@ target/ profile_default/ ipython_config.py -# ruff +# ruff .ruff/ # pyenv @@ -166,4 +168,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ diff --git a/front/app/install/install.tsx b/front/app/install/install.tsx new file mode 100644 index 00000000..e5f060ea --- /dev/null +++ b/front/app/install/install.tsx @@ -0,0 +1,80 @@ +'use client' +import React, { use, useEffect } from 'react' +import { INSTALL_STEPS } from './steps/steps' +import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper' +import { useRouter, useSearchParams } from 'next/navigation' + + + + +function InstallClient() { + const searchParams = useSearchParams() + const router = useRouter() + const step: any = parseInt(searchParams.get('step') || '0'); + const [stepNumber, setStepNumber] = React.useState(step) + const [stepsState, setStepsState] = React.useState(INSTALL_STEPS) + + function handleStepChange(stepNumber: number) { + setStepNumber(stepNumber) + router.push(`/install?step=${stepNumber}`) + } + + useEffect(() => { + setStepNumber(step) + }, [step]) + + return ( + +
+
+ +
+
+
+ {stepsState.map((step, index) => ( +
handleStepChange(index)} + > +
+ {index} +
+
{step.name}
+ +
+ ))} +
+
+
+ +
+

{stepsState[stepNumber].name}

+
+ {stepsState[stepNumber].component} +
+
+
+ ) +} + +const LearnHouseLogo = () => { + return ( + + + + + + + + + + + + + + ) + +} + +export default InstallClient \ No newline at end of file diff --git a/front/app/install/page.tsx b/front/app/install/page.tsx new file mode 100644 index 00000000..18b90b4d --- /dev/null +++ b/front/app/install/page.tsx @@ -0,0 +1,18 @@ +import React from 'react' +import InstallClient from './install' + + +export const metadata = { + title: "Install LearnHouse", + description: "Install Learnhouse on your server", +} + +function InstallPage() { + return ( +
+ +
+ ) +} + +export default InstallPage \ No newline at end of file diff --git a/front/app/install/steps/account_creation.tsx b/front/app/install/steps/account_creation.tsx new file mode 100644 index 00000000..7580d861 --- /dev/null +++ b/front/app/install/steps/account_creation.tsx @@ -0,0 +1,132 @@ +import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form' +import * as Form from '@radix-ui/react-form'; +import { getAPIUrl } from '@services/config/config'; +import { createNewUserInstall, updateInstall } from '@services/install/install'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { useFormik } from 'formik'; +import { useRouter } from 'next/navigation'; +import React from 'react' +import { BarLoader } from 'react-spinners'; +import useSWR, { mutate } from "swr"; + +const validate = (values: any) => { + const errors: any = {}; + + if (!values.email) { + errors.email = 'Required'; + } + else if ( + !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email) + ) { + errors.email = 'Invalid email address'; + } + + if (!values.password) { + errors.password = 'Required'; + } + else if (values.password.length < 8) { + errors.password = 'Password must be at least 8 characters'; + } + + if (!values.confirmPassword) { + errors.confirmPassword = 'Required'; + } + else if (values.confirmPassword !== values.password) { + errors.confirmPassword = 'Passwords must match'; + } + + if (!values.username) { + errors.username = 'Required'; + } + else if (values.username.length < 3) { + errors.username = 'Username must be at least 3 characters'; + } + + return errors; +}; + +function AccountCreation() { + const [isSubmitting, setIsSubmitting] = React.useState(false); + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const router = useRouter( + + ) + const formik = useFormik({ + initialValues: { + org_slug: '', + email: '', + password: '', + confirmPassword: '', + username: '', + }, + validate, + onSubmit: values => { + console.log(install.data[1].slug) + let finalvalues = { ...values, org_slug: install.data[1].slug } + let finalvalueswithoutpasswords = { ...values, password: '', confirmPassword: '', org_slug: install.data[1].slug } + let install_data = { ...install.data, 3: finalvalues } + let install_data_without_passwords = { ...install.data, 3: finalvalueswithoutpasswords } + updateInstall({ ...install_data_without_passwords }, 4) + createNewUserInstall(finalvalues) + + // await 2 seconds + setTimeout(() => { + setIsSubmitting(false) + }, 2000) + + router.push('/install?step=4') + + }, + }); + + return ( +
+ + + + + + + + {/* for password */} + + + + + + + + {/* for confirm password */} + + + + + + + + + {/* for username */} + + + + + + + + + +
+ + + {isSubmitting ? + : "Create Admin Account"} + + +
+ +
+
+ ) +} + +export default AccountCreation \ No newline at end of file diff --git a/front/app/install/steps/default_elements.tsx b/front/app/install/steps/default_elements.tsx new file mode 100644 index 00000000..04fe8a65 --- /dev/null +++ b/front/app/install/steps/default_elements.tsx @@ -0,0 +1,45 @@ +import { getAPIUrl } from '@services/config/config'; +import { createDefaultElements, updateInstall } from '@services/install/install'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { useRouter } from 'next/navigation'; +import React from 'react' +import useSWR from "swr"; + +function DefaultElements() { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [isSubmitted, setIsSubmitted] = React.useState(false); + const router = useRouter() + + function createDefElementsAndUpdateInstall() { + try { + createDefaultElements() + // add an {} to the install.data object + + let install_data = { ...install.data, 2: { status: 'OK' } } + + updateInstall(install_data, 3) + // await 2 seconds + setTimeout(() => { + setIsSubmitting(false) + }, 2000) + + router.push('/install?step=3') + setIsSubmitted(true) + } + catch (e) { + console.log(e) + } + } + + return ( +
+

Install Default Elements

+
+ Install +
+
+ ) +} + +export default DefaultElements \ No newline at end of file diff --git a/front/app/install/steps/disable_install_mode.tsx b/front/app/install/steps/disable_install_mode.tsx new file mode 100644 index 00000000..84acb6f2 --- /dev/null +++ b/front/app/install/steps/disable_install_mode.tsx @@ -0,0 +1,19 @@ +import { Check, Link } from 'lucide-react' +import React from 'react' + +function DisableInstallMode() { + return ( +
+
+ +
+

You have reached the end of the Installation process, please don't forget to disable installation mode.

+
+ + LearnHouse Docs +
+
+ ) +} + +export default DisableInstallMode \ No newline at end of file diff --git a/front/app/install/steps/finish.tsx b/front/app/install/steps/finish.tsx new file mode 100644 index 00000000..fbedc19e --- /dev/null +++ b/front/app/install/steps/finish.tsx @@ -0,0 +1,39 @@ +import { getAPIUrl } from '@services/config/config'; +import { updateInstall } from '@services/install/install'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { Check } from 'lucide-react' +import { useRouter } from 'next/navigation'; +import React from 'react' +import useSWR from "swr"; + +const Finish = () => { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const router = useRouter() + + async function finishInstall() { + console.log('install_data') + let install_data = { ...install.data, 5: { status: 'OK' } } + + let data = await updateInstall(install_data, 6) + if (data) { + router.push('/install?step=6') + } + else { + console.log('Error') + } + } + + return ( +
+

Installation Complete

+
+ +
+ Next Step +
+
+ + ) +} + +export default Finish \ No newline at end of file diff --git a/front/app/install/steps/get_started.tsx b/front/app/install/steps/get_started.tsx new file mode 100644 index 00000000..980912da --- /dev/null +++ b/front/app/install/steps/get_started.tsx @@ -0,0 +1,67 @@ +import PageLoading from '@components/Objects/Loaders/PageLoading'; +import { getAPIUrl } from '@services/config/config'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { useRouter } from 'next/navigation'; +import React, { use, useEffect } from 'react' +import useSWR, { mutate } from "swr"; + +function GetStarted() { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const router = useRouter() + + function startInstallation() { + fetch(`${getAPIUrl()}install/start`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({}) + }).then(res => res.json()).then(res => { + if (res.success) { + mutate(`${getAPIUrl()}install/latest`) + router.push(`/install?step=1`) + } + }) + + } + + function redirectToStep() { + const step = install.step + router.push(`/install?step=${step}`) + } + + + + useEffect(() => { + if (install) { + redirectToStep() + } + }, [install]) + + + if (error) return
+

Start a new installation

+
+ Start +
+
+ + if (isLoading) return + if (install) { + return ( +
+
+

You already started an installation

+
+ Continue +
+
+ Start +
+
+
+ ) + } +} + +export default GetStarted \ No newline at end of file diff --git a/front/app/install/steps/org_creation.tsx b/front/app/install/steps/org_creation.tsx new file mode 100644 index 00000000..58edc2d2 --- /dev/null +++ b/front/app/install/steps/org_creation.tsx @@ -0,0 +1,138 @@ + +import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input } from '@components/StyledElements/Form/Form' +import * as Form from '@radix-ui/react-form'; +import { useFormik } from 'formik'; +import { BarLoader } from 'react-spinners'; +import React from 'react' +import { createNewOrganization } from '@services/organizations/orgs'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { getAPIUrl } from '@services/config/config'; +import useSWR, { mutate } from "swr"; +import { createNewOrgInstall, updateInstall } from '@services/install/install'; +import { useRouter } from 'next/navigation'; +import { Check } from 'lucide-react'; + +const validate = (values: any) => { + const errors: any = {}; + + if (!values.name) { + errors.name = 'Required'; + } + + if (!values.description) { + errors.description = 'Required'; + } + + if (!values.slug) { + errors.slug = 'Required'; + } + + if (!values.email) { + errors.email = 'Required'; + } + else if ( + !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email) + ) { + errors.email = 'Invalid email address'; + } + + + + return errors; +}; + +function OrgCreation() { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [isSubmitted, setIsSubmitted] = React.useState(false); + const router = useRouter() + + + function createOrgAndUpdateInstall(values: any) { + try { + createNewOrgInstall(values) + install.data = { + 1: values + } + let install_data = { ...install.data, 1: values } + updateInstall(install_data, 2) + // await 2 seconds + setTimeout(() => { + setIsSubmitting(false) + }, 2000) + + router.push('/install?step=2') + setIsSubmitted(true) + } + catch (e) { + console.log(e) + } + + } + + const formik = useFormik({ + initialValues: { + name: '', + description: '', + slug: '', + email: '', + }, + validate, + onSubmit: values => { + createOrgAndUpdateInstall(values) + }, + }); + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + {/* for username */} + + + + + + + + + +
+ + + {isSubmitting ? + : "Create Organization"} + + +
+ + {isSubmitted &&
Organization Created Successfully
} + + +
+
+ ) +} + +export default OrgCreation \ No newline at end of file diff --git a/front/app/install/steps/sample_data.tsx b/front/app/install/steps/sample_data.tsx new file mode 100644 index 00000000..d89db078 --- /dev/null +++ b/front/app/install/steps/sample_data.tsx @@ -0,0 +1,43 @@ +import { getAPIUrl } from '@services/config/config'; +import { createSampleDataInstall, updateInstall } from '@services/install/install'; +import { swrFetcher } from '@services/utils/ts/requests'; +import { useRouter } from 'next/navigation'; +import React from 'react' +import useSWR, { mutate } from "swr"; + +function SampleData() { + const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher); + const router = useRouter() + + function createSampleData() { + + try { + let username = install.data[3].username + let slug = install.data[1].slug + console.log(install.data) + createSampleDataInstall(username, slug) + + let install_data = { ...install.data, 4: { status: 'OK' } } + updateInstall(install_data, 5) + + router.push('/install?step=5') + + } + catch (e) { + console.log(e) + } + } + + + + return ( +
+

Install Sample data on your organization

+
+ Start +
+
+ ) +} + +export default SampleData \ No newline at end of file diff --git a/front/app/install/steps/steps.tsx b/front/app/install/steps/steps.tsx new file mode 100644 index 00000000..2a55de13 --- /dev/null +++ b/front/app/install/steps/steps.tsx @@ -0,0 +1,53 @@ +import AccountCreation from "./account_creation"; +import DefaultElements from "./default_elements"; +import DisableInstallMode from "./disable_install_mode"; +import Finish from "./finish"; +import GetStarted from "./get_started"; +import OrgCreation from "./org_creation"; +import SampleData from "./sample_data"; + +export const INSTALL_STEPS = [ + { + id: "INSTALL_STATUS", + name: "Get started", + component: , + completed: false, + }, + { + id: "ORGANIZATION_CREATION", + name: "Organization Creation", + component: , + completed: false, + }, + { + id: "DEFAULT_ELEMENTS", + name: "Default Elements", + component: , + completed: false, + }, + { + id: "ACCOUNT_CREATION", + name: "Account Creation", + component: , + completed: false, + }, + { + id: "SAMPLE_DATA", + name: "Sample Data", + component: , + completed: false, + }, + { + id: "FINISH", + name: "Finish", + component: , + completed: false, + + }, + { + id: "DISABLING_INSTALLATION_MODE", + name: "Disabling Installation Mode", + component: , + completed: false, + }, +]; diff --git a/front/components/StyledElements/Form/Form.tsx b/front/components/StyledElements/Form/Form.tsx index 17f84702..f038aefc 100644 --- a/front/components/StyledElements/Form/Form.tsx +++ b/front/components/StyledElements/Form/Form.tsx @@ -2,6 +2,7 @@ import React from 'react'; import * as Form from '@radix-ui/react-form'; import { styled, keyframes } from '@stitches/react'; import { blackA, violet, mauve } from '@radix-ui/colors'; +import { Info } from 'lucide-react'; const FormLayout = (props: any, onSubmit: any) => ( @@ -9,6 +10,13 @@ const FormLayout = (props: any, onSubmit: any) => ( ); +export const FormLabelAndMessage = (props: { label: string, message?: string }) => ( +
+ {props.label} + {props.message &&
{props.message}
|| <>} +
+); + export const FormRoot = styled(Form.Root, { margin: 7 }); diff --git a/front/middleware.ts b/front/middleware.ts index 4a8f5280..f784f958 100644 --- a/front/middleware.ts +++ b/front/middleware.ts @@ -28,6 +28,11 @@ export default function middleware(req: NextRequest) { return NextResponse.rewrite(new URL(pathname, req.url)); } + // Install Page + if (pathname.startsWith("/install")) { + return NextResponse.rewrite(new URL(pathname, req.url)); + } + // Dynamic Pages Editor if (pathname.match(/^\/course\/[^/]+\/activity\/[^/]+\/edit$/)) { return NextResponse.rewrite(new URL(`/editor${pathname}`, req.url)); diff --git a/front/services/install/install.ts b/front/services/install/install.ts new file mode 100644 index 00000000..445b2a04 --- /dev/null +++ b/front/services/install/install.ts @@ -0,0 +1,32 @@ +import { getAPIUrl } from "@services/config/config"; +import { RequestBody, errorHandling } from "@services/utils/ts/requests"; + +export async function updateInstall(body: any, step: number) { + const result = await fetch(`${getAPIUrl()}install/update?step=${step}`, RequestBody("POST", body, null)); + const res = await errorHandling(result); + return res; +} + +export async function createNewOrgInstall(body: any) { + const result = await fetch(`${getAPIUrl()}install/org`, RequestBody("POST", body, null)); + const res = await errorHandling(result); + return res; +} + +export async function createNewUserInstall(body: any) { + const result = await fetch(`${getAPIUrl()}install/user?org_slug=${body.org_slug}`, RequestBody("POST", body, null)); + const res = await errorHandling(result); + return res; +} + +export async function createSampleDataInstall(username: string, org_slug: string) { + const result = await fetch(`${getAPIUrl()}install/sample?username=${username}&org_slug=${org_slug}`, RequestBody("POST", null, null)); + const res = await errorHandling(result); + return res; +} + +export async function createDefaultElements() { + const result = await fetch(`${getAPIUrl()}install/default_elements`, RequestBody("POST", null, null)); + const res = await errorHandling(result); + return res; +} diff --git a/src/router.py b/src/router.py index 66f8b633..2fd4f9d0 100644 --- a/src/router.py +++ b/src/router.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Depends from src.routers import blocks, dev, trail, users, auth, orgs, roles from src.routers.courses import chapters, collections, courses, activities +from src.routers.install import install from src.services.dev.dev import isDevModeEnabled @@ -23,3 +24,6 @@ v1_router.include_router(trail.router, prefix="/trail", tags=["trail"]) v1_router.include_router( dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabled)] ) + +# Install Routes +v1_router.include_router(install.router, prefix="/install", tags=["install"]) diff --git a/src/routers/install/__init__.py b/src/routers/install/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/routers/install/install.py b/src/routers/install/install.py new file mode 100644 index 00000000..e555bdfe --- /dev/null +++ b/src/routers/install/install.py @@ -0,0 +1,70 @@ +from fastapi import APIRouter, Request + +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): + # create install + install = await create_install_instance(request, data) + + return install + + +@router.get("/latest") +async def api_get_latest_install_instance(request: Request): + # get latest created install + install = await get_latest_install_instance(request) + + return install + + +@router.post("/default_elements") +async def api_install_def_elements(request: Request): + elements = await install_default_elements(request, {}) + + return elements + + +@router.post("/org") +async def api_install_org(request: Request, org: Organization): + organization = await install_create_organization(request, org) + + 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) + + 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): + installs = request.app.db["installs"] + + # get latest created install + install = await update_install_instance(request, data, step) + + return install diff --git a/src/security/security.py b/src/security/security.py index b5918389..fdd58323 100644 --- a/src/security/security.py +++ b/src/security/security.py @@ -133,7 +133,7 @@ async def check_user_role_org_with_element_org( # Check if The role belongs to the same organization as the element role_db = await roles.find_one({"role_id": role.role_id}) role = RoleInDB(**role_db) - if role.org_id == element_org["org_id"] or role.org_id == "*": + if (role.org_id == element_org["org_id"]) or role.org_id == "*": # Check if user has the right role for role in roles_list: role_db = await roles.find_one({"role_id": role.role_id}) diff --git a/src/services/install/__init__.py b/src/services/install/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/services/install/install.py b/src/services/install/install.py new file mode 100644 index 00000000..4e9ad1bc --- /dev/null +++ b/src/services/install/install.py @@ -0,0 +1,469 @@ +from datetime import datetime +import os +from re import A +from uuid import uuid4 +from fastapi import HTTPException, Request, status +from pydantic import BaseModel +import requests +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.orgs import create_org +from src.services.courses.thumbnails import upload_thumbnail + +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 create_install_instance(request: Request, data: dict): + installs = request.app.db["installs"] + + # 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 + + # create install + install = InstallInstance( + install_id=install_id, + created_date=created_date, + updated_date=updated_date, + step=step, + data=data, + ) + + # insert install + installs.insert_one(install.dict()) + + 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} + ) + + if install is None: + raise HTTPException( + status_code=404, + detail="No install instance found", + ) + + else: + install = InstallInstance(**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} + ) + + if install is None: + return None + + else: + # update install + install["data"] = data + install["step"] = step + install["updated_date"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # update install + await installs.update_one( + {"install_id": install["install_id"]}, {"$set": install} + ) + + install = InstallInstance(**install) + + return install + + +############################################################################################################ +# Steps +############################################################################################################ + + +# Install Default roles +async def install_default_elements(request: Request, data: dict): + roles = request.app.db["roles"] + + # check if default roles ADMIN_ROLE and USER_ROLE already exist + admin_role = await roles.find_one({"role_id": "role_super_admin"}) + user_role = await roles.find_one({"role_id": "role_user"}) + + if admin_role is not None or user_role is not None: + raise HTTPException( + status_code=400, + detail="Default roles already exist", + ) + + # get default roles + SUPER_ADMIN_ROLE = RoleInDB( + name="SuperAdmin Role", + description="This role grants all permissions to the user", + 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="*", + role_id="role_super_admin", + created_at=str(datetime.now()), + updated_at=str(datetime.now()), + ) + + ADMIN_ROLE = RoleInDB( + name="SuperAdmin Role", + description="This role grants all permissions to the user", + 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="*", + role_id="role_super_admin", + created_at=str(datetime.now()), + updated_at=str(datetime.now()), + ) + + USER_ROLE = RoleInDB( + name="User role", + description="This role grants read-only permissions to the user", + elements=Elements( + courses=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + users=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + houses=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + collections=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + organizations=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + coursechapters=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + activities=Permission( + action_create=False, + action_read=True, + action_update=False, + action_delete=False, + ), + ), + org_id="*", + role_id="role_user", + created_at=str(datetime.now()), + updated_at=str(datetime.now()), + ) + + try: + # insert default roles + await roles.insert_many( + [ADMIN_ROLE.dict(), USER_ROLE.dict(), SUPER_ADMIN_ROLE.dict()] + ) + return True + + except Exception as e: + raise HTTPException( + status_code=400, + detail="Error while inserting default roles", + ) + + +# Organization creation +async def install_create_organization( + request: Request, + org_object: Organization, +): + orgs = request.app.db["organizations"] + user = request.app.db["users"] + + # find if org already exists using name + + isOrgAvailable = await orgs.find_one({"slug": org_object.slug.lower()}) + + 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() + + +async def install_create_organization_user( + request: Request, user_object: UserWithPassword, org_slug: str +): + users = request.app.db["users"] + + isUsernameAvailable = await users.find_one({"username": user_object.username}) + isEmailAvailable = await users.find_one({"email": user_object.email}) + + if isUsernameAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Username already exists" + ) + + if isEmailAvailable: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Email already exists" + ) + + # Generate user_id with uuid4 + user_id = str(f"user_{uuid4()}") + + # 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: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="You are trying to create a user in an organization that does not exist", + ) + + org_id = isOrgExists["org_id"] + + # Create initial orgs list with the org_id passed in + orgs = [UserOrganization(org_id=org_id, org_role="owner")] + + # Give role + roles = [UserRolesInOrganization(role_id="role_super_admin", org_id=org_id)] + + # Create the user + user = UserInDB( + user_id=user_id, + 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()) + + return User(**user.dict()) + + +async def create_sample_data(org_slug: str, username: str, request: Request): + fake = 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) + + print(current_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"] + name_in_disk = f"test_mock{course_id}.jpeg" + + + + 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, + ) diff --git a/src/services/trail.py b/src/services/trail.py index ab2d0329..30e86143 100644 --- a/src/services/trail.py +++ b/src/services/trail.py @@ -201,7 +201,7 @@ async def add_course_to_trail( ) -> Trail: trails = request.app.db["trails"] orgs = request.app.db["organizations"] - + org = await orgs.find_one({"slug": orgslug}) org = PublicOrganization(**org)