Merge pull request #99 from learnhouse/swve/eng-39-implement-storage-with-s3-r2

Init S3/R2 Integration + Remake content upload and diffusion across the app
This commit is contained in:
Badr B 2023-07-03 14:31:21 +01:00 committed by GitHub
commit f4c596278d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 553 additions and 231 deletions

View file

@ -1,4 +1,4 @@
from typing import Optional from typing import Literal, Optional
from pydantic import BaseModel from pydantic import BaseModel
import os import os
import yaml import yaml
@ -22,6 +22,16 @@ class SecurityConfig(BaseModel):
auth_jwt_secret_key: str auth_jwt_secret_key: str
class S3ApiConfig(BaseModel):
bucket_name: str | None
endpoint_url: str | None
class ContentDeliveryConfig(BaseModel):
type: Literal["filesystem", "s3api"]
s3api: S3ApiConfig
class HostingConfig(BaseModel): class HostingConfig(BaseModel):
domain: str domain: str
ssl: bool ssl: bool
@ -31,6 +41,7 @@ class HostingConfig(BaseModel):
self_hosted: bool self_hosted: bool
sentry_config: Optional[SentryConfig] sentry_config: Optional[SentryConfig]
cookie_config: CookieConfig cookie_config: CookieConfig
content_delivery: ContentDeliveryConfig
class DatabaseConfig(BaseModel): class DatabaseConfig(BaseModel):
@ -116,6 +127,33 @@ def get_learnhouse_config() -> LearnHouseConfig:
).get("domain") ).get("domain")
cookie_config = CookieConfig(domain=cookies_domain) cookie_config = CookieConfig(domain=cookies_domain)
env_content_delivery_type = os.environ.get("LEARNHOUSE_CONTENT_DELIVERY_TYPE")
content_delivery_type: str = (
(yaml_config.get("hosting_config", {}).get("content_delivery", {}).get("type"))
or env_content_delivery_type
or "filesystem"
) # default to filesystem
env_bucket_name = os.environ.get("LEARNHOUSE_S3_API_BUCKET_NAME")
env_endpoint_url = os.environ.get("LEARNHOUSE_S3_API_ENDPOINT_URL")
bucket_name = (
yaml_config.get("hosting_config", {})
.get("content_delivery", {})
.get("s3api", {})
.get("bucket_name")
) or env_bucket_name
endpoint_url = (
yaml_config.get("hosting_config", {})
.get("content_delivery", {})
.get("s3api", {})
.get("endpoint_url")
) or env_endpoint_url
content_delivery = ContentDeliveryConfig(
type=content_delivery_type, # type: ignore
s3api=S3ApiConfig(bucket_name=bucket_name, endpoint_url=endpoint_url), # type: ignore
)
# Database config # Database config
mongodb_connection_string = env_mongodb_connection_string or yaml_config.get( mongodb_connection_string = env_mongodb_connection_string or yaml_config.get(
"database_config", {} "database_config", {}
@ -158,6 +196,7 @@ def get_learnhouse_config() -> LearnHouseConfig:
self_hosted=bool(self_hosted), self_hosted=bool(self_hosted),
sentry_config=sentry_config, sentry_config=sentry_config,
cookie_config=cookie_config, cookie_config=cookie_config,
content_delivery=content_delivery,
) )
database_config = DatabaseConfig( database_config = DatabaseConfig(
mongodb_connection_string=mongodb_connection_string mongodb_connection_string=mongodb_connection_string

View file

@ -17,6 +17,11 @@ hosting_config:
cookies_config: cookies_config:
domain: ".localhost" domain: ".localhost"
allowed_regexp: '\b((?:https?://)[^\s/$.?#].[^\s]*)\b' allowed_regexp: '\b((?:https?://)[^\s/$.?#].[^\s]*)\b'
content_delivery:
type: "filesystem" # "filesystem" or "s3api"
s3api:
bucket_name: ""
endpoint_url: ""
database_config: database_config:
mongodb_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/ mongodb_connection_string: mongodb://learnhouse:learnhouse@mongo:27017/

View file

View file

@ -1,6 +1,7 @@
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper"; import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { getBackendUrl, getUriWithOrg } from "@services/config/config"; import { getBackendUrl, getUriWithOrg } from "@services/config/config";
import { getCollectionByIdWithAuthHeader } from "@services/courses/collections"; import { getCollectionByIdWithAuthHeader } from "@services/courses/collections";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { getOrganizationContextInfo } from "@services/organizations/orgs"; import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { Metadata } from "next"; import { Metadata } from "next";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
@ -47,7 +48,7 @@ const CollectionPage = async (params : any) => {
{col.courses.map((course: any) => ( {col.courses.map((course: any) => (
<div className="pr-8" key={course.course_id}> <div className="pr-8" key={course.course_id}>
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}> <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})` }}> <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(${getCourseThumbnailMediaDirectory(course.org_id, course.course_id, course.thumbnail)})` }}>
</div> </div>
</Link> </Link>
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2> <h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>

View file

@ -8,6 +8,7 @@ import { Metadata } from "next";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import Link from "next/link"; import Link from "next/link";
import CollectionAdminEditsArea from "./admin"; import CollectionAdminEditsArea from "./admin";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
type MetadataProps = { type MetadataProps = {
params: { orgslug: string, courseid: string }; params: { orgslug: string, courseid: string };
@ -60,7 +61,7 @@ const CollectionsPage = async (params: any) => {
<div className="flex -space-x-4"> <div className="flex -space-x-4">
{collection.courses.slice(0, 3).map((course: any) => ( {collection.courses.slice(0, 3).map((course: any) => (
<Link key={course.course_id} href={getUriWithOrg(orgslug, "/course/" + course.course_id.substring(7))}> <Link key={course.course_id} href={getUriWithOrg(orgslug, "/course/" + course.course_id.substring(7))}>
<img className="w-12 h-12 rounded-full flex items-center justify-center shadow-lg ring-2 ring-white z-50" key={course.course_id} src={`${getBackendUrl()}content/uploads/img/${course.thumbnail}`} alt={course.name} /> <img className="w-12 h-12 rounded-full flex items-center justify-center shadow-lg ring-2 ring-white z-50" key={course.course_id} src={`${getCourseThumbnailMediaDirectory(course.org_id, course.course_id, course.thumbnail)}`} alt={course.name} />
</Link> </Link>
))} ))}
</div> </div>

View file

@ -11,6 +11,7 @@ import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper"; import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement"; import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
interface ActivityClientProps { interface ActivityClientProps {
activityid: string; activityid: string;
@ -49,7 +50,7 @@ function ActivityClient(props: ActivityClientProps) {
<div className="flex space-x-6"> <div className="flex space-x-6">
<div className="flex"> <div className="flex">
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}> <Link href={getUriWithOrg(orgslug, "") + `/course/${courseid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getBackendUrl()}content/uploads/img/${course.course.thumbnail}`} alt="" /> <img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_id, course.course.thumbnail)}`} alt="" />
</Link> </Link>
</div> </div>
<div className="flex flex-col -space-y-1"> <div className="flex flex-col -space-y-1">

View file

@ -12,6 +12,7 @@ import { revalidateTags } from "@services/utils/ts/requests";
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators"; import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper"; import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
const CourseClient = (props: any) => { const CourseClient = (props: any) => {
const courseid = props.courseid; const courseid = props.courseid;
@ -55,7 +56,7 @@ const CourseClient = (props: any) => {
</div> </div>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getBackendUrl()}content/uploads/img/${course.course.thumbnail})` }}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[300px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.course.org_id, course.course.course_id, course.course.thumbnail)})` }}>
</div> </div>

View file

@ -15,6 +15,7 @@ import { useRouter } from 'next/navigation';
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'; import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'; import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
interface CourseProps { interface CourseProps {
orgslug: string; orgslug: string;
@ -73,7 +74,7 @@ function Courses(props: CourseProps) {
<div className="px-3" key={course.course_id}> <div className="px-3" key={course.course_id}>
<AdminEditsArea course={course} orgSlug={orgslug} courseId={course.course_id} deleteCourses={deleteCourses} /> <AdminEditsArea course={course} orgSlug={orgslug} courseId={course.course_id} deleteCourses={deleteCourses} />
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}> <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})` }}> <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(${getCourseThumbnailMediaDirectory(course.org_id, course.course_id, course.thumbnail)})` }}>
</div> </div>
</Link> </Link>

View file

@ -11,6 +11,7 @@ import { getOrganizationContextInfo } from '@services/organizations/orgs';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'; import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'; import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
type MetadataProps = { type MetadataProps = {
params: { orgslug: string }; params: { orgslug: string };
@ -63,7 +64,7 @@ const OrgHomePage = async (params: any) => {
<div className="flex -space-x-4"> <div className="flex -space-x-4">
{collection.courses.slice(0, 3).map((course: any) => ( {collection.courses.slice(0, 3).map((course: any) => (
<Link key={course.course_id} href={getUriWithOrg(orgslug, "/course/" + course.course_id.substring(7))}> <Link key={course.course_id} href={getUriWithOrg(orgslug, "/course/" + course.course_id.substring(7))}>
<img className="w-12 h-12 rounded-full flex items-center justify-center shadow-lg ring-2 ring-white z-50" key={course.course_id} src={`${getBackendUrl()}content/uploads/img/${course.thumbnail}`} alt={course.name} /> <img className="w-12 h-12 rounded-full flex items-center justify-center shadow-lg ring-2 ring-white z-50" key={course.course_id} src={`url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_id, course.thumbnail)})`} alt={course.name} />
</Link> </Link>
))} ))}
</div> </div>
@ -80,7 +81,7 @@ const OrgHomePage = async (params: any) => {
{courses.map((course: any) => ( {courses.map((course: any) => (
<div className="py-3 px-3" key={course.course_id}> <div className="py-3 px-3" key={course.course_id}>
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}> <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 transition-all hover:scale-102" style={{ backgroundImage: `url(${getBackendUrl()}content/uploads/img/${course.thumbnail})` }}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover transition-all hover:scale-102" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(course.org_id, course.course_id, course.thumbnail)})` }}>
</div> </div>
</Link> </Link>
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2> <h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>

View file

@ -7,12 +7,14 @@ import * as AspectRatio from '@radix-ui/react-aspect-ratio';
import { AlertCircle, AlertTriangle, Image, ImagePlus, Info } from "lucide-react"; import { AlertCircle, AlertTriangle, Image, ImagePlus, Info } from "lucide-react";
import { getImageFile, uploadNewImageFile } from "../../../../../services/blocks/Image/images"; import { getImageFile, uploadNewImageFile } from "../../../../../services/blocks/Image/images";
import { getBackendUrl } from "../../../../../services/config/config"; import { getBackendUrl } from "../../../../../services/config/config";
import { getActivityBlockMediaDirectory } from "@services/media/media";
function ImageBlockComponent(props: any) { function ImageBlockComponent(props: any) {
const [image, setImage] = React.useState(null); const [image, setImage] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject); const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
const [imageSize, setImageSize] = React.useState({ width: props.node.attrs.size ? props.node.attrs.size.width : 300 }); const [imageSize, setImageSize] = React.useState({ width: props.node.attrs.size ? props.node.attrs.size.width : 300 });
const fileId = blockObject ? `${blockObject.block_data.file_id}.${blockObject.block_data.file_format}` : null;
const handleImageChange = (event: React.ChangeEvent<any>) => { const handleImageChange = (event: React.ChangeEvent<any>) => {
setImage(event.target.files[0]); setImage(event.target.files[0]);
@ -30,11 +32,6 @@ function ImageBlockComponent(props: any) {
}); });
}; };
console.log(props.node.attrs);
console.log(imageSize);
return ( return (
<NodeViewWrapper className="block-image"> <NodeViewWrapper className="block-image">
{!blockObject && ( {!blockObject && (
@ -79,10 +76,14 @@ function ImageBlockComponent(props: any) {
<BlockImage> <BlockImage>
<AspectRatio.Root ratio={16 / 9}> <AspectRatio.Root ratio={16 / 9}>
<img <img
src={`${getBackendUrl()}content/uploads/files/activities/${props.extension.options.activity.activity_id}/blocks/imageBlock/${blockObject.block_id}/${blockObject.block_data.file_id}.${blockObject.block_data.file_format src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
}`} props.extension.options.activity.course_id,
props.extension.options.activity.activity_id,
blockObject.block_id,
blockObject ? fileId : ' ', 'imageBlock')}`}
alt="" alt=""
/> />
{blockObject.block_id}
</AspectRatio.Root> </AspectRatio.Root>

View file

@ -4,11 +4,13 @@ import styled from "styled-components";
import { AlertCircle, AlertTriangle, FileText, Image, ImagePlus, Info } from "lucide-react"; import { AlertCircle, AlertTriangle, FileText, Image, ImagePlus, Info } from "lucide-react";
import { getPDFFile, uploadNewPDFFile } from "../../../../../services/blocks/Pdf/pdf"; import { getPDFFile, uploadNewPDFFile } from "../../../../../services/blocks/Pdf/pdf";
import { getBackendUrl } from "../../../../../services/config/config"; import { getBackendUrl } from "../../../../../services/config/config";
import { getActivityBlockMediaDirectory } from "@services/media/media";
function PDFBlockComponent(props: any) { function PDFBlockComponent(props: any) {
const [pdf, setPDF] = React.useState(null); const [pdf, setPDF] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject); const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
const fileId = blockObject ? `${blockObject.block_data.file_id}.${blockObject.block_data.file_format}` : null;
const handlePDFChange = (event: React.ChangeEvent<any>) => { const handlePDFChange = (event: React.ChangeEvent<any>) => {
setPDF(event.target.files[0]); setPDF(event.target.files[0]);
@ -41,9 +43,11 @@ function PDFBlockComponent(props: any) {
{blockObject && ( {blockObject && (
<BlockPDF> <BlockPDF>
<iframe <iframe
src={`${getBackendUrl()}content/uploads/files/activities/${props.extension.options.activity.activity_id}/blocks/pdfBlock/${blockObject.block_id}/${blockObject.block_data.file_id}.${ src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
blockObject.block_data.file_format props.extension.options.activity.course_id,
}`} props.extension.options.activity.activity_id,
blockObject.block_id,
blockObject ? fileId : ' ', 'pdfBlock')}`}
/> />
</BlockPDF> </BlockPDF>
)} )}

View file

@ -4,11 +4,13 @@ import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { getBackendUrl } from "../../../../../services/config/config"; import { getBackendUrl } from "../../../../../services/config/config";
import { uploadNewVideoFile } from "../../../../../services/blocks/Video/video"; import { uploadNewVideoFile } from "../../../../../services/blocks/Video/video";
import { getActivityBlockMediaDirectory } from "@services/media/media";
function VideoBlockComponents(props: any) { function VideoBlockComponents(props: any) {
const [video, setVideo] = React.useState(null); const [video, setVideo] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject); const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
const fileId = blockObject ? `${blockObject.block_data.file_id}.${blockObject.block_data.file_format}` : null;
const handleVideoChange = (event: React.ChangeEvent<any>) => { const handleVideoChange = (event: React.ChangeEvent<any>) => {
setVideo(event.target.files[0]); setVideo(event.target.files[0]);
@ -42,9 +44,11 @@ function VideoBlockComponents(props: any) {
<BlockVideo> <BlockVideo>
<video <video
controls controls
src={`${getBackendUrl()}content/uploads/files/activities/${props.extension.options.activity.activity_id}/blocks/videoBlock/${blockObject.block_id}/${blockObject.block_data.file_id}.${ src={`${getActivityBlockMediaDirectory(props.extension.options.activity.org_id,
blockObject.block_data.file_format props.extension.options.activity.course_id,
}`} props.extension.options.activity.activity_id,
blockObject.block_id,
blockObject ? fileId : ' ', 'videoBlock')}`}
></video> ></video>
</BlockVideo> </BlockVideo>
)} )}

View file

@ -8,6 +8,7 @@ import ClientComponentSkeleton from "@components/Utils/ClientComp";
import { HeaderProfileBox } from "@components/Security/HeaderProfileBox"; import { HeaderProfileBox } from "@components/Security/HeaderProfileBox";
import { swrFetcher } from "@services/utils/ts/requests"; import { swrFetcher } from "@services/utils/ts/requests";
import MenuLinks from "./MenuLinks"; import MenuLinks from "./MenuLinks";
import { getOrgLogoMediaDirectory } from "@services/media/media";
export const Menu = async (props: any) => { export const Menu = async (props: any) => {
const orgslug = props.orgslug; const orgslug = props.orgslug;
@ -22,7 +23,7 @@ export const Menu = async (props: any) => {
<div className="flex w-auto h-9 rounded-md items-center m-auto justify-center" > <div className="flex w-auto h-9 rounded-md items-center m-auto justify-center" >
{org?.logo ? ( {org?.logo ? (
<img <img
src={`${getBackendUrl()}content/uploads/logos/${org?.logo}`} src={`${getOrgLogoMediaDirectory(org.org_id, org?.logo)}`}
alt="Learnhouse" alt="Learnhouse"
style={{ width: "auto", height: "100%" }} style={{ width: "auto", height: "100%" }}
className="rounded-md" className="rounded-md"

View file

@ -1,4 +1,5 @@
import { getBackendUrl } from "@services/config/config"; import { getBackendUrl } from "@services/config/config";
import { getActivityMediaDirectory } from "@services/media/media";
import React from "react"; import React from "react";
function DocumentPdfActivity({ activity, course }: { activity: any; course: any }) { function DocumentPdfActivity({ activity, course }: { activity: any; course: any }) {
@ -7,7 +8,7 @@ function DocumentPdfActivity({ activity, course }: { activity: any; course: any
<div className="m-8 bg-zinc-900 rounded-md mt-14"> <div className="m-8 bg-zinc-900 rounded-md mt-14">
<iframe <iframe
className="rounded-lg w-full h-[900px]" className="rounded-lg w-full h-[900px]"
src={`${getBackendUrl()}content/uploads/documents/documentpdf/${activity.content.documentpdf.activity_id}/${activity.content.documentpdf.filename}`} src={getActivityMediaDirectory(activity.org_id, activity.course_id, activity.activity_id, activity.content.documentpdf.filename, 'documentpdf')}
/> />
</div> </div>
); );

View file

@ -2,6 +2,7 @@ import { getBackendUrl } from "@services/config/config";
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import YouTube from 'react-youtube'; import YouTube from 'react-youtube';
import { getActivityMediaDirectory } from "@services/media/media";
function VideoActivity({ activity, course }: { activity: any; course: any }) { function VideoActivity({ activity, course }: { activity: any; course: any }) {
const [videoId, setVideoId] = React.useState(''); const [videoId, setVideoId] = React.useState('');
@ -36,7 +37,10 @@ function VideoActivity({ activity, course }: { activity: any; course: any }) {
<div> <div>
{videoType === 'video' && ( {videoType === 'video' && (
<div className="m-8 bg-zinc-900 rounded-md mt-14"> <div className="m-8 bg-zinc-900 rounded-md mt-14">
<video className="rounded-lg w-full h-[500px]" controls src={`${getBackendUrl()}content/uploads/video/${activity.content.video.activity_id}/${activity.content.video.filename}`}></video> <video className="rounded-lg w-full h-[500px]" controls
src={getActivityMediaDirectory(activity.org_id, activity.course_id, activity.activity_id, activity.content.video.filename, 'video')}
></video>
</div> </div>
)} )}
{videoType === 'external_video' && ( {videoType === 'external_video' && (

View file

@ -1,6 +1,7 @@
'use client'; 'use client';
import { getAPIUrl, getBackendUrl, getUriWithOrg } from '@services/config/config'; import { getAPIUrl, getBackendUrl, getUriWithOrg } from '@services/config/config';
import { removeCourse } from '@services/courses/activity'; import { removeCourse } from '@services/courses/activity';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import { revalidateTags } from '@services/utils/ts/requests'; import { revalidateTags } from '@services/utils/ts/requests';
import Link from 'next/link'; import Link from 'next/link';
import { mutate } from 'swr'; import { mutate } from 'swr';
@ -28,7 +29,7 @@ function TrailCourseElement(props: TrailCourseElementProps) {
<div className='trailcoursebox flex p-3 bg-white rounded-xl' style={{ boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}> <div className='trailcoursebox flex p-3 bg-white rounded-xl' style={{ boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}>
<Link href={getUriWithOrg(props.orgslug, "/course/" + courseid)}> <Link href={getUriWithOrg(props.orgslug, "/course/" + courseid)}>
<div className="course_tumbnail inset-0 ring-1 ring-inset ring-black/10 rounded-lg relative h-[50px] w-[72px] bg-cover bg-center" style={{ backgroundImage: `url(${getBackendUrl()}content/uploads/img/${props.course.course_object.thumbnail})`, boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}></div> <div className="course_tumbnail inset-0 ring-1 ring-inset ring-black/10 rounded-lg relative h-[50px] w-[72px] bg-cover bg-center" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(props.course.course_object.org_id, props.course.course_object.course_id, props.course.course_object.thumbnail)})`, boxShadow: '0px 4px 7px 0px rgba(0, 0, 0, 0.03)' }}></div>
</Link> </Link>
<div className="course_meta pl-5 flex-grow space-y-1"> <div className="course_meta pl-5 flex-grow space-y-1">
<div className="course_top"> <div className="course_top">

View file

@ -0,0 +1,46 @@
import { getBackendUrl } from "@services/config/config";
const LEARNHOUSE_MEDIA_URL = process.env.NEXT_PUBLIC_LEARNHOUSE_MEDIA_URL;
function getMediaUrl() {
if (LEARNHOUSE_MEDIA_URL) {
return LEARNHOUSE_MEDIA_URL;
} else {
return getBackendUrl();
}
}
export function getCourseThumbnailMediaDirectory(orgId: string, courseId: string, fileId: string) {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/thumbnails/${fileId}`;
return uri;
}
export function getActivityBlockMediaDirectory(orgId: string, courseId: string, activityId: string, blockId: any, fileId: any, type: string) {
if (type == "pdfBlock") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/dynamic/blocks/pdfBlock/${blockId}/${fileId}`;
return uri;
}
if (type == "videoBlock") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/dynamic/blocks/videoBlock/${blockId}/${fileId}`;
return uri;
}
if (type == "imageBlock") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/dynamic/blocks/imageBlock/${blockId}/${fileId}`;
return uri;
}
}
export function getActivityMediaDirectory(orgId: string, courseId: string, activityId: string, fileId: string, activityType: string) {
if (activityType == "video") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/video/${fileId}`;
return uri;
}
if (activityType == "documentpdf") {
let uri = `${getMediaUrl()}content/${orgId}/courses/${courseId}/activities/${activityId}/documentpdf/${fileId}`;
return uri;
}
}
export function getOrgLogoMediaDirectory(orgId: string, fileId: string) {
let uri = `${getMediaUrl()}content/${orgId}/logos/${fileId}`;
return uri;
}

View file

@ -4,6 +4,8 @@ uvicorn==0.20.0
pymongo==4.3.3 pymongo==4.3.3
motor==3.1.1 motor==3.1.1
python-multipart python-multipart
boto3
botocore
python-jose python-jose
passlib passlib
fastapi-jwt-auth fastapi-jwt-auth

View file

@ -6,24 +6,51 @@ from src.services.blocks.utils.upload_files import upload_file_and_return_file_o
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
async def create_image_block(request: Request, image_file: UploadFile, activity_id: str): async def create_image_block(
request: Request, image_file: UploadFile, activity_id: str
):
blocks = request.app.db["blocks"] blocks = request.app.db["blocks"]
activity = request.app.db["activities"] activity = request.app.db["activities"]
courses = request.app.db["courses"]
block_type = "imageBlock" block_type = "imageBlock"
# get org_id from activity # get org_id from activity
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1}) activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
org_id = activity["org_id"] org_id = activity["org_id"]
coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
# get block id # get block id
block_id = str(f"block_{uuid4()}") block_id = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(request, image_file, activity_id, block_id, ["jpg", "jpeg", "png", "gif"], block_type) block_data = await upload_file_and_return_file_object(
request,
image_file,
activity_id,
block_id,
["jpg", "jpeg", "png", "gif"],
block_type,
org_id,
course["course_id"],
)
# create block # create block
block = Block(block_id=block_id, activity_id=activity_id, block = Block(
block_type=block_type, block_data=block_data, org_id=org_id) block_id=block_id,
activity_id=activity_id,
block_type=block_type,
block_data=block_data,
org_id=org_id,
course_id=course["course_id"],
)
# insert block # insert block
await blocks.insert_one(block.dict()) await blocks.insert_one(block.dict())
@ -41,4 +68,5 @@ async def get_image_block(request: Request, file_id: str, current_user: PublicUs
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist") status_code=status.HTTP_409_CONFLICT, detail="Image block does not exist"
)

View file

@ -9,21 +9,45 @@ from src.services.users.users import PublicUser
async def create_pdf_block(request: Request, pdf_file: UploadFile, activity_id: str): async def create_pdf_block(request: Request, pdf_file: UploadFile, activity_id: str):
blocks = request.app.db["blocks"] blocks = request.app.db["blocks"]
activity = request.app.db["activities"] activity = request.app.db["activities"]
courses = request.app.db["courses"]
block_type = "pdfBlock" block_type = "pdfBlock"
# get org_id from activity # get org_id from activity
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1}) activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0})
org_id = activity["org_id"] org_id = activity["org_id"]
# get block id # get block id
block_id = str(f"block_{uuid4()}") block_id = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(request, pdf_file, activity_id, block_id, ["pdf"], block_type) coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
block_data = await upload_file_and_return_file_object(
request,
pdf_file,
activity_id,
block_id,
["pdf"],
block_type,
org_id,
course["course_id"],
)
# create block # create block
block = Block(block_id=block_id, activity_id=activity_id, block = Block(
block_type=block_type, block_data=block_data, org_id=org_id) block_id=block_id,
activity_id=activity_id,
block_type=block_type,
block_data=block_data,
org_id=org_id,
course_id=course["course_id"],
)
# insert block # insert block
await blocks.insert_one(block.dict()) await blocks.insert_one(block.dict())
@ -41,4 +65,5 @@ async def get_pdf_block(request: Request, file_id: str, current_user: PublicUser
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist") status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist"
)

View file

@ -31,16 +31,20 @@ class quizBlock(BaseModel):
async def create_quiz_block(request: Request, quizBlock: quizBlock, activity_id: str, user: PublicUser): async def create_quiz_block(request: Request, quizBlock: quizBlock, activity_id: str, user: PublicUser):
blocks = request.app.db["blocks"] blocks = request.app.db["blocks"]
activities = request.app.db["activities"] activities = request.app.db["activities"]
request.app.db["courses"]
# Get org_id from activity # Get org_id from activity
activity = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1}) activity = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1})
org_id = activity["org_id"] org_id = activity["org_id"]
# Get course_id from activity
course = await activities.find_one({"activity_id": activity_id}, {"_id": 0, "course_id": 1})
block_id = str(f"block_{uuid4()}") block_id = str(f"block_{uuid4()}")
# create block # create block
block = Block(block_id=block_id, activity_id=activity_id, block = Block(block_id=block_id, activity_id=activity_id,
block_type="quizBlock", block_data=quizBlock, org_id=org_id) block_type="quizBlock", block_data=quizBlock, org_id=org_id, course_id=course["course_id"])
# insert block # insert block
await blocks.insert_one(block.dict()) await blocks.insert_one(block.dict())

View file

@ -6,24 +6,52 @@ from src.services.blocks.utils.upload_files import upload_file_and_return_file_o
from src.services.users.users import PublicUser from src.services.users.users import PublicUser
async def create_video_block(request: Request, video_file: UploadFile, activity_id: str): async def create_video_block(
request: Request, video_file: UploadFile, activity_id: str
):
blocks = request.app.db["blocks"] blocks = request.app.db["blocks"]
activity = request.app.db["activities"] activity = request.app.db["activities"]
courses = request.app.db["courses"]
block_type = "videoBlock" block_type = "videoBlock"
# get org_id from activity # get org_id from activity
activity = await activity.find_one({"activity_id": activity_id}, {"_id": 0, "org_id": 1}) activity = await activity.find_one(
{"activity_id": activity_id}, {"_id": 0}
)
org_id = activity["org_id"] org_id = activity["org_id"]
# get block id # get block id
block_id = str(f"block_{uuid4()}") block_id = str(f"block_{uuid4()}")
block_data = await upload_file_and_return_file_object(request, video_file, activity_id, block_id, ["mp4", "webm", "ogg"], block_type) coursechapter_id = activity["coursechapter_id"]
# get course_id from coursechapter
course = await courses.find_one(
{"chapters": coursechapter_id},
{"_id": 0},
)
block_data = await upload_file_and_return_file_object(
request,
video_file,
activity_id,
block_id,
["mp4", "webm", "ogg"],
block_type,
org_id,
course["course_id"],
)
# create block # create block
block = Block(block_id=block_id, activity_id=activity_id, block = Block(
block_type=block_type, block_data=block_data, org_id=org_id) block_id=block_id,
activity_id=activity_id,
block_type=block_type,
block_data=block_data,
org_id=org_id,
course_id=course["course_id"],
)
# insert block # insert block
await blocks.insert_one(block.dict()) await blocks.insert_one(block.dict())
@ -41,4 +69,5 @@ async def get_video_block(request: Request, file_id: str, current_user: PublicUs
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist") status_code=status.HTTP_409_CONFLICT, detail="Video file does not exist"
)

View file

@ -6,6 +6,7 @@ from pydantic import BaseModel
class Block(BaseModel): class Block(BaseModel):
block_id: str block_id: str
activity_id: str activity_id: str
course_id: str
org_id: str org_id: str
block_type: Literal["quizBlock", "videoBlock", "pdfBlock", "imageBlock"] block_type: Literal["quizBlock", "videoBlock", "pdfBlock", "imageBlock"]
block_data: Any block_data: Any

View file

@ -1,11 +1,19 @@
import os
import uuid import uuid
from fastapi import HTTPException, Request, UploadFile, status from fastapi import HTTPException, Request, UploadFile, status
from src.services.blocks.schemas.files import BlockFile from src.services.blocks.schemas.files import BlockFile
from src.services.utils.upload_content import upload_content
async def upload_file_and_return_file_object(
async def upload_file_and_return_file_object(request: Request, file: UploadFile, activity_id: str, block_id: str, list_of_allowed_file_formats: list, type_of_block: str): request: Request,
file: UploadFile,
activity_id: str,
block_id: str,
list_of_allowed_file_formats: list,
type_of_block: str,
org_id: str,
course_id: str,
):
# get file id # get file id
file_id = str(uuid.uuid4()) file_id = str(uuid.uuid4())
@ -15,7 +23,8 @@ async def upload_file_and_return_file_object(request: Request, file: UploadFile,
# validate file format # validate file format
if file_format not in list_of_allowed_file_formats: if file_format not in list_of_allowed_file_formats:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="File format not supported") status_code=status.HTTP_409_CONFLICT, detail="File format not supported"
)
# create file # create file
file_binary = await file.read() file_binary = await file.read()
@ -36,18 +45,14 @@ async def upload_file_and_return_file_object(request: Request, file: UploadFile,
file_name=file_name, file_name=file_name,
file_size=file_size, file_size=file_size,
file_type=file_type, file_type=file_type,
activity_id=activity_id activity_id=activity_id,
) )
# create folder for activity await upload_content(
if not os.path.exists(f"content/uploads/files/activities/{activity_id}/blocks/{type_of_block}/{block_id}"): f"courses/{course_id}/activities/{activity_id}/dynamic/blocks/{type_of_block}/{block_id}",
# create folder for activity org_id=org_id,
os.makedirs(f"content/uploads/files/activities/{activity_id}/blocks/{type_of_block}/{block_id}") file_binary=file_binary,
file_and_format=f"{file_id}.{file_format}",
# upload file to server )
with open(f"content/uploads/files/activities/{activity_id}/blocks/{type_of_block}/{block_id}/{file_id}.{file_format}", 'wb') as f:
f.write(file_binary)
f.close()
return uploadable_file return uploadable_file

View file

@ -16,6 +16,7 @@ class Activity(BaseModel):
class ActivityInDB(Activity): class ActivityInDB(Activity):
activity_id: str activity_id: str
course_id: str
coursechapter_id: str coursechapter_id: str
org_id: str org_id: str
creationDate: str creationDate: str
@ -47,6 +48,9 @@ async def create_activity(
request, "create", current_user.user_id, activity_id, org_id request, "create", current_user.user_id, activity_id, org_id
) )
# get course_id from activity
course = await courses.find_one({"chapters": coursechapter_id})
if not hasRoleRights: if not hasRoleRights:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=status.HTTP_409_CONFLICT,
@ -61,6 +65,7 @@ async def create_activity(
updateDate=str(datetime.now()), updateDate=str(datetime.now()),
activity_id=activity_id, activity_id=activity_id,
org_id=org_id, org_id=org_id,
course_id=course["course_id"],
) )
await activities.insert_one(activity.dict()) await activities.insert_one(activity.dict())

View file

@ -7,7 +7,13 @@ from uuid import uuid4
from datetime import datetime from datetime import datetime
async def create_documentpdf_activity(request: Request, name: str, coursechapter_id: str, current_user: PublicUser, pdf_file: UploadFile | None = None): async def create_documentpdf_activity(
request: Request,
name: str,
coursechapter_id: str,
current_user: PublicUser,
pdf_file: UploadFile | None = None,
):
activities = request.app.db["activities"] activities = request.app.db["activities"]
courses = request.app.db["courses"] courses = request.app.db["courses"]
@ -16,18 +22,21 @@ async def create_documentpdf_activity(request: Request, name: str, coursechapte
# get org_id from course # get org_id from course
coursechapter = await courses.find_one( coursechapter = await courses.find_one(
{"chapters_content.coursechapter_id": coursechapter_id}) {"chapters_content.coursechapter_id": coursechapter_id}
)
org_id = coursechapter["org_id"] org_id = coursechapter["org_id"]
# check if pdf_file is not None # check if pdf_file is not None
if not pdf_file: if not pdf_file:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided") status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
)
if pdf_file.content_type not in ["application/pdf"]: if pdf_file.content_type not in ["application/pdf"]:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Pdf : Wrong pdf format") status_code=status.HTTP_409_CONFLICT, detail="Pdf : Wrong pdf format"
)
# get pdf format # get pdf format
if pdf_file.filename: if pdf_file.filename:
@ -35,7 +44,8 @@ async def create_documentpdf_activity(request: Request, name: str, coursechapte
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided") status_code=status.HTTP_409_CONFLICT, detail="Pdf : No pdf file provided"
)
activity_object = ActivityInDB( activity_object = ActivityInDB(
org_id=org_id, org_id=org_id,
@ -43,6 +53,7 @@ async def create_documentpdf_activity(request: Request, name: str, coursechapte
coursechapter_id=coursechapter_id, coursechapter_id=coursechapter_id,
name=name, name=name,
type="documentpdf", type="documentpdf",
course_id=coursechapter["course_id"],
content={ content={
"documentpdf": { "documentpdf": {
"filename": "documentpdf." + pdf_format, "filename": "documentpdf." + pdf_format,
@ -53,11 +64,15 @@ async def create_documentpdf_activity(request: Request, name: str, coursechapte
updateDate=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) hasRoleRights = await verify_user_rights_with_roles(
request, "create", current_user.user_id, activity_id, element_org_id=org_id
)
if not hasRoleRights: if not hasRoleRights:
raise HTTPException( 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 # create activity
activity = ActivityInDB(**activity_object.dict()) activity = ActivityInDB(**activity_object.dict())
@ -66,11 +81,13 @@ async def create_documentpdf_activity(request: Request, name: str, coursechapte
# upload pdf # upload pdf
if pdf_file: if pdf_file:
# get pdffile format # get pdffile format
await upload_pdf(pdf_file, activity_id) await upload_pdf(pdf_file, activity_id, org_id, coursechapter["course_id"])
# todo : choose whether to update the chapter or not # todo : choose whether to update the chapter or not
# update chapter # update chapter
await courses.update_one({"chapters_content.coursechapter_id": coursechapter_id}, { await courses.update_one(
"$addToSet": {"chapters_content.$.activities": activity_id}}) {"chapters_content.coursechapter_id": coursechapter_id},
{"$addToSet": {"chapters_content.$.activities": activity_id}},
)
return activity return activity

View file

@ -1,23 +1,18 @@
import os
from src.services.utils.upload_content import upload_content
async def upload_pdf(pdf_file, activity_id): async def upload_pdf(pdf_file, activity_id, org_id, course_id):
contents = pdf_file.file.read() contents = pdf_file.file.read()
pdf_format = pdf_file.filename.split(".")[-1] pdf_format = pdf_file.filename.split(".")[-1]
if not os.path.exists("content/uploads/documents/documentpdf"):
# create folder
os.makedirs("content/uploads/documents/documentpdf")
# create folder
os.mkdir(f"content/uploads/documents/documentpdf/{activity_id}")
try: try:
with open(f"content/uploads/documents/documentpdf/{activity_id}/documentpdf.{pdf_format}", 'wb') as f: await upload_content(
f.write(contents) f"courses/{course_id}/activities/{activity_id}/documentpdf",
f.close() org_id,
contents,
f"documentpdf.{pdf_format}",
)
except Exception: except Exception:
return {"message": "There was an error uploading the file"} return {"message": "There was an error uploading the file"}
finally:
pdf_file.file.close()

View file

@ -1,23 +1,18 @@
import os
from src.services.utils.upload_content import upload_content
async def upload_video(video_file, activity_id): async def upload_video(video_file, activity_id, org_id, course_id):
contents = video_file.file.read() contents = video_file.file.read()
video_format = video_file.filename.split(".")[-1] video_format = video_file.filename.split(".")[-1]
if not os.path.exists("content/uploads/video"):
# create folder
os.makedirs("content/uploads/video")
# create folder
os.mkdir(f"content/uploads/video/{activity_id}")
try: try:
with open(f"content/uploads/video/{activity_id}/video.{video_format}", 'wb') as f: await upload_content(
f.write(contents) f"courses/{course_id}/activities/{activity_id}/video",
f.close() org_id,
contents,
f"video.{video_format}",
)
except Exception: except Exception:
return {"message": "There was an error uploading the file"} return {"message": "There was an error uploading the file"}
finally:
video_file.file.close()

View file

@ -62,6 +62,7 @@ async def create_video_activity(
org_id=org_id, org_id=org_id,
activity_id=activity_id, activity_id=activity_id,
coursechapter_id=coursechapter_id, coursechapter_id=coursechapter_id,
course_id=coursechapter["course_id"],
name=name, name=name,
type="video", type="video",
content={ content={
@ -91,7 +92,7 @@ async def create_video_activity(
# upload video # upload video
if video_file: if video_file:
# get videofile format # get videofile format
await upload_video(video_file, activity_id) await upload_video(video_file, activity_id, org_id, coursechapter["course_id"])
# todo : choose whether to update the chapter or not # todo : choose whether to update the chapter or not
# update chapter # update chapter
@ -109,6 +110,7 @@ class ExternalVideo(BaseModel):
type: Literal["youtube", "vimeo"] type: Literal["youtube", "vimeo"]
coursechapter_id: str coursechapter_id: str
class ExternalVideoInDB(BaseModel): class ExternalVideoInDB(BaseModel):
activity_id: str activity_id: str
@ -150,6 +152,7 @@ async def create_external_video_activity(
"type": data.type, "type": data.type,
} }
}, },
course_id=coursechapter["course_id"],
creationDate=str(datetime.now()), creationDate=str(datetime.now()),
updateDate=str(datetime.now()), updateDate=str(datetime.now()),
) )

View file

@ -184,7 +184,9 @@ async def create_course(
name_in_disk = ( name_in_disk = (
f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
) )
await upload_thumbnail(thumbnail_file, name_in_disk) await upload_thumbnail(
thumbnail_file, name_in_disk, course_object.org_id, course_id
)
course_object.thumbnail = name_in_disk course_object.thumbnail = name_in_disk
course = CourseInDB( course = CourseInDB(
@ -225,7 +227,9 @@ async def update_course_thumbnail(
if thumbnail_file and thumbnail_file.filename: if thumbnail_file and thumbnail_file.filename:
name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}"
course = Course(**course).copy(update={"thumbnail": name_in_disk}) course = Course(**course).copy(update={"thumbnail": name_in_disk})
await upload_thumbnail(thumbnail_file, name_in_disk) await upload_thumbnail(
thumbnail_file, name_in_disk, course.org_id, course_id
)
updated_course = CourseInDB( updated_course = CourseInDB(
course_id=course_id, course_id=course_id,

View file

@ -1,17 +1,16 @@
import os
from src.services.utils.upload_content import upload_content
async def upload_thumbnail(thumbnail_file, name_in_disk): async def upload_thumbnail(thumbnail_file, name_in_disk, org_id, course_id):
contents = thumbnail_file.file.read() contents = thumbnail_file.file.read()
try: try:
if not os.path.exists("content/uploads/img"): await upload_content(
os.makedirs("content/uploads/img") f"courses/{course_id}/thumbnails",
org_id,
with open(f"content/uploads/img/{name_in_disk}", 'wb') as f: contents,
f.write(contents) f"{name_in_disk}",
f.close() )
except Exception: except Exception:
return {"message": "There was an error uploading the file"} return {"message": "There was an error uploading the file"}
finally:
thumbnail_file.file.close()

View file

@ -1,26 +1,17 @@
import os
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, status
from src.services.utils.upload_content import upload_content
async def upload_org_logo(logo_file): async def upload_org_logo(logo_file, org_id):
contents = logo_file.file.read() contents = logo_file.file.read()
name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}" name_in_disk = f"{uuid4()}.{logo_file.filename.split('.')[-1]}"
try: await upload_content(
if not os.path.exists("content/uploads/logos"): "logos",
os.makedirs("content/uploads/logos") org_id,
contents,
with open(f"content/uploads/logos/{name_in_disk}", "wb") as f: name_in_disk,
f.write(contents)
f.close()
except Exception:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="There was an error uploading the file",
) )
finally:
logo_file.file.close()
return name_in_disk return name_in_disk

View file

@ -115,7 +115,7 @@ async def update_org_logo(
await orgs.find_one({"org_id": org_id}) await orgs.find_one({"org_id": org_id})
name_in_disk = await upload_org_logo(logo_file) name_in_disk = await upload_org_logo(logo_file, org_id)
# update org # update org
await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}}) await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}})

View file

@ -19,8 +19,8 @@ class ActivityData(BaseModel):
class TrailCourse(BaseModel): class TrailCourse(BaseModel):
course_id: str course_id: str
elements_type: Optional[Literal['course']] = 'course' elements_type: Optional[Literal["course"]] = "course"
status: Optional[Literal['ongoing', 'done', 'closed']] = 'ongoing' status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
course_object: dict course_object: dict
masked: Optional[bool] = False masked: Optional[bool] = False
activities_marked_complete: Optional[List[str]] activities_marked_complete: Optional[List[str]]
@ -29,7 +29,7 @@ class TrailCourse(BaseModel):
class Trail(BaseModel): class Trail(BaseModel):
status: Optional[Literal['ongoing', 'done', 'closed']] = 'ongoing' status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing"
masked: Optional[bool] = False masked: Optional[bool] = False
courses: Optional[List[TrailCourse]] courses: Optional[List[TrailCourse]]
@ -45,7 +45,9 @@ class TrailInDB(Trail):
#### Classes #################################################### #### Classes ####################################################
async def create_trail(request: Request, user: PublicUser, org_id: str, trail_object: Trail) -> Trail: async def create_trail(
request: Request, user: PublicUser, org_id: str, trail_object: Trail
) -> Trail:
trails = request.app.db["trails"] trails = request.app.db["trails"]
# get list of courses # get list of courses
@ -55,21 +57,26 @@ async def create_trail(request: Request, user: PublicUser, org_id: str, trail_ob
course_ids = [course.course_id for course in courses] course_ids = [course.course_id for course in courses]
# find if the user has already started the course # find if the user has already started the course
existing_trail = await trails.find_one({'user_id': user.user_id, 'courses.course_id': {'$in': course_ids}}) existing_trail = await trails.find_one(
{"user_id": user.user_id, "courses.course_id": {"$in": course_ids}}
)
if existing_trail: if existing_trail:
# update the status of the element with the matching course_id to "ongoing" # update the status of the element with the matching course_id to "ongoing"
for element in existing_trail['courses']: for element in existing_trail["courses"]:
if element['course_id'] in course_ids: if element["course_id"] in course_ids:
element['status'] = 'ongoing' element["status"] = "ongoing"
# update the existing trail in the database # update the existing trail in the database
await trails.replace_one({'trail_id': existing_trail['trail_id']}, existing_trail) await trails.replace_one(
{"trail_id": existing_trail["trail_id"]}, existing_trail
)
# create trail id # create trail id
trail_id = f"trail_{uuid4()}" trail_id = f"trail_{uuid4()}"
# create trail # create trail
trail = TrailInDB(**trail_object.dict(), trail_id=trail_id, trail = TrailInDB(
user_id=user.user_id, org_id=org_id) **trail_object.dict(), trail_id=trail_id, user_id=user.user_id, org_id=org_id
)
await trails.insert_one(trail.dict()) await trails.insert_one(trail.dict())
@ -81,22 +88,27 @@ async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> T
trail = await trails.find_one({"user_id": user.user_id}) trail = await trails.find_one({"user_id": user.user_id})
if not trail: if not trail:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found") status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
)
for element in trail["courses"]: for element in trail["courses"]:
course_id = element["course_id"] course_id = element["course_id"]
chapters_meta = await get_coursechapters_meta(request, course_id, user) chapters_meta = await get_coursechapters_meta(request, course_id, user)
activities = chapters_meta["activities"] activities = chapters_meta["activities"]
num_activities = len(activities) num_activities = len(activities)
num_completed_activities = len( num_completed_activities = len(element.get("activities_marked_complete", []))
element.get("activities_marked_complete", [])) element["progress"] = (
element["progress"] = round( round((num_completed_activities / num_activities) * 100, 2)
(num_completed_activities / num_activities) * 100, 2) if num_activities > 0 else 0 if num_activities > 0
else 0
)
return Trail(**trail) return Trail(**trail)
async def get_user_trail_with_orgslug(request: Request, user: PublicUser, org_slug: str) -> Trail: async def get_user_trail_with_orgslug(
request: Request, user: PublicUser, org_slug: str
) -> Trail:
trails = request.app.db["trails"] trails = request.app.db["trails"]
orgs = request.app.db["organizations"] orgs = request.app.db["organizations"]
courses_mongo = request.app.db["courses"] courses_mongo = request.app.db["courses"]
@ -111,10 +123,13 @@ async def get_user_trail_with_orgslug(request: Request, user: PublicUser, org_sl
# Check if these courses still exist in the database # Check if these courses still exist in the database
for course in trail["courses"]: for course in trail["courses"]:
course_id = course["course_id"] course_id = course["course_id"]
course_object = await courses_mongo.find_one({"course_id": course_id}, {"_id": 0}) course_object = await courses_mongo.find_one(
{"course_id": course_id}, {"_id": 0}
)
print('checking course ' + course_id)
if not course_object: if not course_object:
print("Course not found " + course_id)
trail["courses"].remove(course) trail["courses"].remove(course)
continue continue
@ -127,20 +142,26 @@ async def get_user_trail_with_orgslug(request: Request, user: PublicUser, org_sl
activities = chapters_meta["activities"] activities = chapters_meta["activities"]
# get course object without _id # get course object without _id
course_object = await courses_mongo.find_one({"course_id": course_id}, {"_id": 0}) course_object = await courses_mongo.find_one(
{"course_id": course_id}, {"_id": 0}
)
courses["course_object"] = course_object courses["course_object"] = course_object
num_activities = len(activities) num_activities = len(activities)
num_completed_activities = len( num_completed_activities = len(courses.get("activities_marked_complete", []))
courses.get("activities_marked_complete", [])) courses["progress"] = (
courses["progress"] = round( round((num_completed_activities / num_activities) * 100, 2)
(num_completed_activities / num_activities) * 100, 2) if num_activities > 0 else 0 if num_activities > 0
else 0
)
return Trail(**trail) return Trail(**trail)
async def add_activity_to_trail(request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str) -> Trail: async def add_activity_to_trail(
request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str
) -> Trail:
trails = request.app.db["trails"] trails = request.app.db["trails"]
orgs = request.app.db["organizations"] orgs = request.app.db["organizations"]
courseid = "course_" + course_id courseid = "course_" + course_id
@ -150,9 +171,10 @@ async def add_activity_to_trail(request: Request, user: PublicUser, course_id:
org = await orgs.find_one({"slug": org_slug}) org = await orgs.find_one({"slug": org_slug})
org_id = org["org_id"] org_id = org["org_id"]
# find a trail with the user_id and course_id in the courses array # find a trail with the user_id and course_id in the courses array
trail = await trails.find_one({"user_id": user.user_id, "courses.course_id": courseid , "org_id": org_id}) trail = await trails.find_one(
{"user_id": user.user_id, "courses.course_id": courseid, "org_id": org_id}
)
if not trail: if not trail:
return Trail(masked=False, courses=[]) return Trail(masked=False, courses=[])
@ -161,13 +183,14 @@ async def add_activity_to_trail(request: Request, user: PublicUser, course_id:
for element in trail["courses"]: for element in trail["courses"]:
if element["course_id"] == courseid: if element["course_id"] == courseid:
if "activities_marked_complete" in element: if "activities_marked_complete" in element:
# check if activity_id is already in the array # check if activity_id is already in the array
if activityid not in element["activities_marked_complete"]: if activityid not in element["activities_marked_complete"]:
element["activities_marked_complete"].append(activityid) element["activities_marked_complete"].append(activityid)
else: else:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Activity already marked complete") status_code=status.HTTP_400_BAD_REQUEST,
detail="Activity already marked complete",
)
else: else:
element["activities_marked_complete"] = [activity_id] element["activities_marked_complete"] = [activity_id]
@ -177,21 +200,25 @@ async def add_activity_to_trail(request: Request, user: PublicUser, course_id:
return Trail(**trail) return Trail(**trail)
async def add_course_to_trail(request: Request, user: PublicUser, orgslug: str, course_id: str) -> Trail: async def add_course_to_trail(
request: Request, user: PublicUser, orgslug: str, course_id: str
) -> Trail:
trails = request.app.db["trails"] trails = request.app.db["trails"]
orgs = request.app.db["organizations"] orgs = request.app.db["organizations"]
org = await orgs.find_one({"slug": orgslug}) org = await orgs.find_one({"slug": orgslug})
org = PublicOrganization(**org) org = PublicOrganization(**org)
trail = await trails.find_one( trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
{"user_id": user.user_id, "org_id": org["org_id"]})
if not trail: if not trail:
trail_to_insert = TrailInDB(trail_id=f"trail_{uuid4()}", user_id=user.user_id, org_id=org["org_id"], courses=[]) trail_to_insert = TrailInDB(
trail_id=f"trail_{uuid4()}",
user_id=user.user_id,
org_id=org["org_id"],
courses=[],
)
trail_to_insert = await trails.insert_one(trail_to_insert.dict()) trail_to_insert = await trails.insert_one(trail_to_insert.dict())
trail = await trails.find_one({"_id": trail_to_insert.inserted_id}) trail = await trails.find_one({"_id": trail_to_insert.inserted_id})
@ -200,31 +227,42 @@ async def add_course_to_trail(request: Request, user: PublicUser, orgslug: str,
for element in trail["courses"]: for element in trail["courses"]:
if element["course_id"] == course_id: if element["course_id"] == course_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Course already present in the trail") status_code=status.HTTP_400_BAD_REQUEST,
detail="Course already present in the trail",
)
updated_trail = TrailCourse(course_id=course_id, activities_data=[ updated_trail = TrailCourse(
], activities_marked_complete=[], progress=0, course_object={}, status="ongoing", masked=False) course_id=course_id,
activities_data=[],
activities_marked_complete=[],
progress=0,
course_object={},
status="ongoing",
masked=False,
)
trail["courses"].append(updated_trail.dict()) trail["courses"].append(updated_trail.dict())
await trails.replace_one({"trail_id": trail['trail_id']}, trail) await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
return Trail(**trail) return Trail(**trail)
async def remove_course_from_trail(request: Request, user: PublicUser, orgslug: str, course_id: str) -> Trail:
async def remove_course_from_trail(
request: Request, user: PublicUser, orgslug: str, course_id: str
) -> Trail:
trails = request.app.db["trails"] trails = request.app.db["trails"]
orgs = request.app.db["organizations"] orgs = request.app.db["organizations"]
org = await orgs.find_one({"slug": orgslug}) org = await orgs.find_one({"slug": orgslug})
org = PublicOrganization(**org) org = PublicOrganization(**org)
print(org) print(org)
trail = await trails.find_one( trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
{"user_id": user.user_id, "org_id": org["org_id"]})
if not trail: if not trail:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found") status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
)
# check if course is already present in the trail # check if course is already present in the trail
@ -233,5 +271,5 @@ async def remove_course_from_trail(request: Request, user: PublicUser, orgslug:
trail["courses"].remove(element) trail["courses"].remove(element)
break break
await trails.replace_one({"trail_id": trail['trail_id']}, trail) await trails.replace_one({"trail_id": trail["trail_id"]}, trail)
return Trail(**trail) return Trail(**trail)

View file

@ -0,0 +1,69 @@
import boto3
from botocore.exceptions import ClientError
import os
from config.config import get_learnhouse_config
async def upload_content(
directory: str, org_id: str, file_binary: bytes, file_and_format: str
):
# Get Learnhouse Config
learnhouse_config = get_learnhouse_config()
# Get content delivery method
content_delivery = learnhouse_config.hosting_config.content_delivery.type
if content_delivery == "filesystem":
# create folder for activity
if not os.path.exists(f"content/{org_id}/{directory}"):
# create folder for activity
os.makedirs(f"content/{org_id}/{directory}")
# upload file to server
with open(
f"content/{org_id}/{directory}/{file_and_format}",
"wb",
) as f:
f.write(file_binary)
f.close()
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
s3 = boto3.client(
"s3",
endpoint_url=learnhouse_config.hosting_config.content_delivery.s3api.endpoint_url,
)
# Create folder for activity
if not os.path.exists(f"content/{org_id}/{directory}"):
# create folder for activity
os.makedirs(f"content/{org_id}/{directory}")
# Upload file to server
with open(
f"content/{org_id}/{directory}/{file_and_format}",
"wb",
) as f:
f.write(file_binary)
f.close()
print("Uploading to s3 using boto3...")
try:
s3.upload_file(
f"content/{org_id}/{directory}/{file_and_format}",
"learnhouse-media",
f"content/{org_id}/{directory}/{file_and_format}",
)
except ClientError as e:
print(e)
print("Checking if file exists in s3...")
try:
s3.head_object(
Bucket="learnhouse-media",
Key=f"content/{org_id}/{directory}/{file_and_format}",
)
print("File upload successful!")
except Exception as e:
print(f"An error occurred: {str(e)}")