From 130b5cc2ba3a21c4177f6960561b65663d1f6ed6 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Sun, 15 Dec 2024 21:37:22 -0500 Subject: [PATCH 1/2] Documents: Add support for URL-specific document variations - refs #5956 --- .../components/basecomponents/ChamiloIcons.js | 1 + assets/vue/router/documents.js | 5 + assets/vue/views/documents/AddVariation.vue | 189 ++++++++++++++++++ assets/vue/views/documents/DocumentsList.vue | 24 ++- .../AddVariantResourceFileAction.php | 71 +++++++ .../PlatformConfigurationController.php | 1 + .../Controller/ResourceController.php | 147 +++++++++++--- src/CoreBundle/Entity/ResourceFile.php | 49 +++++ .../Schema/V200/Version20241214083500.php | 60 ++++++ .../Repository/ResourceNodeRepository.php | 37 ++-- .../Settings/CourseSettingsSchema.php | 2 + 11 files changed, 544 insertions(+), 42 deletions(-) create mode 100644 assets/vue/views/documents/AddVariation.vue create mode 100644 src/CoreBundle/Controller/AddVariantResourceFileAction.php create mode 100644 src/CoreBundle/Migrations/Schema/V200/Version20241214083500.php diff --git a/assets/vue/components/basecomponents/ChamiloIcons.js b/assets/vue/components/basecomponents/ChamiloIcons.js index 3466f590755..7e5cd2667c7 100644 --- a/assets/vue/components/basecomponents/ChamiloIcons.js +++ b/assets/vue/components/basecomponents/ChamiloIcons.js @@ -58,6 +58,7 @@ export const chamiloIconToClass = { "file-text": "mdi mdi-file-document", "file-upload": "mdi mdi-file-upload", "file-video": "mdi mdi-file-video", + "file-replace": "mdi mdi-file-replace", "fit-to-screen": "", "folder-generic": "mdi mdi-folder", "folder-multiple-plus": "mdi mdi-folder-multiple-plus", diff --git a/assets/vue/router/documents.js b/assets/vue/router/documents.js index 0306507e0b0..379c0f27f8d 100644 --- a/assets/vue/router/documents.js +++ b/assets/vue/router/documents.js @@ -42,6 +42,11 @@ export default { path: 'show', component: () => import('../views/documents/DocumentShow.vue') }, + { + name: 'DocumentsAddVariation', + path: 'add_variation/:resourceFileId', + component: () => import('../views/documents/AddVariation.vue'), + }, { name: 'DocumentForHtmlEditor', path: 'manager', diff --git a/assets/vue/views/documents/AddVariation.vue b/assets/vue/views/documents/AddVariation.vue new file mode 100644 index 00000000000..bd784fcaf2c --- /dev/null +++ b/assets/vue/views/documents/AddVariation.vue @@ -0,0 +1,189 @@ + + + diff --git a/assets/vue/views/documents/DocumentsList.vue b/assets/vue/views/documents/DocumentsList.vue index bf2a0817061..ebfe60ab1d2 100644 --- a/assets/vue/views/documents/DocumentsList.vue +++ b/assets/vue/views/documents/DocumentsList.vue @@ -205,6 +205,15 @@ @click="btnChangeVisibilityOnClick(slotProps.data)" /> + + "false" !== platformConfigStore.getSetting("course.access_url_specific_files")) + const { t } = useI18n() const { filters, options, onUpdateOptions, deleteItem } = useDatatableList("Documents") const notification = useNotification() const { cid, sid, gid } = useCidReq() -const { isImage, isHtml } = useFileUtils() +const { isImage, isHtml, isFile } = useFileUtils() const { relativeDatetime } = useFormatDate() const isAllowedToEdit = ref(false) @@ -559,6 +572,15 @@ const showBackButtonIfNotRootFolder = computed(() => { return resourceNode.value.resourceType.title !== "courses" }) +function goToAddVariation(item) { + const resourceFileId = item.resourceNode.firstResourceFile.id + router.push({ + name: 'DocumentsAddVariation', + params: { resourceFileId, node: route.params.node }, + query: { cid, sid, gid }, + }) +} + function back() { if (!resourceNode.value) { return diff --git a/src/CoreBundle/Controller/AddVariantResourceFileAction.php b/src/CoreBundle/Controller/AddVariantResourceFileAction.php new file mode 100644 index 00000000000..39f17f1643d --- /dev/null +++ b/src/CoreBundle/Controller/AddVariantResourceFileAction.php @@ -0,0 +1,71 @@ +files->get('file'); + if (!$uploadedFile) { + throw new BadRequestHttpException('"file" is required'); + } + + $resourceNodeId = $request->get('resourceNodeId'); + if (!$resourceNodeId) { + throw new BadRequestHttpException('"resourceNodeId" is required'); + } + + $resourceNode = $em->getRepository(ResourceNode::class)->find($resourceNodeId); + if (!$resourceNode) { + throw new NotFoundHttpException('ResourceNode not found'); + } + + $accessUrlId = $request->get('accessUrlId'); + $accessUrl = null; + if ($accessUrlId) { + $accessUrl = $em->getRepository(AccessUrl::class)->find($accessUrlId); + if (!$accessUrl) { + throw new NotFoundHttpException('AccessUrl not found'); + } + } + + $existingResourceFile = $em->getRepository(ResourceFile::class)->findOneBy([ + 'resourceNode' => $resourceNode, + 'accessUrl' => $accessUrl, + ]); + + if ($existingResourceFile) { + $existingResourceFile->setTitle($uploadedFile->getClientOriginalName()); + $existingResourceFile->setFile($uploadedFile); + $existingResourceFile->setUpdatedAt(\DateTime::createFromImmutable(new \DateTimeImmutable())); + $resourceFile = $existingResourceFile; + } else { + $resourceFile = new ResourceFile(); + $resourceFile->setTitle($uploadedFile->getClientOriginalName()); + $resourceFile->setFile($uploadedFile); + $resourceFile->setResourceNode($resourceNode); + + if ($accessUrl) { + $resourceFile->setAccessUrl($accessUrl); + } + } + + $em->persist($resourceFile); + $em->flush(); + + return $resourceFile; + } +} diff --git a/src/CoreBundle/Controller/PlatformConfigurationController.php b/src/CoreBundle/Controller/PlatformConfigurationController.php index 91915bc79bd..3c2fd761173 100644 --- a/src/CoreBundle/Controller/PlatformConfigurationController.php +++ b/src/CoreBundle/Controller/PlatformConfigurationController.php @@ -92,6 +92,7 @@ public function list(SettingsManager $settingsManager): Response 'social.hide_social_groups_block', 'course.show_course_duration', 'exercise.allow_exercise_auto_launch', + 'course.access_url_specific_files', ]; $user = $this->userHelper->getCurrent(); diff --git a/src/CoreBundle/Controller/ResourceController.php b/src/CoreBundle/Controller/ResourceController.php index eb8a38c5e90..96f7568a864 100644 --- a/src/CoreBundle/Controller/ResourceController.php +++ b/src/CoreBundle/Controller/ResourceController.php @@ -7,15 +7,19 @@ namespace Chamilo\CoreBundle\Controller; use Chamilo\CoreBundle\Entity\Course; +use Chamilo\CoreBundle\Entity\ResourceFile; use Chamilo\CoreBundle\Entity\ResourceLink; use Chamilo\CoreBundle\Entity\ResourceNode; use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Entity\User; +use Chamilo\CoreBundle\Repository\ResourceFileRepository; use Chamilo\CoreBundle\Repository\ResourceNodeRepository; use Chamilo\CoreBundle\Repository\ResourceWithLinkInterface; use Chamilo\CoreBundle\Repository\TrackEDownloadsRepository; use Chamilo\CoreBundle\Security\Authorization\Voter\ResourceNodeVoter; +use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper; use Chamilo\CoreBundle\ServiceHelper\UserHelper; +use Chamilo\CoreBundle\Settings\SettingsManager; use Chamilo\CoreBundle\Tool\ToolChain; use Chamilo\CoreBundle\Traits\ControllerTrait; use Chamilo\CoreBundle\Traits\CourseControllerTrait; @@ -59,6 +63,7 @@ class ResourceController extends AbstractResourceController implements CourseCon public function __construct( private readonly UserHelper $userHelper, private readonly ResourceNodeRepository $resourceNodeRepository, + private readonly ResourceFileRepository $resourceFileRepository ) {} #[Route(path: '/{tool}/{type}/{id}/disk_space', methods: ['GET', 'POST'], name: 'chamilo_core_resource_disk_space')] @@ -136,21 +141,55 @@ public function diskSpace(Request $request): Response * View file of a resource node. */ #[Route('/{tool}/{type}/{id}/view', name: 'chamilo_core_resource_view', methods: ['GET'])] - public function view(Request $request, TrackEDownloadsRepository $trackEDownloadsRepository): Response - { + public function view( + Request $request, + TrackEDownloadsRepository $trackEDownloadsRepository, + SettingsManager $settingsManager, + AccessUrlHelper $accessUrlHelper + ): Response { + $id = $request->get('id'); - $filter = (string) $request->get('filter'); // See filters definitions in /config/services.yml. + $resourceFileId = $request->get('resourceFileId'); + $filter = (string) $request->get('filter'); $resourceNode = $this->getResourceNodeRepository()->findOneBy(['uuid' => $id]); if (null === $resourceNode) { throw new FileNotFoundException($this->trans('Resource not found')); } + $resourceFile = null; + if ($resourceFileId) { + $resourceFile = $this->resourceFileRepository->find($resourceFileId); + } + + if (!$resourceFile) { + $accessUrlSpecificFiles = $settingsManager->getSetting('course.access_url_specific_files') && $accessUrlHelper->isMultiple(); + $currentUrl = $accessUrlHelper->getCurrent()?->getUrl(); + + $resourceFiles = $resourceNode->getResourceFiles(); + + if ($accessUrlSpecificFiles) { + foreach ($resourceFiles as $file) { + if ($file->getAccessUrl() && $file->getAccessUrl()->getUrl() === $currentUrl) { + $resourceFile = $file; + break; + } + } + } + + if (!$resourceFile) { + $resourceFile = $resourceFiles->filter(fn($file) => $file->getAccessUrl() === null)->first(); + } + } + + if (!$resourceFile) { + throw new FileNotFoundException($this->trans('Resource file not found for the given resource node')); + } + $user = $this->userHelper->getCurrent(); $firstResourceLink = $resourceNode->getResourceLinks()->first(); - $firstResourceFile = $resourceNode->getResourceFiles()->first(); - if ($firstResourceLink && $user && $firstResourceFile) { - $url = $firstResourceFile->getOriginalName(); + if ($firstResourceLink && $user) { + $url = $resourceFile->getOriginalName(); $trackEDownloadsRepository->saveDownload($user, $firstResourceLink, $url); } @@ -166,7 +205,7 @@ public function view(Request $request, TrackEDownloadsRepository $trackEDownload ); } - return $this->processFile($request, $resourceNode, 'show', $filter, $allUserInfo); + return $this->processFile($request, $resourceNode, 'show', $filter, $allUserInfo, $resourceFile); } /** @@ -212,8 +251,12 @@ public function link(Request $request, RouterInterface $router, CLinkRepository * Download file of a resource node. */ #[Route('/{tool}/{type}/{id}/download', name: 'chamilo_core_resource_download', methods: ['GET'])] - public function download(Request $request, TrackEDownloadsRepository $trackEDownloadsRepository): Response - { + public function download( + Request $request, + TrackEDownloadsRepository $trackEDownloadsRepository, + SettingsManager $settingsManager, + AccessUrlHelper $accessUrlHelper + ): Response { $id = $request->get('id'); $resourceNode = $this->getResourceNodeRepository()->findOneBy(['uuid' => $id]); @@ -229,21 +272,38 @@ public function download(Request $request, TrackEDownloadsRepository $trackEDown $this->trans('Unauthorised access to resource') ); + $accessUrlSpecificFiles = $settingsManager->getSetting('course.access_url_specific_files') && $accessUrlHelper->isMultiple(); + $currentUrl = $accessUrlHelper->getCurrent()?->getUrl(); + + $resourceFiles = $resourceNode->getResourceFiles(); + $resourceFile = null; + + if ($accessUrlSpecificFiles) { + foreach ($resourceFiles as $file) { + if ($file->getAccessUrl() && $file->getAccessUrl()->getUrl() === $currentUrl) { + $resourceFile = $file; + break; + } + } + } + + $resourceFile ??= $resourceFiles->filter(fn($file) => $file->getAccessUrl() === null)->first(); + // If resource node has a file just download it. Don't download the children. - if ($resourceNode->hasResourceFile()) { + if ($resourceFile) { $user = $this->userHelper->getCurrent(); $firstResourceLink = $resourceNode->getResourceLinks()->first(); - if ($firstResourceLink) { - $url = $resourceNode->getResourceFiles()->first()->getOriginalName(); + + if ($firstResourceLink && $user) { + $url = $resourceFile->getOriginalName(); $trackEDownloadsRepository->saveDownload($user, $firstResourceLink, $url); } // Redirect to download single file. - return $this->processFile($request, $resourceNode, 'download'); + return $this->processFile($request, $resourceNode, 'download', '', null, $resourceFile); } $zipName = $resourceNode->getSlug().'.zip'; - // $rootNodePath = $resourceNode->getPathForDisplay(); $resourceNodeRepo = $repo->getResourceNodeRepository(); $type = $repo->getResourceType(); @@ -282,12 +342,14 @@ function () use ($zipName, $children, $repo): void { /** @var ResourceNode $node */ foreach ($children as $node) { - $stream = $repo->getResourceNodeFileStream($node); - $fileName = $node->getResourceFiles()->first()->getOriginalName(); - // $fileToDisplay = basename($node->getPathForDisplay()); - // $fileToDisplay = str_replace($rootNodePath, '', $node->getPathForDisplay()); - // error_log($fileToDisplay); - $zip->addFileFromStream($fileName, $stream); + $resourceFiles = $node->getResourceFiles(); + $resourceFile = $resourceFiles->filter(fn($file) => $file->getAccessUrl() === null)->first(); + + if ($resourceFile) { + $stream = $repo->getResourceNodeFileStream($resourceFile); + $fileName = $resourceFile->getOriginalName(); + $zip->addFileFromStream($fileName, $stream); + } } $zip->finish(); } @@ -458,7 +520,38 @@ public function changeVisibilityAll( return new Response(null, Response::HTTP_NO_CONTENT); } - private function processFile(Request $request, ResourceNode $resourceNode, string $mode = 'show', string $filter = '', ?array $allUserInfo = null): mixed + #[Route('/resource_files/{resourceNodeId}/variants', name: 'chamilo_core_resource_files_variants', methods: ['GET'])] + public function getVariants(string $resourceNodeId, EntityManagerInterface $em): JsonResponse + { + $variants = $em->getRepository(ResourceFile::class)->createQueryBuilder('rf') + ->join('rf.resourceNode', 'rn') + ->leftJoin('rn.creator', 'creator') + ->where('rf.resourceNode = :resourceNodeId') + ->andWhere('rf.accessUrl IS NOT NULL') + ->setParameter('resourceNodeId', $resourceNodeId) + ->getQuery() + ->getResult(); + + $data = []; + + /* @var ResourceFile $variant */ + foreach ($variants as $variant) { + $data[] = [ + 'id' => $variant->getId(), + 'title' => $variant->getOriginalName(), + 'mimeType' => $variant->getMimeType(), + 'size' => $variant->getSize(), + 'updatedAt' => $variant->getUpdatedAt()->format('Y-m-d H:i:s'), + 'url' => $variant->getAccessUrl() ? $variant->getAccessUrl()->getUrl() : null, + 'path' => $this->resourceNodeRepository->getResourceFileUrl($variant->getResourceNode(), [], null, $variant), + 'creator' => $variant->getResourceNode()->getCreator() ? $variant->getResourceNode()->getCreator()->getFullName() : 'Unknown', + ]; + } + + return $this->json($data); + } + + private function processFile(Request $request, ResourceNode $resourceNode, string $mode = 'show', string $filter = '', ?array $allUserInfo = null, ?ResourceFile $resourceFile = null): mixed { $this->denyAccessUnlessGranted( ResourceNodeVoter::VIEW, @@ -466,7 +559,7 @@ private function processFile(Request $request, ResourceNode $resourceNode, strin $this->trans('Unauthorised view access to resource') ); - $resourceFile = $resourceNode->getResourceFiles()->first(); + $resourceFile ??= $resourceNode->getResourceFiles()->first(); if (!$resourceFile) { throw $this->createNotFoundException($this->trans('File not found for resource')); @@ -523,7 +616,7 @@ private function processFile(Request $request, ResourceNode $resourceNode, strin // Modify the HTML content before displaying it. if (str_contains($mimeType, 'html')) { - $content = $resourceNodeRepo->getResourceNodeFileContent($resourceNode); + $content = $resourceNodeRepo->getResourceNodeFileContent($resourceNode, $resourceFile); if (null !== $allUserInfo) { $tagsToReplace = $allUserInfo[0]; @@ -569,8 +662,8 @@ private function processFile(Request $request, ResourceNode $resourceNode, strin } $response = new StreamedResponse( - function () use ($resourceNode, $start, $length): void { - $this->streamFileContent($resourceNode, $start, $length); + function () use ($resourceNodeRepo, $resourceFile, $start, $length): void { + $this->streamFileContent($resourceNodeRepo, $resourceFile, $start, $length); } ); @@ -611,9 +704,9 @@ private function getRange(Request $request, int $fileSize): array return [$start, $end, $length]; } - private function streamFileContent(ResourceNode $resourceNode, int $start, int $length): void + private function streamFileContent(ResourceNodeRepository $resourceNodeRepo, ResourceFile $resourceFile, int $start, int $length): void { - $stream = $this->resourceNodeRepository->getResourceNodeFileStream($resourceNode); + $stream = $resourceNodeRepo->getResourceNodeFileStream($resourceFile->getResourceNode(), $resourceFile); fseek($stream, $start); diff --git a/src/CoreBundle/Entity/ResourceFile.php b/src/CoreBundle/Entity/ResourceFile.php index 377bc442cd5..e3bf41212b4 100644 --- a/src/CoreBundle/Entity/ResourceFile.php +++ b/src/CoreBundle/Entity/ResourceFile.php @@ -14,6 +14,7 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Post; use ApiPlatform\Serializer\Filter\PropertyFilter; +use Chamilo\CoreBundle\Controller\AddVariantResourceFileAction; use Chamilo\CoreBundle\Controller\CreateResourceFileAction; use Chamilo\CoreBundle\Repository\ResourceFileRepository; use DateTime; @@ -40,6 +41,7 @@ new Post( controller: CreateResourceFileAction::class, openapiContext: [ + 'summary' => 'Create a new resource file', 'requestBody' => [ 'content' => [ 'multipart/form-data' => [ @@ -62,6 +64,37 @@ ], deserialize: false ), + new Post( + uriTemplate: '/resource_files/add_variant', + controller: AddVariantResourceFileAction::class, + openapiContext: [ + 'summary' => 'Add a variant to an existing resource file', + 'requestBody' => [ + 'content' => [ + 'multipart/form-data' => [ + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'file' => [ + 'type' => 'string', + 'format' => 'binary', + ], + 'resourceNodeId' => [ + 'type' => 'integer', + ], + 'accessUrlId' => [ + 'type' => 'integer', + ], + ], + ], + ], + ], + ], + ], + security: 'is_granted(\'ROLE_USER\')', + deserialize: false, + name: 'add_variant' + ), new GetCollection(), ], normalizationContext: [ @@ -150,6 +183,11 @@ class ResourceFile implements Stringable #[ORM\Column(type: 'datetime')] protected $updatedAt; + #[ORM\ManyToOne(targetEntity: AccessUrl::class)] + #[ORM\JoinColumn(name: 'access_url_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + protected ?AccessUrl $accessUrl = null; + + #[Groups(['resource_file:read', 'resource_node:read', 'document:read'])] #[ORM\ManyToOne(inversedBy: 'resourceFiles')] private ?ResourceNode $resourceNode = null; @@ -332,6 +370,17 @@ public function setFile(File|UploadedFile|null $file = null): self return $this; } + public function getAccessUrl(): ?AccessUrl + { + return $this->accessUrl; + } + + public function setAccessUrl(?AccessUrl $accessUrl): self + { + $this->accessUrl = $accessUrl; + return $this; + } + public function getResourceNode(): ?ResourceNode { return $this->resourceNode; diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20241214083500.php b/src/CoreBundle/Migrations/Schema/V200/Version20241214083500.php new file mode 100644 index 00000000000..a742b5ca6fd --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20241214083500.php @@ -0,0 +1,60 @@ +hasTable('resource_file')) { + $this->addSql( + 'ALTER TABLE resource_file ADD access_url_id INT DEFAULT NULL' + ); + $this->addSql( + 'ALTER TABLE resource_file ADD CONSTRAINT FK_RESOURCE_FILE_ACCESS_URL FOREIGN KEY (access_url_id) REFERENCES access_url (id) ON DELETE SET NULL' + ); + $this->addSql( + 'CREATE INDEX IDX_RESOURCE_FILE_ACCESS_URL ON resource_file (access_url_id)' + ); + } + + $result = $this->connection + ->executeQuery( + "SELECT COUNT(1) FROM settings WHERE variable = 'access_url_specific_files' AND category = 'course'" + ) + ; + $count = $result->fetchNumeric()[0]; + if (empty($count)) { + $this->addSql( + "INSERT INTO settings (variable, category, selected_value, title, comment, scope, subkeytext, access_url_changeable) VALUES ('access_url_specific_files','course','false','Access Url Specific Files','','',NULL, 1)" + ); + } + } + + public function down(Schema $schema): void + { + if ($schema->hasTable('resource_file')) { + $this->addSql( + 'ALTER TABLE resource_file DROP FOREIGN KEY FK_RESOURCE_FILE_ACCESS_URL' + ); + $this->addSql( + 'DROP INDEX IDX_RESOURCE_FILE_ACCESS_URL ON resource_file' + ); + $this->addSql( + 'ALTER TABLE resource_file DROP COLUMN access_url_id' + ); + } + } +} diff --git a/src/CoreBundle/Repository/ResourceNodeRepository.php b/src/CoreBundle/Repository/ResourceNodeRepository.php index 876b6bdcf5e..58c7387ab5e 100644 --- a/src/CoreBundle/Repository/ResourceNodeRepository.php +++ b/src/CoreBundle/Repository/ResourceNodeRepository.php @@ -11,6 +11,8 @@ use Chamilo\CoreBundle\Entity\ResourceNode; use Chamilo\CoreBundle\Entity\ResourceType; use Chamilo\CoreBundle\Entity\Session; +use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper; +use Chamilo\CoreBundle\Settings\SettingsManager; use Doctrine\ORM\EntityManagerInterface; use Gedmo\Tree\Entity\Repository\MaterializedPathRepository; use League\Flysystem\FilesystemOperator; @@ -25,17 +27,18 @@ */ class ResourceNodeRepository extends MaterializedPathRepository { - protected FlysystemStorage $storage; protected FilesystemOperator $filesystem; - protected RouterInterface $router; - public function __construct(EntityManagerInterface $manager, FlysystemStorage $storage, FilesystemOperator $resourceFilesystem, RouterInterface $router) - { + public function __construct( + private readonly EntityManagerInterface $manager, + private readonly FlysystemStorage $storage, + private readonly FilesystemOperator $resourceFilesystem, + private readonly RouterInterface $router, + private readonly AccessUrlHelper $accessUrlHelper, + private readonly SettingsManager $settingsManager + ) { + $this->filesystem = $resourceFilesystem; // Asignar el filesystem correcto parent::__construct($manager, $manager->getClassMetadata(ResourceNode::class)); - $this->storage = $storage; - // Flysystem mount name is saved in config/packages/oneup_flysystem.yaml - $this->filesystem = $resourceFilesystem; - $this->router = $router; } public function getFilename(ResourceFile $resourceFile): ?string @@ -61,10 +64,10 @@ public function getFileSystem(): FilesystemOperator return $this->filesystem; } - public function getResourceNodeFileContent(ResourceNode $resourceNode): string + public function getResourceNodeFileContent(ResourceNode $resourceNode, ?ResourceFile $resourceFile = null): string { try { - $resourceFile = $resourceNode->getResourceFiles()->first(); + $resourceFile ??= $resourceNode->getResourceFiles()->first(); if ($resourceFile) { $fileName = $this->getFilename($resourceFile); @@ -81,10 +84,10 @@ public function getResourceNodeFileContent(ResourceNode $resourceNode): string /** * @return false|resource */ - public function getResourceNodeFileStream(ResourceNode $resourceNode) + public function getResourceNodeFileStream(ResourceNode $resourceNode, ?ResourceFile $resourceFile = null) { try { - $resourceFile = $resourceNode->getResourceFiles()->first(); + $resourceFile ??= $resourceNode->getResourceFiles()->first(); if ($resourceFile) { $fileName = $this->getFilename($resourceFile); @@ -98,16 +101,22 @@ public function getResourceNodeFileStream(ResourceNode $resourceNode) } } - public function getResourceFileUrl(ResourceNode $resourceNode, array $extraParams = [], ?int $referenceType = null): string + public function getResourceFileUrl(?ResourceNode $resourceNode, array $extraParams = [], ?int $referenceType = null, ?ResourceFile $resourceFile = null): string { try { - if ($resourceNode->hasResourceFile()) { + $file = $resourceFile ?? $resourceNode?->getResourceFiles()->first(); + + if ($file) { $params = [ 'tool' => $resourceNode->getResourceType()->getTool(), 'type' => $resourceNode->getResourceType(), 'id' => $resourceNode->getUuid(), ]; + if ($resourceFile) { + $params['resourceFileId'] = $resourceFile->getId(); + } + if (!empty($extraParams)) { $params = array_merge($params, $extraParams); } diff --git a/src/CoreBundle/Settings/CourseSettingsSchema.php b/src/CoreBundle/Settings/CourseSettingsSchema.php index 305c52b0450..ab0d67ab5db 100644 --- a/src/CoreBundle/Settings/CourseSettingsSchema.php +++ b/src/CoreBundle/Settings/CourseSettingsSchema.php @@ -120,6 +120,7 @@ public function buildSettings(AbstractSettingsBuilder $builder): void 'course_creation_user_course_extra_field_relation_to_prefill' => '', 'allow_edit_tool_visibility_in_session' => 'true', 'show_course_duration' => 'false', + 'access_url_specific_files' => 'false', ] ) ->setTransformer( @@ -369,6 +370,7 @@ public function buildForm(FormBuilderInterface $builder): void ) ->add('allow_edit_tool_visibility_in_session', YesNoType::class) ->add('show_course_duration', YesNoType::class) + ->add('access_url_specific_files', YesNoType::class) ; $this->updateFormFieldsFromSettingsInfo($builder); From 2fc19326bbad49f41e2492a25edbf6fb9a148c8b Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Fri, 3 Jan 2025 18:47:00 -0500 Subject: [PATCH 2/2] Documents: Improve file variation - refs #5956 --- assets/vue/views/documents/AddVariation.vue | 40 +++++++++++++++++-- assets/vue/views/documents/DocumentsList.vue | 2 +- .../Controller/ResourceController.php | 14 +++++++ 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/assets/vue/views/documents/AddVariation.vue b/assets/vue/views/documents/AddVariation.vue index bd784fcaf2c..585c9f2944a 100644 --- a/assets/vue/views/documents/AddVariation.vue +++ b/assets/vue/views/documents/AddVariation.vue @@ -74,6 +74,24 @@ + + + + + + + @@ -90,10 +108,10 @@ import SectionHeader from "../../components/layout/SectionHeader.vue" import BaseButton from "../../components/basecomponents/BaseButton.vue" import BaseFileUpload from "../../components/basecomponents/BaseFileUpload.vue" import prettyBytes from 'pretty-bytes' -import { useStore } from "vuex" import { useCidReq } from "../../composables/cidReq" +import { useSecurityStore } from "../../store/securityStore" -const store = useStore() +const securityStore = useSecurityStore() const route = useRoute() const router = useRouter() const { t } = useI18n() @@ -101,11 +119,17 @@ const { cid, sid, gid } = useCidReq() const file = ref(null) const variations = ref([]) const originalFile = ref(null) -const resourceFileId = route.params.resourceFileId; +const resourceFileId = route.params.resourceFileId const selectedAccessUrl = ref(null) const accessUrls = ref([]) +const isAdmin = computed(() => securityStore.isAdmin) onMounted(async () => { + if (!isAdmin.value) { + await router.push({ name: 'DocumentsList' }) + return + } + await fetchOriginalFile() await fetchVariations() await fetchAccessUrls() @@ -178,6 +202,16 @@ async function uploadVariant(file, resourceNodeId, accessUrlId) { } } +async function deleteVariant(variantId) { + try { + await axios.delete(`/r/resource_files/${variantId}/delete_variant`) + console.log('Variant deleted successfully.') + await fetchVariations() + } catch (error) { + console.error('Error deleting variant:', error) + } +} + function onFileSelected(selectedFile) { file.value = selectedFile } diff --git a/assets/vue/views/documents/DocumentsList.vue b/assets/vue/views/documents/DocumentsList.vue index ebfe60ab1d2..cd1543f3270 100644 --- a/assets/vue/views/documents/DocumentsList.vue +++ b/assets/vue/views/documents/DocumentsList.vue @@ -206,7 +206,7 @@ /> json($data); } + #[Route('/resource_files/{id}/delete_variant', methods: ['DELETE'], name: 'chamilo_core_resource_files_delete_variant')] + public function deleteVariant(int $id, EntityManagerInterface $em): JsonResponse + { + $variant = $em->getRepository(ResourceFile::class)->find($id); + if (!$variant) { + return $this->json(['error' => 'Variant not found'], Response::HTTP_NOT_FOUND); + } + + $em->remove($variant); + $em->flush(); + + return $this->json(['success' => true]); + } + private function processFile(Request $request, ResourceNode $resourceNode, string $mode = 'show', string $filter = '', ?array $allUserInfo = null, ?ResourceFile $resourceFile = null): mixed { $this->denyAccessUnlessGranted(