pull/5976/merge
christianbeeznest 7 months ago committed by GitHub
commit f319c706d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      assets/vue/components/basecomponents/ChamiloIcons.js
  2. 5
      assets/vue/router/documents.js
  3. 223
      assets/vue/views/documents/AddVariation.vue
  4. 24
      assets/vue/views/documents/DocumentsList.vue
  5. 71
      src/CoreBundle/Controller/AddVariantResourceFileAction.php
  6. 2
      src/CoreBundle/Controller/PlatformConfigurationController.php
  7. 159
      src/CoreBundle/Controller/ResourceController.php
  8. 49
      src/CoreBundle/Entity/ResourceFile.php
  9. 60
      src/CoreBundle/Migrations/Schema/V200/Version20241214083500.php
  10. 37
      src/CoreBundle/Repository/ResourceNodeRepository.php
  11. 2
      src/CoreBundle/Settings/CourseSettingsSchema.php

@ -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",

@ -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",

@ -0,0 +1,223 @@
<template>
<div class="p-4 space-y-8">
<SectionHeader :title="t('Add File Variation')">
<BaseButton
:label="t('Back to Documents')"
icon="back"
type="gray"
@click="goBack"
/>
</SectionHeader>
<div v-if="originalFile" class="bg-gray-100 p-4 rounded-md shadow-md">
<h3 class="text-lg font-semibold">{{ t('Original File') }}</h3>
<p><strong>{{ t('Title:') }}</strong> {{ originalFile.originalName }}</p>
<p><strong>{{ t('Format:') }}</strong> {{ originalFile.mimeType }}</p>
<p><strong>{{ t('Size:') }}</strong> {{ prettyBytes(originalFile.size) }}</p>
</div>
<div class="space-y-6">
<h3 class="text-xl font-bold">{{ t('Upload New Variation') }}</h3>
<form @submit.prevent="uploadVariation" class="flex flex-col space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<BaseFileUpload
@file-selected="onFileSelected"
:label="t('Choose file')"
accept=".pdf,.html,.docx,.mp4"
required
class="w-full"
/>
<Dropdown
v-model="selectedAccessUrl"
:options="accessUrls"
optionLabel="url"
optionValue="id"
placeholder="Select a URL"
class="w-full"
/>
</div>
<div class="flex justify-end">
<BaseButton
:label="t('Upload')"
icon="file-upload"
type="success"
:disabled="!file"
@click="uploadVariant(file, originalFile?.resourceNode?.id, selectedAccessUrl)"
/>
</div>
</form>
</div>
<div>
<h3 class="text-xl font-bold mb-4">{{ t('Current Variations') }}</h3>
<DataTable :value="variations" class="w-full">
<Column field="title" :header="t('Title')" />
<Column field="mimeType" :header="t('Format')" />
<Column field="size" :header="t('Size')">
<template #body="slotProps">
{{ prettyBytes(slotProps.data.size) }}
</template>
</Column>
<Column field="updatedAt" :header="t('Updated At')" />
<Column field="url" :header="t('URL')">
<template #body="slotProps">
<a
:href="slotProps.data.path"
target="_blank"
class="text-blue-500 hover:underline"
>
{{ t('View') }}
</a>
</template>
</Column>
<Column field="creator" :header="t('Creator')" />
<Column field="accessUrl" :header="t('Associated URL')">
<template #body="slotProps">
<span>
{{ slotProps.data.url ? slotProps.data.url : t('Default (No URL)') }}
</span>
</template>
</Column>
<Column>
<template #header>{{ t('Actions') }}</template>
<template #body="slotProps">
<BaseButton
:label="t('Delete')"
icon="delete"
type="danger"
@click="deleteVariant(slotProps.data.id)"
/>
</template>
</Column>
</DataTable>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from "vue"
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import axios from 'axios'
import DataTable from 'primevue/datatable'
import Column from 'primevue/column'
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 { useCidReq } from "../../composables/cidReq"
import { useSecurityStore } from "../../store/securityStore"
const securityStore = useSecurityStore()
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const { cid, sid, gid } = useCidReq()
const file = ref(null)
const variations = ref([])
const originalFile = ref(null)
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()
})
async function fetchVariations() {
if (!originalFile.value?.resourceNode?.id) {
console.error('ResourceNodeId is undefined. Cannot fetch variations.')
return
}
try {
const resourceNodeId = originalFile.value.resourceNode.id
const response = await axios.get(`/r/resource_files/${resourceNodeId}/variants`)
variations.value = response.data
} catch (error) {
console.error('Error fetching variations:', error)
}
}
async function fetchAccessUrls() {
try {
const response = await axios.get('/api/access_urls')
if (Array.isArray(response.data['hydra:member'])) {
const currentAccessUrlId = window.access_url_id
accessUrls.value = response.data['hydra:member'].filter(
(url) => url.id !== currentAccessUrlId
)
} else {
accessUrls.value = []
}
} catch (error) {
console.error('Error fetching access URLs:', error)
accessUrls.value = []
}
}
async function fetchOriginalFile() {
try {
const response = await axios.get(`/api/resource_files/${resourceFileId}`)
originalFile.value = response.data
} catch (error) {
console.error('Error fetching original file:', error)
}
}
async function uploadVariant(file, resourceNodeId, accessUrlId) {
if (!resourceNodeId) {
console.error('ResourceNodeId is undefined. Check originalFile:', originalFile.value)
return
}
const formData = new FormData()
formData.append('file', file)
formData.append('resourceNodeId', resourceNodeId)
if (accessUrlId) {
formData.append('accessUrlId', accessUrlId)
}
try {
const response = await axios.post('/api/resource_files/add_variant', formData)
console.log('Variant uploaded or updated successfully:', response.data)
await fetchVariations()
file.value = null
selectedAccessUrl.value = null
} catch (error) {
console.error('Error uploading variant:', error)
}
}
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
}
function goBack() {
let queryParams = { cid, sid, gid }
router.push({ name: "DocumentsList", params: { node: parent.id }, query: queryParams })
}
</script>

@ -205,6 +205,15 @@
@click="btnChangeVisibilityOnClick(slotProps.data)"
/>
<BaseButton
v-if="canEdit(slotProps.data) && allowAccessUrlFiles && isFile(slotProps.data) && securityStore.isAdmin"
icon="file-replace"
size="small"
type="secondary"
:title="t('Add File Variation')"
@click="goToAddVariation(slotProps.data)"
/>
<BaseButton
v-if="canEdit(slotProps.data)"
:title="t('Edit')"
@ -439,17 +448,21 @@ import BaseFileUpload from "../../components/basecomponents/BaseFileUpload.vue"
import { useDocumentActionButtons } from "../../composables/document/documentActionButtons"
import SectionHeader from "../../components/layout/SectionHeader.vue"
import { checkIsAllowedToEdit } from "../../composables/userPermissions"
import { usePlatformConfig } from "../../store/platformConfig"
const store = useStore()
const route = useRoute()
const router = useRouter()
const securityStore = useSecurityStore()
const platformConfigStore = usePlatformConfig()
const allowAccessUrlFiles = computed(() => "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

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\ResourceFile;
use Chamilo\CoreBundle\Entity\ResourceNode;
use Chamilo\CoreBundle\Entity\AccessUrl;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class AddVariantResourceFileAction
{
public function __invoke(Request $request, EntityManagerInterface $em): ResourceFile
{
$uploadedFile = $request->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;
}
}

@ -91,6 +91,8 @@ class PlatformConfigurationController extends AbstractController
'document.students_download_folders',
'social.hide_social_groups_block',
'course.show_course_duration',
'exercise.allow_exercise_auto_launch',
'course.access_url_specific_files',
];
$user = $this->userHelper->getCurrent();

@ -7,15 +7,19 @@ declare(strict_types=1);
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 @@ class ResourceController extends AbstractResourceController implements CourseCon
* 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 @@ class ResourceController extends AbstractResourceController implements CourseCon
);
}
return $this->processFile($request, $resourceNode, 'show', $filter, $allUserInfo);
return $this->processFile($request, $resourceNode, 'show', $filter, $allUserInfo, $resourceFile);
}
/**
@ -212,8 +251,12 @@ class ResourceController extends AbstractResourceController implements CourseCon
* 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 @@ class ResourceController extends AbstractResourceController implements CourseCon
$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,13 +342,15 @@ class ResourceController extends AbstractResourceController implements CourseCon
/** @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);
$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,52 @@ class ResourceController extends AbstractResourceController implements CourseCon
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);
}
#[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(
ResourceNodeVoter::VIEW,
@ -466,7 +573,7 @@ class ResourceController extends AbstractResourceController implements CourseCon
$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 +630,7 @@ class ResourceController extends AbstractResourceController implements CourseCon
// 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 +676,8 @@ class ResourceController extends AbstractResourceController implements CourseCon
}
$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 +718,9 @@ class ResourceController extends AbstractResourceController implements CourseCon
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);

@ -14,6 +14,7 @@ use ApiPlatform\Metadata\Get;
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 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich;
new Post(
controller: CreateResourceFileAction::class,
openapiContext: [
'summary' => 'Create a new resource file',
'requestBody' => [
'content' => [
'multipart/form-data' => [
@ -62,6 +64,37 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich;
],
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 @@ class ResourceFile implements Stringable
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;

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
final class Version20241214083500 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Add access_url_id field to resource_file table';
}
public function up(Schema $schema): void
{
if ($schema->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'
);
}
}
}

@ -11,6 +11,8 @@ use Chamilo\CoreBundle\Entity\ResourceFile;
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 @@ use Vich\UploaderBundle\Storage\FlysystemStorage;
*/
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 @@ class ResourceNodeRepository extends MaterializedPathRepository
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 @@ class ResourceNodeRepository extends MaterializedPathRepository
/**
* @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 @@ class ResourceNodeRepository extends MaterializedPathRepository
}
}
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);
}

@ -125,6 +125,7 @@ class CourseSettingsSchema extends AbstractSettingsSchema
'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(
@ -374,6 +375,7 @@ class CourseSettingsSchema extends AbstractSettingsSchema
)
->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);

Loading…
Cancel
Save