Skip to content

Document: Add LP usage warning when deleting documents - refs #4454 #6239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
140 changes: 125 additions & 15 deletions assets/vue/views/documents/DocumentsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,34 @@
/>
</form>
</BaseDialogConfirmCancel>
<BaseDialogConfirmCancel
v-model:is-visible="isDeleteWarningLpDialogVisible"
:title="t('Confirm deletion')"
@confirm-clicked="forceDeleteItem"
@cancel-clicked="isDeleteWarningLpDialogVisible = false"
>
<div class="confirmation-content">
<BaseIcon
class="mr-2"
icon="alert"
size="big"
/>
<p class="mb-2">
{{ t("The following documents are used in learning paths:") }}
</p>
<ul class="pl-4 mb-4">
<li
v-for="lp in lpListWarning"
:key="lp.lpId + lp.documentTitle"
>
<b>{{ lp.documentTitle }}</b> → {{ lp.lpTitle }}
</li>
</ul>
<p class="mt-4 font-semibold">
{{ t("Do you still want to delete them?") }}
</p>
</div>
</BaseDialogConfirmCancel>
</template>

<script setup>
Expand Down Expand Up @@ -486,7 +514,9 @@ const router = useRouter()
const securityStore = useSecurityStore()

const platformConfigStore = usePlatformConfig()
const allowAccessUrlFiles = computed(() => "false" !== platformConfigStore.getSetting("course.access_url_specific_files"))
const allowAccessUrlFiles = computed(
() => "false" !== platformConfigStore.getSetting("course.access_url_specific_files"),
)

const { t } = useI18n()
const { filters, options, onUpdateOptions, deleteItem } = useDatatableList("Documents")
Expand All @@ -499,6 +529,8 @@ const isAllowedToEdit = ref(false)
const folders = ref([])
const selectedFolder = ref(null)
const isDownloading = ref(false)
const isDeleteWarningLpDialogVisible = ref(false)
const lpListWarning = ref([])

const {
showNewDocumentButton,
Expand Down Expand Up @@ -610,7 +642,7 @@ const showBackButtonIfNotRootFolder = computed(() => {
function goToAddVariation(item) {
const resourceFileId = item.resourceNode.firstResourceFile.id
router.push({
name: 'DocumentsAddVariation',
name: "DocumentsAddVariation",
params: { resourceFileId, node: route.params.node },
query: { cid, sid, gid },
})
Expand Down Expand Up @@ -672,9 +704,41 @@ function showDeleteMultipleDialog() {
isDeleteMultipleDialogVisible.value = true
}

function confirmDeleteItem(itemToDelete) {
item.value = itemToDelete
isDeleteItemDialogVisible.value = true
async function confirmDeleteItem(itemToDelete) {
try {
const response = await axios.get(`/api/documents/${itemToDelete.iid}/lp-usage`)
if (response.data.usedInLp) {
lpListWarning.value = response.data.lpList.map((lp) => ({
...lp,
documentTitle: itemToDelete.title,
documentId: itemToDelete.iid,
}))
item.value = itemToDelete
isDeleteWarningLpDialogVisible.value = true
} else {
item.value = itemToDelete
isDeleteItemDialogVisible.value = true
}
} catch (error) {
console.error("Error checking LP usage for individual item:", error)
}
}

async function forceDeleteItem() {
try {
const docIdsToDelete = [...new Set(lpListWarning.value.map((lp) => lp.documentId))]

await Promise.all(docIdsToDelete.map((iid) => axios.delete(`/api/documents/${iid}`)))

notification.showSuccessNotification(t("Documents deleted"))
isDeleteWarningLpDialogVisible.value = false
item.value = {}
unselectAll()
onUpdateOptions(options.value)
} catch (error) {
console.error("Error deleting documents forcibly:", error)
notification.showErrorNotification(t("Error deleting document(s)."))
}
}

async function downloadSelectedItems() {
Expand All @@ -688,8 +752,8 @@ async function downloadSelectedItems() {
try {
const response = await axios.post(
"/api/documents/download-selected",
{ ids: selectedItems.value.map(item => item.iid) },
{ responseType: "blob" }
{ ids: selectedItems.value.map((item) => item.iid) },
{ responseType: "blob" },
)

const url = window.URL.createObjectURL(new Blob([response.data]))
Expand All @@ -705,15 +769,61 @@ async function downloadSelectedItems() {
console.error("Error downloading selected items:", error)
notification.showErrorNotification(t("Error downloading selected items."))
} finally {
isDownloading.value = false;
isDownloading.value = false
}
}

async function deleteMultipleItems() {
await store.dispatch("documents/delMultiple", selectedItems.value)
const itemsWithoutLp = []
const documentsWithLpMap = {}

for (const item of selectedItems.value) {
try {
const response = await axios.get(`/api/documents/${item.iid}/lp-usage`)
if (response.data.usedInLp) {
if (!documentsWithLpMap[item.iid]) {
documentsWithLpMap[item.iid] = {
iid: item.iid,
title: item.title,
lpList: [],
}
}
documentsWithLpMap[item.iid].lpList.push(...response.data.lpList)
} else {
itemsWithoutLp.push(item)
}
} catch (error) {
console.error(`Error checking LP usage for document ${item.iid}:`, error)
}
}

const documentsWithLp = Object.values(documentsWithLpMap)

if (itemsWithoutLp.length > 0) {
try {
await store.dispatch("documents/delMultiple", itemsWithoutLp)
} catch (e) {
console.error("Error deleting documents without LP:", e)
}
}

if (documentsWithLp.length > 0) {
lpListWarning.value = documentsWithLp.flatMap((doc) =>
doc.lpList.map((lp) => ({
...lp,
documentTitle: doc.title,
documentId: doc.iid,
})),
)

item.value = {}
isDeleteWarningLpDialogVisible.value = true
} else {
notification.showSuccessNotification(t("Documents deleted"))
unselectAll()
}

isDeleteMultipleDialogVisible.value = false
notification.showSuccessNotification(t("Deleted"))
unselectAll()
onUpdateOptions(options.value)
}

Expand Down Expand Up @@ -884,19 +994,19 @@ async function replaceDocument() {
return
}

if (documentToReplace.value.filetype !== 'file') {
if (documentToReplace.value.filetype !== "file") {
notification.showErrorNotification(t("Only files can be replaced."))
return
}

const formData = new FormData()
console.log(selectedReplaceFile.value)
formData.append('file', selectedReplaceFile.value)
formData.append("file", selectedReplaceFile.value)

try {
await axios.post(`/api/documents/${documentToReplace.value.iid}/replace`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
"Content-Type": "multipart/form-data",
},
})
notification.showSuccessNotification(t("File replaced"))
Expand All @@ -911,7 +1021,7 @@ async function replaceDocument() {
async function fetchFolders(nodeId = null, parentPath = "") {
const foldersList = [
{
label: t('Documents'),
label: t("Documents"),
value: nodeId || route.params.node || route.query.node || "root-node-id",
},
]
Expand Down
9 changes: 6 additions & 3 deletions public/main/inc/lib/document.lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -2294,12 +2294,15 @@ class="moved ui-sortable-handle link_with_id"
->innerJoin('node.resourceType', 'type')
->innerJoin('node.resourceLinks', 'links')
->innerJoin('node.resourceFiles', 'files')
->innerJoin(CDocument::class, 'doc', 'WITH', 'doc.resourceNode = node')
->addSelect('files')
->where('type = :type')
->andWhere('links.course = :course')
->setParameters(['type' => $type, 'course' => $course])
->orderBy('node.parent', 'ASC')
;
->setParameters([
'type' => $type,
'course' => $course,
])
->orderBy('node.parent', 'ASC');

$sessionId = api_get_session_id();
if (empty($sessionId)) {
Expand Down
30 changes: 30 additions & 0 deletions src/CoreBundle/Controller/Api/DocumentLearningPathUsageAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/* For licensing terms, see /license.txt */

namespace Chamilo\CoreBundle\Controller\Api;

use Chamilo\CourseBundle\Repository\CLpItemRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\AsController;

#[AsController]
class DocumentLearningPathUsageAction extends AbstractController
{
public function __construct(
private CLpItemRepository $lpItemRepo
) {}

public function __invoke($iid): JsonResponse
{
$lpUsages = $this->lpItemRepo->findLearningPathsUsingDocument((int) $iid);

return new JsonResponse([
'usedInLp' => !empty($lpUsages),
'lpList' => $lpUsages,
]);
}
}
22 changes: 22 additions & 0 deletions src/CoreBundle/Entity/Listener/ResourceListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
use Chamilo\CoreBundle\Tool\ToolChain;
use Chamilo\CoreBundle\Traits\AccessUrlListenerTrait;
use Chamilo\CourseBundle\Entity\CCalendarEvent;
use Chamilo\CourseBundle\Entity\CDocument;
use Cocur\Slugify\SlugifyInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
Expand Down Expand Up @@ -350,4 +352,24 @@ private function addCCalendarEventGlobalLink(CCalendarEvent $event, PrePersistEv
}
}
}

public function preRemove(AbstractResource $resource, LifecycleEventArgs $args): void
{
if (!$resource instanceof CDocument) {
return;
}

$em = $args->getObjectManager();
$resourceNode = $resource->getResourceNode();

if (!$resourceNode) {
return;
}

$docID = $resource->getIid();
$em->createQuery('DELETE FROM Chamilo\CourseBundle\Entity\CLpItem i WHERE i.path = :path AND i.itemType = :type')
->setParameter('path', $docID)
->setParameter('type', 'document')
->execute();
}
}
2 changes: 1 addition & 1 deletion src/CoreBundle/Repository/ResourceNodeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public function __construct(
private readonly AccessUrlHelper $accessUrlHelper,
private readonly SettingsManager $settingsManager
) {
$this->filesystem = $resourceFilesystem; // Asignar el filesystem correcto
$this->filesystem = $resourceFilesystem;
parent::__construct($manager, $manager->getClassMetadata(ResourceNode::class));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Chamilo\CoreBundle\Traits\Repository\ORM;

use Gedmo\Exception\RuntimeException;
use Gedmo\Tool\Wrapper\EntityWrapper;
use Doctrine\ORM\Query;
use Gedmo\Tree\Strategy;
Expand Down Expand Up @@ -668,12 +669,8 @@ public function moveUp($node, $number = 1)
* UNSAFE: be sure to backup before running this method when necessary
*
* Removes given $node from the tree and reparents its descendants
*
* @param object $node
*
* @throws \RuntimeException - if something fails in transaction
*/
public function removeFromTree($node)
public function removeFromTree(object $node): void
{
$meta = $this->getClassMetadata();
$em = $this->getEntityManager();
Expand All @@ -685,6 +682,11 @@ public function removeFromTree($node)
$left = $wrapped->getPropertyValue($config['left']);
$rootId = isset($config['root']) ? $wrapped->getPropertyValue($config['root']) : null;

if (!is_numeric($left) || !is_numeric($right)) {
$this->removeSingle($wrapped);
return;
}

if ($right == $left + 1) {
$this->removeSingle($wrapped);
$this->listener
Expand Down Expand Up @@ -775,7 +777,7 @@ public function removeFromTree($node)
} catch (\Exception $e) {
$em->close();
$em->getConnection()->rollback();
throw new \Gedmo\Exception\RuntimeException('Transaction failed', null, $e);
throw new RuntimeException('Transaction failed', null, $e);
}
} else {
throw new InvalidArgumentException("Node is not related to this repository");
Expand Down
8 changes: 8 additions & 0 deletions src/CourseBundle/Entity/CDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use ApiPlatform\Metadata\Put;
use ApiPlatform\Serializer\Filter\PropertyFilter;
use Chamilo\CoreBundle\Controller\Api\CreateDocumentFileAction;
use Chamilo\CoreBundle\Controller\Api\DocumentLearningPathUsageAction;
use Chamilo\CoreBundle\Controller\Api\DownloadSelectedDocumentsAction;
use Chamilo\CoreBundle\Controller\Api\ReplaceDocumentFileAction;
use Chamilo\CoreBundle\Controller\Api\UpdateDocumentFileAction;
Expand Down Expand Up @@ -88,6 +89,13 @@
deserialize: false
),
new Get(security: "is_granted('VIEW', object.resourceNode)"),
new Get(
uriTemplate: '/documents/{iid}/lp-usage',
controller: DocumentLearningPathUsageAction::class,
security: "is_granted('ROLE_USER')",
read: false,
name: 'api_documents_lp_usage'
),
new Delete(security: "is_granted('DELETE', object.resourceNode)"),
new Post(
controller: CreateDocumentFileAction::class,
Expand Down
2 changes: 1 addition & 1 deletion src/CourseBundle/Entity/CLpItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* Items from a learning path (LP).
*/
#[ORM\Table(name: 'c_lp_item')]
#[ORM\Index(name: 'lp_id', columns: ['lp_id'])]
#[ORM\Index(columns: ['lp_id'], name: 'lp_id')]
#[Gedmo\Tree(type: 'nested')]
#[ORM\Entity(repositoryClass: CLpItemRepository::class)]
class CLpItem implements Stringable
Expand Down
Loading
Loading