diff --git a/.gitignore b/.gitignore
index d61c99ea..60e1bcdd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
diff --git a/front/components/modals/CourseEdit/NewElement.tsx b/front/components/modals/CourseEdit/NewElement.tsx
index 6903a210..e8a0f4ae 100644
--- a/front/components/modals/CourseEdit/NewElement.tsx
+++ b/front/components/modals/CourseEdit/NewElement.tsx
@@ -1,39 +1,62 @@
import React, { useState } from "react";
+import { ArrowLeftIcon, Cross1Icon } from "@radix-ui/react-icons";
import Modal from "../Modal";
+import styled from "styled-components";
+import dynamic from "next/dynamic";
+import DynamicCanvaModal from "./NewElementModal/DynamicCanva";
+import VideoModal from "./NewElementModal/Video";
-function NewElementModal({ closeModal, submitElement, chapterId }: any) {
- const [elementName, setElementName] = useState("");
- const [elementDescription, setElementDescription] = useState("");
-
- const handleElementNameChange = (e: any) => {
- setElementName(e.target.value);
- };
-
- const handleElementDescriptionChange = (e: any) => {
- setElementDescription(e.target.value);
- };
-
- const handleSubmit = async (e: any) => {
- e.preventDefault();
- console.log({ elementName, elementDescription, chapterId });
- submitElement({
- name: elementName,
- chapterId: chapterId,
- type: "dynamic",
- });
- };
+function NewElementModal({ closeModal, submitElement, submitFileElement, chapterId }: any) {
+ const [selectedView, setSelectedView] = useState("home");
return (
-
- Add New Element
-
-
-
+
+
+ Add New Element
-
+
+ {selectedView === "home" && (
+
+ {setSelectedView("dynamic")}}>✨📄
+ {setSelectedView("video")}}>📹
+
+ )}
+
+ {selectedView === "dynamic" && (
+
+ )}
+
+ {selectedView === "video" && (
+
+ )}
+
);
}
+const ElementChooserWrapper = styled.div`
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: 20px;
+`;
+
+const ElementButton = styled.button`
+ padding: 20px;
+ border-radius: 10px;
+ border: none;
+ font-size: 50px;
+ background-color: #8c949c33;
+ cursor: pointer;
+ &:hover {
+ background-color: #8c949c7b;
+ }
+`;
+
export default NewElementModal;
diff --git a/front/components/modals/CourseEdit/NewElementModal/DynamicCanva.tsx b/front/components/modals/CourseEdit/NewElementModal/DynamicCanva.tsx
new file mode 100644
index 00000000..5caf5577
--- /dev/null
+++ b/front/components/modals/CourseEdit/NewElementModal/DynamicCanva.tsx
@@ -0,0 +1,36 @@
+import React, { useState } from "react";
+
+function DynamicCanvaModal({ submitElement, chapterId }: any) {
+ const [elementName, setElementName] = useState("");
+ const [elementDescription, setElementDescription] = useState("");
+
+ const handleElementNameChange = (e: any) => {
+ setElementName(e.target.value);
+ };
+
+ const handleElementDescriptionChange = (e: any) => {
+ setElementDescription(e.target.value);
+ };
+
+ const handleSubmit = async (e: any) => {
+ e.preventDefault();
+ console.log({ elementName, elementDescription, chapterId });
+ submitElement({
+ name: elementName,
+ chapterId: chapterId,
+ type: "dynamic",
+ });
+ };
+ return (
+
+ );
+}
+
+export default DynamicCanvaModal;
diff --git a/front/components/modals/CourseEdit/NewElementModal/Video.tsx b/front/components/modals/CourseEdit/NewElementModal/Video.tsx
new file mode 100644
index 00000000..fce8f192
--- /dev/null
+++ b/front/components/modals/CourseEdit/NewElementModal/Video.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+
+function VideoModal({ submitFileElement, chapterId }: any) {
+ const [video, setVideo] = React.useState(null) as any;
+ const [name, setName] = React.useState("");
+
+ const handleVideoChange = (event: React.ChangeEvent) => {
+ setVideo(event.target.files[0]);
+ };
+
+ const handleNameChange = (event: React.ChangeEvent) => {
+ setName(event.target.value);
+ };
+
+ const handleSubmit = async (e: any) => {
+ e.preventDefault();
+ let status = await submitFileElement(video, "video", { name, type: "video" }, chapterId);
+ };
+
+ /* TODO : implement some sort of progress bar for file uploads, it is not possible yet because i'm not using axios.
+ and the actual upload isn't happening here anyway, it's in the submitFileElement function */
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default VideoModal;
diff --git a/front/pages/org/[orgslug]/course/[courseid]/edit/index.tsx b/front/pages/org/[orgslug]/course/[courseid]/edit/index.tsx
index 50f0e1d2..17574061 100644
--- a/front/pages/org/[orgslug]/course/[courseid]/edit/index.tsx
+++ b/front/pages/org/[orgslug]/course/[courseid]/edit/index.tsx
@@ -11,7 +11,7 @@ import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChapters
import { useRouter } from "next/router";
import NewChapterModal from "../../../../../../components/modals/CourseEdit/NewChapter";
import NewElementModal from "../../../../../../components/modals/CourseEdit/NewElement";
-import { createElement } from "../../../../../../services/courses/elements";
+import { createElement, createFileElement } from "../../../../../../services/courses/elements";
function CourseEdit() {
const router = useRouter();
@@ -79,6 +79,15 @@ function CourseEdit() {
setNewElementModal(false);
};
+ // Submit File Upload
+ const submitFileElement = async (file: any, type: any, element: any, chapterId: string) => {
+ console.log("submitFileElement", file);
+ await updateChaptersMetadata(courseid, data);
+ await createFileElement(file, type, element, chapterId);
+ await getCourseChapters();
+ setNewElementModal(false);
+ };
+
const deleteChapterUI = async (chapterId: any) => {
console.log("deleteChapter", chapterId);
await deleteChapter(chapterId);
@@ -87,7 +96,7 @@ function CourseEdit() {
const updateChapters = () => {
console.log(data);
- updateChaptersMetadata(courseid,data);
+ updateChaptersMetadata(courseid, data);
};
/*
@@ -234,7 +243,14 @@ function CourseEdit() {
{newChapterModal && }
- {newElementModal && }
+ {newElementModal && (
+
+ )}
{winReady && (
diff --git a/front/pages/org/[orgslug]/course/[courseid]/element/[elementid]/index.tsx b/front/pages/org/[orgslug]/course/[courseid]/element/[elementid]/index.tsx
index 1a2a8300..94316459 100644
--- a/front/pages/org/[orgslug]/course/[courseid]/element/[elementid]/index.tsx
+++ b/front/pages/org/[orgslug]/course/[courseid]/element/[elementid]/index.tsx
@@ -8,6 +8,7 @@ import { useRouter } from "next/router";
import React, { useMemo } from "react";
import Layout from "../../../../../../../components/ui/Layout";
import { getElement } from "../../../../../../../services/courses/elements";
+import { getBackendUrl } from "../../../../../../../services/config";
function ElementPage() {
const router = useRouter();
@@ -31,25 +32,30 @@ function ElementPage() {
const output = useMemo(() => {
if (router.isReady && !isLoading) {
- console.log( "el",element.content);
-
- let content = Object.keys(element.content).length > 0 ? element.content : {
- "type": "doc",
- "content": [
- {
- "type": "paragraph",
- "content": [
- {
- "type": "text",
- "text": "Hello world, this is a example Canva ⚡️"
- }
- ]
- }
- ]
- }
- console.log("element", content);
+ console.log(element);
- return generateHTML(content, [Document, StarterKit, Paragraph, Text, Bold]);
+ if (element.type == "dynamic") {
+ let content =
+ Object.keys(element.content).length > 0
+ ? element.content
+ : {
+ type: "doc",
+ content: [
+ {
+ type: "paragraph",
+ content: [
+ {
+ type: "text",
+ text: "Hello world, this is a example Canva ⚡️",
+ },
+ ],
+ },
+ ],
+ };
+ console.log("element", content);
+
+ return generateHTML(content, [Document, StarterKit, Paragraph, Text, Bold]);
+ }
}
}, [element.content]);
@@ -62,7 +68,12 @@ function ElementPage() {
element
{element.name}
-
+
+ {element.type == "dynamic" && }
+ {/* todo : use apis & streams instead of this */}
+ {element.type == "video" && (
+
+ )}
)}
diff --git a/front/services/courses/elements.ts b/front/services/courses/elements.ts
index e0228b20..cf4b2777 100644
--- a/front/services/courses/elements.ts
+++ b/front/services/courses/elements.ts
@@ -1,59 +1,100 @@
import { getAPIUrl } from "../config";
export async function createElement(data: any, chapter_id: any) {
- data.content = {}
- console.log("data", data, chapter_id);
+ data.content = {};
+ console.log("data", data, chapter_id);
- // remove chapter_id from data
- delete data.chapterId;
-
- const HeadersConfig = new Headers({ "Content-Type": "application/json" });
+ // remove chapter_id from data
+ delete data.chapterId;
+
+ const HeadersConfig = new Headers({ "Content-Type": "application/json" });
+
+ const requestOptions: any = {
+ method: "POST",
+ headers: HeadersConfig,
+ redirect: "follow",
+ credentials: "include",
+ body: JSON.stringify(data),
+ };
+
+ const result: any = await fetch(`${getAPIUrl()}elements/?coursechapter_id=${chapter_id}`, requestOptions)
+ .then((result) => result.json())
+ .catch((error) => console.log("error", error));
+
+ console.log("result", result);
+
+ return result;
+}
+
+export async function createFileElement(file: File, type: string, data: any, chapter_id: any) {
- const requestOptions: any = {
- method: "POST",
- headers: HeadersConfig,
- redirect: "follow",
- credentials: "include",
- body: JSON.stringify(data),
- };
+
+ const HeadersConfig = new Headers();
+
+ // Send file thumbnail as form data
+ const formData = new FormData();
+ formData.append("coursechapter_id", chapter_id);
+ console.log("type" , type);
- const result: any = await fetch(`${getAPIUrl()}elements/?coursechapter_id=${chapter_id}`, requestOptions)
- .then((result) => result.json())
- .catch((error) => console.log("error", error));
-
- console.log("result", result);
-
- return result;
+
+ let endpoint = `${getAPIUrl()}elements/video`;
+
+ if (type === "video") {
+ formData.append("name", data.name);
+ formData.append("video_file", file);
+ endpoint = `${getAPIUrl()}elements/video`;
}
- export async function getElement(element_id: any) {
- const requestOptions: any = {
- method: "GET",
- redirect: "follow",
- credentials: "include",
- };
+ console.log();
- const result: any = await fetch(`${getAPIUrl()}elements/${element_id}`, requestOptions)
- .then((result) => result.json())
- .catch((error) => console.log("error", error));
-
- return result;
- }
- export async function updateElement(data: any, element_id: any) {
- const HeadersConfig = new Headers({ "Content-Type": "application/json" });
+ const requestOptions: any = {
+ method: "POST",
+ headers: HeadersConfig,
+ redirect: "follow",
+ credentials: "include",
+ body: formData,
+ };
+
+ const result: any = await fetch(endpoint, requestOptions)
+ .then((result) => result.json())
+ .catch((error) => console.log("error", error));
+
- const requestOptions: any = {
- method: "PUT",
- headers: HeadersConfig,
- redirect: "follow",
- credentials: "include",
- body: JSON.stringify(data),
- };
- const result: any = await fetch(`${getAPIUrl()}elements/${element_id}`, requestOptions)
- .then((result) => result.json())
- .catch((error) => console.log("error", error));
-
- return result;
- }
\ No newline at end of file
+ console.log("result", result);
+
+ return result;
+}
+
+export async function getElement(element_id: any) {
+ const requestOptions: any = {
+ method: "GET",
+ redirect: "follow",
+ credentials: "include",
+ };
+
+ const result: any = await fetch(`${getAPIUrl()}elements/${element_id}`, requestOptions)
+ .then((result) => result.json())
+ .catch((error) => console.log("error", error));
+
+ return result;
+}
+
+export async function updateElement(data: any, element_id: any) {
+ const HeadersConfig = new Headers({ "Content-Type": "application/json" });
+
+ const requestOptions: any = {
+ method: "PUT",
+ headers: HeadersConfig,
+ redirect: "follow",
+ credentials: "include",
+ body: JSON.stringify(data),
+ };
+
+ const result: any = await fetch(`${getAPIUrl()}elements/${element_id}`, requestOptions)
+ .then((result) => result.json())
+ .catch((error) => console.log("error", error));
+
+ return result;
+}
diff --git a/src/routers/courses/courses.py b/src/routers/courses/courses.py
index 450d63ad..32ad4238 100644
--- a/src/routers/courses/courses.py
+++ b/src/routers/courses/courses.py
@@ -9,7 +9,7 @@ router = APIRouter()
@router.post("/")
-async def api_create_course(org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None):
+async def api_create_course(org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None):
"""
Create new Course
"""
diff --git a/src/routers/courses/elements.py b/src/routers/courses/elements.py
index 6d2e3d99..32e6ae9d 100644
--- a/src/routers/courses/elements.py
+++ b/src/routers/courses/elements.py
@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, UploadFile, Form
-from src.services.courses.elements import *
+from src.services.courses.elements.elements import *
from src.dependencies.auth import get_current_user
+from src.services.courses.elements.video import create_video_element
router = APIRouter()
@@ -20,6 +21,7 @@ async def api_get_element(element_id: str, current_user: PublicUser = Depends(ge
"""
return await get_element(element_id, current_user=current_user)
+
@router.get("/coursechapter/{coursechapter_id}")
async def api_get_elements(coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
"""
@@ -27,6 +29,7 @@ async def api_get_elements(coursechapter_id: str, current_user: PublicUser = Dep
"""
return await get_elements(coursechapter_id, current_user)
+
@router.put("/{element_id}")
async def api_update_element(element_object: Element, element_id: str, current_user: PublicUser = Depends(get_current_user)):
"""
@@ -42,4 +45,12 @@ async def api_delete_element(element_id: str, current_user: PublicUser = Depends
"""
return await delete_element(element_id, current_user)
+# Video Element
+
+@router.post("/video")
+async def api_create_video_element(name: str = Form() , coursechapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), video_file: UploadFile | None = None):
+ """
+ Create new Element
+ """
+ return await create_video_element(name, coursechapter_id, current_user, video_file)
diff --git a/src/services/courses/chapters.py b/src/services/courses/chapters.py
index 11e1460f..824eb2ac 100644
--- a/src/services/courses/chapters.py
+++ b/src/services/courses/chapters.py
@@ -4,7 +4,7 @@ from typing import List
from uuid import uuid4
from pydantic import BaseModel
from src.services.courses.courses import Course, CourseInDB
-from src.services.courses.elements import Element, ElementInDB
+from src.services.courses.elements.elements import Element, ElementInDB
from src.services.database import create_config_collection, check_database, create_database, learnhouseDB, learnhouseDB
from src.services.security import verify_user_rights_with_roles
from src.services.users import PublicUser
diff --git a/src/services/courses/courses.py b/src/services/courses/courses.py
index 15178904..5f44ddde 100644
--- a/src/services/courses/courses.py
+++ b/src/services/courses/courses.py
@@ -3,7 +3,7 @@ import os
from typing import List
from uuid import uuid4
from pydantic import BaseModel
-from src.services.courses.elements import ElementInDB
+from src.services.courses.elements.elements import ElementInDB
from src.services.uploads import upload_thumbnail
from src.services.users import PublicUser, User
from src.services.database import create_config_collection, check_database, create_database, learnhouseDB
diff --git a/src/services/courses/elements.py b/src/services/courses/elements/elements.py
similarity index 100%
rename from src/services/courses/elements.py
rename to src/services/courses/elements/elements.py
diff --git a/src/services/courses/elements/video.py b/src/services/courses/elements/video.py
new file mode 100644
index 00000000..d66d0851
--- /dev/null
+++ b/src/services/courses/elements/video.py
@@ -0,0 +1,58 @@
+from pydantic import BaseModel
+from src.services.database import create_config_collection, check_database, create_database, learnhouseDB
+from src.services.security import verify_user_rights_with_roles
+from src.services.uploads import upload_video
+from src.services.users import PublicUser, User
+from src.services.courses.elements.elements import ElementInDB, Element
+from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File
+from uuid import uuid4
+from datetime import datetime
+
+
+async def create_video_element(name: str, coursechapter_id: str, current_user: PublicUser, video_file: UploadFile | None = None):
+ await check_database()
+ elements = learnhouseDB["elements"]
+ coursechapters = learnhouseDB["coursechapters"]
+
+ # generate element_id
+ element_id = str(f"element_{uuid4()}")
+
+ video_format = video_file.filename.split(".")[-1]
+ element_object = ElementInDB(
+ element_id=element_id,
+ coursechapter_id=coursechapter_id,
+ name=name,
+ type="video",
+ content={
+ "video": {
+ "filename": "video."+video_format,
+ "element_id": element_id,
+ }
+ },
+ creationDate=str(datetime.now()),
+ updateDate=str(datetime.now()),
+ )
+
+ hasRoleRights = await verify_user_rights_with_roles("create", current_user.user_id, element_id)
+
+ if not hasRoleRights:
+ raise HTTPException(
+ status_code=status.HTTP_409_CONFLICT, detail="Roles : Insufficient rights to perform this action")
+
+ # create element
+ element = ElementInDB(**element_object.dict())
+ elements.insert_one(element.dict())
+
+ # upload video
+ if video_file:
+ print("uploading video")
+ # get videofile format
+
+ await upload_video(video_file, video_file.filename, element_id)
+
+ # todo : choose whether to update the chapter or not
+ # update chapter
+ coursechapters.update_one({"coursechapter_id": coursechapter_id}, {
+ "$addToSet": {"elements": element_id}})
+
+ return element
diff --git a/src/services/uploads.py b/src/services/uploads.py
index 8ac027fa..516f0b1c 100644
--- a/src/services/uploads.py
+++ b/src/services/uploads.py
@@ -1,12 +1,33 @@
+import os
+
+
async def upload_thumbnail(thumbnail_file, name_in_disk):
- contents = thumbnail_file.file.read()
- try:
- with open(f"content/uploads/img/{name_in_disk}", 'wb') as f:
- f.write(contents)
- f.close()
-
- except Exception as e:
- print(e)
- return {"message": "There was an error uploading the file"}
- finally:
- thumbnail_file.file.close()
\ No newline at end of file
+ contents = thumbnail_file.file.read()
+ try:
+ with open(f"content/uploads/img/{name_in_disk}", 'wb') as f:
+ f.write(contents)
+ f.close()
+
+ except Exception as e:
+ print(e)
+ return {"message": "There was an error uploading the file"}
+ finally:
+ thumbnail_file.file.close()
+
+
+async def upload_video(video_file, name_in_disk, element_id):
+ contents = video_file.file.read()
+ video_format = video_file.filename.split(".")[-1]
+ # create folder
+ os.mkdir(f"content/uploads/video/{element_id}")
+
+ try:
+ with open(f"content/uploads/video/{element_id}/video.{video_format}", 'wb') as f:
+ f.write(contents)
+ f.close()
+
+ except Exception as e:
+ print(e)
+ return {"message": "There was an error uploading the file"}
+ finally:
+ video_file.file.close()