From 40496f7cedf2854655c18a3d5b9fc0067af2432b Mon Sep 17 00:00:00 2001 From: swve Date: Mon, 31 Oct 2022 16:20:46 +0100 Subject: [PATCH] fix: refactor course chapters --- front/components/drags/data.ts | 4 +- src/main.py | 3 +- src/routers/chapters.py | 64 ++++++++++ src/routers/courses.py | 63 ++-------- src/services/chapters.py | 220 +++++++++++++++++++++++++++++++++ src/services/courses.py | 176 ++++---------------------- 6 files changed, 321 insertions(+), 209 deletions(-) create mode 100644 src/routers/chapters.py create mode 100644 src/services/chapters.py diff --git a/front/components/drags/data.ts b/front/components/drags/data.ts index 5e1ee3b8..cdf98dd5 100644 --- a/front/components/drags/data.ts +++ b/front/components/drags/data.ts @@ -7,10 +7,10 @@ export const initialData = { "element-5": { id: "element-5", content: "Fifth element" }, }, chapters: { - "chapter-1": { id: "chapter-1", title: "Chapter 1", elementIds: ["element-1", "element-2", "element-3", ] }, + "chapter-1": { id: "chapter-1", title: "Chapter 1", elementIds: ["element-1", "element-2", "element-3"] }, "chapter-2": { id: "chapter-2", title: "Chapter 2", elementIds: ["element-4"] }, "chapter-3": { id: "chapter-3", title: "Chapter 3", elementIds: ["element-5"] }, - }, + chapterOrder: ["chapter-1", "chapter-2", "chapter-3"], }; diff --git a/src/main.py b/src/main.py index a2f145b3..31bbd354 100644 --- a/src/main.py +++ b/src/main.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from src.routers import collections, courses, users, auth, houses, orgs, roles +from src.routers import chapters, collections, courses, users, auth, houses, orgs, roles global_router = APIRouter(prefix="/api") @@ -12,5 +12,6 @@ global_router.include_router(houses.router, prefix="/houses", tags=["houses"]) global_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"]) global_router.include_router(roles.router, prefix="/roles", tags=["roles"]) global_router.include_router(courses.router, prefix="/courses", tags=["courses"]) +global_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"]) global_router.include_router(collections.router, prefix="/collections", tags=["collections"]) diff --git a/src/routers/chapters.py b/src/routers/chapters.py new file mode 100644 index 00000000..2ebb7b9f --- /dev/null +++ b/src/routers/chapters.py @@ -0,0 +1,64 @@ +from fastapi import APIRouter, Depends, UploadFile, Form + +from src.services.chapters import CourseChapter, CourseChapterMetaData, create_coursechapter, delete_coursechapter, get_coursechapter, get_coursechapters, get_coursechapters_meta, update_coursechapter, update_coursechapters_meta +from src.services.users import PublicUser +from src.services.auth import get_current_user + +router = APIRouter() + + +@router.post("/") +async def api_create_coursechapter(coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser = Depends(get_current_user)): + """ + Create new CourseChapter + """ + return await create_coursechapter(coursechapter_object, course_id, current_user) + + +@router.get("/{coursechapter_id}") +async def api_get_coursechapter(coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): + """ + Get single CourseChapter by coursechapter_id + """ + return await get_coursechapter(coursechapter_id, current_user=current_user) + + +@router.get("/meta/{course_id}") +async def api_get_coursechapter(course_id: str, current_user: PublicUser = Depends(get_current_user)): + """ + Get coursechapter metadata + """ + return await get_coursechapters_meta(course_id, current_user=current_user) + + +@router.put("/meta/{course_id}") +async def api_update_coursechapter_meta(course_id: str, coursechapters_metadata: CourseChapterMetaData, current_user: PublicUser = Depends(get_current_user)): + """ + Update coursechapter metadata + """ + return await update_coursechapters_meta(course_id, coursechapters_metadata, current_user=current_user) + + +@router.get("/{course_id}/page/{page}/limit/{limit}") +async def api_get_coursechapter_by(course_id: str, page: int, limit: int): + """ + Get CourseChapters by page and limit + """ + return await get_coursechapters(course_id, page, limit) + + +@router.put("/{coursechapter_id}") +async def api_update_coursechapter(coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): + """ + Update CourseChapters by course_id + """ + return await update_coursechapter(coursechapter_object, coursechapter_id, current_user) + + +@router.delete("/{coursechapter_id}") +async def api_delete_coursechapter(coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): + """ + Delete CourseChapters by ID + """ + + return await delete_coursechapter(coursechapter_id, current_user) diff --git a/src/routers/courses.py b/src/routers/courses.py index fdaaa8b3..68e173ac 100644 --- a/src/routers/courses.py +++ b/src/routers/courses.py @@ -1,23 +1,25 @@ -from fastapi import APIRouter, Depends, File, UploadFile, Form +from fastapi import APIRouter, Depends, UploadFile, Form from src.services.auth import get_current_user -from src.services.courses import Course, CourseChapter, create_course, create_coursechapter, delete_coursechapter, get_course, get_coursechapter, get_coursechapters, get_courses, update_course, delete_course, update_course_thumbnail, update_coursechapter -from src.services.users import PublicUser, User +from src.services.courses import Course, create_course, get_course, get_courses, update_course, delete_course, update_course_thumbnail +from src.services.users import PublicUser router = APIRouter() @router.post("/") -async def api_create_course(org_id :str , name : str = Form(), mini_description : str = Form() , description :str = Form(), public : bool = Form(), current_user: PublicUser = Depends(get_current_user) , thumbnail: UploadFile | None = None): +async def api_create_course(org_id: str, name: str = Form(), mini_description: str = Form(), description: str = Form(), public: bool = Form(), current_user: PublicUser = Depends(get_current_user), thumbnail: UploadFile | None = None): """ Create new Course """ - course = Course(name=name, mini_description=mini_description, description=description, org_id=org_id, public=public , thumbnail="" , chapters=[], learnings=[]) - return await create_course(course, org_id , current_user, thumbnail) + course = Course(name=name, mini_description=mini_description, description=description, + org_id=org_id, public=public, thumbnail="", chapters=[], learnings=[]) + return await create_course(course, org_id, current_user, thumbnail) + @router.put("/thumbnail/{course_id}") -async def api_create_course_thumbnail(course_id : str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)): +async def api_create_course_thumbnail(course_id: str, thumbnail: UploadFile | None = None, current_user: PublicUser = Depends(get_current_user)): """ Update new Course Thumbnail """ @@ -29,7 +31,7 @@ async def api_get_course(course_id: str, current_user: PublicUser = Depends(get """ Get single Course by course_id """ - return await get_course(course_id,current_user=current_user) + return await get_course(course_id, current_user=current_user) @router.get("/{org_id}/page/{page}/limit/{limit}") @@ -54,47 +56,4 @@ async def api_delete_course(course_id: str, current_user: PublicUser = Depends(g Delete Course by ID """ - return await delete_course(course_id, current_user) - -# CoursesChapters - - -@router.post("/chapters/") -async def api_create_coursechapter(coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Create new CourseChapter - """ - return await create_coursechapter(coursechapter_object, course_id, current_user) - - -@router.get("/chapters/{coursechapter_id}") -async def api_get_coursechapter(coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Get single CourseChapter by coursechapter_id - """ - return await get_coursechapter(coursechapter_id, current_user=current_user) - - -@router.get("/chapters/{course_id}/page/{page}/limit/{limit}") -async def api_get_coursechapter_by(course_id: str, page: int, limit: int): - """ - Get CourseChapters by page and limit - """ - return await get_coursechapters(course_id, page, limit) - - -@router.put("/chapters/{coursechapter_id}") -async def api_update_coursechapter(coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Update CourseChapters by course_id - """ - return await update_coursechapter(coursechapter_object, coursechapter_id, current_user) - - -@router.delete("/chapters/{coursechapter_id}") -async def api_delete_coursechapter(coursechapter_id: str, current_user: PublicUser = Depends(get_current_user)): - """ - Delete CourseChapters by ID - """ - - return await delete_coursechapter(coursechapter_id, current_user) + return await delete_course(course_id, current_user) \ No newline at end of file diff --git a/src/services/chapters.py b/src/services/chapters.py new file mode 100644 index 00000000..b8ed2237 --- /dev/null +++ b/src/services/chapters.py @@ -0,0 +1,220 @@ +from cmath import log +from datetime import datetime +import json +from typing import List +from uuid import uuid4 +from pydantic import BaseModel +from src.services.courses import Course, CourseInDB +from src.services.database import create_config_collection, check_database, create_database, learnhouseDB, learnhouseDB +from src.services.security import verify_user_rights_with_roles +from src.services.users import PublicUser +from fastapi import FastAPI, HTTPException, status, Request, Response, BackgroundTasks, UploadFile, File + + +class CourseElement(BaseModel): + element_id: str + content: str + content_type: str + + +class CourseChapter(BaseModel): + name: str + description: str + elements: List[CourseElement] + + +class CourseChapterInDB(CourseChapter): + coursechapter_id: str + course_id: str + creationDate: str + updateDate: str + + + +# Frontend +class CourseChapterMetaData(BaseModel): + chapterOrder: List[str] + chapters: List + +# CoursesChapters + + +async def create_coursechapter(coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser): + await check_database() + coursechapters = learnhouseDB["coursechapters"] + courses = learnhouseDB["courses"] + + # generate coursechapter_id with uuid4 + coursechapter_id = str(f"coursechapter_{uuid4()}") + + hasRoleRights = await verify_user_rights_with_roles("create", current_user.user_id, coursechapter_id) + + if not hasRoleRights: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Roles : Insufficient rights to perform this action") + + coursechapter = CourseChapterInDB(coursechapter_id=coursechapter_id, creationDate=str( + datetime.now()), updateDate=str(datetime.now()), course_id=course_id, **coursechapter_object.dict()) + + coursechapter_in_db = coursechapters.insert_one(coursechapter.dict()) + courses.update_one({"course_id": course_id}, { + "$addToSet": {"chapters": coursechapter_id}}) + + if not coursechapter_in_db: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + + return coursechapter.dict() + + +async def get_coursechapter(coursechapter_id: str, current_user: PublicUser): + await check_database() + coursechapters = learnhouseDB["coursechapters"] + + coursechapter = coursechapters.find_one( + {"coursechapter_id": coursechapter_id}) + + if coursechapter: + # verify course rights + await verify_rights(coursechapter["course_id"], current_user, "read") + coursechapter = CourseChapter(**coursechapter) + + return coursechapter + + else: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="CourseChapter does not exist") + + +async def get_coursechapters_meta(course_id: str, current_user: PublicUser): + await check_database() + coursechapters = learnhouseDB["coursechapters"] + courses = learnhouseDB["courses"] + + coursechapters = coursechapters.find( + {"course_id": course_id}).sort("name", 1) + + course = courses.find_one({"course_id": course_id}) + course = Course(**course) + + + # chapters + chapters = [json.loads(json.dumps(chapter, default=str)) + for chapter in coursechapters] + + final = { + "chapters": chapters, + "chapterOrder": course.chapters + } + + return final + + +async def update_coursechapters_meta(course_id: str, coursechapters_metadata: CourseChapterMetaData, current_user: PublicUser): + await check_database() + coursechapters = learnhouseDB["coursechapters"] + courses = learnhouseDB["courses"] + + course = courses.find_one({"course_id": course_id}) + course = Course(**course) + + # update chapters in course + courseInDB = courses.update_one({"course_id": course_id}, { + "$set": {"chapters": coursechapters_metadata.chapterOrder}}) + + # TODO : update chapters in coursechapters + + + return {courseInDB} + + +async def update_coursechapter(coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser): + await check_database() + coursechapters = learnhouseDB["coursechapters"] + + coursechapter = coursechapters.find_one( + {"coursechapter_id": coursechapter_id}) + + if coursechapter: + # verify course rights + await verify_rights(coursechapter["course_id"], current_user, "update") + creationDate = coursechapter["creationDate"] + + # get today's date + datetime_object = datetime.now() + + updated_coursechapter = CourseChapterInDB( + coursechapter_id=coursechapter_id, creationDate=creationDate, course_id=coursechapter["course_id"], updateDate=str(datetime_object), **coursechapter_object.dict()) + + coursechapters.update_one({"coursechapter_id": coursechapter_id}, { + "$set": updated_coursechapter.dict()}) + + return CourseChapterInDB(**updated_coursechapter.dict()) + + else: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Coursechapter does not exist") + + +async def delete_coursechapter(coursechapter_id: str, current_user: PublicUser): + await check_database() + + coursechapters = learnhouseDB["coursechapters"] + + coursechapter = coursechapters.find_one( + {"coursechapter_id": coursechapter_id}) + + if coursechapter: + # verify course rights + await verify_rights(coursechapter["course_id"], current_user, "delete") + + isDeleted = coursechapters.delete_one( + {"coursechapter_id": coursechapter_id}) + + # TODO : delete coursechapter from course using $pull https://www.mongodb.com/docs/v4.2/reference/operator/update/pull/ + + if isDeleted: + return {"detail": "coursechapter deleted"} + else: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") + + else: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") + + +async def get_coursechapters(course_id: str, page: int = 1, limit: int = 10): + await check_database() + courses = learnhouseDB["coursechapters"] + # TODO : Get only courses that user is admin/has roles of + # get all courses from database + all_coursechapters = courses.find({"course_id": course_id}).sort( + "name", 1).skip(10 * (page - 1)).limit(limit) + + return [json.loads(json.dumps(coursechapter, default=str)) for coursechapter in all_coursechapters] + + +#### Security #################################################### + + +async def verify_rights(course_id: str, current_user: PublicUser, action: str): + await check_database() + courses = learnhouseDB["courses"] + + course = courses.find_one({"course_id": course_id}) + + if not course: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, detail=f"Course/CourseChapter 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"] + + if not hasRoleRights and not isAuthor: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Roles/Ownership : Insufficient rights to perform this action") + + return True + +#### Security #################################################### diff --git a/src/services/courses.py b/src/services/courses.py index 8b9b2690..18eb3e21 100644 --- a/src/services/courses.py +++ b/src/services/courses.py @@ -18,7 +18,7 @@ class Course(BaseModel): mini_description: str description: str learnings: List[str] - thumbnail : str + thumbnail: str public: bool chapters: List[str] org_id: str @@ -33,23 +33,6 @@ class CourseInDB(Course): ##### -class CourseElement(BaseModel): - element_id: str - content: str - content_type: str - - -class CourseChapter(BaseModel): - name: str - description: str - elements: List[CourseElement] - - -class CourseChapterInDB(CourseChapter): - coursechapter_id: str - course_id: str - creationDate: str - updateDate: str #### Classes #################################################### @@ -59,7 +42,7 @@ class CourseChapterInDB(CourseChapter): # Courses -async def get_course(course_id: str , current_user: PublicUser): +async def get_course(course_id: str, current_user: PublicUser): await check_database() courses = learnhouseDB["courses"] @@ -76,27 +59,26 @@ async def get_course(course_id: str , current_user: PublicUser): return course -async def create_course(course_object: Course, org_id : str , current_user: PublicUser, thumbnail_file: UploadFile | None = None): +async def create_course(course_object: Course, org_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): await check_database() courses = learnhouseDB["courses"] # generate course_id with uuid4 course_id = str(f"course_{uuid4()}") - + # TODO(fix) : the implementation here is clearly not the best one (this entire function) course_object.org_id = org_id hasRoleRights = await verify_user_rights_with_roles("create", current_user.user_id, course_id) - - + if not hasRoleRights: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Roles : Insufficient rights to perform this action") - + if thumbnail_file: name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" await upload_thumbnail(thumbnail_file, name_in_disk) course_object.thumbnail = name_in_disk - + course = CourseInDB(course_id=course_id, authors=[ current_user.user_id], creationDate=str(datetime.now()), updateDate=str(datetime.now()), **course_object.dict()) @@ -108,12 +90,13 @@ async def create_course(course_object: Course, org_id : str , current_user: Pu return course.dict() -async def update_course_thumbnail(course_id: str , current_user: PublicUser, thumbnail_file: UploadFile | None = None): + +async def update_course_thumbnail(course_id: str, current_user: PublicUser, thumbnail_file: UploadFile | None = None): await check_database() # verify course rights await verify_rights(course_id, current_user, "update") - + courses = learnhouseDB["courses"] course = courses.find_one({"course_id": course_id}) @@ -121,19 +104,18 @@ async def update_course_thumbnail(course_id: str , current_user: PublicUser, thu if course: creationDate = course["creationDate"] authors = course["authors"] - if thumbnail_file: + if thumbnail_file: name_in_disk = f"{course_id}_thumbnail_{uuid4()}.{thumbnail_file.filename.split('.')[-1]}" course = Course(**course).copy(update={"thumbnail": name_in_disk}) - await upload_thumbnail( thumbnail_file, name_in_disk) - - - updated_course = CourseInDB(course_id=course_id, creationDate=creationDate, authors=authors, updateDate=str(datetime.now()) , **course.dict()) + await upload_thumbnail(thumbnail_file, name_in_disk) + + updated_course = CourseInDB(course_id=course_id, creationDate=creationDate, + authors=authors, updateDate=str(datetime.now()), **course.dict()) courses.update_one({"course_id": course_id}, { - "$set": updated_course.dict()}) + "$set": updated_course.dict()}) return CourseInDB(**updated_course.dict()) - else: raise HTTPException( @@ -145,7 +127,7 @@ async def update_course(course_object: Course, course_id: str, current_user: Pub # verify course rights await verify_rights(course_id, current_user, "update") - + courses = learnhouseDB["courses"] course = courses.find_one({"course_id": course_id}) @@ -156,12 +138,12 @@ async def update_course(course_object: Course, course_id: str, current_user: Pub # get today's date datetime_object = datetime.now() - + updated_course = CourseInDB( course_id=course_id, creationDate=creationDate, authors=authors, updateDate=str(datetime_object), **course_object.dict()) courses.update_one({"course_id": course_id}, { - "$set": updated_course.dict()}) + "$set": updated_course.dict()}) return CourseInDB(**updated_course.dict()) @@ -169,8 +151,6 @@ async def update_course(course_object: Course, course_id: str, current_user: Pub raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") - - async def delete_course(course_id: str, current_user: PublicUser): await check_database() @@ -195,129 +175,17 @@ async def delete_course(course_id: str, current_user: PublicUser): status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") -async def get_courses(page: int = 1, limit: int = 10 , org_id : str | None = None): +async def get_courses(page: int = 1, limit: int = 10, org_id: str | None = None): await check_database() courses = learnhouseDB["courses"] # TODO : Get only courses that user is admin/has roles of # get all courses from database - all_courses = courses.find({"org_id": org_id}).sort("name", 1).skip(10 * (page - 1)).limit(limit) + all_courses = courses.find({"org_id": org_id}).sort( + "name", 1).skip(10 * (page - 1)).limit(limit) return [json.loads(json.dumps(course, default=str)) for course in all_courses] -# CoursesChapters -async def create_coursechapter(coursechapter_object: CourseChapter, course_id: str, current_user: PublicUser): - await check_database() - coursechapters = learnhouseDB["coursechapters"] - - # generate coursechapter_id with uuid4 - coursechapter_id = str(f"coursechapter_{uuid4()}") - - hasRoleRights = await verify_user_rights_with_roles("create", current_user.user_id, coursechapter_id) - - if not hasRoleRights: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Roles : Insufficient rights to perform this action") - - coursechapter = CourseChapterInDB(coursechapter_id=coursechapter_id, creationDate=str( - datetime.now()), updateDate=str(datetime.now()), course_id=course_id, **coursechapter_object.dict()) - - coursechapter_in_db = coursechapters.insert_one(coursechapter.dict()) - - if not coursechapter_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") - - return coursechapter.dict() - - -async def get_coursechapter(coursechapter_id: str, current_user: PublicUser): - await check_database() - coursechapters = learnhouseDB["coursechapters"] - - coursechapter = coursechapters.find_one( - {"coursechapter_id": coursechapter_id}) - - if coursechapter: - # verify course rights - await verify_rights(coursechapter["course_id"], current_user, "read") - coursechapter = CourseChapter(**coursechapter) - - return coursechapter - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="CourseChapter does not exist") - - - -async def update_coursechapter(coursechapter_object: CourseChapter, coursechapter_id: str, current_user: PublicUser): - await check_database() - coursechapters = learnhouseDB["coursechapters"] - - coursechapter = coursechapters.find_one( - {"coursechapter_id": coursechapter_id}) - - if coursechapter: - # verify course rights - await verify_rights(coursechapter["course_id"], current_user, "update") - creationDate = coursechapter["creationDate"] - - # get today's date - datetime_object = datetime.now() - - updated_coursechapter = CourseChapterInDB( - coursechapter_id=coursechapter_id, creationDate=creationDate, course_id=coursechapter["course_id"], updateDate=str(datetime_object), **coursechapter_object.dict()) - - coursechapters.update_one({"coursechapter_id": coursechapter_id}, { - "$set": updated_coursechapter.dict()}) - - return CourseChapterInDB(**updated_coursechapter.dict()) - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Coursechapter does not exist") - - - - -async def delete_coursechapter(coursechapter_id: str, current_user: PublicUser): - await check_database() - - coursechapters = learnhouseDB["coursechapters"] - - coursechapter = coursechapters.find_one( - {"coursechapter_id": coursechapter_id}) - - if coursechapter: - # verify course rights - await verify_rights(coursechapter["course_id"], current_user, "delete") - - isDeleted = coursechapters.delete_one( - {"coursechapter_id": coursechapter_id}) - - if isDeleted: - return {"detail": "coursechapter deleted"} - else: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Unavailable database") - - else: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Course does not exist") - - - - -async def get_coursechapters(course_id: str, page: int = 1, limit: int = 10): - await check_database() - courses = learnhouseDB["coursechapters"] - # TODO : Get only courses that user is admin/has roles of - # get all courses from database - all_coursechapters = courses.find({"course_id": course_id}).sort( - "name", 1).skip(10 * (page - 1)).limit(limit) - - return [json.loads(json.dumps(coursechapter, default=str)) for coursechapter in all_coursechapters] #### Security ####################################################