Skip to content

Commit 940701f

Browse files
Documents: Add replace document functionality - refs #5957
Author: @christianbeeznest
1 parent 43398f6 commit 940701f

File tree

4 files changed

+188
-0
lines changed

4 files changed

+188
-0
lines changed

assets/vue/components/basecomponents/ChamiloIcons.js

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const chamiloIconToClass = {
5656
"file-generic": "mdi mdi-file",
5757
"file-image": "mdi mdi-file-image",
5858
"file-pdf": "mdi mdi-file-pdf-box",
59+
"file-swap": "mdi mdi-swap-horizontal",
5960
"file-text": "mdi mdi-file-document",
6061
"file-upload": "mdi mdi-file-upload",
6162
"file-video": "mdi mdi-file-video",

assets/vue/views/documents/DocumentsList.vue

+62
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,14 @@
182182
type="secondary"
183183
@click="openMoveDialog(slotProps.data)"
184184
/>
185+
<BaseButton
186+
:disabled="slotProps.data.filetype !== 'file'"
187+
:title="slotProps.data.filetype !== 'file' ? t('Replace (files only)') : t('Replace file')"
188+
icon="file-swap"
189+
size="small"
190+
type="secondary"
191+
@click="slotProps.data.filetype === 'file' && openReplaceDialog(slotProps.data)"
192+
/>
185193
<BaseButton
186194
:title="t('Information')"
187195
icon="information"
@@ -361,6 +369,21 @@
361369
</div>
362370
</BaseDialogConfirmCancel>
363371

372+
<BaseDialogConfirmCancel
373+
v-model:is-visible="isReplaceDialogVisible"
374+
:title="t('Replace file')"
375+
@confirm-clicked="replaceDocument"
376+
@cancel-clicked="isReplaceDialogVisible = false"
377+
>
378+
<BaseFileUpload
379+
id="replace-file"
380+
:label="t('Select replacement file')"
381+
accept="*/*"
382+
model-value="selectedReplaceFile"
383+
@file-selected="selectedReplaceFile = $event"
384+
/>
385+
</BaseDialogConfirmCancel>
386+
364387
<BaseDialog
365388
v-model:is-visible="isFileUsageDialogVisible"
366389
:style="{ width: '28rem' }"
@@ -532,6 +555,10 @@ const isSessionDocument = (item) => {
532555
533556
const isHtmlFile = (fileData) => isHtml(fileData)
534557
558+
const isReplaceDialogVisible = ref(false)
559+
const selectedReplaceFile = ref(null)
560+
const documentToReplace = ref(null)
561+
535562
onMounted(async () => {
536563
isAllowedToEdit.value = await checkIsAllowedToEdit(true, true, true)
537564
filters.value.loadNode = 1
@@ -806,6 +833,41 @@ function openMoveDialog(document) {
806833
isMoveDialogVisible.value = true
807834
}
808835
836+
function openReplaceDialog(document) {
837+
documentToReplace.value = document
838+
isReplaceDialogVisible.value = true
839+
}
840+
841+
async function replaceDocument() {
842+
if (!selectedReplaceFile.value) {
843+
notification.showErrorNotification(t("No file selected."))
844+
return
845+
}
846+
847+
if (documentToReplace.value.filetype !== 'file') {
848+
notification.showErrorNotification(t("Only files can be replaced."))
849+
return
850+
}
851+
852+
const formData = new FormData()
853+
console.log(selectedReplaceFile.value)
854+
formData.append('file', selectedReplaceFile.value)
855+
856+
try {
857+
await axios.post(`/api/documents/${documentToReplace.value.iid}/replace`, formData, {
858+
headers: {
859+
'Content-Type': 'multipart/form-data',
860+
},
861+
})
862+
notification.showSuccessNotification(t("File replaced"))
863+
isReplaceDialogVisible.value = false
864+
onUpdateOptions(options.value)
865+
} catch (error) {
866+
notification.showErrorNotification(t("Error replacing file."))
867+
console.error(error)
868+
}
869+
}
870+
809871
async function fetchFolders(nodeId = null, parentPath = "") {
810872
const foldersList = [
811873
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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\CoreBundle\Repository\ResourceNodeRepository;
10+
use Chamilo\CourseBundle\Entity\CDocument;
11+
use Doctrine\ORM\EntityManagerInterface;
12+
use Symfony\Component\HttpFoundation\Request;
13+
use Symfony\Component\HttpFoundation\Response;
14+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
15+
use Symfony\Component\HttpKernel\KernelInterface;
16+
use Symfony\Component\HttpFoundation\File\Exception\FileException;
17+
18+
class ReplaceDocumentFileAction extends BaseResourceFileAction
19+
{
20+
private string $uploadBasePath;
21+
22+
public function __construct(KernelInterface $kernel)
23+
{
24+
$this->uploadBasePath = $kernel->getProjectDir() . '/var/upload/resource';
25+
}
26+
27+
public function __invoke(
28+
CDocument $document,
29+
Request $request,
30+
ResourceNodeRepository $resourceNodeRepository,
31+
EntityManagerInterface $em
32+
): Response {
33+
$uploadedFile = $request->files->get('file');
34+
if (!$uploadedFile) {
35+
throw new BadRequestHttpException('"file" is required.');
36+
}
37+
38+
$resourceNode = $document->getResourceNode();
39+
if (!$resourceNode) {
40+
throw new BadRequestHttpException('ResourceNode not found.');
41+
}
42+
43+
$resourceFile = $resourceNode->getFirstResourceFile();
44+
if (!$resourceFile) {
45+
throw new BadRequestHttpException('No file found in the resource node.');
46+
}
47+
48+
$filePath = $this->uploadBasePath . $resourceNodeRepository->getFilename($resourceFile);
49+
if (!$filePath) {
50+
throw new BadRequestHttpException('File path could not be resolved.');
51+
}
52+
53+
$this->prepareDirectory($filePath);
54+
55+
try {
56+
$uploadedFile->move(dirname($filePath), basename($filePath));
57+
} catch (FileException $e) {
58+
throw new BadRequestHttpException(sprintf('Failed to move the file: %s', $e->getMessage()));
59+
}
60+
61+
$movedFilePath = $filePath;
62+
if (!file_exists($movedFilePath)) {
63+
throw new \RuntimeException('The moved file does not exist at the expected location.');
64+
}
65+
$fileSize = filesize($movedFilePath);
66+
$resourceFile->setSize($fileSize);
67+
68+
$newFileName = $uploadedFile->getClientOriginalName();
69+
$document->setTitle($newFileName);
70+
$resourceFile->setOriginalName($newFileName);
71+
72+
$resourceNode->setUpdatedAt(new \DateTime());
73+
74+
$em->persist($document);
75+
$em->persist($resourceFile);
76+
$em->flush();
77+
78+
return new Response('Document replaced successfully.', Response::HTTP_OK);
79+
}
80+
81+
/**
82+
* Prepares the directory to ensure it exists and is writable.
83+
*/
84+
protected function prepareDirectory(string $filePath): void
85+
{
86+
$directory = dirname($filePath);
87+
88+
if (!is_dir($directory)) {
89+
if (!mkdir($directory, 0775, true) && !is_dir($directory)) {
90+
throw new \RuntimeException(sprintf('Unable to create directory "%s".', $directory));
91+
}
92+
}
93+
94+
if (!is_writable($directory)) {
95+
throw new \RuntimeException(sprintf('Directory "%s" is not writable.', $directory));
96+
}
97+
}
98+
99+
}

src/CourseBundle/Entity/CDocument.php

+26
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\ReplaceDocumentFileAction;
2122
use Chamilo\CoreBundle\Controller\Api\UpdateDocumentFileAction;
2223
use Chamilo\CoreBundle\Controller\Api\UpdateVisibilityDocument;
2324
use Chamilo\CoreBundle\Entity\AbstractResource;
@@ -60,6 +61,31 @@
6061
security: "is_granted('EDIT', object.resourceNode)",
6162
deserialize: true
6263
),
64+
new Post(
65+
uriTemplate: '/documents/{iid}/replace',
66+
controller: ReplaceDocumentFileAction::class,
67+
openapiContext: [
68+
'summary' => 'Replace a document file, maintaining the same IDs.',
69+
'requestBody' => [
70+
'content' => [
71+
'multipart/form-data' => [
72+
'schema' => [
73+
'type' => 'object',
74+
'properties' => [
75+
'file' => [
76+
'type' => 'string',
77+
'format' => 'binary',
78+
],
79+
],
80+
],
81+
],
82+
],
83+
],
84+
],
85+
security: "is_granted('ROLE_CURRENT_COURSE_TEACHER') or is_granted('ROLE_CURRENT_COURSE_SESSION_TEACHER') or is_granted('ROLE_TEACHER')",
86+
validationContext: ['groups' => ['Default', 'media_object_create', 'document:write']],
87+
deserialize: false
88+
),
6389
new Get(security: "is_granted('VIEW', object.resourceNode)"),
6490
new Delete(security: "is_granted('DELETE', object.resourceNode)"),
6591
new Post(

0 commit comments

Comments
 (0)