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 (
+
+ )
+}
+
+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.
+
+
+ )
+}
+
+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 (
+
+ )
+}
+
+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 &&
|| <>>}
+
+);
+
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)