Merge branch 'learnhouse:dev' into Ordered-List

This commit is contained in:
Sanjeev Kumar 2023-08-24 01:43:36 +05:30 committed by GitHub
commit 6b6b3e65f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 4057 additions and 8816 deletions

View file

@ -14,4 +14,4 @@ RUN pip install --no-cache-dir --upgrade -r /usr/learnhouse/requirements.txt
COPY ./ /usr/learnhouse COPY ./ /usr/learnhouse
# #
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" ] CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80" , "--reload" ]

2
app.py
View file

@ -8,8 +8,6 @@ from fastapi.staticfiles import StaticFiles
from fastapi_jwt_auth.exceptions import AuthJWTException from fastapi_jwt_auth.exceptions import AuthJWTException
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from src.security.rbac.rbac import authorization_verify_based_on_roles, authorization_verify_if_element_is_public, authorization_verify_if_user_is_author
from src.services.users.schemas.users import UserRolesInOrganization
# from src.services.mocks.initial import create_initial_data # from src.services.mocks.initial import create_initial_data

View file

@ -15,7 +15,7 @@ const CollectionAdminEditsArea = (props: any) => {
const deleteCollectionUI = async (collectionId: number) => { const deleteCollectionUI = async (collectionId: number) => {
await deleteCollection(collectionId); await deleteCollection(collectionId);
revalidateTags(["collections"], props.orgslug); await revalidateTags(["collections"], props.orgslug);
// reload the page // reload the page
router.refresh(); router.refresh();
router.push(getUriWithOrg(props.orgslug, "/collections")); router.push(getUriWithOrg(props.orgslug, "/collections"));

View file

@ -44,11 +44,10 @@ function NewCollection(params: any) {
org_id: org.org_id, org_id: org.org_id,
}; };
await createCollection(collection); await createCollection(collection);
revalidateTags(["collections"], orgslug); await revalidateTags(["collections"], orgslug);
router.refresh();
router.prefetch(getUriWithOrg(orgslug, "/collections")); router.prefetch(getUriWithOrg(orgslug, "/collections"));
router.push(getUriWithOrg(orgslug, "/collections")); router.push(getUriWithOrg(orgslug, "/collections"));
router.refresh();
}; };

View file

@ -39,7 +39,7 @@ const CollectionsPage = async (params: any) => {
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const org_id = org.org_id; const org_id = org.org_id;
const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token_cookie ? access_token_cookie.value : null); const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['collections'] });
return ( return (
<GeneralWrapperStyled> <GeneralWrapperStyled>
@ -54,7 +54,7 @@ const CollectionsPage = async (params: any) => {
<div className="home_collections flex flex-wrap"> <div className="home_collections flex flex-wrap">
{collections.map((collection: any) => ( {collections.map((collection: any) => (
<div className="flex flex-col py-3 px-3" key={collection.collection_id}> <div className="flex flex-col py-3 px-3" key={collection.collection_id}>
<CollectionAdminEditsArea org_id={org_id} collection_id={collection.collection_id} collection={collection} /> <CollectionAdminEditsArea orgslug={orgslug} org_id={org_id} collection_id={collection.collection_id} collection={collection} />
<Link href={getUriWithOrg(orgslug, "/collection/" + removeCollectionPrefix(collection.collection_id))}> <Link href={getUriWithOrg(orgslug, "/collection/" + removeCollectionPrefix(collection.collection_id))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[180px] bg-cover flex flex-col items-center justify-center bg-indigo-600 font-bold text-zinc-50" > <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[180px] bg-cover flex flex-col items-center justify-center bg-indigo-600 font-bold text-zinc-50" >
<h1 className="font-bold text-lg py-2 justify-center mb-2">{collection.name}</h1> <h1 className="font-bold text-lg py-2 justify-center mb-2">{collection.name}</h1>

View file

@ -23,7 +23,7 @@ const CourseClient = (props: any) => {
async function startCourseUI() { async function startCourseUI() {
// Create activity // Create activity
await startCourse("course_" + courseid, orgslug); await startCourse("course_" + courseid, orgslug);
revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
// refresh page (FIX for Next.js BUG) // refresh page (FIX for Next.js BUG)
@ -34,7 +34,7 @@ const CourseClient = (props: any) => {
// Close activity // Close activity
let activity = await removeCourse("course_" + courseid, orgslug); let activity = await removeCourse("course_" + courseid, orgslug);
// Mutate course // Mutate course
revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
// refresh page (FIX for Next.js BUG) // refresh page (FIX for Next.js BUG)

View file

@ -0,0 +1,152 @@
"use client";
import React, { FC, use, useEffect, useReducer } from 'react'
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import useSWR, { mutate } from 'swr';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import Link from 'next/link';
import CourseEdition from '../subpages/CourseEdition';
import CourseContentEdition from '../subpages/CourseContentEdition';
import ErrorUI from '@components/StyledElements/Error/Error';
import { updateChaptersMetadata } from '@services/courses/chapters';
import { Check, SaveAllIcon, Timer } from 'lucide-react';
import Loading from '../../loading';
import { updateCourse } from '@services/courses/courses';
import { useRouter } from 'next/navigation';
function CourseEditClient({ courseid, subpage, params }: { courseid: string, subpage: string, params: any }) {
const { data: chapters_meta, error: chapters_meta_error, isLoading: chapters_meta_isloading } = useSWR(`${getAPIUrl()}chapters/meta/course_${courseid}`, swrFetcher);
const { data: course, error: course_error, isLoading: course_isloading } = useSWR(`${getAPIUrl()}courses/course_${courseid}`, swrFetcher);
const [courseChaptersMetadata, dispatchCourseChaptersMetadata] = useReducer(courseChaptersReducer, {});
const [courseState, dispatchCourseMetadata] = useReducer(courseReducer, {});
const [savedContent, dispatchSavedContent] = useReducer(savedContentReducer, true);
const router = useRouter();
function courseChaptersReducer(state: any, action: any) {
switch (action.type) {
case 'updated_chapter':
// action will contain the entire state, just update the entire state
return action.payload;
default:
throw new Error();
}
}
function courseReducer(state: any, action: any) {
switch (action.type) {
case 'updated_course':
// action will contain the entire state, just update the entire state
return action.payload;
default:
throw new Error();
}
}
function savedContentReducer(state: any, action: any) {
switch (action.type) {
case 'saved_content':
return true;
case 'unsaved_content':
return false;
default:
throw new Error();
}
}
async function saveCourse() {
if (subpage.toString() === 'content') {
await updateChaptersMetadata(courseid, courseChaptersMetadata)
dispatchSavedContent({ type: 'saved_content' })
await mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`)
await revalidateTags(['courses'], params.params.orgslug)
router.refresh()
}
else if (subpage.toString() === 'general') {
await updateCourse(courseid, courseState)
dispatchSavedContent({ type: 'saved_content' })
await mutate(`${getAPIUrl()}courses/course_${courseid}`)
await revalidateTags(['courses'], params.params.orgslug)
router.refresh()
}
}
useEffect(() => {
if (chapters_meta) {
dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: chapters_meta })
dispatchSavedContent({ type: 'saved_content' })
}
if (course) {
dispatchCourseMetadata({ type: 'updated_course', payload: course })
dispatchSavedContent({ type: 'saved_content' })
}
}, [chapters_meta, course])
return (
<>
<div className='bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
{course_isloading && <div className='text-sm text-gray-500'>Loading...</div>}
{course && <>
<div className='flex items-center'><div className='info flex space-x-5 items-center grow'>
<div className='flex'>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}`}>
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(course.org_id, "course_" + courseid, course.thumbnail)}`} alt="" />
</Link>
</div>
<div className="flex flex-col ">
<div className='text-sm text-gray-500'>Edit Course</div>
<div className='text-2xl font-bold first-letter:uppercase'>{course.name}</div>
</div>
</div>
<div className='flex space-x-5 items-center'>
{savedContent ? <></> : <div className='text-gray-600 flex space-x-2 items-center antialiased'>
<Timer size={15} />
<div>
Unsaved changes
</div>
</div>}
<div className={`' px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` + (savedContent ? 'bg-gray-600 text-white' : 'bg-black text-white border hover:bg-gray-900 ')
} onClick={saveCourse}>
{savedContent ? <Check size={20} /> : <SaveAllIcon size={20} />}
{savedContent ? <div className=''>Saved</div> : <div className=''>Save</div>}
</div>
</div>
</div>
</>}
<div className='flex space-x-5 pt-3 font-black text-sm'>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/general`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>General</div>
</Link>
<Link href={getUriWithOrg(params.params.orgslug, "") + `/course/${courseid}/edit/content`}>
<div className={`py-2 w-16 text-center border-black transition-all ease-linear ${subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>Content</div>
</Link>
</div>
</div>
</div>
<CoursePageViewer dispatchSavedContent={dispatchSavedContent} courseState={courseState} courseChaptersMetadata={courseChaptersMetadata} dispatchCourseMetadata={dispatchCourseMetadata} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} subpage={subpage} courseid={courseid} orgslug={params.params.orgslug} />
</>
)
}
const CoursePageViewer = ({ subpage, courseid, orgslug, dispatchCourseMetadata, dispatchCourseChaptersMetadata, courseChaptersMetadata, dispatchSavedContent, courseState }: { subpage: string, courseid: string, orgslug: string, dispatchCourseChaptersMetadata: React.Dispatch<any>, dispatchCourseMetadata: React.Dispatch<any>, dispatchSavedContent: React.Dispatch<any>, courseChaptersMetadata: any, courseState: any }) => {
if (subpage.toString() === 'general' && Object.keys(courseState).length !== 0) {
return <CourseEdition data={courseState} dispatchCourseMetadata={dispatchCourseMetadata} dispatchSavedContent={dispatchSavedContent} />
}
else if (subpage.toString() === 'content' && Object.keys(courseChaptersMetadata).length !== 0) {
return <CourseContentEdition data={courseChaptersMetadata} dispatchSavedContent={dispatchSavedContent} dispatchCourseChaptersMetadata={dispatchCourseChaptersMetadata} courseid={courseid} orgslug={orgslug} />
}
else if (subpage.toString() === 'content' || subpage.toString() === 'general') {
return <Loading />
}
else {
return <ErrorUI />
}
}
export default CourseEditClient

View file

@ -0,0 +1,40 @@
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import CourseEditClient from "./edit";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers";
import { Metadata } from 'next';
type MetadataProps = {
params: { orgslug: string, courseid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)
return {
title: `Edit Course - ` + course_meta.course.name,
description: course_meta.course.mini_description,
};
}
function CourseEdit(params: any) {
let subpage = params.params.subpage ? params.params.subpage : 'general';
return (
<>
<CourseEditClient params={params} subpage={subpage} courseid={params.params.courseid} />
</>
);
}
export default CourseEdit;

View file

@ -1,346 +0,0 @@
"use client";
import React from "react";
import { useState, useEffect } from "react";
import styled from "styled-components";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import { initialData, initialData2 } from "@components/Pages/CourseEdit/Draggables/data";
import Chapter from "@components/Pages/CourseEdit/Draggables/Chapter";
import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters";
import { useRouter } from "next/navigation";
import NewChapterModal from "@components/Objects/Modals/Chapters/NewChapter";
import NewActivityModal from "@components/Objects/Modals/Activities/Create/NewActivity";
import { createActivity, createFileActivity, createExternalVideoActivity } from "@services/courses/activities";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import Modal from "@components/StyledElements/Modal/Modal";
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
import { Folders, Package2, SaveIcon } from "lucide-react";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { revalidateTags } from "@services/utils/ts/requests";
function CourseEdit(params: any) {
const router = useRouter();
// Initial Course State
const [data, setData] = useState(initialData2) as any;
// New Chapter Modal State
const [newChapterModal, setNewChapterModal] = useState(false) as any;
// New Activity Modal State
const [newActivityModal, setNewActivityModal] = useState(false) as any;
const [newActivityModalData, setNewActivityModalData] = useState("") as any;
// Check window availability
const [winReady, setwinReady] = useState(false);
const courseid = params.params.courseid;
const orgslug = params.params.orgslug;
async function getCourseChapters() {
try {
const courseChapters = await getCourseChaptersMetadata(courseid, { revalidate: 120 });
setData(courseChapters);
} catch (error: any) {
denyAccessToUser(error, router)
}
}
useEffect(() => {
if (courseid && orgslug) {
getCourseChapters();
}
setwinReady(true);
}, [courseid, orgslug]);
// get a list of chapters order by chapter order
const getChapters = () => {
const chapterOrder = data.chapterOrder ? data.chapterOrder : [];
return chapterOrder.map((chapterId: any) => {
const chapter = data.chapters[chapterId];
let activities = [];
if (data.activities) {
activities = chapter.activityIds.map((activityId: any) => data.activities[activityId])
? chapter.activityIds.map((activityId: any) => data.activities[activityId])
: [];
}
return {
list: {
chapter: chapter,
activities: activities,
},
};
});
};
// Submit new chapter
const submitChapter = async (chapter: any) => {
await createChapter(chapter, courseid);
await getCourseChapters();
revalidateTags(['courses'], orgslug);
router.refresh();
setNewChapterModal(false);
};
// Submit new activity
const submitActivity = async (activity: any) => {
let org = await getOrganizationContextInfo(orgslug, { revalidate: 1800 });
await updateChaptersMetadata(courseid, data);
await createActivity(activity, activity.chapterId, org.org_id);
await getCourseChapters();
setNewActivityModal(false);
revalidateTags(['courses'], orgslug);
router.refresh();
};
// Submit File Upload
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
await updateChaptersMetadata(courseid, data);
await createFileActivity(file, type, activity, chapterId);
await getCourseChapters();
setNewActivityModal(false);
revalidateTags(['courses'], orgslug);
router.refresh();
};
// Submit YouTube Video Upload
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
await updateChaptersMetadata(courseid, data);
await createExternalVideoActivity(external_video_data, activity, chapterId);
await getCourseChapters();
setNewActivityModal(false);
revalidateTags(['courses'], orgslug);
router.refresh();
};
const deleteChapterUI = async (chapterId: any) => {
await deleteChapter(chapterId);
getCourseChapters();
revalidateTags(['courses'], orgslug);
router.refresh();
};
const updateChapters = () => {
updateChaptersMetadata(courseid, data);
revalidateTags(['courses'], orgslug);
router.refresh();
};
/*
Modals
*/
const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true);
setNewActivityModalData(chapterId);
};
// Close new chapter modal
const closeNewChapterModal = () => {
setNewChapterModal(false);
};
const closeNewActivityModal = () => {
setNewActivityModal(false);
};
/*
Drag and drop functions
*/
const onDragEnd = (result: any) => {
const { destination, source, draggableId, type } = result;
// check if the activity is dropped outside the droppable area
if (!destination) {
return;
}
// check if the activity is dropped in the same place
if (destination.droppableId === source.droppableId && destination.index === source.index) {
return;
}
//////////////////////////// CHAPTERS ////////////////////////////
if (type === "chapter") {
const newChapterOrder = Array.from(data.chapterOrder);
newChapterOrder.splice(source.index, 1);
newChapterOrder.splice(destination.index, 0, draggableId);
const newState = {
...data,
chapterOrder: newChapterOrder,
};
setData(newState);
return;
}
//////////////////////// ACTIVITIES IN SAME CHAPTERS ////////////////////////////
// check if the activity is dropped in the same chapter
const start = data.chapters[source.droppableId];
const finish = data.chapters[destination.droppableId];
// check if the activity is dropped in the same chapter
if (start === finish) {
// create new arrays for chapters and activities
const chapter = data.chapters[source.droppableId];
const newActivityIds = Array.from(chapter.activityIds);
// remove the activity from the old position
newActivityIds.splice(source.index, 1);
// add the activity to the new position
newActivityIds.splice(destination.index, 0, draggableId);
const newChapter = {
...chapter,
activityIds: newActivityIds,
};
const newState = {
...data,
chapters: {
...data.chapters,
[newChapter.id]: newChapter,
},
};
setData(newState);
return;
}
//////////////////////// ACTIVITIES IN DIFF CHAPTERS ////////////////////////////
// check if the activity is dropped in a different chapter
if (start !== finish) {
// create new arrays for chapters and activities
const startChapterActivityIds = Array.from(start.activityIds);
// remove the activity from the old position
startChapterActivityIds.splice(source.index, 1);
const newStart = {
...start,
activityIds: startChapterActivityIds,
};
// add the activity to the new position within the chapter
const finishChapterActivityIds = Array.from(finish.activityIds);
finishChapterActivityIds.splice(destination.index, 0, draggableId);
const newFinish = {
...finish,
activityIds: finishChapterActivityIds,
};
const newState = {
...data,
chapters: {
...data.chapters,
[newStart.id]: newStart,
[newFinish.id]: newFinish,
},
};
setData(newState);
return;
}
};
return (
<>
<div
>
<GeneralWrapperStyled>
<div className="font-bold text-lg flex space-x-2 items-center">
<p> Edit Course {" "}</p>
<div
className="bg-black z-20 hover:bg-gray-950 text-white font-bold p-1 px-2 text-sm rounded flex items-center cursor-pointer space-x-2 hover:cursor-pointer"
onClick={() => {
updateChapters();
}}
>
<SaveIcon className="w-4 h-4" />
<p>Save</p>
</div>
</div>
<Modal
isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal}
minHeight="no-min"
addDefCloseButton={false}
dialogContent={<NewActivityModal
closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity}
chapterId={newActivityModalData}
></NewActivityModal>}
dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course"
/>
<br />
{winReady && (
<div className="flex flex-col max-w-7xl justify-center items-center mx-auto">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable key="chapters" droppableId="chapters" type="chapter">
{(provided) => (
<>
<div key={"chapters"} {...provided.droppableProps} ref={provided.innerRef}>
{getChapters().map((info: any, index: any) => (
<>
<Chapter
orgslug={orgslug}
courseid={courseid}
openNewActivityModal={openNewActivityModal}
deleteChapter={deleteChapterUI}
key={index}
info={info}
index={index}
></Chapter>
</>
))}
{provided.placeholder}
</div>
</>
)}
</Droppable>
</DragDropContext>
<Modal
isDialogOpen={newChapterModal}
onOpenChange={setNewChapterModal}
minHeight="sm"
dialogContent={<NewChapterModal
closeModal={closeNewChapterModal}
submitChapter={submitChapter}
></NewChapterModal>}
dialogTitle="Create chapter"
dialogDescription="Add a new chapter to the course"
dialogTrigger={
<div className="flex max-w-7xl bg-black shadow rounded-md text-white justify-center space-x-2 p-3 w-72 hover:bg-gray-900 hover:cursor-pointer">
<Folders size={20} />
<div>Add chapter +</div>
</div>
}
/>
</div>
)}
</GeneralWrapperStyled >
</div>
</>
);
}
export default CourseEdit;

View file

@ -0,0 +1,318 @@
"use client";
import React from "react";
import { useState, useEffect } from "react";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import Chapter from "@components/Pages/CourseEdit/Draggables/Chapter";
import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters";
import { useRouter } from "next/navigation";
import NewChapterModal from "@components/Objects/Modals/Chapters/NewChapter";
import NewActivityModal from "@components/Objects/Modals/Activities/Create/NewActivity";
import { createActivity, createFileActivity, createExternalVideoActivity } from "@services/courses/activities";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import Modal from "@components/StyledElements/Modal/Modal";
import { denyAccessToUser } from "@services/utils/react/middlewares/views";
import { Folders, SaveIcon } from "lucide-react";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { mutate } from "swr";
import { getAPIUrl } from "@services/config/config";
function CourseContentEdition(props: any) {
const router = useRouter();
// Initial Course State
const data = props.data;
// New Chapter Modal State
const [newChapterModal, setNewChapterModal] = useState(false) as any;
// New Activity Modal State
const [newActivityModal, setNewActivityModal] = useState(false) as any;
const [newActivityModalData, setNewActivityModalData] = useState("") as any;
// Check window availability
const [winReady, setwinReady] = useState(false);
const courseid = props.courseid;
const orgslug = props.orgslug;
useEffect(() => {
setwinReady(true);
}, [courseid, orgslug]);
// get a list of chapters order by chapter order
const getChapters = () => {
const chapterOrder = data.chapterOrder ? data.chapterOrder : [];
return chapterOrder.map((chapterId: any) => {
const chapter = data.chapters[chapterId];
let activities = [];
if (data.activities) {
activities = chapter.activityIds.map((activityId: any) => data.activities[activityId])
? chapter.activityIds.map((activityId: any) => data.activities[activityId])
: [];
}
return {
list: {
chapter: chapter,
activities: activities,
},
};
});
};
// Submit new chapter
const submitChapter = async (chapter: any) => {
await createChapter(chapter, courseid);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
await revalidateTags(['courses'], orgslug);
router.refresh();
setNewChapterModal(false);
};
// Submit new activity
const submitActivity = async (activity: any) => {
let org = await getOrganizationContextInfo(orgslug, { revalidate: 1800 });
await updateChaptersMetadata(courseid, data);
await createActivity(activity, activity.chapterId, org.org_id);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
// Submit File Upload
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
await updateChaptersMetadata(courseid, data);
await createFileActivity(file, type, activity, chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
// Submit YouTube Video Upload
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
await updateChaptersMetadata(courseid, data);
await createExternalVideoActivity(external_video_data, activity, chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
setNewActivityModal(false);
await revalidateTags(['courses'], orgslug);
router.refresh();
};
const deleteChapterUI = async (chapterId: any) => {
await deleteChapter(chapterId);
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
// await getCourseChapters();
await revalidateTags(['courses'], orgslug);
router.refresh();
};
const updateChapters = () => {
updateChaptersMetadata(courseid, data);
revalidateTags(['courses'], orgslug);
router.refresh();
};
/*
Modals
*/
const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true);
setNewActivityModalData(chapterId);
};
// Close new chapter modal
const closeNewChapterModal = () => {
setNewChapterModal(false);
};
const closeNewActivityModal = () => {
setNewActivityModal(false);
};
/*
Drag and drop functions
*/
const onDragEnd = async (result: any) => {
const { destination, source, draggableId, type } = result;
// check if the activity is dropped outside the droppable area
if (!destination) {
return;
}
// check if the activity is dropped in the same place
if (destination.droppableId === source.droppableId && destination.index === source.index) {
return;
}
//////////////////////////// CHAPTERS ////////////////////////////
if (type === "chapter") {
const newChapterOrder = Array.from(data.chapterOrder);
newChapterOrder.splice(source.index, 1);
newChapterOrder.splice(destination.index, 0, draggableId);
const newState = {
...data,
chapterOrder: newChapterOrder,
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
//////////////////////// ACTIVITIES IN SAME CHAPTERS ////////////////////////////
// check if the activity is dropped in the same chapter
const start = data.chapters[source.droppableId];
const finish = data.chapters[destination.droppableId];
// check if the activity is dropped in the same chapter
if (start === finish) {
// create new arrays for chapters and activities
const chapter = data.chapters[source.droppableId];
const newActivityIds = Array.from(chapter.activityIds);
// remove the activity from the old position
newActivityIds.splice(source.index, 1);
// add the activity to the new position
newActivityIds.splice(destination.index, 0, draggableId);
const newChapter = {
...chapter,
activityIds: newActivityIds,
};
const newState = {
...data,
chapters: {
...data.chapters,
[newChapter.id]: newChapter,
},
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
//////////////////////// ACTIVITIES IN DIFF CHAPTERS ////////////////////////////
// check if the activity is dropped in a different chapter
if (start !== finish) {
// create new arrays for chapters and activities
const startChapterActivityIds = Array.from(start.activityIds);
// remove the activity from the old position
startChapterActivityIds.splice(source.index, 1);
const newStart = {
...start,
activityIds: startChapterActivityIds,
};
// add the activity to the new position within the chapter
const finishChapterActivityIds = Array.from(finish.activityIds);
finishChapterActivityIds.splice(destination.index, 0, draggableId);
const newFinish = {
...finish,
activityIds: finishChapterActivityIds,
};
const newState = {
...data,
chapters: {
...data.chapters,
[newStart.id]: newStart,
[newFinish.id]: newFinish,
},
};
props.dispatchCourseChaptersMetadata({ type: 'updated_chapter', payload: newState })
props.dispatchSavedContent({ type: 'unsaved_content' })
//setData(newState);
return;
}
};
return (
<>
<div className=""
>
<GeneralWrapperStyled>
<Modal
isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal}
minHeight="no-min"
addDefCloseButton={false}
dialogContent={<NewActivityModal
closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity}
chapterId={newActivityModalData}
></NewActivityModal>}
dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course"
/>
{winReady && (
<div className="flex flex-col">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable key="chapters" droppableId="chapters" type="chapter">
{(provided) => (
<>
<div key={"chapters"} {...provided.droppableProps} ref={provided.innerRef}>
{getChapters().map((info: any, index: any) => (
<>
<Chapter
orgslug={orgslug}
courseid={courseid}
openNewActivityModal={openNewActivityModal}
deleteChapter={deleteChapterUI}
key={index}
info={info}
index={index}
></Chapter>
</>
))}
{provided.placeholder}
</div>
</>
)}
</Droppable>
</DragDropContext>
<Modal
isDialogOpen={newChapterModal}
onOpenChange={setNewChapterModal}
minHeight="sm"
dialogContent={<NewChapterModal
closeModal={closeNewChapterModal}
submitChapter={submitChapter}
></NewChapterModal>}
dialogTitle="Create chapter"
dialogDescription="Add a new chapter to the course"
dialogTrigger={
<div className="flex max-w-7xl bg-black text-sm shadow rounded-md items-center text-white justify-center mx-auto space-x-2 p-3 w-72 hover:bg-gray-900 hover:cursor-pointer">
<Folders size={16} />
<div>Add chapter +</div>
</div>
}
/>
</div>
)}
</GeneralWrapperStyled >
</div>
</>
);
}
export default CourseContentEdition;

View file

@ -0,0 +1,116 @@
"use client";
import FormLayout, { ButtonBlack, FormField, FormLabel, FormLabelAndMessage, FormMessage, Input, Textarea } from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form';
import { useFormik } from 'formik';
import { AlertTriangle } from "lucide-react";
import React from "react";
const validate = (values: any) => {
const errors: any = {};
if (!values.name) {
errors.name = 'Required';
}
if (values.name.length > 100) {
errors.name = 'Must be 80 characters or less';
}
if (!values.mini_description) {
errors.mini_description = 'Required';
}
if (values.mini_description.length > 200) {
errors.mini_description = 'Must be 200 characters or less';
}
if (!values.description) {
errors.description = 'Required';
}
if (values.description.length > 1000) {
errors.description = 'Must be 1000 characters or less';
}
if (!values.learnings) {
errors.learnings = 'Required';
}
return errors;
};
function CourseEdition(props: any) {
const [error, setError] = React.useState('');
const formik = useFormik({
initialValues: {
name: String(props.data.name),
mini_description: String(props.data.mini_description),
description: String(props.data.description),
learnings: String(props.data.learnings),
},
validate,
onSubmit: async values => {
},
});
React.useEffect(() => {
// This code will run whenever form values are updated
if (formik.values !== formik.initialValues) {
props.dispatchSavedContent({ type: 'unsaved_content' });
const updatedCourse = {
...props.data,
name: formik.values.name,
mini_description: formik.values.mini_description,
description: formik.values.description,
learnings: formik.values.learnings.split(", "),
};
props.dispatchCourseMetadata({ type: 'updated_course', payload: updatedCourse });
}
}, [formik.values, formik.initialValues]);
return (
<div className='max-w-screen-2xl mx-auto px-16 pt-5 tracking-tight'>
<div className="login-form">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label='Name' message={formik.errors.name} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control>
</FormField>
<FormField name="mini_description">
<FormLabelAndMessage label='Mini description' message={formik.errors.mini_description} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.mini_description} type="text" required />
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label='Description' message={formik.errors.description} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
</Form.Control>
</FormField>
<FormField name="learnings">
<FormLabelAndMessage label='Learnings (Separated by , )' message={formik.errors.learnings} />
<Form.Control asChild>
<Textarea placeholder='Science, Design, Architecture' style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
</Form.Control>
</FormField>
</FormLayout>
</div>
</div>
)
}
export default CourseEdition

View file

@ -1,7 +1,6 @@
'use client'; 'use client';
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse'; import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
import Modal from '@components/StyledElements/Modal/Modal'; import Modal from '@components/StyledElements/Modal/Modal';
import { Edit2, Trash } from "lucide-react";
import { getBackendUrl, getUriWithOrg } from '@services/config/config'; import { getBackendUrl, getUriWithOrg } from '@services/config/config';
import CoursesLogo from "public/svg/courses.svg"; import CoursesLogo from "public/svg/courses.svg";
import CollectionsLogo from "public/svg/collections.svg"; import CollectionsLogo from "public/svg/collections.svg";
@ -37,7 +36,7 @@ function Courses(props: CourseProps) {
async function deleteCourses(course_id: any) { async function deleteCourses(course_id: any) {
await deleteCourseFromBackend(course_id); await deleteCourseFromBackend(course_id);
revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
router.refresh(); router.refresh();
} }
@ -85,10 +84,7 @@ function Courses(props: CourseProps) {
</div> </div>
))} ))}
</div> </div>
</GeneralWrapperStyled> </GeneralWrapperStyled>
</div> </div>
) )
} }
@ -102,14 +98,14 @@ const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any,
dialogTitle={'Delete ' + props.course.name + ' ?'} dialogTitle={'Delete ' + props.course.name + ' ?'}
dialogTrigger={ dialogTrigger={
<button className="rounded-md text-sm px-3 font-bold text-red-800 bg-red-200 w-16 flex justify-center items-center" > <button className="rounded-md text-sm px-3 font-bold text-red-800 bg-red-200 w-16 flex justify-center items-center" >
Delete <Trash size={10}></Trash> Delete
</button>} </button>}
functionToExecute={() => props.deleteCourses(props.courseId)} functionToExecute={() => props.deleteCourses(props.courseId)}
status='warning' status='warning'
></ConfirmationModal> ></ConfirmationModal>
<Link href={getUriWithOrg(props.orgSlug, "/course/" + removeCoursePrefix(props.courseId) + "/edit")}> <Link href={getUriWithOrg(props.orgSlug, "/course/" + removeCoursePrefix(props.courseId) + "/edit")}>
<button className="rounded-md text-sm px-3 font-bold text-orange-800 bg-orange-200 w-16 flex justify-center items-center"> <button className="rounded-md text-sm px-3 font-bold text-orange-800 bg-orange-200 w-16 flex justify-center items-center">
Edit <Edit2 size={10}></Edit2> Edit
</button> </button>
</Link> </Link>
</div> </div>

View file

@ -2,12 +2,11 @@ import "@styles/globals.css";
import { Menu } from "@components/Objects/Menu/Menu"; import { Menu } from "@components/Objects/Menu/Menu";
import AuthProvider from "@components/Security/AuthProvider"; import AuthProvider from "@components/Security/AuthProvider";
export default async function RootLayout({ children, params }: { children: React.ReactNode , params:any}) { export default async function RootLayout({ children, params }: { children: React.ReactNode , params :any}) {
return ( return (
<> <>
<AuthProvider> <AuthProvider>
<Menu orgslug={params.orgslug}></Menu> <Menu orgslug={params?.orgslug}></Menu>
{children} {children}
</AuthProvider> </AuthProvider>
</> </>

View file

@ -38,7 +38,7 @@ const OrgHomePage = async (params: any) => {
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null); const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null);
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] }); const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token_cookie ? access_token_cookie.value : null); const collections = await getOrgCollectionsWithAuthHeader(org.org_id, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['courses'] });
// function to remove "course_" from the course_id // function to remove "course_" from the course_id

View file

@ -33,7 +33,7 @@ function OrganizationClient(props: any) {
let org_id = org.org_id; let org_id = org.org_id;
await uploadOrganizationLogo(org_id, selectedFile); await uploadOrganizationLogo(org_id, selectedFile);
setSelectedFile(null); // Reset the selected file setSelectedFile(null); // Reset the selected file
revalidateTags(['organizations'], org.slug); await revalidateTags(['organizations'], org.slug);
router.refresh(); router.refresh();
} }
@ -54,7 +54,7 @@ function OrganizationClient(props: any) {
await updateOrganization(org_id, values); await updateOrganization(org_id, values);
// Mutate the org // Mutate the org
revalidateTags(['organizations'], org.slug); await revalidateTags(['organizations'], org.slug);
router.refresh(); router.refresh();
} }

View file

@ -1,12 +1,10 @@
'use client'; 'use client';
import React from "react"; import React from "react";
import useSWR from "swr";
import Link from "next/link"; import Link from "next/link";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config"; import { getUriWithOrg } from "@services/config/config";
import { getOrganizationContextInfo } from "@services/organizations/orgs"; import { getOrganizationContextInfo } from "@services/organizations/orgs";
import ClientComponentSkeleton from "@components/Utils/ClientComp"; 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 MenuLinks from "./MenuLinks"; import MenuLinks from "./MenuLinks";
import { getOrgLogoMediaDirectory } from "@services/media/media"; import { getOrgLogoMediaDirectory } from "@services/media/media";
@ -23,33 +21,35 @@ export const Menu = async (props: any) => {
}}></div> }}></div>
</div> </div>
<div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center px-16 space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50"> <div className="backdrop-blur-lg bg-white/90 fixed flex top-0 left-0 right-0 h-[60px] ring-1 ring-inset ring-gray-500/10 items-center space-x-5 shadow-[0px_4px_16px_rgba(0,0,0,0.03)] z-50">
<div className="logo flex "> <div className="flex items-center space-x-5 w-full max-w-screen-2xl mx-auto px-16">
<Link href={getUriWithOrg(orgslug, "/")}> <div className="logo flex ">
<div className="flex w-auto h-9 rounded-md items-center m-auto py-1 justify-center" > <Link href={getUriWithOrg(orgslug, "/")}>
{org?.logo ? ( <div className="flex w-auto h-9 rounded-md items-center m-auto py-1 justify-center" >
<img {org?.logo ? (
src={`${getOrgLogoMediaDirectory(org.org_id, org?.logo)}`} <img
alt="Learnhouse" src={`${getOrgLogoMediaDirectory(org.org_id, org?.logo)}`}
style={{ width: "auto", height: "100%" }} alt="Learnhouse"
className="rounded-md" style={{ width: "auto", height: "100%" }}
/> className="rounded-md"
) : ( />
<LearnHouseLogo></LearnHouseLogo> ) : (
)} <LearnHouseLogo></LearnHouseLogo>
</div> )}
</Link> </div>
</div> </Link>
<div className="links flex grow"> </div>
<ClientComponentSkeleton> <div className="links flex grow">
<MenuLinks orgslug={orgslug} /> <ClientComponentSkeleton>
</ClientComponentSkeleton> <MenuLinks orgslug={orgslug} />
</ClientComponentSkeleton>
</div> </div>
<div className="profile flex"> <div className="profile flex">
<ClientComponentSkeleton> <ClientComponentSkeleton>
<HeaderProfileBox /> <HeaderProfileBox />
</ClientComponentSkeleton> </ClientComponentSkeleton>
</div>
</div> </div>
</div> </div>

View file

@ -43,13 +43,13 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
e.preventDefault(); e.preventDefault();
setIsSubmitting(true); setIsSubmitting(true);
let status = await createNewCourse(orgId, { name, description }, thumbnail); let status = await createNewCourse(orgId, { name, description }, thumbnail);
revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
setIsSubmitting(false); setIsSubmitting(false);
if (status.org_id == orgId) { if (status.org_id == orgId) {
closeModal(); closeModal();
router.refresh(); router.refresh();
revalidateTags(['courses'], orgslug); await revalidateTags(['courses'], orgslug);
// refresh page (FIX for Next.js BUG) // refresh page (FIX for Next.js BUG)
// window.location.reload(); // window.location.reload();

View file

@ -14,7 +14,7 @@ function Chapter(props: any) {
{...provided.draggableProps} {...provided.draggableProps}
ref={provided.innerRef} ref={provided.innerRef}
// isDragging={snapshot.isDragging} // isDragging={snapshot.isDragging}
className="" className="max-w-screen-2xl mx-auto"
key={props.info.list.chapter.id} key={props.info.list.chapter.id}
> >
<h3 className="flex space-x-2 pt-3 font-bold text-md items-center"> <h3 className="flex space-x-2 pt-3 font-bold text-md items-center">
@ -64,7 +64,6 @@ const ChapterWrapper = styled.div`
margin-bottom: 20px; margin-bottom: 20px;
padding: 4px; padding: 4px;
background-color: #ffffff9d; background-color: #ffffff9d;
width: 900px;
font-size: 15px; font-size: 15px;
display: block; display: block;
border-radius: 9px; border-radius: 9px;

View file

@ -21,7 +21,7 @@ function TrailCourseElement(props: TrailCourseElementProps) {
// Close activity // Close activity
let activity = await removeCourse(course_id, props.orgslug); let activity = await removeCourse(course_id, props.orgslug);
// Mutate course // Mutate course
revalidateTags(['courses'], props.orgslug); await revalidateTags(['courses'], props.orgslug);
router.refresh(); router.refresh();
// Mutate // Mutate

View file

@ -43,7 +43,6 @@ export const HeaderProfileBox = () => {
}; };
const AccountArea = styled.div` const AccountArea = styled.div`
padding-right: 20px;
display: flex; display: flex;
place-items: center; place-items: center;

11772
front/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -26,8 +26,8 @@
"avvvatars-react": "^0.4.2", "avvvatars-react": "^0.4.2",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^7.3.6", "framer-motion": "^7.3.6",
"lucide-react": "^0.248.0", "lucide-react": "^0.268.0",
"next": "^13.4.8", "next": "^13.4.19",
"re-resizable": "^6.9.9", "re-resizable": "^6.9.9",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",

View file

@ -40,8 +40,8 @@ export async function getOrgCollections() {
return res; return res;
} }
export async function getOrgCollectionsWithAuthHeader(org_id: string, access_token: string) { export async function getOrgCollectionsWithAuthHeader(org_id: string, access_token: string, next: any) {
const result: any = await fetch(`${getAPIUrl()}collections/org_id/${org_id}/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, { revalidate: 3 }, access_token)); const result: any = await fetch(`${getAPIUrl()}collections/org_id/${org_id}/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, next, access_token));
const res = await errorHandling(result); const res = await errorHandling(result);
return res; return res;
} }

View file

@ -24,6 +24,12 @@ export async function getCourseMetadataWithAuthHeader(course_id: any, next: any,
return res; return res;
} }
export async function updateCourse(course_id: any, data: any) {
const result: any = await fetch(`${getAPIUrl()}courses/course_${course_id}`, RequestBody("PUT", data, null));
const res = await errorHandling(result);
return res;
}
export async function getCourse(course_id: string, next: any) { export async function getCourse(course_id: string, next: any) {
const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("GET", null, next)); const result: any = await fetch(`${getAPIUrl()}courses/${course_id}`, RequestBody("GET", null, next));
const res = await errorHandling(result); const res = await errorHandling(result);

View file

@ -85,7 +85,7 @@ export const errorHandling = (res: any) => {
return res.json(); return res.json();
}; };
export const revalidateTags = (tags: string[], orgslug: string) => { export const revalidateTags = async (tags: string[], orgslug: string) => {
const url = getUriWithOrg(orgslug, ""); const url = getUriWithOrg(orgslug, "");
tags.forEach((tag) => { tags.forEach((tag) => {
fetch(`${url}/api/revalidate?tag=${tag}`); fetch(`${url}/api/revalidate?tag=${tag}`);

View file

@ -1,6 +1,6 @@
fastapi==0.92.0 fastapi==0.101.1
pydantic>=1.8.0,<2.0.0 pydantic>=1.8.0,<2.0.0
uvicorn==0.20.0 uvicorn==0.23.2
pymongo==4.3.3 pymongo==4.3.3
motor==3.1.1 motor==3.1.1
python-multipart python-multipart

View file

@ -1,7 +1,6 @@
import os import os
import requests import requests
from datetime import datetime from datetime import datetime
from pprint import pprint
from uuid import uuid4 from uuid import uuid4
from fastapi import Request from fastapi import Request
from src.security.security import security_hash_password from src.security.security import security_hash_password