diff --git a/.gitignore b/.gitignore
index 3eeb18b3..d3de4f6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
-
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -10,8 +9,11 @@ __pycache__/
# Visual Studio Code
.vscode/
-# Learnhouse
-content/*
+# Learnhouse
+content/org_*
+
+# Flyio
+fly.toml
# Distribution / packaging
.Python
@@ -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/Dockerfile b/Dockerfile
index 577b83f3..f42ea0a1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,4 +14,4 @@ RUN pip install --no-cache-dir --upgrade -r /usr/learnhouse/requirements.txt
COPY ./ /usr/learnhouse
#
-CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" , "--reload"]
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" ]
diff --git a/app.py b/app.py
index 9dce8265..0baf3d42 100644
--- a/app.py
+++ b/app.py
@@ -25,48 +25,44 @@ app = FastAPI(
title=learnhouse_config.site_name,
description=learnhouse_config.site_description,
version="0.1.0",
- root_path="/"
+ root_path="/",
)
app.add_middleware(
CORSMiddleware,
allow_origin_regex=learnhouse_config.hosting_config.allowed_regexp,
- allow_origins=learnhouse_config.hosting_config.allowed_origins,
allow_methods=["*"],
allow_credentials=True,
- allow_headers=["*"]
+ allow_headers=["*"],
)
# Gzip Middleware (will add brotli later)
app.add_middleware(GZipMiddleware, minimum_size=1000)
-# Static Files
-app.mount("/content", StaticFiles(directory="content"), name="content")
-
-
# Events
app.add_event_handler("startup", startup_app(app))
app.add_event_handler("shutdown", shutdown_app(app))
# JWT Exception Handler
-@ app.exception_handler(AuthJWTException)
+@app.exception_handler(AuthJWTException)
def authjwt_exception_handler(request: Request, exc: AuthJWTException):
return JSONResponse(
status_code=exc.status_code, # type: ignore
- content={"detail": exc.message} # type: ignore
+ content={"detail": exc.message}, # type: ignore
)
+# Static Files
+app.mount("/content", StaticFiles(directory="content"), name="content")
+
# Global Routes
app.include_router(v1_router)
# General Routes
-@ app.get("/")
+@app.get("/")
async def root():
return {"Message": "Welcome to LearnHouse ✨"}
-
-
diff --git a/config/config.py b/config/config.py
index 024728f3..93c3fa73 100644
--- a/config/config.py
+++ b/config/config.py
@@ -16,6 +16,7 @@ class CookieConfig(BaseModel):
class GeneralConfig(BaseModel):
development_mode: bool
+ install_mode: bool
class SecurityConfig(BaseModel):
@@ -71,6 +72,10 @@ def get_learnhouse_config() -> LearnHouseConfig:
development_mode = env_development_mode or yaml_config.get("general", {}).get(
"development_mode"
)
+ env_install_mode = os.environ.get("LEARNHOUSE_INSTALL_MODE")
+ install_mode = env_install_mode or yaml_config.get("general", {}).get(
+ "install_mode"
+ )
# Security Config
env_auth_jwt_secret_key = os.environ.get("LEARNHOUSE_AUTH_JWT_SECRET_KEY")
@@ -128,9 +133,8 @@ def get_learnhouse_config() -> LearnHouseConfig:
cookie_config = CookieConfig(domain=cookies_domain)
env_content_delivery_type = os.environ.get("LEARNHOUSE_CONTENT_DELIVERY_TYPE")
- content_delivery_type: str = (
+ content_delivery_type: str = env_content_delivery_type or (
(yaml_config.get("hosting_config", {}).get("content_delivery", {}).get("type"))
- or env_content_delivery_type
or "filesystem"
) # default to filesystem
@@ -207,7 +211,9 @@ def get_learnhouse_config() -> LearnHouseConfig:
site_name=site_name,
site_description=site_description,
contact_email=contact_email,
- general_config=GeneralConfig(development_mode=bool(development_mode)),
+ general_config=GeneralConfig(
+ development_mode=bool(development_mode), install_mode=bool(install_mode)
+ ),
hosting_config=hosting_config,
database_config=database_config,
security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key),
diff --git a/config/config.yaml b/config/config.yaml
index bf151243..12ee9e60 100644
--- a/config/config.yaml
+++ b/config/config.yaml
@@ -3,7 +3,8 @@ site_description: LearnHouse is an open-source platform tailored for learning ex
contact_email: hi@learnhouse.app
general:
- development_mode: true
+ development_mode: false
+ install_mode: false
security:
auth_jwt_secret_key: secret
diff --git a/content/__init__.py b/content/__init__.py
new file mode 100644
index 00000000..e69de29b
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/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx
index 77d3319f..4086f673 100644
--- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx
+++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/activity/[activityid]/activity.tsx
@@ -107,7 +107,7 @@ export function MarkStatus(props: { activityid: string, course: any, orgslug: st
router.refresh();
// refresh page (FIX for Next.js BUG)
- window.location.reload();
+ //window.location.reload();
}
diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx
index d1aafc72..41c999b1 100644
--- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx
+++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx
@@ -27,7 +27,7 @@ const CourseClient = (props: any) => {
router.refresh();
// refresh page (FIX for Next.js BUG)
- window.location.reload();
+ // window.location.reload();
}
async function quitCourse() {
@@ -38,7 +38,7 @@ const CourseClient = (props: any) => {
router.refresh();
// refresh page (FIX for Next.js BUG)
- window.location.reload();
+ //window.location.reload();
}
@@ -62,7 +62,7 @@ const CourseClient = (props: any) => {
-
+
Description
diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx
index 5bbecf64..5bf52f01 100644
--- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx
+++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/edit/page.tsx
@@ -16,6 +16,7 @@ import Modal from "@components/StyledElements/Modal/Modal";
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
import { Folders, Package2, SaveIcon } from "lucide-react";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
+import { revalidateTags } from "@services/utils/ts/requests";
function CourseEdit(params: any) {
@@ -75,6 +76,8 @@ function CourseEdit(params: any) {
const submitChapter = async (chapter: any) => {
await createChapter(chapter, courseid);
await getCourseChapters();
+ revalidateTags(['courses'], orgslug);
+ router.refresh();
setNewChapterModal(false);
};
@@ -86,6 +89,8 @@ function CourseEdit(params: any) {
await createActivity(activity, activity.chapterId, org.org_id);
await getCourseChapters();
setNewActivityModal(false);
+ revalidateTags(['courses'], orgslug);
+ router.refresh();
};
// Submit File Upload
@@ -94,6 +99,8 @@ function CourseEdit(params: any) {
await createFileActivity(file, type, activity, chapterId);
await getCourseChapters();
setNewActivityModal(false);
+ revalidateTags(['courses'], orgslug);
+ router.refresh();
};
// Submit YouTube Video Upload
@@ -103,17 +110,23 @@ function CourseEdit(params: any) {
await createExternalVideoActivity(external_video_data, activity, chapterId);
await getCourseChapters();
setNewActivityModal(false);
+ revalidateTags(['courses'], orgslug);
+ router.refresh();
};
const deleteChapterUI = async (chapterId: any) => {
console.log("deleteChapter", chapterId);
await deleteChapter(chapterId);
getCourseChapters();
+ revalidateTags(['courses'], orgslug);
+ router.refresh();
};
const updateChapters = () => {
console.log(data);
updateChaptersMetadata(courseid, data);
+ revalidateTags(['courses'], orgslug);
+ router.refresh();
};
/*
diff --git a/front/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx b/front/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx
index 38015cce..d19a8c78 100644
--- a/front/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx
+++ b/front/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx
@@ -22,7 +22,12 @@ function Trail(params: any) {
) : (
{trail.courses.map((course: any) => (
-
+ !course.masked ? (
+
+ ) : (
+ <>>
+ )
+
))}
diff --git a/front/components/Objects/Editor/Editor.tsx b/front/components/Objects/Editor/Editor.tsx
index 9bcd84d2..0b44e7a6 100644
--- a/front/components/Objects/Editor/Editor.tsx
+++ b/front/components/Objects/Editor/Editor.tsx
@@ -25,6 +25,7 @@ import PDFBlock from "./Extensions/PDF/PDFBlock";
import QuizBlock from "./Extensions/Quiz/QuizBlock";
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
import Link from "next/link";
+import { getCourseThumbnailMediaDirectory } from "@services/media/media";
interface Editor {
content: string;
@@ -120,7 +121,7 @@ function Editor(props: Editor) {
-
+
{" "}
diff --git a/front/components/Objects/Modals/Course/Create/CreateCourse.tsx b/front/components/Objects/Modals/Course/Create/CreateCourse.tsx
index e9870b9f..dd92a7d1 100644
--- a/front/components/Objects/Modals/Course/Create/CreateCourse.tsx
+++ b/front/components/Objects/Modals/Course/Create/CreateCourse.tsx
@@ -49,9 +49,10 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
if (status.org_id == orgId) {
closeModal();
router.refresh();
+ revalidateTags(['courses'], orgslug);
// refresh page (FIX for Next.js BUG)
- window.location.reload();
+ // window.location.reload();
} else {
alert("Error creating course, please see console logs");
console.log(status);
diff --git a/front/components/Pages/Trail/TrailCourseElement.tsx b/front/components/Pages/Trail/TrailCourseElement.tsx
index 57cef41d..67332711 100644
--- a/front/components/Pages/Trail/TrailCourseElement.tsx
+++ b/front/components/Pages/Trail/TrailCourseElement.tsx
@@ -4,6 +4,7 @@ import { removeCourse } from '@services/courses/activity';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import { revalidateTags } from '@services/utils/ts/requests';
import Link from 'next/link';
+import { useRouter } from 'next/navigation';
import { mutate } from 'swr';
interface TrailCourseElementProps {
@@ -14,12 +15,14 @@ interface TrailCourseElementProps {
function TrailCourseElement(props: TrailCourseElementProps) {
const courseid = props.course.course_id.replace("course_", "")
const course = props.course
+ const router = useRouter();
async function quitCourse(course_id: string) {
// Close activity
let activity = await removeCourse(course_id, props.orgslug);
// Mutate course
revalidateTags(['courses'], props.orgslug);
+ router.refresh();
// Mutate
mutate(`${getAPIUrl()}trail/org_slug/${props.orgslug}/trail`);
diff --git a/front/components/Security/AuthenticatedClientElement.tsx b/front/components/Security/AuthenticatedClientElement.tsx
index 6ced1f88..e46618bd 100644
--- a/front/components/Security/AuthenticatedClientElement.tsx
+++ b/front/components/Security/AuthenticatedClientElement.tsx
@@ -14,9 +14,9 @@ export const AuthenticatedClientElement = (props: AuthenticatedClientElementProp
// Available roles
const org_roles_values = ["admin", "owner"];
- const user_roles_values = ["role_admin"];
+ const user_roles_values = ["role_admin", "role_super_admin"];
+
-
function checkRoles() {
const org_id = props.orgId;
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..ac547697 100644
--- a/front/middleware.ts
+++ b/front/middleware.ts
@@ -1,3 +1,4 @@
+import { isInstallModeEnabled } from "@services/install/install";
import { LEARNHOUSE_DOMAIN, getDefaultOrg, isMultiOrgModeEnabled } from "./services/config/config";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
@@ -16,7 +17,7 @@ export const config = {
],
};
-export default function middleware(req: NextRequest) {
+export default async function middleware(req: NextRequest) {
// Get initial data
const hosting_mode = isMultiOrgModeEnabled() ? "multi" : "single";
const default_org = getDefaultOrg();
@@ -28,6 +29,17 @@ export default function middleware(req: NextRequest) {
return NextResponse.rewrite(new URL(pathname, req.url));
}
+ // Install Page
+ if (pathname.startsWith("/install")) {
+ // Check if install mode is enabled
+ const install_mode = await isInstallModeEnabled();
+ if (install_mode) {
+ return NextResponse.rewrite(new URL(pathname, req.url));
+ } else {
+ return NextResponse.redirect(new URL("/", req.url));
+ }
+ }
+
// Dynamic Pages Editor
if (pathname.match(/^\/course\/[^/]+\/activity\/[^/]+\/edit$/)) {
return NextResponse.rewrite(new URL(`/editor${pathname}`, req.url));
diff --git a/front/package-lock.json b/front/package-lock.json
index fa7645e5..5ca159b2 100644
--- a/front/package-lock.json
+++ b/front/package-lock.json
@@ -26,7 +26,7 @@
"formik": "^2.2.9",
"framer-motion": "^7.3.6",
"lucide-react": "^0.248.0",
- "next": "^13.4.7-canary.4",
+ "next": "^13.4.8",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
@@ -2146,9 +2146,9 @@
}
},
"node_modules/@next/env": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.7.tgz",
- "integrity": "sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw=="
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.8.tgz",
+ "integrity": "sha512-twuSf1klb3k9wXI7IZhbZGtFCWvGD4wXTY2rmvzIgVhXhs7ISThrbNyutBx3jWIL8Y/Hk9+woytFz5QsgtcRKQ=="
},
"node_modules/@next/eslint-plugin-next": {
"version": "13.0.6",
@@ -2160,9 +2160,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.7.tgz",
- "integrity": "sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.8.tgz",
+ "integrity": "sha512-MSFplVM4dTWOuKAUv0XR9gY7AWtMSBu9os9f+kp+s5rWhM1I2CdR3obFttd6366nS/W/VZxbPM5oEIdlIa46zA==",
"cpu": [
"arm64"
],
@@ -2175,9 +2175,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.7.tgz",
- "integrity": "sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.8.tgz",
+ "integrity": "sha512-Reox+UXgonon9P0WNDE6w85DGtyBqGitl/ryznOvn6TvfxEaZIpTgeu3ZrJLU9dHSMhiK7YAM793mE/Zii2/Qw==",
"cpu": [
"x64"
],
@@ -2190,9 +2190,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.7.tgz",
- "integrity": "sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.8.tgz",
+ "integrity": "sha512-kdyzYvAYtqQVgzIKNN7e1rLU8aZv86FDSRqPlOkKZlvqudvTO0iohuTPmnEEDlECeBM6qRPShNffotDcU/R2KA==",
"cpu": [
"arm64"
],
@@ -2205,9 +2205,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.7.tgz",
- "integrity": "sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.8.tgz",
+ "integrity": "sha512-oWxx4yRkUGcR81XwbI+T0zhZ3bDF6V1aVLpG+C7hSG50ULpV8gC39UxVO22/bv93ZlcfMY4zl8xkz9Klct6dpQ==",
"cpu": [
"arm64"
],
@@ -2220,9 +2220,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.7.tgz",
- "integrity": "sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.8.tgz",
+ "integrity": "sha512-anhtvuO6eE9YRhYnaEGTfbpH3L5gT/9qPFcNoi6xS432r/4DAtpJY8kNktqkTVevVIC/pVumqO8tV59PR3zbNg==",
"cpu": [
"x64"
],
@@ -2235,9 +2235,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.7.tgz",
- "integrity": "sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.8.tgz",
+ "integrity": "sha512-aR+J4wWfNgH1DwCCBNjan7Iumx0lLtn+2/rEYuhIrYLY4vnxqSVGz9u3fXcgUwo6Q9LT8NFkaqK1vPprdq+BXg==",
"cpu": [
"x64"
],
@@ -2250,9 +2250,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.7.tgz",
- "integrity": "sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.8.tgz",
+ "integrity": "sha512-OWBKIrJwQBTqrat0xhxEB/jcsjJR3+diD9nc/Y8F1mRdQzsn4bPsomgJyuqPVZs6Lz3K18qdIkvywmfSq75SsQ==",
"cpu": [
"arm64"
],
@@ -2265,9 +2265,9 @@
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.7.tgz",
- "integrity": "sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.8.tgz",
+ "integrity": "sha512-agiPWGjUndXGTOn4ChbKipQXRA6/UPkywAWIkx7BhgGv48TiJfHTK6MGfBoL9tS6B4mtW39++uy0wFPnfD0JWg==",
"cpu": [
"ia32"
],
@@ -2280,9 +2280,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.7.tgz",
- "integrity": "sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.8.tgz",
+ "integrity": "sha512-UIRKoByVKbuR6SnFG4JM8EMFlJrfEGuUQ1ihxzEleWcNwRMMiVaCj1KyqfTOW8VTQhJ0u8P1Ngg6q1RwnIBTtw==",
"cpu": [
"x64"
],
@@ -6379,11 +6379,11 @@
"dev": true
},
"node_modules/next": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/next/-/next-13.4.7.tgz",
- "integrity": "sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.4.8.tgz",
+ "integrity": "sha512-lxUjndYKjZHGK3CWeN2RI+/6ni6EUvjiqGWXAYPxUfGIdFGQ5XoisrqAJ/dF74aP27buAfs8MKIbIMMdxjqSBg==",
"dependencies": {
- "@next/env": "13.4.7",
+ "@next/env": "13.4.8",
"@swc/helpers": "0.5.1",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
@@ -6399,15 +6399,15 @@
"node": ">=16.8.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "13.4.7",
- "@next/swc-darwin-x64": "13.4.7",
- "@next/swc-linux-arm64-gnu": "13.4.7",
- "@next/swc-linux-arm64-musl": "13.4.7",
- "@next/swc-linux-x64-gnu": "13.4.7",
- "@next/swc-linux-x64-musl": "13.4.7",
- "@next/swc-win32-arm64-msvc": "13.4.7",
- "@next/swc-win32-ia32-msvc": "13.4.7",
- "@next/swc-win32-x64-msvc": "13.4.7"
+ "@next/swc-darwin-arm64": "13.4.8",
+ "@next/swc-darwin-x64": "13.4.8",
+ "@next/swc-linux-arm64-gnu": "13.4.8",
+ "@next/swc-linux-arm64-musl": "13.4.8",
+ "@next/swc-linux-x64-gnu": "13.4.8",
+ "@next/swc-linux-x64-musl": "13.4.8",
+ "@next/swc-win32-arm64-msvc": "13.4.8",
+ "@next/swc-win32-ia32-msvc": "13.4.8",
+ "@next/swc-win32-x64-msvc": "13.4.8"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -10063,9 +10063,9 @@
}
},
"@next/env": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.7.tgz",
- "integrity": "sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw=="
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.8.tgz",
+ "integrity": "sha512-twuSf1klb3k9wXI7IZhbZGtFCWvGD4wXTY2rmvzIgVhXhs7ISThrbNyutBx3jWIL8Y/Hk9+woytFz5QsgtcRKQ=="
},
"@next/eslint-plugin-next": {
"version": "13.0.6",
@@ -10077,57 +10077,57 @@
}
},
"@next/swc-darwin-arm64": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.7.tgz",
- "integrity": "sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.8.tgz",
+ "integrity": "sha512-MSFplVM4dTWOuKAUv0XR9gY7AWtMSBu9os9f+kp+s5rWhM1I2CdR3obFttd6366nS/W/VZxbPM5oEIdlIa46zA==",
"optional": true
},
"@next/swc-darwin-x64": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.7.tgz",
- "integrity": "sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.8.tgz",
+ "integrity": "sha512-Reox+UXgonon9P0WNDE6w85DGtyBqGitl/ryznOvn6TvfxEaZIpTgeu3ZrJLU9dHSMhiK7YAM793mE/Zii2/Qw==",
"optional": true
},
"@next/swc-linux-arm64-gnu": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.7.tgz",
- "integrity": "sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.8.tgz",
+ "integrity": "sha512-kdyzYvAYtqQVgzIKNN7e1rLU8aZv86FDSRqPlOkKZlvqudvTO0iohuTPmnEEDlECeBM6qRPShNffotDcU/R2KA==",
"optional": true
},
"@next/swc-linux-arm64-musl": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.7.tgz",
- "integrity": "sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.8.tgz",
+ "integrity": "sha512-oWxx4yRkUGcR81XwbI+T0zhZ3bDF6V1aVLpG+C7hSG50ULpV8gC39UxVO22/bv93ZlcfMY4zl8xkz9Klct6dpQ==",
"optional": true
},
"@next/swc-linux-x64-gnu": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.7.tgz",
- "integrity": "sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.8.tgz",
+ "integrity": "sha512-anhtvuO6eE9YRhYnaEGTfbpH3L5gT/9qPFcNoi6xS432r/4DAtpJY8kNktqkTVevVIC/pVumqO8tV59PR3zbNg==",
"optional": true
},
"@next/swc-linux-x64-musl": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.7.tgz",
- "integrity": "sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.8.tgz",
+ "integrity": "sha512-aR+J4wWfNgH1DwCCBNjan7Iumx0lLtn+2/rEYuhIrYLY4vnxqSVGz9u3fXcgUwo6Q9LT8NFkaqK1vPprdq+BXg==",
"optional": true
},
"@next/swc-win32-arm64-msvc": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.7.tgz",
- "integrity": "sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.8.tgz",
+ "integrity": "sha512-OWBKIrJwQBTqrat0xhxEB/jcsjJR3+diD9nc/Y8F1mRdQzsn4bPsomgJyuqPVZs6Lz3K18qdIkvywmfSq75SsQ==",
"optional": true
},
"@next/swc-win32-ia32-msvc": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.7.tgz",
- "integrity": "sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.8.tgz",
+ "integrity": "sha512-agiPWGjUndXGTOn4ChbKipQXRA6/UPkywAWIkx7BhgGv48TiJfHTK6MGfBoL9tS6B4mtW39++uy0wFPnfD0JWg==",
"optional": true
},
"@next/swc-win32-x64-msvc": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.7.tgz",
- "integrity": "sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.8.tgz",
+ "integrity": "sha512-UIRKoByVKbuR6SnFG4JM8EMFlJrfEGuUQ1ihxzEleWcNwRMMiVaCj1KyqfTOW8VTQhJ0u8P1Ngg6q1RwnIBTtw==",
"optional": true
},
"@nicolo-ribaudo/chokidar-2": {
@@ -13083,20 +13083,20 @@
"dev": true
},
"next": {
- "version": "13.4.7",
- "resolved": "https://registry.npmjs.org/next/-/next-13.4.7.tgz",
- "integrity": "sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==",
+ "version": "13.4.8",
+ "resolved": "https://registry.npmjs.org/next/-/next-13.4.8.tgz",
+ "integrity": "sha512-lxUjndYKjZHGK3CWeN2RI+/6ni6EUvjiqGWXAYPxUfGIdFGQ5XoisrqAJ/dF74aP27buAfs8MKIbIMMdxjqSBg==",
"requires": {
- "@next/env": "13.4.7",
- "@next/swc-darwin-arm64": "13.4.7",
- "@next/swc-darwin-x64": "13.4.7",
- "@next/swc-linux-arm64-gnu": "13.4.7",
- "@next/swc-linux-arm64-musl": "13.4.7",
- "@next/swc-linux-x64-gnu": "13.4.7",
- "@next/swc-linux-x64-musl": "13.4.7",
- "@next/swc-win32-arm64-msvc": "13.4.7",
- "@next/swc-win32-ia32-msvc": "13.4.7",
- "@next/swc-win32-x64-msvc": "13.4.7",
+ "@next/env": "13.4.8",
+ "@next/swc-darwin-arm64": "13.4.8",
+ "@next/swc-darwin-x64": "13.4.8",
+ "@next/swc-linux-arm64-gnu": "13.4.8",
+ "@next/swc-linux-arm64-musl": "13.4.8",
+ "@next/swc-linux-x64-gnu": "13.4.8",
+ "@next/swc-linux-x64-musl": "13.4.8",
+ "@next/swc-win32-arm64-msvc": "13.4.8",
+ "@next/swc-win32-ia32-msvc": "13.4.8",
+ "@next/swc-win32-x64-msvc": "13.4.8",
"@swc/helpers": "0.5.1",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406",
diff --git a/front/package.json b/front/package.json
index c5b09c2d..cb2eec57 100644
--- a/front/package.json
+++ b/front/package.json
@@ -27,7 +27,7 @@
"formik": "^2.2.9",
"framer-motion": "^7.3.6",
"lucide-react": "^0.248.0",
- "next": "^13.4.7-canary.4",
+ "next": "^13.4.8",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
diff --git a/front/services/install/install.ts b/front/services/install/install.ts
new file mode 100644
index 00000000..ab69dc22
--- /dev/null
+++ b/front/services/install/install.ts
@@ -0,0 +1,42 @@
+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;
+}
+
+export async function isInstallModeEnabled() {
+ const result = await fetch(`${getAPIUrl()}install/latest`, RequestBody("GET", null, null));
+ if (result.status === 200) {
+ return true;
+ }
+ else {
+ return false;
+ }
+}
diff --git a/src/core/events/content.py b/src/core/events/content.py
new file mode 100644
index 00000000..f4ca4353
--- /dev/null
+++ b/src/core/events/content.py
@@ -0,0 +1,8 @@
+import os
+
+
+async def check_content_directory():
+ if not os.path.exists("content"):
+ # create folder for activity
+ print("Creating content directory...")
+ os.makedirs("content")
diff --git a/src/core/events/events.py b/src/core/events/events.py
index bc95d16e..f5a552e0 100644
--- a/src/core/events/events.py
+++ b/src/core/events/events.py
@@ -1,6 +1,7 @@
from typing import Callable
from fastapi import FastAPI
from config.config import LearnHouseConfig, get_learnhouse_config
+from src.core.events.content import check_content_directory
from src.core.events.database import close_database, connect_to_db
from src.core.events.logs import create_logs_dir
from src.core.events.sentry import init_sentry
@@ -10,7 +11,7 @@ def startup_app(app: FastAPI) -> Callable:
async def start_app() -> None:
# Get LearnHouse Config
learnhouse_config: LearnHouseConfig = get_learnhouse_config()
- app.learnhouse_config = learnhouse_config # type: ignore
+ app.learnhouse_config = learnhouse_config # type: ignore
# Init Sentry
await init_sentry(app)
@@ -21,10 +22,14 @@ def startup_app(app: FastAPI) -> Callable:
# Create logs directory
await create_logs_dir()
+ # Create content directory
+ await check_content_directory()
+
return start_app
def shutdown_app(app: FastAPI) -> Callable:
async def close_app() -> None:
await close_database(app)
+
return close_app
diff --git a/src/router.py b/src/router.py
index 66f8b633..895873b3 100644
--- a/src/router.py
+++ b/src/router.py
@@ -1,7 +1,9 @@
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.services.dev.dev import isDevModeEnabled
+from src.routers.install import install
+from src.services.dev.dev import isDevModeEnabledOrRaise
+from src.services.install.install import isInstallModeEnabled
v1_router = APIRouter(prefix="/api/v1")
@@ -16,10 +18,20 @@ v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
v1_router.include_router(courses.router, prefix="/courses", tags=["courses"])
v1_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
v1_router.include_router(activities.router, prefix="/activities", tags=["activities"])
-v1_router.include_router( collections.router, prefix="/collections", tags=["collections"])
+v1_router.include_router(
+ collections.router, prefix="/collections", tags=["collections"]
+)
v1_router.include_router(trail.router, prefix="/trail", tags=["trail"])
# Dev Routes
v1_router.include_router(
- dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabled)]
+ dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabledOrRaise)]
+)
+
+# Install Routes
+v1_router.include_router(
+ install.router,
+ prefix="/install",
+ tags=["install"],
+ dependencies=[Depends(isInstallModeEnabled)],
)
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..52f6d5f2
--- /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):
+ request.app.db["installs"]
+
+ # get latest created install
+ install = await update_install_instance(request, data, step)
+
+ return install
diff --git a/src/routers/trail.py b/src/routers/trail.py
index aac85c77..f2eb38b5 100644
--- a/src/routers/trail.py
+++ b/src/routers/trail.py
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends, Request
from src.security.auth import get_current_user
-from src.services.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_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail
router = APIRouter()
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/courses/activities/activities.py b/src/services/courses/activities/activities.py
index 0a1c704c..57d1d410 100644
--- a/src/services/courses/activities/activities.py
+++ b/src/services/courses/activities/activities.py
@@ -148,6 +148,7 @@ async def update_activity(
coursechapter_id=activity["coursechapter_id"],
creationDate=creationDate,
updateDate=str(datetime_object),
+ course_id=activity["course_id"],
org_id=activity["org_id"],
**activity_object.dict(),
)
diff --git a/src/services/dev/dev.py b/src/services/dev/dev.py
index 6c1e1879..53a51397 100644
--- a/src/services/dev/dev.py
+++ b/src/services/dev/dev.py
@@ -7,8 +7,12 @@ def isDevModeEnabled():
if config.general_config.development_mode:
return True
else:
- raise HTTPException(
- status_code=403,
- detail="Development mode is not enabled",
- )
+ return False
+
+def isDevModeEnabledOrRaise():
+ config = get_learnhouse_config()
+ if config.general_config.development_mode:
+ return True
+ else:
+ raise HTTPException(status_code=403, detail="Development mode is not enabled")
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..e32b7185
--- /dev/null
+++ b/src/services/install/install.py
@@ -0,0 +1,475 @@
+from datetime import datetime
+from uuid import uuid4
+from fastapi import HTTPException, Request, status
+from pydantic import BaseModel
+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.courses.courses import CourseInDB
+
+from src.services.orgs.schemas.orgs import Organization, OrganizationInDB
+from faker import Faker
+
+
+from src.services.roles.schemas.roles import Elements, Permission, RoleInDB
+from src.services.users.schemas.users import (
+ PublicUser,
+ User,
+ UserInDB,
+ UserOrganization,
+ UserRolesInOrganization,
+ UserWithPassword,
+)
+
+
+class InstallInstance(BaseModel):
+ install_id: str
+ created_date: str
+ updated_date: str
+ step: int
+ data: dict
+
+
+async def isInstallModeEnabled():
+ config = get_learnhouse_config()
+
+ if config.general_config.install_mode:
+ return True
+ else:
+ raise HTTPException(
+ status_code=403,
+ detail="Install mode is not enabled",
+ )
+
+
+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:
+ 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"]
+ 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):
+ 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"]
+
+ 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/__init__.py b/src/services/trail/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/src/services/trail.py b/src/services/trail/trail.py
similarity index 89%
rename from src/services/trail.py
rename to src/services/trail/trail.py
index 6c133b3c..08e38700 100644
--- a/src/services/trail.py
+++ b/src/services/trail/trail.py
@@ -121,23 +121,19 @@ async def get_user_trail_with_orgslug(
if not trail:
return Trail(masked=False, courses=[])
- # Check if these courses still exist in the database
+ course_ids = [course["course_id"] for course in trail["courses"]]
+
+ live_courses = await courses_mongo.find({"course_id": {"$in": course_ids}}).to_list(
+ length=None
+ )
+
for course in trail["courses"]:
course_id = course["course_id"]
- course_object = await courses_mongo.find_one(
- {"course_id": course_id}, {"_id": 0}
- )
- print('checking course ' + course_id)
- if not course_object:
- print("Course not found " + course_id)
- trail["courses"].remove(course)
+
+ if course_id not in [course["course_id"] for course in live_courses]:
+ course["masked"] = True
continue
- course["course_object"] = course_object
-
- for courses in trail["courses"]:
- course_id = courses["course_id"]
-
chapters_meta = await get_coursechapters_meta(request, course_id, user)
activities = chapters_meta["activities"]
@@ -146,11 +142,11 @@ async def get_user_trail_with_orgslug(
{"course_id": course_id}, {"_id": 0}
)
- courses["course_object"] = course_object
+ course["course_object"] = course_object
num_activities = len(activities)
- num_completed_activities = len(courses.get("activities_marked_complete", []))
- courses["progress"] = (
+ 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
@@ -176,6 +172,12 @@ async def add_activity_to_trail(
{"user_id": user.user_id, "courses.course_id": courseid, "org_id": org_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=[])
@@ -206,6 +208,12 @@ async def add_course_to_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)
@@ -251,12 +259,15 @@ async def remove_course_from_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)
-
- print(org)
-
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
if not trail:
diff --git a/src/services/utils/upload_content.py b/src/services/utils/upload_content.py
index 81d766a2..0776fe94 100644
--- a/src/services/utils/upload_content.py
+++ b/src/services/utils/upload_content.py
@@ -30,6 +30,7 @@ async def upload_content(
elif content_delivery == "s3api":
# Upload to server then to s3 (AWS Keys are stored in environment variables and are loaded by boto3)
# TODO: Improve implementation of this
+ print("Uploading to s3...")
s3 = boto3.client(
"s3",
endpoint_url=learnhouse_config.hosting_config.content_delivery.s3api.endpoint_url,