From 42d74aebde72ac05d96b543f9e900cfcf24bce10 Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 29 Nov 2022 20:39:13 +0100 Subject: [PATCH 1/3] feat: backend create & upload video --- .gitignore | 1 + src/routers/courses/courses.py | 2 +- src/routers/courses/elements.py | 13 ++++- src/services/courses/chapters.py | 2 +- src/services/courses/courses.py | 2 +- .../courses/{ => elements}/elements.py | 0 src/services/courses/elements/video.py | 55 +++++++++++++++++++ src/services/uploads.py | 43 +++++++++++---- 8 files changed, 103 insertions(+), 15 deletions(-) rename src/services/courses/{ => elements}/elements.py (100%) create mode 100644 src/services/courses/elements/video.py 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/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..3c149408 --- /dev/null +++ b/src/services/courses/elements/video.py @@ -0,0 +1,55 @@ +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()}") + + element_object = ElementInDB( + element_id=element_id, + coursechapter_id=coursechapter_id, + name=name, + type="video", + content={ + "video": { + "filename": video_file.filename, + "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") + 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..243ad8d0 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() + + # create folder + os.mkdir(f"content/uploads/video/{element_id}") + + try: + with open(f"content/uploads/video/{element_id}/{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: + video_file.file.close() From c425b55c93de38dbafe4d281cd4caa6a67c4f7eb Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 29 Nov 2022 23:38:42 +0100 Subject: [PATCH 2/3] feat: add video upload element --- .../modals/CourseEdit/NewElement.tsx | 77 ++++++---- .../NewElementModal/DynamicCanva.tsx | 36 +++++ .../CourseEdit/NewElementModal/Video.tsx | 37 +++++ .../course/[courseid]/edit/index.tsx | 22 ++- front/services/courses/elements.ts | 135 ++++++++++++------ 5 files changed, 230 insertions(+), 77 deletions(-) create mode 100644 front/components/modals/CourseEdit/NewElementModal/DynamicCanva.tsx create mode 100644 front/components/modals/CourseEdit/NewElementModal/Video.tsx 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/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; +} From 0997ea4135d67d376eac3ff489125f8727d70da9 Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 29 Nov 2022 23:55:46 +0100 Subject: [PATCH 3/3] feat: init impl video watch --- .../[courseid]/element/[elementid]/index.tsx | 49 ++++++++++++------- src/services/courses/elements/video.py | 5 +- src/services/uploads.py | 4 +- 3 files changed, 36 insertions(+), 22 deletions(-) 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/src/services/courses/elements/video.py b/src/services/courses/elements/video.py index 3c149408..d66d0851 100644 --- a/src/services/courses/elements/video.py +++ b/src/services/courses/elements/video.py @@ -17,6 +17,7 @@ async def create_video_element(name: str, coursechapter_id: str, current_user: # 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, @@ -24,7 +25,7 @@ async def create_video_element(name: str, coursechapter_id: str, current_user: type="video", content={ "video": { - "filename": video_file.filename, + "filename": "video."+video_format, "element_id": element_id, } }, @@ -45,6 +46,8 @@ async def create_video_element(name: str, coursechapter_id: str, current_user: # 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 diff --git a/src/services/uploads.py b/src/services/uploads.py index 243ad8d0..516f0b1c 100644 --- a/src/services/uploads.py +++ b/src/services/uploads.py @@ -17,12 +17,12 @@ async def upload_thumbnail(thumbnail_file, name_in_disk): 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}/{name_in_disk}", 'wb') as f: + with open(f"content/uploads/video/{element_id}/video.{video_format}", 'wb') as f: f.write(contents) f.close()