Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
55beb10
Module removal and editing improvements
anurag2787 Dec 27, 2025
e867b1e
fixed coderabbit review
anurag2787 Dec 27, 2025
18b1b20
feat: implement module deletion and editing with proper permissions
anurag2787 Dec 27, 2025
8aec659
fixed check error
anurag2787 Dec 27, 2025
c525845
fixed coderabbit review
anurag2787 Dec 27, 2025
afcdc8d
Merge branch 'main' of github.com:anurag2787/Nest into module-removal…
anurag2787 Dec 29, 2025
454d961
added functionaility to edit module by mentor
anurag2787 Dec 29, 2025
b0b4220
fixed sonar and coderabbit review
anurag2787 Dec 29, 2025
e7540f2
Added no sonar
anurag2787 Dec 29, 2025
44b51e9
fixed nosoanr
anurag2787 Dec 31, 2025
f729e3d
updated nosonar comment
anurag2787 Dec 31, 2025
3b7162d
update nosonar warning
anurag2787 Dec 31, 2025
968b9da
fixed coderabbit review
anurag2787 Dec 31, 2025
8afb6ed
Merge branch 'main' of github.com:anurag2787/Nest into module-removal…
anurag2787 Dec 31, 2025
f11d484
Merge branch 'main' of github.com:anurag2787/Nest into module-removal…
anurag2787 Jan 9, 2026
3d13803
Fixed coderabbit review
anurag2787 Jan 9, 2026
9292706
Resolve coderabbit review
anurag2787 Jan 9, 2026
dca555f
fix code
anurag2787 Jan 9, 2026
1233914
fixed coderabbit comment
anurag2787 Jan 9, 2026
4b79b24
fixed
anurag2787 Jan 9, 2026
0a330f6
fixed check command fail
anurag2787 Jan 9, 2026
9a0284a
Merge branch 'main' of github.com:anurag2787/Nest into module-removal…
anurag2787 Jan 15, 2026
d2afc06
fixed sonarqube warning
anurag2787 Jan 15, 2026
04e794c
Fixed sonarqube warning
anurag2787 Jan 15, 2026
851203b
Remove view Issues from mentor
anurag2787 Jan 16, 2026
a79a8e5
Merge branch 'main' into module-removal-and-editing
anurag2787 Jan 16, 2026
9be6301
Merge branch 'main' into module-removal-and-editing
anurag2787 Jan 24, 2026
3cf76b3
fixed merge conflict
anurag2787 Jan 24, 2026
0e63158
fixed sonarqube issue
anurag2787 Jan 24, 2026
3da7d25
fixed code rabbit review
anurag2787 Jan 24, 2026
b12ad1e
fixed check
anurag2787 Jan 24, 2026
0992bab
fixed coderabbit review
anurag2787 Jan 24, 2026
4da0ada
Merge branch 'main' into module-removal-and-editing
anurag2787 Jan 24, 2026
96d96a5
Merge branch 'main' into module-removal-and-editing
anurag2787 Jan 26, 2026
b224d61
Merge branch 'main' into module-removal-and-editing
anurag2787 Feb 20, 2026
03fdca3
fixed merge conflict error and updated mentor logic
anurag2787 Feb 20, 2026
95ece1c
fix review
anurag2787 Feb 20, 2026
b4e3ce4
Merge branch 'main' of github.com:OWASP/Nest into pr/anurag2787/3054
kasya Feb 22, 2026
7be0b5f
Update mentors permissions to view module issues
kasya Feb 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions backend/apps/mentorship/api/internal/mutations/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,64 @@ def update_module(self, info: strawberry.Info, input_data: UpdateModuleInput) ->
module.program.save(update_fields=["experience_levels"])

return module

@strawberry.mutation(permission_classes=[IsAuthenticated])
@transaction.atomic
def delete_module(
self,
info: strawberry.Info,
program_key: str,
module_key: str,
) -> str:
"""Delete a mentorship module. User must be an admin of the program."""
user = info.context.request.user

try:
module = Module.objects.select_related("program").get(
key=module_key, program__key=program_key
)
except Module.DoesNotExist as e:
msg = "Module not found."
raise ObjectDoesNotExist(msg) from e

try:
admin_as_mentor = Mentor.objects.get(nest_user=user)
except Mentor.DoesNotExist as err:
msg = "Only mentors can delete modules."
logger.warning(
"User '%s' is not a mentor and cannot delete modules.",
user.username,
exc_info=True,
)
raise PermissionDenied(msg) from err

if not module.program.admins.filter(id=admin_as_mentor.id).exists():
raise PermissionDenied
Comment thread
anurag2787 marked this conversation as resolved.
Outdated

program = module.program
module_name = module.name

# Clean up experience levels if this module is the only one using it
experience_level_to_remove = module.experience_level
if (
experience_level_to_remove in program.experience_levels
and not Module.objects.filter(
program=program, experience_level=experience_level_to_remove
)
.exclude(id=module.id)
.exists()
):
program.experience_levels.remove(experience_level_to_remove)
program.save(update_fields=["experience_levels"])

# Delete the module
module.delete()

logger.info(
"User '%s' deleted module '%s' from program '%s'.",
user.username,
module_name,
program_key,
)

return f"Module '{module_name}' has been deleted successfully."
Comment thread
anurag2787 marked this conversation as resolved.
Comment thread
anurag2787 marked this conversation as resolved.
Outdated
186 changes: 152 additions & 34 deletions frontend/src/components/EntityActions.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
'use client'

import { gql } from '@apollo/client'
import { useMutation } from '@apollo/client/react'
import { Button } from '@heroui/button'
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@heroui/modal'
import { addToast } from '@heroui/toast'
import { useRouter } from 'next/navigation'
import type React from 'react'
import { useState, useRef, useEffect } from 'react'
import { FaEllipsisV } from 'react-icons/fa'
import { ProgramStatusEnum } from 'types/__generated__/graphql'
import { GetProgramAndModulesDocument } from 'types/__generated__/programsQueries.generated'

interface DeleteModuleResponse {
deleteModule: boolean
}

const DELETE_MODULE_MUTATION = gql`
mutation DeleteModule($programKey: String!, $moduleKey: String!) {
deleteModule(programKey: $programKey, moduleKey: $moduleKey)
}
`
Comment thread
anurag2787 marked this conversation as resolved.

interface EntityActionsProps {
type: 'program' | 'module'
Expand All @@ -23,8 +39,12 @@ const EntityActions: React.FC<EntityActionsProps> = ({
}) => {
const router = useRouter()
const [dropdownOpen, setDropdownOpen] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)

const [deleteModule] = useMutation<DeleteModuleResponse>(DELETE_MODULE_MUTATION)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const handleAction = (actionKey: string) => {
switch (actionKey) {
case 'edit_program':
Expand All @@ -43,6 +63,9 @@ const EntityActions: React.FC<EntityActionsProps> = ({
router.push(`/my/mentorship/programs/${programKey}/modules/${moduleKey}/issues`)
}
break
case 'delete_module':
setDeleteModalOpen(true)
break
case 'publish':
setStatus?.(ProgramStatusEnum.Published)
break
Expand All @@ -56,6 +79,66 @@ const EntityActions: React.FC<EntityActionsProps> = ({
setDropdownOpen(false)
}

const handleDeleteConfirm = async () => {
if (!moduleKey) return

setIsDeleting(true)

try {
const result = await deleteModule({
variables: { programKey, moduleKey },

update(cache) {
const existing = cache.readQuery({
query: GetProgramAndModulesDocument,
variables: { programKey },
})

if (!existing || !existing.getProgramModules) {
throw new Error('Program modules not found in cache')
}

cache.writeQuery({
query: GetProgramAndModulesDocument,
variables: { programKey },
data: {
...existing,
getProgramModules: existing.getProgramModules.filter(
(module) => module.key !== moduleKey
),
},
})
},
})

if (!result?.data || typeof result.data !== 'object' || !('deleteModule' in result.data)) {
throw new Error('Delete mutation failed on server')
}
Comment thread
anurag2787 marked this conversation as resolved.
Outdated

addToast({
title: 'Success',
description: 'Module has been deleted successfully.',
color: 'success',
})

setDeleteModalOpen(false)
router.push(`/my/mentorship/programs/${programKey}`)
} catch (error) {
const description =
error instanceof Error && error.message.includes('Permission')
? 'You do not have permission to delete this module.'
: 'Failed to delete module. Please try again.'

addToast({
title: 'Error',
description,
color: 'danger',
})
} finally {
setIsDeleting(false)
}
}

const options =
type === 'program'
? [
Expand All @@ -72,6 +155,7 @@ const EntityActions: React.FC<EntityActionsProps> = ({
: [
{ key: 'edit_module', label: 'Edit' },
{ key: 'view_issues', label: 'View Issues' },
{ key: 'delete_module', label: 'Delete', className: 'text-red-500' },
Comment thread
anurag2787 marked this conversation as resolved.
Outdated
]

useEffect(() => {
Expand All @@ -94,42 +178,76 @@ const EntityActions: React.FC<EntityActionsProps> = ({
}

return (
<div className="relative" ref={dropdownRef}>
<button
data-testid={`${type}-actions-button`}
type="button"
onClick={handleToggle}
className="cursor-pointer rounded px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label={`${type === 'program' ? 'Program' : 'Module'} actions menu`}
aria-expanded={dropdownOpen}
aria-haspopup="true"
>
<FaEllipsisV className="text-gray-400 hover:text-gray-500 dark:hover:text-gray-200" />
</button>
{dropdownOpen && (
<div className="absolute right-0 z-20 mt-2 w-40 rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800">
{options.map((option) => {
const handleMenuItemClick = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
handleAction(option.key)
}

return (
<button
key={option.key}
type="button"
role="menuitem"
onClick={handleMenuItemClick}
className="block w-full cursor-pointer px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
<>
<div className="relative" ref={dropdownRef}>
<button
data-testid={`${type}-actions-button`}
type="button"
onClick={handleToggle}
className="cursor-pointer rounded px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700"
aria-label={`${type === 'program' ? 'Program' : 'Module'} actions menu`}
aria-expanded={dropdownOpen}
aria-haspopup="true"
>
<FaEllipsisV className="text-gray-400 hover:text-gray-500 dark:hover:text-gray-200" />
</button>
{dropdownOpen && (
<div className="absolute right-0 z-20 mt-2 w-40 rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-800">
{options.map((option) => {
const handleMenuItemClick = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
handleAction(option.key)
}

return (
<button
key={option.key}
type="button"
role="menuitem"
onClick={handleMenuItemClick}
className={`block w-full cursor-pointer px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 ${
option.className || ''
}`}
>
{option.label}
</button>
)
})}
</div>
)}
</div>

{type === 'module' && (
<Modal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)}>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">Delete Module</ModalHeader>
<ModalBody>
<p>Are you sure you want to delete this module? This action cannot be undone.</p>
</ModalBody>
<ModalFooter>
<Button
color="default"
variant="light"
onPress={() => setDeleteModalOpen(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
color="danger"
onPress={handleDeleteConfirm}
isLoading={isDeleting}
disabled={isDeleting}
className="text-white"
>
{option.label}
</button>
)
})}
</div>
Delete
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)}
</div>
</>
)
}

Expand Down
7 changes: 7 additions & 0 deletions frontend/src/types/__generated__/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions frontend/types/__generated__/EntityActions.generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.