mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #112 from learnhouse/swve/eng-102-edit-course-delete-activity-obfuscate-editing-non-dynamic
Add delete activity option + redesign edit
This commit is contained in:
commit
cd1152022d
10 changed files with 130 additions and 54 deletions
|
|
@ -68,13 +68,20 @@ def get_learnhouse_config() -> LearnHouseConfig:
|
|||
yaml_config = yaml.safe_load(f)
|
||||
|
||||
# General Config
|
||||
env_development_mode = os.environ.get("LEARNHOUSE_DEVELOPMENT_MODE")
|
||||
development_mode = env_development_mode or yaml_config.get("general", {}).get(
|
||||
"development_mode"
|
||||
|
||||
# Development Mode & Install Mode
|
||||
env_development_mode = eval(os.environ.get("LEARNHOUSE_DEVELOPMENT_MODE", "None"))
|
||||
development_mode = (
|
||||
env_development_mode
|
||||
if env_development_mode is not None
|
||||
else yaml_config.get("general", {}).get("development_mode")
|
||||
)
|
||||
env_install_mode = os.environ.get("LEARNHOUSE_INSTALL_MODE")
|
||||
install_mode = env_install_mode or yaml_config.get("general", {}).get(
|
||||
"install_mode"
|
||||
|
||||
env_install_mode = os.environ.get("LEARNHOUSE_INSTALL_MODE", "None")
|
||||
install_mode = (
|
||||
env_install_mode
|
||||
if env_install_mode is not None
|
||||
else yaml_config.get("general", {}).get("install_mode")
|
||||
)
|
||||
|
||||
# Security Config
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ hosting_config:
|
|||
domain: learnhouse.app
|
||||
ssl: true
|
||||
allowed_origins:
|
||||
- http://localhost:3000
|
||||
- http://localhost:3001
|
||||
- http://localhost:3000
|
||||
- http://localhost:3001
|
||||
cookies_config:
|
||||
domain: ".localhost"
|
||||
allowed_regexp: '\b((?:https?://)[^\s/$.?#].[^\s]*)\b'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import ConfirmationModal from '@components/StyledElements/ConfirmationModal/Conf
|
|||
import { getUriWithOrg } from '@services/config/config';
|
||||
import { deleteCollection } from '@services/courses/collections';
|
||||
import { revalidateTags } from '@services/utils/ts/requests';
|
||||
import { Link, Trash } from 'lucide-react';
|
||||
import { Link, Trash, X } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react'
|
||||
|
||||
|
|
@ -26,15 +26,17 @@ const CollectionAdminEditsArea = (props: any) => {
|
|||
|
||||
return (
|
||||
<AuthenticatedClientElement orgId={props.org_id} checkMethod='roles'>
|
||||
<div className="flex space-x-2 py-2">
|
||||
<div className="flex space-x-2 relative top-8 z-20 left-2">
|
||||
<ConfirmationModal
|
||||
confirmationMessage="Are you sure you want to delete this collection?"
|
||||
confirmationButtonText="Delete Collection"
|
||||
dialogTitle={"Delete " + props.collection.name + " ?"}
|
||||
dialogTrigger={
|
||||
<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>
|
||||
</button>}
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
</div>}
|
||||
functionToExecute={() => deleteCollectionUI(props.collection_id)}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { createActivity, createFileActivity, createExternalVideoActivity } from
|
|||
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 { Folders, Hexagon, SaveIcon } from "lucide-react";
|
||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
||||
import { mutate } from "swr";
|
||||
|
|
@ -33,7 +33,7 @@ function CourseContentEdition(props: any) {
|
|||
const courseid = props.courseid;
|
||||
const orgslug = props.orgslug;
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setwinReady(true);
|
||||
|
|
@ -63,7 +63,7 @@ function CourseContentEdition(props: any) {
|
|||
const submitChapter = async (chapter: any) => {
|
||||
await createChapter(chapter, courseid);
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${courseid}`);
|
||||
// await getCourseChapters();
|
||||
// await getCourseChapters();
|
||||
await revalidateTags(['courses'], orgslug);
|
||||
router.refresh();
|
||||
setNewChapterModal(false);
|
||||
|
|
@ -81,6 +81,8 @@ function CourseContentEdition(props: any) {
|
|||
router.refresh();
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Submit File Upload
|
||||
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
|
||||
await updateChaptersMetadata(courseid, data);
|
||||
|
|
@ -301,7 +303,7 @@ function CourseContentEdition(props: any) {
|
|||
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} />
|
||||
<Hexagon size={16} />
|
||||
<div>Add chapter +</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentT
|
|||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
||||
import { Pencil, X } from 'lucide-react';
|
||||
|
||||
interface CourseProps {
|
||||
orgslug: string;
|
||||
|
|
@ -91,22 +92,26 @@ function Courses(props: CourseProps) {
|
|||
|
||||
const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any, deleteCourses: any }) => {
|
||||
return (
|
||||
<AuthenticatedClientElement checkMethod='roles' orgId={props.course.org_id}><div className="flex space-x-2 py-2">
|
||||
<AuthenticatedClientElement checkMethod='roles' orgId={props.course.org_id}><div className="flex space-x-1 relative top-8 z-20 left-2">
|
||||
<ConfirmationModal
|
||||
confirmationButtonText='Delete Course'
|
||||
confirmationMessage='Are you sure you want to delete this course?'
|
||||
dialogTitle={'Delete ' + props.course.name + ' ?'}
|
||||
dialogTrigger={
|
||||
<button className="rounded-md text-sm px-3 font-bold text-red-800 bg-red-200 w-16 flex justify-center items-center" >
|
||||
Delete
|
||||
</button>}
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
</div>}
|
||||
functionToExecute={() => props.deleteCourses(props.courseId)}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
<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">
|
||||
Edit
|
||||
</button>
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-orange-600 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
<Pencil size={15} className="text-orange-200 font-bold" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</AuthenticatedClientElement>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,74 @@
|
|||
import Link from "next/link";
|
||||
import { Draggable } from "react-beautiful-dnd";
|
||||
import { EyeOpenIcon, Pencil2Icon } from '@radix-ui/react-icons'
|
||||
import { getUriWithOrg } from "@services/config/config";
|
||||
import { FileText, Video, Sparkles, File } from "lucide-react";
|
||||
import { EyeOpenIcon, Pencil2Icon, TrashIcon } from '@radix-ui/react-icons'
|
||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||
import { FileText, Video, Sparkles, XSquare, X, Pencil, MoreVertical, Eye } from "lucide-react";
|
||||
import { mutate } from "swr";
|
||||
import { revalidateTags } from "@services/utils/ts/requests";
|
||||
import { useRouter } from "next/navigation";
|
||||
import ConfirmationModal from "@components/StyledElements/ConfirmationModal/ConfirmationModal";
|
||||
import { deleteActivity } from "@services/courses/activities";
|
||||
|
||||
function Activity(props: any) {
|
||||
const router = useRouter();
|
||||
|
||||
async function removeActivity() {
|
||||
await deleteActivity(props.activity.id);
|
||||
mutate(`${getAPIUrl()}chapters/meta/course_${props.courseid}`);
|
||||
await revalidateTags(['courses'], props.orgslug);
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Draggable key={props.activity.id} draggableId={props.activity.id} index={props.index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className="flex flex-row py-2 my-2 rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-2 w-auto items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear" key={props.activity.id} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
|
||||
className="flex flex-row py-2 my-2 rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 w-auto items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear" key={props.activity.id} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
|
||||
<div className="px-3 text-gray-300 space-x-1 w-28" >
|
||||
{props.activity.type === "video" && <><div className="flex space-x-2 items-center"><Video size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Video</div> </div></>}
|
||||
{props.activity.type === "documentpdf" && <><div className="flex space-x-2 items-center"><FileText size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Document</div> </div></>}
|
||||
{props.activity.type === "dynamic" && <><div className="flex space-x-2 items-center"><Sparkles size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Dynamic</div> </div></>}
|
||||
</div>
|
||||
|
||||
<div className="grow items-center space-x-1 flex">
|
||||
<p className="first-letter:uppercase"> {props.activity.name} </p>
|
||||
</div>
|
||||
<div className="flex flex-row space-x-1 px-3"><Link
|
||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.id.replace("activity_", "")}`}
|
||||
className=" hover:cursor-pointer p-1 rounded-md "
|
||||
rel="noopener noreferrer">
|
||||
<EyeOpenIcon className="text-gray-500" />
|
||||
</Link>
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
{props.activity.type === "dynamic" && <>
|
||||
<Link
|
||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.id.replace("activity_", "")}/edit`}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
|
||||
rel="noopener noreferrer">
|
||||
<div className="text-sky-100 font-bold text-xs" >Edit </div>
|
||||
</Link>
|
||||
</>}
|
||||
<Link
|
||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.id.replace("activity_", "")}/edit`}
|
||||
className=" hover:cursor-pointer p-1 rounded-md "
|
||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.courseid}/activity/${props.activity.id.replace("activity_", "")}`}
|
||||
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
<Pencil2Icon className="text-gray-500" />
|
||||
</Link></div>
|
||||
<Eye strokeWidth={2} size={15} className="text-gray-600" />
|
||||
</Link>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div className="flex flex-row pr-3 space-x-1 items-center">
|
||||
<MoreVertical size={15} className="text-gray-300" />
|
||||
<ConfirmationModal
|
||||
confirmationMessage="Are you sure you want to delete this activity ?"
|
||||
confirmationButtonText="Delete Activity"
|
||||
dialogTitle={"Delete " + props.activity.name + " ?"}
|
||||
dialogTrigger={
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
|
||||
rel="noopener noreferrer">
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
</div>}
|
||||
functionToExecute={() => removeActivity()}
|
||||
status='warning'
|
||||
></ConfirmationModal></div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import styled from "styled-components";
|
||||
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
|
||||
import Activity from "./Activity";
|
||||
import { PlusSquare, Sparkle, Sparkles, Trash, Trash2 } from "lucide-react";
|
||||
import { Folders, Hexagon, MoreVertical, PlusSquare, Sparkle, Sparkles, Trash, Trash2, X } from "lucide-react";
|
||||
import ConfirmationModal from "@components/StyledElements/ConfirmationModal/ConfirmationModal";
|
||||
|
||||
function Chapter(props: any) {
|
||||
|
|
@ -14,25 +14,34 @@ function Chapter(props: any) {
|
|||
{...provided.draggableProps}
|
||||
ref={provided.innerRef}
|
||||
// isDragging={snapshot.isDragging}
|
||||
className="max-w-screen-2xl mx-auto"
|
||||
className="max-w-screen-2xl mx-auto bg-white px-5"
|
||||
key={props.info.list.chapter.id}
|
||||
>
|
||||
<h3 className="flex space-x-2 pt-3 font-bold text-md items-center">
|
||||
<p>{props.info.list.chapter.name}
|
||||
</p>
|
||||
<div className="flex pt-3 pr-3 font-bold text-md items-center space-x-2">
|
||||
<div className="flex grow text-lg space-x-3 items-center rounded-md px-3 py-1">
|
||||
<div className="bg-neutral-100 rounded-md p-2">
|
||||
<Hexagon strokeWidth={3} size={16} className="text-neutral-600 " />
|
||||
</div>
|
||||
|
||||
<p className="text-neutral-700 first-letter:uppercase">{props.info.list.chapter.name}</p>
|
||||
</div>
|
||||
<MoreVertical size={15} className="text-gray-300" />
|
||||
<ConfirmationModal
|
||||
confirmationButtonText="Delete Chapter"
|
||||
confirmationMessage="Are you sure you want to delete this chapter?"
|
||||
dialogTitle={"Delete " + props.info.list.chapter.name + " ?"}
|
||||
dialogTrigger={
|
||||
<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>
|
||||
</button>}
|
||||
<div
|
||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md shadow flex space-x-1 items-center text-rose-100 text-sm"
|
||||
rel="noopener noreferrer">
|
||||
<X size={15} className="text-rose-200 font-bold" />
|
||||
<p>Delete Chapter</p>
|
||||
</div>}
|
||||
functionToExecute={() => props.deleteChapter(props.info.list.chapter.id)}
|
||||
status='warning'
|
||||
></ConfirmationModal>
|
||||
|
||||
</h3>
|
||||
</div>
|
||||
<Droppable key={props.info.list.chapter.id} droppableId={props.info.list.chapter.id} type="activity">
|
||||
{(provided) => (
|
||||
<ActivitiesList {...provided.droppableProps} ref={provided.innerRef}>
|
||||
|
|
@ -62,8 +71,7 @@ function Chapter(props: any) {
|
|||
|
||||
const ChapterWrapper = styled.div`
|
||||
margin-bottom: 20px;
|
||||
padding: 4px;
|
||||
background-color: #ffffff9d;
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
display: block;
|
||||
border-radius: 9px;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ export async function getActivity(activity_id: any, next: any) {
|
|||
return res;
|
||||
}
|
||||
|
||||
export async function deleteActivity(activity_id: any) {
|
||||
const result = await fetch(`${getAPIUrl()}activities/${activity_id}`, RequestBody("DELETE", null, null));
|
||||
const res = await result.json();
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function getActivityWithAuthHeader(activity_id: any, next: any, access_token: string) {
|
||||
const result = await fetch(`${getAPIUrl()}activities/activity_${activity_id}`, RequestBodyWithAuthHeader("GET", null, next, access_token));
|
||||
const res = await result.json();
|
||||
|
|
|
|||
|
|
@ -162,10 +162,18 @@ async def delete_activity(request: Request, activity_id: str, current_user: Publ
|
|||
status_code=status.HTTP_409_CONFLICT, detail="activity does not exist"
|
||||
)
|
||||
|
||||
# Remove Activity
|
||||
isDeleted = await activities.delete_one({"activity_id": activity_id})
|
||||
|
||||
if isDeleted:
|
||||
return {"detail": "activity deleted"}
|
||||
# Remove Activity from chapter
|
||||
courses = request.app.db["courses"]
|
||||
isDeletedFromChapter = await courses.update_one(
|
||||
{"chapters_content.activities": activity_id},
|
||||
{"$pull": {"chapters_content.$.activities": activity_id}},
|
||||
)
|
||||
|
||||
if isDeleted and isDeletedFromChapter:
|
||||
return {"detail": "Activity deleted"}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ def isDevModeEnabled():
|
|||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def isDevModeEnabledOrRaise():
|
||||
config = get_learnhouse_config()
|
||||
if config.general_config.development_mode:
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(status_code=403, detail="Development mode is not enabled")
|
||||
|
||||
raise HTTPException(status_code=403, detail="Development mode is disabled")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue