mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
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:
commit
f4c596278d
35 changed files with 553 additions and 231 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,64 @@
|
||||||
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";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string, collectionid: string };
|
params: { orgslug: string, courseid: string, collectionid: string };
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined };
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata(
|
||||||
{ params }: MetadataProps,
|
{ params }: MetadataProps,
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const access_token_cookie: any = cookieStore.get('access_token_cookie');
|
const access_token_cookie: any = cookieStore.get('access_token_cookie');
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
||||||
const col = await getCollectionByIdWithAuthHeader(params.collectionid, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['collections'] });
|
const col = await getCollectionByIdWithAuthHeader(params.collectionid, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['collections'] });
|
||||||
|
|
||||||
console.log(col)
|
|
||||||
|
|
||||||
return {
|
console.log(col)
|
||||||
title: `Collection : ${col.name} — ${org.name}`,
|
|
||||||
description: `${col.description} `,
|
return {
|
||||||
};
|
title: `Collection : ${col.name} — ${org.name}`,
|
||||||
|
description: `${col.description} `,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollectionPage = async (params : any) => {
|
const CollectionPage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const access_token_cookie: any = cookieStore.get('access_token_cookie');
|
const access_token_cookie: any = cookieStore.get('access_token_cookie');
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['collections'] });
|
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['collections'] });
|
||||||
|
|
||||||
const removeCoursePrefix = (courseid: string) => {
|
const removeCoursePrefix = (courseid: string) => {
|
||||||
return courseid.replace("course_", "")
|
return courseid.replace("course_", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return <GeneralWrapperStyled>
|
return <GeneralWrapperStyled>
|
||||||
<h2 className="text-sm font-bold text-gray-400">Collection</h2>
|
<h2 className="text-sm font-bold text-gray-400">Collection</h2>
|
||||||
<h1 className="text-3xl font-bold">{col.name}</h1>
|
<h1 className="text-3xl font-bold">{col.name}</h1>
|
||||||
<br />
|
<br />
|
||||||
<div className="home_courses flex flex-wrap">
|
<div className="home_courses flex flex-wrap">
|
||||||
{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>
|
|
||||||
</Link>
|
|
||||||
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</Link>
|
||||||
|
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</GeneralWrapperStyled>;
|
</GeneralWrapperStyled>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CollectionPage;
|
export default CollectionPage;
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -45,9 +46,9 @@ const CollectionsPage = async (params: any) => {
|
||||||
<div className="flex justify-between" >
|
<div className="flex justify-between" >
|
||||||
<TypeOfContentTitle title="Collections" type="col" />
|
<TypeOfContentTitle title="Collections" type="col" />
|
||||||
<AuthenticatedClientElement checkMethod='authentication'>
|
<AuthenticatedClientElement checkMethod='authentication'>
|
||||||
<Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}>
|
<Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}>
|
||||||
<button className="rounded-md bg-black antialiased ring-offset-purple-800 p-2 px-5 my-auto font text-sm font-bold text-white drop-shadow-lg">Add Collection + </button>
|
<button className="rounded-md bg-black antialiased ring-offset-purple-800 p-2 px-5 my-auto font text-sm font-bold text-white drop-shadow-lg">Add Collection + </button>
|
||||||
</Link>
|
</Link>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
<div className="home_collections flex flex-wrap">
|
<div className="home_collections flex flex-wrap">
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
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 }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,13 +37,16 @@ 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' && (
|
||||||
<div>
|
<div>
|
||||||
<YouTube
|
<YouTube
|
||||||
className="rounded-md overflow-hidden m-8 bg-zinc-900 mt-14"
|
className="rounded-md overflow-hidden m-8 bg-zinc-900 mt-14"
|
||||||
opts={
|
opts={
|
||||||
{
|
{
|
||||||
width: '1300',
|
width: '1300',
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
46
front/services/media/media.ts
Normal file
46
front/services/media/media.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,8 +23,9 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,9 +53,10 @@ 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,
|
||||||
"activity_id": activity_id,
|
"activity_id": activity_id,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
|
||||||
|
|
@ -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()),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}})
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ 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]]
|
||||||
activities_data: Optional[List[ActivityData]]
|
activities_data: Optional[List[ActivityData]]
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -108,39 +120,48 @@ async def get_user_trail_with_orgslug(request: Request, user: PublicUser, org_sl
|
||||||
|
|
||||||
if not trail:
|
if not trail:
|
||||||
return Trail(masked=False, courses=[])
|
return Trail(masked=False, courses=[])
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
course["course_object"] = course_object
|
course["course_object"] = course_object
|
||||||
|
|
||||||
for courses in trail["courses"]:
|
for courses in trail["courses"]:
|
||||||
course_id = courses["course_id"]
|
course_id = courses["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"]
|
||||||
|
|
||||||
# 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
|
||||||
|
|
@ -149,49 +170,55 @@ async def add_activity_to_trail(request: Request, user: PublicUser, course_id:
|
||||||
# get org_id from orgslug
|
# get org_id from orgslug
|
||||||
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(
|
||||||
trail = await trails.find_one({"user_id": user.user_id, "courses.course_id": courseid , "org_id": org_id})
|
{"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=[])
|
||||||
|
|
||||||
# if a trail has course_id in the courses array, then add the activity_id to the activities_marked_complete array
|
# if a trail has course_id in the courses array, then add the activity_id to the activities_marked_complete array
|
||||||
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]
|
||||||
|
|
||||||
# modify trail object
|
# modify trail object
|
||||||
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 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)
|
||||||
|
|
|
||||||
69
src/services/utils/upload_content.py
Normal file
69
src/services/utils/upload_content.py
Normal 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)}")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue