Merge pull request #11 from learnhouse/feat/video-elements

Feat/video elements
This commit is contained in:
Badr B 2022-11-29 23:58:59 +01:00 committed by GitHub
commit a371394670
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 366 additions and 111 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

View file

@ -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 (
<Modal>
<h1>
Add New Element <button onClick={closeModal}>X</button>
</h1>
<input type="text" onChange={handleElementNameChange} placeholder="Element Name" /> <br />
<input type="text" onChange={handleElementDescriptionChange} placeholder="Element Description" />
<button onClick={ () => {setSelectedView("home")}}>
<ArrowLeftIcon />
</button>
<button onClick={closeModal}>
<Cross1Icon />
</button>
<h1>Add New Element</h1>
<br />
<button onClick={handleSubmit}>Add Element</button>
{selectedView === "home" && (
<ElementChooserWrapper>
<ElementButton onClick={() => {setSelectedView("dynamic")}}>📄</ElementButton>
<ElementButton onClick={() => {setSelectedView("video")}}>📹</ElementButton>
</ElementChooserWrapper>
)}
{selectedView === "dynamic" && (
<DynamicCanvaModal submitElement={submitElement} chapterId={chapterId} />
)}
{selectedView === "video" && (
<VideoModal submitFileElement={submitFileElement} chapterId={chapterId} />
)}
</Modal>
);
}
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;

View file

@ -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 (
<div>
<div>
<input type="text" onChange={handleElementNameChange} placeholder="Element Name" /> <br />
<input type="text" onChange={handleElementDescriptionChange} placeholder="Element Description" />
<br />
<button onClick={handleSubmit}>Add Element</button>
</div>
</div>
);
}
export default DynamicCanvaModal;

View file

@ -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<any>) => {
setVideo(event.target.files[0]);
};
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div>
<input type="text" placeholder="video title" onChange={handleNameChange} />
<br />
<br />
<input type="file" onChange={handleVideoChange} name="video" id="" />
<br />
<br />
<button onClick={handleSubmit}>Send</button>
</div>
);
}
export default VideoModal;

View file

@ -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() {
</button>
</Title>
{newChapterModal && <NewChapterModal closeModal={closeNewChapterModal} submitChapter={submitChapter}></NewChapterModal>}
{newElementModal && <NewElementModal closeModal={closeNewElementModal} submitElement={submitElement} chapterId={newElementModalData}></NewElementModal>}
{newElementModal && (
<NewElementModal
closeModal={closeNewElementModal}
submitFileElement={submitFileElement}
submitElement={submitElement}
chapterId={newElementModalData}
></NewElementModal>
)}
<br />
{winReady && (

View file

@ -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);
console.log(element);
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);
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]);
return generateHTML(content, [Document, StarterKit, Paragraph, Text, Bold]);
}
}
}, [element.content]);
@ -62,7 +68,12 @@ function ElementPage() {
<p>element</p>
<h1>{element.name} </h1>
<hr />
<div dangerouslySetInnerHTML={{ __html: output } as any}></div>
{element.type == "dynamic" && <div dangerouslySetInnerHTML={{ __html: output } as any}></div>}
{/* todo : use apis & streams instead of this */}
{element.type == "video" && (
<video controls src={`${getBackendUrl()}content/uploads/video/${element.content.video.element_id}/${element.content.video.filename}`}></video>
)}
</div>
)}
</Layout>

View file

@ -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;
// remove chapter_id from data
delete data.chapterId;
const HeadersConfig = new Headers({ "Content-Type": "application/json" });
const HeadersConfig = new Headers({ "Content-Type": "application/json" });
const requestOptions: any = {
method: "POST",
headers: HeadersConfig,
redirect: "follow",
credentials: "include",
body: JSON.stringify(data),
};
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));
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);
console.log("result", result);
return result;
return result;
}
export async function createFileElement(file: File, type: string, data: any, chapter_id: any) {
const HeadersConfig = new Headers();
// Send file thumbnail as form data
const formData = new FormData();
formData.append("coursechapter_id", chapter_id);
console.log("type" , type);
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;
}
const requestOptions: any = {
method: "POST",
headers: HeadersConfig,
redirect: "follow",
credentials: "include",
body: formData,
};
export async function updateElement(data: any, element_id: any) {
const HeadersConfig = new Headers({ "Content-Type": "application/json" });
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;
}
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;
}

View file

@ -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
"""

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,12 +1,33 @@
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()
import os
except Exception as e:
print(e)
return {"message": "There was an error uploading the file"}
finally:
thumbnail_file.file.close()
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()
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()