Merge pull request #84 from learnhouse/swve/eng-7-youtube-activity

Implement YouTube Activity
This commit is contained in:
Badr B 2023-05-14 17:30:57 +02:00 committed by GitHub
commit 11f008dfe4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 445 additions and 65 deletions

View file

@ -11,7 +11,7 @@ import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChapters
import { useRouter } from "next/navigation";
import NewChapterModal from "@components/Modals/Chapters/NewChapter";
import NewActivityModal from "@components/Modals/Activities/Create/NewActivity";
import { createActivity, createFileActivity } from "@services/courses/activities";
import { createActivity, createFileActivity, createExternalVideoActivity } from "@services/courses/activities";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import Modal from "@components/UI/Modal/Modal";
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
@ -89,13 +89,21 @@ function CourseEdit(params: any) {
// Submit File Upload
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
console.log("submitFileActivity", file);
await updateChaptersMetadata(courseid, data);
await createFileActivity(file, type, activity, chapterId);
await getCourseChapters();
setNewActivityModal(false);
};
// Submit YouTube Video Upload
const submitExternalVideo = async (external_video_data : any, activity: any, chapterId: string) => {
console.log("submitExternalVideo", external_video_data);
await updateChaptersMetadata(courseid, data);
await createExternalVideoActivity(external_video_data , activity, chapterId);
await getCourseChapters();
setNewActivityModal(false);
};
const deleteChapterUI = async (chapterId: any) => {
console.log("deleteChapter", chapterId);
await deleteChapter(chapterId);
@ -268,6 +276,7 @@ function CourseEdit(params: any) {
dialogContent={<NewActivityModal
closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity}
chapterId={newActivityModalData}
></NewActivityModal>}

View file

@ -16,6 +16,11 @@ interface CourseProps {
courses: any;
}
// function to remove "course_" from the course_id
function removeCoursePrefix(course_id: string) {
return course_id.replace("course_", "");
}
function Courses(props: CourseProps) {
const orgslug = props.orgslug;
const courses = props.courses;
@ -29,10 +34,7 @@ function Courses(props: CourseProps) {
setNewCourseModal(false);
}
// function to remove "course_" from the course_id
function removeCoursePrefix(course_id: string) {
return course_id.replace("course_", "");
}
return (
@ -62,7 +64,7 @@ function Courses(props: CourseProps) {
{courses.map((course: any) => (
<div key={course.course_id}>
<AdminEditsArea course={course} course_id={course.course_id} deleteCourses={deleteCourses} />
<AdminEditsArea course={course} orgslug={orgslug} course_id={course.course_id} deleteCourses={deleteCourses} />
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getBackendUrl()}content/uploads/img/${course.thumbnail})` }}>
@ -131,7 +133,7 @@ const AdminEditsArea = (props: any) => {
<button className="rounded-md text-sm px-3 font-bold text-red-800 bg-red-200 w-16 flex justify-center items-center" onClick={() => props.deleteCourses(props.course_id)}>
Delete <Trash size={10}></Trash>
</button>
<Link href={getUriWithOrg(props.orgslug, "/course/" + props.course_id + "/edit")}>
<Link href={getUriWithOrg(props.orgslug, "/course/" + removeCoursePrefix(props.course_id) + "/edit")}>
<button className="rounded-md text-sm px-3 font-bold text-orange-800 bg-orange-200 w-16 flex justify-center items-center">
Edit <Edit2 size={10}></Edit2>
</button>

View file

@ -9,7 +9,7 @@ import VideoModal from "./NewActivityModal/Video";
import Image from "next/image";
import DocumentPdfModal from "./NewActivityModal/DocumentPdf";
function NewActivityModal({ closeModal, submitActivity, submitFileActivity, chapterId }: any) {
function NewActivityModal({ closeModal, submitActivity, submitFileActivity, submitExternalVideo, chapterId }: any) {
const [selectedView, setSelectedView] = useState("home");
@ -43,7 +43,8 @@ function NewActivityModal({ closeModal, submitActivity, submitFileActivity, chap
)}
{selectedView === "video" && (
<VideoModal submitFileActivity={submitFileActivity} chapterId={chapterId} />
<VideoModal submitFileActivity={submitFileActivity} submitExternalVideo={submitExternalVideo}
chapterId={chapterId} />
)}
{selectedView === "documentpdf" && (

View file

@ -2,11 +2,21 @@ import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, FormMessage, Input
import React, { useState } from "react";
import * as Form from '@radix-ui/react-form';
import BarLoader from "react-spinners/BarLoader";
import { Youtube } from "lucide-react";
function VideoModal({ submitFileActivity, chapterId }: any) {
interface ExternalVideoObject {
name: string,
type: string,
uri: string
}
function VideoModal({ submitFileActivity, submitExternalVideo, chapterId }: any) {
const [video, setVideo] = React.useState(null) as any;
const [isSubmitting, setIsSubmitting] = useState(false);
const [name, setName] = React.useState("");
const [youtubeUrl, setYoutubeUrl] = React.useState("");
const [selectedView, setSelectedView] = React.useState("file") as any;
const handleVideoChange = (event: React.ChangeEvent<any>) => {
setVideo(event.target.files[0]);
@ -16,18 +26,35 @@ function VideoModal({ submitFileActivity, chapterId }: any) {
setName(event.target.value);
};
const handleYoutubeUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setYoutubeUrl(event.target.value);
};
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsSubmitting(true);
let status = await submitFileActivity(video, "video", { name, type: "video" }, chapterId);
setIsSubmitting(false);
if (selectedView === "file") {
let status = await submitFileActivity(video, "video", { name, type: "video" }, chapterId);
setIsSubmitting(false);
}
if (selectedView === "youtube") {
let external_video_object: ExternalVideoObject = {
name,
type: "youtube",
uri: youtubeUrl
}
let status = await submitExternalVideo(external_video_object, 'activity' ,chapterId);
setIsSubmitting(false);
}
};
/* 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 submitFileActivity function */
return (
<FormLayout onSubmit={handleSubmit}>
<FormLayout onSubmit={handleSubmit}>
<FormField name="video-activity-name">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Video name</FormLabel>
@ -37,20 +64,43 @@ function VideoModal({ submitFileActivity, chapterId }: any) {
<Input onChange={handleNameChange} type="text" required />
</Form.Control>
</FormField>
<FormField name="video-activity-file">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Video file</FormLabel>
<FormMessage match="valueMissing">Please provide a video for your activity</FormMessage>
</Flex>
<Form.Control asChild>
<input type="file" onChange={handleVideoChange} required />
</Form.Control>
</FormField>
<div className="flex flex-col rounded-md bg-gray-50 outline-dashed outline-gray-200">
<div className="">
<div className="flex m-4 justify-center space-x-2 mb-0">
<div onClick={() => { setSelectedView("file") }} className="rounded-full bg-slate-900 text-zinc-50 py-2 px-4 text-sm drop-shadow-md hover:cursor-pointer hover:bg-slate-700 ">Video upload</div>
<div onClick={() => { setSelectedView("youtube") }} className="rounded-full bg-slate-900 text-zinc-50 py-2 px-4 text-sm drop-shadow-md hover:cursor-pointer hover:bg-slate-700">YouTube Video</div>
</div>
{selectedView === "file" && (<div className="p-4 justify-center m-auto align-middle">
<FormField name="video-activity-file">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Video file</FormLabel>
<FormMessage match="valueMissing">Please provide a video for your activity</FormMessage>
</Flex>
<Form.Control asChild>
<input type="file" onChange={handleVideoChange} required />
</Form.Control>
</FormField>
</div>)}
{selectedView === "youtube" && (
<div className="p-4 justify-center m-auto align-middle">
<FormField name="video-activity-file">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel className="flex justify-center align-middle"><Youtube className="m-auto pr-1" /><span className="flex">YouTube URL</span></FormLabel>
<FormMessage match="valueMissing">Please provide a video for your activity</FormMessage>
</Flex>
<Form.Control asChild>
<Input className="bg-white" onChange={handleYoutubeUrlChange} type="text" required />
</Form.Control>
</FormField>
</div>
)}
</div>
</div>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
<Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? <BarLoader cssOverride={{borderRadius:60,}} width={60} color="#ffffff" /> : "Create activity"}
<ButtonBlack className="bg-black" type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" /> : "Create activity"}
</ButtonBlack>
</Form.Submit>
</Flex>

View file

@ -1,8 +1,12 @@
import { getBackendUrl } from "@services/config/config";
import React from "react";
import styled from "styled-components";
import YouTube from 'react-youtube';
function VideoActivity({ activity, course }: { activity: any; course: any }) {
const [videoId, setVideoId] = React.useState('');
const [videoType, setVideoType] = React.useState('');
function getChapterName() {
let chapterName = "";
let chapterId = activity.chapter_id;
@ -14,19 +18,68 @@ function VideoActivity({ activity, course }: { activity: any; course: any }) {
return chapterName;
}
function getYouTubeEmbed(url: any) {
// Extract video ID from the YouTube URL
var videoId = url.match(/(?:\?v=|\/embed\/|\/\d\/|\/vi\/|\/v\/|https?:\/\/(?:www\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^#\&\?\/]+)/)[1];
// Create the embed object
var embedObject = {
videoId: videoId,
width: 560,
height: 315
};
return embedObject;
}
React.useEffect(() => {
console.log(activity);
if (activity.content.video) {
setVideoType('video');
}
if (activity.content.external_video) {
setVideoType('external_video');
setVideoId(getYouTubeEmbed(activity.content.external_video.uri).videoId);
}
}, [activity]);
return (
<VideoActivityLayout>
<VideoTitle>
<p>Chapter : {getChapterName()}</p>
{activity.name}
<p>{getChapterName()}</p>
<p>{activity.name}</p>
</VideoTitle>
<VideoPlayerWrapper>
<video controls src={`${getBackendUrl()}content/uploads/video/${activity.content.video.activity_id}/${activity.content.video.filename}`}></video>
</VideoPlayerWrapper>
{videoType === 'video' && (
<VideoPlayerWrapper>
<video controls src={`${getBackendUrl()}content/uploads/video/${activity.content.video.activity_id}/${activity.content.video.filename}`}></video>
</VideoPlayerWrapper>
)}
{videoType === 'external_video' && (
<VideoPlayerWrapper>
<YouTube
className="rounded-md overflow-hidden"
opts={
{
width: '1300',
height: '500',
playerVars: {
autoplay: 0,
},
}
}
videoId={videoId} />
</VideoPlayerWrapper>
)}
</VideoActivityLayout>
);
}
export default VideoActivity;
const VideoActivityLayout = styled.div`

101
front/package-lock.json generated
View file

@ -34,6 +34,7 @@
"react-hot-toast": "^2.4.1",
"react-katex": "^3.0.1",
"react-spinners": "^0.13.8",
"react-youtube": "^10.1.0",
"styled-components": "^6.0.0-beta.9",
"swr": "^2.0.1",
"uuid": "^9.0.0",
@ -5176,8 +5177,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.2.12",
@ -6206,6 +6206,11 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
"node_modules/load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA=="
},
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
@ -7283,6 +7288,22 @@
}
}
},
"node_modules/react-youtube": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz",
"integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==",
"dependencies": {
"fast-deep-equal": "3.1.3",
"prop-types": "15.8.1",
"youtube-player": "5.5.2"
},
"engines": {
"node": ">= 14.x"
},
"peerDependencies": {
"react": ">=0.14.1"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -7648,6 +7669,11 @@
"readable-stream": "^3.6.0"
}
},
"node_modules/sister": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz",
"integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA=="
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -8581,6 +8607,29 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/youtube-player": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
"integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==",
"dependencies": {
"debug": "^2.6.6",
"load-script": "^1.0.0",
"sister": "^3.0.0"
}
},
"node_modules/youtube-player/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/youtube-player/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/zeed-dom": {
"version": "0.9.26",
"resolved": "https://registry.npmjs.org/zeed-dom/-/zeed-dom-0.9.26.tgz",
@ -12164,8 +12213,7 @@
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-glob": {
"version": "3.2.12",
@ -12914,6 +12962,11 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true
},
"load-script": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA=="
},
"localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
@ -13659,6 +13712,16 @@
"tslib": "^2.0.0"
}
},
"react-youtube": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-youtube/-/react-youtube-10.1.0.tgz",
"integrity": "sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==",
"requires": {
"fast-deep-equal": "3.1.3",
"prop-types": "15.8.1",
"youtube-player": "5.5.2"
}
},
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -13910,6 +13973,11 @@
"readable-stream": "^3.6.0"
}
},
"sister": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sister/-/sister-3.0.2.tgz",
"integrity": "sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA=="
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -14531,6 +14599,31 @@
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"youtube-player": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/youtube-player/-/youtube-player-5.5.2.tgz",
"integrity": "sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==",
"requires": {
"debug": "^2.6.6",
"load-script": "^1.0.0",
"sister": "^3.0.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}
}
},
"zeed-dom": {
"version": "0.9.26",
"resolved": "https://registry.npmjs.org/zeed-dom/-/zeed-dom-0.9.26.tgz",

View file

@ -35,6 +35,7 @@
"react-hot-toast": "^2.4.1",
"react-katex": "^3.0.1",
"react-spinners": "^0.13.8",
"react-youtube": "^10.1.0",
"styled-components": "^6.0.0-beta.9",
"swr": "^2.0.1",
"uuid": "^9.0.0",

View file

@ -36,6 +36,16 @@ export async function createFileActivity(file: File, type: string, data: any, ch
return res;
}
export async function createExternalVideoActivity(data: any, activity: any, chapter_id: any) {
// add coursechapter_id to data
data.coursechapter_id = chapter_id;
data.activity_id = activity.id;
const result = await fetch(`${getAPIUrl()}activities/external_video?coursechapter_id=${chapter_id}`, RequestBody("POST", data));
const res = await result.json();
return res;
}
export async function getActivity(activity_id: any) {
const result = await fetch(`${getAPIUrl()}activities/${activity_id}`, RequestBody("GET", null));
const res = await result.json();

View file

@ -3,7 +3,6 @@ module.exports = {
content: [
'./app/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
],
theme: {
extend: {},

View file

@ -2,21 +2,37 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request
from src.services.courses.activities.activities import *
from src.security.auth import get_current_user
from src.services.courses.activities.pdf import create_documentpdf_activity
from src.services.courses.activities.video import create_video_activity
from src.services.courses.activities.video import (
ExternalVideo,
create_external_video_activity,
create_video_activity,
)
router = APIRouter()
@router.post("/")
async def api_create_activity(request: Request, activity_object: Activity, org_id: str, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_create_activity(
request: Request,
activity_object: Activity,
org_id: str,
coursechapter_id: str,
current_user: PublicUser = Depends(get_current_user),
):
"""
Create new activity
"""
return await create_activity(request, activity_object, org_id, coursechapter_id, current_user)
return await create_activity(
request, activity_object, org_id, coursechapter_id, current_user
)
@router.get("/{activity_id}")
async def api_get_activity(request: Request, activity_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_get_activity(
request: Request,
activity_id: str,
current_user: PublicUser = Depends(get_current_user),
):
"""
Get single activity by activity_id
"""
@ -24,15 +40,24 @@ async def api_get_activity(request: Request, activity_id: str, current_user: Pu
@router.get("/coursechapter/{coursechapter_id}")
async def api_get_activities(request: Request, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_get_activities(
request: Request,
coursechapter_id: str,
current_user: PublicUser = Depends(get_current_user),
):
"""
Get CourseChapter activities
Get CourseChapter activities
"""
return await get_activities(request, coursechapter_id, current_user)
@router.put("/{activity_id}")
async def api_update_activity(request: Request, activity_object: Activity, activity_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_update_activity(
request: Request,
activity_object: Activity,
activity_id: str,
current_user: PublicUser = Depends(get_current_user),
):
"""
Update activity by activity_id
"""
@ -40,25 +65,61 @@ async def api_update_activity(request: Request, activity_object: Activity, activ
@router.delete("/{activity_id}")
async def api_delete_activity(request: Request, activity_id: str, org_id: str, current_user: PublicUser = Depends(get_current_user)):
async def api_delete_activity(
request: Request,
activity_id: str,
current_user: PublicUser = Depends(get_current_user),
):
"""
Delete activity by activity_id
"""
return await delete_activity(request, activity_id, current_user)
# Video activity
@router.post("/video")
async def api_create_video_activity(request: Request, org_id: str, name: str = Form(), coursechapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), video_file: UploadFile | None = None):
async def api_create_video_activity(
request: Request,
name: str = Form(),
coursechapter_id: str = Form(),
current_user: PublicUser = Depends(get_current_user),
video_file: UploadFile | None = None,
):
"""
Create new activity
"""
return await create_video_activity(request, name, coursechapter_id, current_user, video_file)
return await create_video_activity(
request, name, coursechapter_id, current_user, video_file
)
@router.post("/external_video")
async def api_create_external_video_activity(
request: Request,
external_video: ExternalVideo,
current_user: PublicUser = Depends(get_current_user),
):
"""
Create new activity
"""
return await create_external_video_activity(
request, current_user, external_video
)
@router.post("/documentpdf")
async def api_create_documentpdf_activity(request: Request, org_id: str, name: str = Form(), coursechapter_id: str = Form(), current_user: PublicUser = Depends(get_current_user), pdf_file: UploadFile | None = None):
async def api_create_documentpdf_activity(
request: Request,
name: str = Form(),
coursechapter_id: str = Form(),
current_user: PublicUser = Depends(get_current_user),
pdf_file: UploadFile | None = None,
):
"""
Create new activity
"""
return await create_documentpdf_activity(request, name, coursechapter_id, current_user, pdf_file)
return await create_documentpdf_activity(
request, name, coursechapter_id, current_user, pdf_file
)

View file

@ -1,3 +1,6 @@
from typing import Literal
from pydantic import BaseModel
from src.security.security import verify_user_rights_with_roles
from src.services.courses.activities.uploads.videos import upload_video
from src.services.users.users import PublicUser
@ -7,37 +10,54 @@ from uuid import uuid4
from datetime import datetime
async def create_video_activity(request: Request,name: str, coursechapter_id: str, current_user: PublicUser, video_file: UploadFile | None = None):
async def create_video_activity(
request: Request,
name: str,
coursechapter_id: str,
current_user: PublicUser,
video_file: UploadFile | None = None,
):
activities = request.app.db["activities"]
courses = request.app.db["courses"]
# generate activity_id
activity_id = str(f"activity_{uuid4()}")
# get org_id from course
# get org_id from course
coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id})
org_id = coursechapter["org_id"]
{"chapters_content.coursechapter_id": coursechapter_id}
)
if not coursechapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="CourseChapter : No coursechapter found",
)
org_id = coursechapter["org_id"]
# check if video_file is not None
if not video_file:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video : No video file provided")
status_code=status.HTTP_409_CONFLICT,
detail="Video : No video file provided",
)
if video_file.content_type not in ["video/mp4", "video/webm"]:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video : Wrong video format")
status_code=status.HTTP_409_CONFLICT, detail="Video : Wrong video format"
)
# get video format
if video_file.filename:
if video_file.filename:
video_format = video_file.filename.split(".")[-1]
else:
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video : No video file provided")
status_code=status.HTTP_409_CONFLICT,
detail="Video : No video file provided",
)
activity_object = ActivityInDB(
org_id=org_id,
activity_id=activity_id,
@ -46,7 +66,7 @@ async def create_video_activity(request: Request,name: str, coursechapter_id: s
type="video",
content={
"video": {
"filename": "video."+video_format,
"filename": "video." + video_format,
"activity_id": activity_id,
}
},
@ -54,11 +74,15 @@ async def create_video_activity(request: Request,name: str, coursechapter_id: s
updateDate=str(datetime.now()),
)
hasRoleRights = await verify_user_rights_with_roles(request,"create", current_user.user_id, activity_id, element_org_id=org_id)
hasRoleRights = await verify_user_rights_with_roles(
request, "create", current_user.user_id, activity_id, element_org_id=org_id
)
if not hasRoleRights:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Roles : Insufficient rights to perform this action")
status_code=status.HTTP_409_CONFLICT,
detail="Roles : Insufficient rights to perform this action",
)
# create activity
activity = ActivityInDB(**activity_object.dict())
@ -67,11 +91,88 @@ async def create_video_activity(request: Request,name: str, coursechapter_id: s
# upload video
if video_file:
# get videofile format
await upload_video(video_file, activity_id)
await upload_video(video_file, activity_id)
# todo : choose whether to update the chapter or not
# update chapter
await courses.update_one({"chapters_content.coursechapter_id": coursechapter_id}, {
"$addToSet": {"chapters_content.$.activities": activity_id}})
await courses.update_one(
{"chapters_content.coursechapter_id": coursechapter_id},
{"$addToSet": {"chapters_content.$.activities": activity_id}},
)
return activity
class ExternalVideo(BaseModel):
name: str
uri: str
type: Literal["youtube", "vimeo"]
coursechapter_id: str
class ExternalVideoInDB(BaseModel):
activity_id: str
async def create_external_video_activity(
request: Request,
current_user: PublicUser,
data: ExternalVideo,
):
activities = request.app.db["activities"]
courses = request.app.db["courses"]
# generate activity_id
activity_id = str(f"activity_{uuid4()}")
# get org_id from course
coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": data.coursechapter_id}
)
if not coursechapter:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="CourseChapter : No coursechapter found",
)
org_id = coursechapter["org_id"]
activity_object = ActivityInDB(
org_id=org_id,
activity_id=activity_id,
coursechapter_id=data.coursechapter_id,
name=data.name,
type="video",
content={
"external_video": {
"uri": data.uri,
"activity_id": activity_id,
"type": data.type,
}
},
creationDate=str(datetime.now()),
updateDate=str(datetime.now()),
)
hasRoleRights = await verify_user_rights_with_roles(
request, "create", current_user.user_id, activity_id, element_org_id=org_id
)
if not hasRoleRights:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Roles : Insufficient rights to perform this action",
)
# create activity
activity = ActivityInDB(**activity_object.dict())
await activities.insert_one(activity.dict())
# todo : choose whether to update the chapter or not
# update chapter
await courses.update_one(
{"chapters_content.coursechapter_id": data.coursechapter_id},
{"$addToSet": {"chapters_content.$.activities": activity_id}},
)
return activity