diff --git a/front/components/UI/Elements/Menu.tsx b/front/components/UI/Elements/Menu.tsx index ff741359..57858435 100644 --- a/front/components/UI/Elements/Menu.tsx +++ b/front/components/UI/Elements/Menu.tsx @@ -17,9 +17,7 @@ export const Menu = () => { - -
@@ -32,11 +30,11 @@ export const Menu = () => { diff --git a/front/pages/org/[orgslug]/collections/index.tsx b/front/pages/org/[orgslug]/collections/index.tsx new file mode 100644 index 00000000..ce8f7700 --- /dev/null +++ b/front/pages/org/[orgslug]/collections/index.tsx @@ -0,0 +1,101 @@ +import Layout from "../../../../components/UI/Layout"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import React from "react"; +import styled from "styled-components"; +import { Title } from "../../../../components/UI/Elements/Styles/Title"; +import { deleteCollection, getOrgCollections } from "../../../../services/collections"; +import { getOrganizationContextInfo } from "../../../../services/orgs"; +import { getBackendUrl } from "../../../../services/config"; + +function Collections() { + const router = useRouter(); + const { orgslug } = router.query; + + const [isLoading, setIsLoading] = React.useState(true); + const [collections, setCollections] = React.useState([]); + + async function fetchCollections() { + setIsLoading(true); + const org = await getOrganizationContextInfo(orgslug); + const collections = await getOrgCollections(org.org_id); + setCollections(collections); + setIsLoading(false); + } + + async function deleteCollectionAndFetch(collectionId: number) { + setIsLoading(true); + await deleteCollection(collectionId); + await fetchCollections(); + setIsLoading(false); + } + + React.useEffect(() => { + fetchCollections(); + }, []); + + return ( + + + {orgslug} Collections :{" "} + <Link href={"/org/" + orgslug + "/collections/new"}> + <button>+</button> + </Link>{" "} + + {isLoading ? ( +
Loading...
+ ) : ( +
+ {collections.map((collection: any) => ( + + {collection.name} + + {collection.courses.map((course: any) => ( + + {course.name} + + ))} + + + + ))} +
+ )} +
+ ); +} + +const CollectionItem = styled.div` + display: flex; + flex-direction: row; + place-items: center; + width: 100%; + height: 100%; + padding: 10px; + border: 1px solid #e5e5e5; + border-radius: 5px; + box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.03); + background: #ffffff; + cursor: pointer; + transition: all 0.2s ease-in-out; + &:hover { + box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1); + } +`; + +const CourseMiniThumbnail = styled.div` + display: flex; + flex-direction: row; + img { + width: 20px; + height: 20px; + border-radius: 5px; + margin: 5px; + transition: all 0.2s ease-in-out; + } + + &:hover { + opacity: 0.8; + } +`; +export default Collections; diff --git a/front/pages/org/[orgslug]/collections/new/index.tsx b/front/pages/org/[orgslug]/collections/new/index.tsx new file mode 100644 index 00000000..64156ab0 --- /dev/null +++ b/front/pages/org/[orgslug]/collections/new/index.tsx @@ -0,0 +1,95 @@ +import { useRouter } from "next/router"; +import React from "react"; +import { Title } from "../../../../../components/UI/Elements/Styles/Title"; +import Layout from "../../../../../components/UI/Layout"; +import { getOrganizationContextInfo } from "../../../../../services/orgs"; +import { getOrgCourses } from "../../../../../services/courses/courses"; +import { createCollection } from "../../../../../services/collections"; + +function NewCollection() { + const router = useRouter(); + const { orgslug } = router.query; + const [name, setName] = React.useState(""); + const [org, setOrg] = React.useState({}) as any; + const [description, setDescription] = React.useState(""); + const [selectedCourses, setSelectedCourses] = React.useState([]) as any; + const [courses, setCourses] = React.useState([]) as any; + const [isLoading, setIsLoading] = React.useState(false); + + async function getCourses() { + setIsLoading(true); + const org = await getOrganizationContextInfo(orgslug); + setOrg(org); + const courses = await getOrgCourses(org.org_id); + setCourses(courses); + setIsLoading(false); + } + + const handleNameChange = (event: React.ChangeEvent) => { + setName(event.target.value); + }; + + const handleDescriptionChange = (event: React.ChangeEvent) => { + setDescription(event.target.value); + }; + + const handleSubmit = async (e: any) => { + e.preventDefault(); + console.log("selectedCourses", selectedCourses); + const collection = { + name: name, + description: description, + courses: selectedCourses, + org_id: org.org_id, + }; + await createCollection(collection); + router.push("/org/" + orgslug + "/collections"); + }; + + React.useEffect(() => { + if (router.isReady) { + getCourses(); + } + return () => {}; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [router.isReady]); + + return ( + + Add new +
+ + {isLoading ? ( +

Loading...

+ ) : ( +
+ {courses.map((course: any) => ( +
+ { + if (e.target.checked) { + setSelectedCourses([...selectedCourses, e.target.value]); + } else { + setSelectedCourses(selectedCourses.filter((item: any) => item !== e.target.value)); + } + }} + /> + +
+ ))} +
+ )} + +
+ +
+ +
+ ); +} + +export default NewCollection; diff --git a/front/pages/org/[orgslug]/courses/index.tsx b/front/pages/org/[orgslug]/courses/index.tsx index fb862feb..7e35c9a4 100644 --- a/front/pages/org/[orgslug]/courses/index.tsx +++ b/front/pages/org/[orgslug]/courses/index.tsx @@ -48,7 +48,7 @@ const CoursesIndexPage = () => {
- {orgslug} courses :{" "} + {orgslug} Courses :{" "} <Link href={"/org/" + orgslug + "/courses/new"}> <button>+</button> diff --git a/front/services/collections.ts b/front/services/collections.ts new file mode 100644 index 00000000..fae3b7c2 --- /dev/null +++ b/front/services/collections.ts @@ -0,0 +1,73 @@ +import { getAPIUrl } from "./config"; + +export async function getOrgCollections(org_slug: any) { + const HeadersConfig = new Headers({ "Content-Type": "application/json" }); + + const requestOptions: any = { + method: "GET", + headers: HeadersConfig, + redirect: "follow", + credentials: "include", + }; + + return fetch(`${getAPIUrl()}collections/page/1/limit/10`, requestOptions) + .then((result) => result.json()) + .catch((error) => console.log("error", error)); +} + +export async function getCollection(collection_slug: any) { + const HeadersConfig = new Headers({ "Content-Type": "application/json" }); + + const requestOptions: any = { + method: "GET", + headers: HeadersConfig, + redirect: "follow", + credentials: "include", + }; + + return fetch( + `${getAPIUrl()}collections/${collection_slug}`, + requestOptions + ) + .then((result) => result.json()) + .catch((error) => console.log("error", error)); +} + + + + + +export async function deleteCollection(collection_id: any) { + const HeadersConfig = new Headers({ "Content-Type": "application/json" }); + + const requestOptions: any = { + method: "DELETE", + headers: HeadersConfig, + redirect: "follow", + credentials: "include", + }; + + return fetch( + `${getAPIUrl()}collections/${collection_id}`, + requestOptions + ) + .then((result) => result.json()) + .catch((error) => console.log("error", error)); +} + +// Create a new collection +export async function createCollection(collection: any) { + const HeadersConfig = new Headers({ "Content-Type": "application/json" }); + + const requestOptions: any = { + method: "POST", + headers: HeadersConfig, + redirect: "follow", + credentials: "include", + body: JSON.stringify(collection), + }; + + return fetch(`${getAPIUrl()}collections/`, requestOptions) + .then((result) => result.json()) + .catch((error) => console.log("error", error)); +} \ No newline at end of file diff --git a/src/routers/courses/collections.py b/src/routers/courses/collections.py index f62f3b32..e2ce3dc5 100644 --- a/src/routers/courses/collections.py +++ b/src/routers/courses/collections.py @@ -24,7 +24,7 @@ async def api_get_collection(collection_id: str, current_user: PublicUser = Depe @router.get("/page/{page}/limit/{limit}") -async def api_get_collection_by(page: int, limit: int, current_user: PublicUser = Depends(get_current_user)): +async def api_get_collections_by(page: int, limit: int, current_user: PublicUser = Depends(get_current_user)): """ Get collections by page and limit """ diff --git a/src/services/courses/chapters.py b/src/services/courses/chapters.py index 824eb2ac..ebd5270c 100644 --- a/src/services/courses/chapters.py +++ b/src/services/courses/chapters.py @@ -238,7 +238,7 @@ async def verify_rights(course_id: str, current_user: PublicUser, action: str): if not course: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail=f"Course/CourseChapter does not exist") + status_code=status.HTTP_409_CONFLICT, detail=f"Course does not exist") hasRoleRights = await verify_user_rights_with_roles(action, current_user.user_id, course_id) isAuthor = current_user.user_id in course["authors"] diff --git a/src/services/courses/collections.py b/src/services/courses/collections.py index 95cdea39..abb020f1 100644 --- a/src/services/courses/collections.py +++ b/src/services/courses/collections.py @@ -14,7 +14,8 @@ from datetime import datetime class Collection(BaseModel): name: str description: str - courses: List[str] # course_id + courses: List[str] # course_id + org_id: str # org_id class CollectionInDB(Collection): @@ -35,7 +36,7 @@ async def get_collection(collection_id: str, current_user: PublicUser): # verify collection rights await verify_collection_rights(collection_id, current_user, "read") - + if not collection: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist") @@ -49,9 +50,11 @@ async def create_collection(collection_object: Collection, current_user: PublicU collections = learnhouseDB["collections"] # find if collection already exists using name - isCollectionNameAvailable = collections.find_one({"name": collection_object.name}) - - await verify_collection_rights("*", current_user, "create") + isCollectionNameAvailable = collections.find_one( + {"name": collection_object.name}) + + # TODO + # await verify_collection_rights("*", current_user, "create") if isCollectionNameAvailable: raise HTTPException( @@ -60,7 +63,8 @@ async def create_collection(collection_object: Collection, current_user: PublicU # generate collection_id with uuid4 collection_id = str(f"collection_{uuid4()}") - collection = CollectionInDB(collection_id=collection_id, **collection_object.dict()) + collection = CollectionInDB( + collection_id=collection_id, **collection_object.dict()) collection_in_db = collections.insert_one(collection.dict()) @@ -81,7 +85,6 @@ async def update_collection(collection_object: Collection, collection_id: str, c collection = collections.find_one({"collection_id": collection_id}) - if not collection: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist") @@ -89,7 +92,8 @@ async def update_collection(collection_object: Collection, collection_id: str, c updated_collection = CollectionInDB( collection_id=collection_id, **collection_object.dict()) - collections.update_one({"collection_id": collection_id}, {"$set": updated_collection.dict()}) + collections.update_one({"collection_id": collection_id}, { + "$set": updated_collection.dict()}) return Collection(**updated_collection.dict()) @@ -97,7 +101,7 @@ async def update_collection(collection_object: Collection, collection_id: str, c async def delete_collection(collection_id: str, current_user: PublicUser): await check_database() - await verify_collection_rights(collection_id, current_user,"delete") + await verify_collection_rights(collection_id, current_user, "delete") collections = learnhouseDB["collections"] @@ -119,27 +123,43 @@ async def delete_collection(collection_id: str, current_user: PublicUser): # Misc #################################################### + async def get_collections(page: int = 1, limit: int = 10): ## TODO : auth await check_database() collections = learnhouseDB["collections"] - # get all collections from database - all_collections = collections.find().sort("name", 1).skip(10 * (page - 1)).limit(limit) - - # TODO : Check rights for each collection - return [json.loads(json.dumps(collection, default=str)) for collection in all_collections] + # get all collections from database without ObjectId + all_collections = collections.find({}).sort( + "name", 1).skip(10 * (page - 1)).limit(limit) + # create list of collections and include courses in each collection + collections_list = [] + for collection in all_collections: + collection = CollectionInDB(**collection) + collections_list.append(collection) + + collection_courses = [course for course in collection.courses] + # add courses to collection + courses = learnhouseDB["courses"] + collection.courses = [] + collection.courses = courses.find( + {"course_id": {"$in": collection_courses}}, {'_id': 0}) + + collection.courses = [course for course in collection.courses] + + return collections_list #### Security #################################################### + async def verify_collection_rights(collection_id: str, current_user: PublicUser, action: str): await check_database() collections = learnhouseDB["collections"] collection = collections.find_one({"collection_id": collection_id}) - if not collection: + if not collection and action != "create": raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist")