Skip to content

Commit ffb2765

Browse files
Document: Add LP usage warning when deleting documents - refs #4454
Author: @christianbeeznest
1 parent a86bce9 commit ffb2765

File tree

9 files changed

+199
-16
lines changed

9 files changed

+199
-16
lines changed

assets/vue/views/documents/DocumentsList.vue

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,34 @@
451451
/>
452452
</form>
453453
</BaseDialogConfirmCancel>
454+
<BaseDialogConfirmCancel
455+
v-model:is-visible="isDeleteWarningLpDialogVisible"
456+
:title="t('Confirm deletion')"
457+
@confirm-clicked="forceDeleteItem"
458+
@cancel-clicked="isDeleteWarningLpDialogVisible = false"
459+
>
460+
<div class="confirmation-content">
461+
<BaseIcon
462+
class="mr-2"
463+
icon="alert"
464+
size="big"
465+
/>
466+
<p class="mb-2">
467+
{{ t("The following documents are used in learning paths:") }}
468+
</p>
469+
<ul class="pl-4 mb-4">
470+
<li
471+
v-for="lp in lpListWarning"
472+
:key="lp.lpId + lp.documentTitle"
473+
>
474+
<b>{{ lp.documentTitle }}</b> → {{ lp.lpTitle }}
475+
</li>
476+
</ul>
477+
<p class="mt-4 font-semibold">
478+
{{ t("Do you still want to delete them?") }}
479+
</p>
480+
</div>
481+
</BaseDialogConfirmCancel>
454482
</template>
455483

456484
<script setup>
@@ -504,6 +532,8 @@ const isAllowedToEdit = ref(false)
504532
const folders = ref([])
505533
const selectedFolder = ref(null)
506534
const isDownloading = ref(false)
535+
const isDeleteWarningLpDialogVisible = ref(false)
536+
const lpListWarning = ref([])
507537
508538
const {
509539
showNewDocumentButton,
@@ -678,9 +708,41 @@ function showDeleteMultipleDialog() {
678708
isDeleteMultipleDialogVisible.value = true
679709
}
680710
681-
function confirmDeleteItem(itemToDelete) {
682-
item.value = itemToDelete
683-
isDeleteItemDialogVisible.value = true
711+
async function confirmDeleteItem(itemToDelete) {
712+
try {
713+
const response = await axios.get(`/api/documents/${itemToDelete.iid}/lp-usage`)
714+
if (response.data.usedInLp) {
715+
lpListWarning.value = response.data.lpList.map((lp) => ({
716+
...lp,
717+
documentTitle: itemToDelete.title,
718+
documentId: itemToDelete.iid,
719+
}))
720+
item.value = itemToDelete
721+
isDeleteWarningLpDialogVisible.value = true
722+
} else {
723+
item.value = itemToDelete
724+
isDeleteItemDialogVisible.value = true
725+
}
726+
} catch (error) {
727+
console.error("Error checking LP usage for individual item:", error)
728+
}
729+
}
730+
731+
async function forceDeleteItem() {
732+
try {
733+
const docIdsToDelete = [...new Set(lpListWarning.value.map((lp) => lp.documentId))]
734+
735+
await Promise.all(docIdsToDelete.map((iid) => axios.delete(`/api/documents/${iid}`)))
736+
737+
notification.showSuccessNotification(t("Documents deleted"))
738+
isDeleteWarningLpDialogVisible.value = false
739+
item.value = {}
740+
unselectAll()
741+
onUpdateOptions(options.value)
742+
} catch (error) {
743+
console.error("Error deleting documents forcibly:", error)
744+
notification.showErrorNotification(t("Error deleting document(s)."))
745+
}
684746
}
685747
686748
function getReplaceButtonTitle(item) {
@@ -726,10 +788,56 @@ async function downloadSelectedItems() {
726788
}
727789
728790
async function deleteMultipleItems() {
729-
await store.dispatch("documents/delMultiple", selectedItems.value)
791+
const itemsWithoutLp = []
792+
const documentsWithLpMap = {}
793+
794+
for (const item of selectedItems.value) {
795+
try {
796+
const response = await axios.get(`/api/documents/${item.iid}/lp-usage`)
797+
if (response.data.usedInLp) {
798+
if (!documentsWithLpMap[item.iid]) {
799+
documentsWithLpMap[item.iid] = {
800+
iid: item.iid,
801+
title: item.title,
802+
lpList: [],
803+
}
804+
}
805+
documentsWithLpMap[item.iid].lpList.push(...response.data.lpList)
806+
} else {
807+
itemsWithoutLp.push(item)
808+
}
809+
} catch (error) {
810+
console.error(`Error checking LP usage for document ${item.iid}:`, error)
811+
}
812+
}
813+
814+
const documentsWithLp = Object.values(documentsWithLpMap)
815+
816+
if (itemsWithoutLp.length > 0) {
817+
try {
818+
await store.dispatch("documents/delMultiple", itemsWithoutLp)
819+
} catch (e) {
820+
console.error("Error deleting documents without LP:", e)
821+
}
822+
}
823+
824+
if (documentsWithLp.length > 0) {
825+
lpListWarning.value = documentsWithLp.flatMap((doc) =>
826+
doc.lpList.map((lp) => ({
827+
...lp,
828+
documentTitle: doc.title,
829+
documentId: doc.iid,
830+
})),
831+
)
832+
833+
item.value = {}
834+
isDeleteWarningLpDialogVisible.value = true
835+
} else {
836+
notification.showSuccessNotification(t("Documents deleted"))
837+
unselectAll()
838+
}
839+
730840
isDeleteMultipleDialogVisible.value = false
731-
notification.showSuccessNotification(t("Deleted"))
732-
unselectAll()
733841
onUpdateOptions(options.value)
734842
}
735843
@@ -901,7 +1009,7 @@ async function replaceDocument() {
9011009
}
9021010
9031011
if (!(documentToReplace.value.filetype === "file" || documentToReplace.value.filetype === "video")) {
904-
notification.showErrorNotification(t("Only files or videos can be replaced."))
1012+
notification.showErrorNotification(t("Only files can be replaced."))
9051013
return
9061014
}
9071015

public/main/inc/lib/document.lib.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2298,6 +2298,7 @@ class="moved ui-sortable-handle link_with_id"
22982298
'WITH',
22992299
'doc.resourceNode = node'
23002300
)
2301+
->addSelect('files')
23012302
->where('type = :type')
23022303
->andWhere('links.course = :course')
23032304
->setParameters([
@@ -2306,7 +2307,6 @@ class="moved ui-sortable-handle link_with_id"
23062307
])
23072308
->orderBy('node.parent', 'ASC');
23082309

2309-
23102310
$sessionId = api_get_session_id();
23112311
if (empty($sessionId)) {
23122312
$qb->andWhere('links.session IS NULL');
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/* For licensing terms, see /license.txt */
6+
7+
namespace Chamilo\CoreBundle\Controller\Api;
8+
9+
use Chamilo\CourseBundle\Repository\CLpItemRepository;
10+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
11+
use Symfony\Component\HttpFoundation\JsonResponse;
12+
use Symfony\Component\HttpKernel\Attribute\AsController;
13+
14+
#[AsController]
15+
class DocumentLearningPathUsageAction extends AbstractController
16+
{
17+
public function __construct(
18+
private CLpItemRepository $lpItemRepo
19+
) {}
20+
21+
public function __invoke($iid): JsonResponse
22+
{
23+
$lpUsages = $this->lpItemRepo->findLearningPathsUsingDocument((int) $iid);
24+
25+
return new JsonResponse([
26+
'usedInLp' => !empty($lpUsages),
27+
'lpList' => $lpUsages,
28+
]);
29+
}
30+
}

src/CoreBundle/Entity/Listener/ResourceListener.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
use Chamilo\CoreBundle\Tool\ToolChain;
2424
use Chamilo\CoreBundle\Traits\AccessUrlListenerTrait;
2525
use Chamilo\CourseBundle\Entity\CCalendarEvent;
26+
use Chamilo\CourseBundle\Entity\CDocument;
2627
use Cocur\Slugify\SlugifyInterface;
28+
use Doctrine\Persistence\Event\LifecycleEventArgs;
2729
use Doctrine\ORM\Event\PostPersistEventArgs;
2830
use Doctrine\ORM\Event\PostRemoveEventArgs;
2931
use Doctrine\ORM\Event\PostUpdateEventArgs;
@@ -389,4 +391,24 @@ private function addCCalendarEventGlobalLink(CCalendarEvent $event, PrePersistEv
389391
}
390392
}
391393
}
394+
395+
public function preRemove(AbstractResource $resource, LifecycleEventArgs $args): void
396+
{
397+
if (!$resource instanceof CDocument) {
398+
return;
399+
}
400+
401+
$em = $args->getObjectManager();
402+
$resourceNode = $resource->getResourceNode();
403+
404+
if (!$resourceNode) {
405+
return;
406+
}
407+
408+
$docID = $resource->getIid();
409+
$em->createQuery('DELETE FROM Chamilo\CourseBundle\Entity\CLpItem i WHERE i.path = :path AND i.itemType = :type')
410+
->setParameter('path', $docID)
411+
->setParameter('type', 'document')
412+
->execute();
413+
}
392414
}

src/CoreBundle/Repository/ResourceNodeRepository.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function __construct(
3737
private readonly AccessUrlHelper $accessUrlHelper,
3838
private readonly SettingsManager $settingsManager
3939
) {
40-
$this->filesystem = $resourceFilesystem; // Asignar el filesystem correcto
40+
$this->filesystem = $resourceFilesystem;
4141
parent::__construct($manager, $manager->getClassMetadata(ResourceNode::class));
4242
}
4343

src/CoreBundle/Traits/Repository/ORM/NestedTreeRepositoryTrait.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Chamilo\CoreBundle\Traits\Repository\ORM;
44

5+
use Gedmo\Exception\RuntimeException;
56
use Gedmo\Tool\Wrapper\EntityWrapper;
67
use Doctrine\ORM\Query;
78
use Gedmo\Tree\Strategy;
@@ -668,12 +669,8 @@ public function moveUp($node, $number = 1)
668669
* UNSAFE: be sure to backup before running this method when necessary
669670
*
670671
* Removes given $node from the tree and reparents its descendants
671-
*
672-
* @param object $node
673-
*
674-
* @throws \RuntimeException - if something fails in transaction
675672
*/
676-
public function removeFromTree($node)
673+
public function removeFromTree(object $node): void
677674
{
678675
$meta = $this->getClassMetadata();
679676
$em = $this->getEntityManager();
@@ -685,6 +682,11 @@ public function removeFromTree($node)
685682
$left = $wrapped->getPropertyValue($config['left']);
686683
$rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null;
687684

685+
if (!is_numeric($left) || !is_numeric($right)) {
686+
$this->removeSingle($wrapped);
687+
return;
688+
}
689+
688690
if ($right == $left + 1) {
689691
$this->removeSingle($wrapped);
690692
$this->listener
@@ -775,7 +777,7 @@ public function removeFromTree($node)
775777
} catch (\Exception $e) {
776778
$em->close();
777779
$em->getConnection()->rollback();
778-
throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
780+
throw new RuntimeException('Transaction failed', null, $e);
779781
}
780782
} else {
781783
throw new InvalidArgumentException("Node is not related to this repository");

src/CourseBundle/Entity/CDocument.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use ApiPlatform\Metadata\Put;
1919
use ApiPlatform\Serializer\Filter\PropertyFilter;
2020
use Chamilo\CoreBundle\Controller\Api\CreateDocumentFileAction;
21+
use Chamilo\CoreBundle\Controller\Api\DocumentLearningPathUsageAction;
2122
use Chamilo\CoreBundle\Controller\Api\DownloadSelectedDocumentsAction;
2223
use Chamilo\CoreBundle\Controller\Api\ReplaceDocumentFileAction;
2324
use Chamilo\CoreBundle\Controller\Api\UpdateDocumentFileAction;
@@ -88,6 +89,13 @@
8889
deserialize: false
8990
),
9091
new Get(security: "is_granted('VIEW', object.resourceNode)"),
92+
new Get(
93+
uriTemplate: '/documents/{iid}/lp-usage',
94+
controller: DocumentLearningPathUsageAction::class,
95+
security: "is_granted('ROLE_USER')",
96+
read: false,
97+
name: 'api_documents_lp_usage'
98+
),
9199
new Delete(security: "is_granted('DELETE', object.resourceNode)"),
92100
new Post(
93101
controller: CreateDocumentFileAction::class,

src/CourseBundle/Entity/CLpItem.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* Items from a learning path (LP).
1919
*/
2020
#[ORM\Table(name: 'c_lp_item')]
21-
#[ORM\Index(name: 'lp_id', columns: ['lp_id'])]
21+
#[ORM\Index(columns: ['lp_id'], name: 'lp_id')]
2222
#[Gedmo\Tree(type: 'nested')]
2323
#[ORM\Entity(repositoryClass: CLpItemRepository::class)]
2424
class CLpItem implements Stringable

src/CourseBundle/Repository/CLpItemRepository.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,17 @@ public function findItemsByLearningPathAndType(int $learningPathId, string $item
5151

5252
return $query->getResult();
5353
}
54+
55+
public function findLearningPathsUsingDocument(int $resourceFileId): array
56+
{
57+
return $this->createQueryBuilder('i')
58+
->select('lp.iid AS lpId, lp.title AS lpTitle')
59+
->join('i.lp', 'lp')
60+
->where('i.itemType = :type')
61+
->andWhere('i.path = :path')
62+
->setParameter('type', 'document')
63+
->setParameter('path', $resourceFileId)
64+
->getQuery()
65+
->getArrayResult();
66+
}
5467
}

0 commit comments

Comments
 (0)