diff --git a/assets/vue/components/basecomponents/ChamiloIcons.js b/assets/vue/components/basecomponents/ChamiloIcons.js index abcaed0c0e..c367a45e46 100644 --- a/assets/vue/components/basecomponents/ChamiloIcons.js +++ b/assets/vue/components/basecomponents/ChamiloIcons.js @@ -111,4 +111,6 @@ export const chamiloIconToClass = { "ticket": "mdi mdi-ticket-account", "certificate-selected": "mdi mdi-star", "certificate-not-selected": "mdi mdi-star-outline", + "template-selected": "mdi mdi-file-check", + "template-not-selected": "mdi mdi-file-outline", }; diff --git a/assets/vue/composables/fileUtils.js b/assets/vue/composables/fileUtils.js index 4128e7226d..3d2a8f583c 100644 --- a/assets/vue/composables/fileUtils.js +++ b/assets/vue/composables/fileUtils.js @@ -16,6 +16,12 @@ export function useFileUtils() { return isFile(fileData) && isAudio } + const isHtml = (fileData) => { + const mimeType = fileData.resourceNode.resourceFile.mimeType + const isHtml = mimeType.split("/")[1].toLowerCase() === "html" + return isFile(fileData) && isHtml + } + const isFile = (fileData) => { return fileData.resourceNode && fileData.resourceNode.resourceFile } @@ -25,5 +31,6 @@ export function useFileUtils() { isImage, isVideo, isAudio, + isHtml, } } diff --git a/assets/vue/views/documents/CreateFile.vue b/assets/vue/views/documents/CreateFile.vue index 08394920a1..32bcd185d5 100644 --- a/assets/vue/views/documents/CreateFile.vue +++ b/assets/vue/views/documents/CreateFile.vue @@ -97,13 +97,14 @@ export default { this.item.contentFile = templateContent; }, fetchTemplates() { - axios.get('/system-templates') + const courseId = this.$route.query.cid; + axios.get(`/template/all-templates/${courseId}`) .then(response => { - console.log(response.data); this.templates = response.data; + console.log('Templates fetched successfully:', this.templates); }) .catch(error => { - console.error('There was an error fetching the templates:', error); + console.error('Error fetching templates:', error); }); }, getCertificateTags(){ diff --git a/assets/vue/views/documents/DocumentsList.vue b/assets/vue/views/documents/DocumentsList.vue index 28bfba9a1a..4739660823 100644 --- a/assets/vue/views/documents/DocumentsList.vue +++ b/assets/vue/views/documents/DocumentsList.vue @@ -191,6 +191,13 @@ type="black" @click="selectAsDefaultCertificate(slotProps.data)" /> + @@ -305,6 +312,41 @@ @document-not-saved="recordedAudioNotSaved" /> + +
+
+ +
+ + + +
diff --git a/assets/vue/views/documents/UpdateFile.vue b/assets/vue/views/documents/UpdateFile.vue index 9d1ce4c2f0..8feb72d891 100644 --- a/assets/vue/views/documents/UpdateFile.vue +++ b/assets/vue/views/documents/UpdateFile.vue @@ -87,9 +87,11 @@ export default { this.$router.back(); }, fetchTemplates() { - axios.get('/system-templates') + const cid = this.$route.query.cid; + axios.get(`/template/all-templates/${cid}`) .then(response => { this.templates = response.data; + console.log('Templates fetched successfully:', this.templates); }) .catch(error => { console.error('Error fetching the templates:', error); diff --git a/public/main/admin/settings.lib.php b/public/main/admin/settings.lib.php index bd0331503d..abdde4942a 100644 --- a/public/main/admin/settings.lib.php +++ b/public/main/admin/settings.lib.php @@ -1067,30 +1067,17 @@ function actionsFilter($id) return $return; } -/** - * Display the image of the template in the sortable table. - * - * @param string $image the image - * - * @return string code for the image - * - * @author Patrick Cool , Ghent University, Belgium - * - * @version August 2008 - * - * @since v1.8.6 - */ -function searchImageFilter($id) +function searchImageFilter(int $id): string { $em = Database::getManager(); /** @var SystemTemplate $template */ $template = $em->find(SystemTemplate::class, $id); - $assetRepo = Container::getAssetRepository(); - $imageUrl = $assetRepo->getAssetUrl($template->getImage()); + if (null !== $template->getImage()) { + $assetRepo = Container::getAssetRepository(); + $imageUrl = $assetRepo->getAssetUrl($template->getImage()); - if (!empty($imageUrl)) { return ''.get_lang('Template preview').''; } else { return ''.get_lang('NoTemplate preview').''; diff --git a/src/CoreBundle/Controller/TemplateController.php b/src/CoreBundle/Controller/TemplateController.php index 8a69322a7d..05fcec36ab 100644 --- a/src/CoreBundle/Controller/TemplateController.php +++ b/src/CoreBundle/Controller/TemplateController.php @@ -5,29 +5,169 @@ declare(strict_types=1); namespace Chamilo\CoreBundle\Controller; +use Chamilo\CoreBundle\Entity\Asset; +use Chamilo\CoreBundle\Entity\Course; +use Chamilo\CoreBundle\Entity\Templates; +use Chamilo\CoreBundle\Entity\User; use Chamilo\CoreBundle\Repository\AssetRepository; +use Chamilo\CoreBundle\Repository\Node\CourseRepository; use Chamilo\CoreBundle\Repository\SystemTemplateRepository; +use Chamilo\CoreBundle\Repository\TemplatesRepository; +use Chamilo\CourseBundle\Entity\CDocument; +use Chamilo\CourseBundle\Repository\CDocumentRepository; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Annotation\Route; +#[Route('/template')] class TemplateController extends AbstractController { - #[Route('/system-templates', name: 'system-templates')] - public function getTemplates(SystemTemplateRepository $templateRepository, AssetRepository $assetRepository): JsonResponse + #[Route('/document-templates/create', methods: ['POST'])] + public function createDocumentTemplate(Request $request, EntityManagerInterface $entityManager, AssetRepository $assetRepo): Response { - $templates = $templateRepository->findAll(); + $documentId = (int) $request->request->get('refDoc'); + $title = $request->request->get('title'); + $cid = $request->request->get('cid'); + $imageFile = $request->files->get('thumbnail'); + + if (!$imageFile) { + return $this->json(['error' => 'No image provided.'], Response::HTTP_BAD_REQUEST); + } + + /** @var User $user */ + $user = $this->getUser(); + $course = null; + if ($cid) { + $course = $entityManager->getRepository(Course::class)->find($cid); + } + + $asset = new Asset(); + $asset->setCategory(Asset::TEMPLATE); + $asset->setFile($imageFile); + $asset->setTitle($imageFile->getClientOriginalName()); + $entityManager->persist($asset); + $entityManager->flush(); + + $template = new Templates(); + $template->setTitle($title); + $template->setDescription(''); + $template->setRefDoc($documentId); + $template->setCourse($course); + $template->setUser($user); + $template->setImage($asset); + $entityManager->persist($template); + + $document = $entityManager->getRepository(CDocument::class)->find($documentId); + if ($document) { + $document->setTemplate(true); + $entityManager->persist($document); + } else { + return $this->json(['error' => 'Document not found.'], Response::HTTP_NOT_FOUND); + } + + $entityManager->flush(); + + return $this->json(['message' => 'Template created successfully.']); + } + + #[Route('/document-templates/{documentId}/is-template', methods: ['GET'])] + public function isDocumentTemplate(int $documentId, EntityManagerInterface $entityManager): Response + { + $template = $entityManager->getRepository(Templates::class)->findOneBy(['refDoc' => $documentId]); + + return $this->json([ + 'isTemplate' => null !== $template, + ]); + } + + #[Route('/document-templates/{documentId}/delete', methods: ['POST'])] + public function deleteDocumentTemplate(int $documentId, EntityManagerInterface $entityManager): Response + { + $template = $entityManager->getRepository(Templates::class)->findOneBy(['refDoc' => $documentId]); + + if (!$template) { + return $this->json(['error' => 'Template not found.'], Response::HTTP_NOT_FOUND); + } + + $entityManager->remove($template); + + $document = $entityManager->getRepository(CDocument::class)->find($documentId); + if ($document) { + $document->setTemplate(false); + $entityManager->persist($document); + } else { + return $this->json(['error' => 'Document not found.'], Response::HTTP_NOT_FOUND); + } + + $entityManager->flush(); + + return $this->json(['message' => 'Template deleted successfully']); + } + + #[Route('/all-templates/{courseId}', name: 'all-templates')] + public function getAllTemplates($courseId, SystemTemplateRepository $systemTemplateRepository, TemplatesRepository $templatesRepository, CourseRepository $courseRepository, AssetRepository $assetRepository, CDocumentRepository $documentRepository): JsonResponse + { + $course = $courseRepository->find($courseId); + if (!$course) { + throw new NotFoundHttpException("Course not found"); + } + + $systemTemplates = $systemTemplateRepository->findAll(); + $platformTemplates = $this->formatSystemTemplates($systemTemplates, $assetRepository); + + $courseDocumentTemplates = $this->formatCourseDocumentTemplates($course, $templatesRepository, $assetRepository, $documentRepository); + + $allTemplates = array_merge($platformTemplates, $courseDocumentTemplates); + + return $this->json($allTemplates); + } + + + private function formatSystemTemplates(array $systemTemplates, AssetRepository $assetRepository): array + { + return array_map(function ($template) use ($assetRepository) { + $imageUrl = null; + if ($template->hasImage()) { + $imageUrl = $assetRepository->getAssetUrl($template->getImage()); + } - $data = array_map(function ($template) use ($assetRepository) { return [ 'id' => $template->getId(), 'title' => $template->getTitle(), 'comment' => $template->getComment(), 'content' => $template->getContent(), - 'image' => $template->getImage() ? $assetRepository->getAssetUrl($template->getImage()) : null, + 'image' => $imageUrl, ]; - }, $templates); + }, $systemTemplates); + } + + private function formatCourseDocumentTemplates(Course $course, TemplatesRepository $templatesRepository, AssetRepository $assetRepository, CDocumentRepository $documentRepository): array + { + $courseTemplates = $templatesRepository->findCourseDocumentTemplates($course); + + return array_map(function ($template) use ($assetRepository, $documentRepository) { + $imageUrl = null; + if ($template->hasImage()) { + $imageUrl = $assetRepository->getAssetUrl($template->getImage()); + } + + $document = $documentRepository->find($template->getRefDoc()); + $content = ''; + if (null !== $document && null !== $document->getResourceNode() && null !== $document->getResourceNode()->getResourceFile()) { + $content = $documentRepository->getResourceFileContent($document); + } - return $this->json($data); + return [ + 'id' => $template->getId(), + 'title' => $template->getTitle(), + 'comment' => $template->getDescription(), + 'content' => $content, + 'image' => $imageUrl, + ]; + }, $courseTemplates); } } diff --git a/src/CoreBundle/Entity/Asset.php b/src/CoreBundle/Entity/Asset.php index 9f2770e5b4..c8d4d5fb33 100644 --- a/src/CoreBundle/Entity/Asset.php +++ b/src/CoreBundle/Entity/Asset.php @@ -37,6 +37,7 @@ class Asset implements Stringable public const EXERCISE_ATTEMPT = 'exercise_attempt'; public const EXERCISE_FEEDBACK = 'exercise_feedback'; public const SYSTEM_TEMPLATE = 'system_template'; + public const TEMPLATE = 'template'; public const SESSION = 'session'; #[ORM\Id] diff --git a/src/CoreBundle/Entity/Templates.php b/src/CoreBundle/Entity/Templates.php index c9b044c3dd..b14171ba71 100644 --- a/src/CoreBundle/Entity/Templates.php +++ b/src/CoreBundle/Entity/Templates.php @@ -41,8 +41,9 @@ class Templates #[ORM\Column(name: 'ref_doc', type: 'integer', nullable: false)] protected int $refDoc; - #[ORM\Column(name: 'image', type: 'string', length: 250, nullable: false)] - protected string $image; + #[ORM\ManyToOne(targetEntity: Asset::class, cascade: ['persist'])] + #[ORM\JoinColumn(name: 'image_id', referencedColumnName: 'id', onDelete: 'SET NULL')] + protected ?Asset $image = null; /** * Set title. @@ -110,26 +111,21 @@ class Templates return $this->refDoc; } - /** - * Set image. - * - * @return Templates - */ - public function setImage(string $image) + public function getImage(): ?Asset + { + return $this->image; + } + + public function setImage(?Asset $image): self { $this->image = $image; return $this; } - /** - * Get image. - * - * @return string - */ - public function getImage() + public function hasImage(): bool { - return $this->image; + return null !== $this->image; } /** diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240129225700.php b/src/CoreBundle/Migrations/Schema/V200/Version20240129225700.php new file mode 100644 index 0000000000..59619908d8 --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240129225700.php @@ -0,0 +1,32 @@ +addSql('ALTER TABLE templates ADD image_id BINARY(16) DEFAULT NULL COMMENT \'(DC2Type:uuid)\''); + $this->addSql('ALTER TABLE templates ADD CONSTRAINT FK_6F287D8E3DA5256D FOREIGN KEY (image_id) REFERENCES asset (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX IDX_6F287D8E3DA5256D ON templates (image_id)'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE templates DROP FOREIGN KEY FK_6F287D8E3DA5256D'); + $this->addSql('DROP INDEX IDX_6F287D8E3DA5256D ON templates'); + $this->addSql('ALTER TABLE templates DROP image_id'); + } +} diff --git a/src/CoreBundle/Migrations/Schema/V200/Version20240130161800.php b/src/CoreBundle/Migrations/Schema/V200/Version20240130161800.php new file mode 100644 index 0000000000..577458c4ae --- /dev/null +++ b/src/CoreBundle/Migrations/Schema/V200/Version20240130161800.php @@ -0,0 +1,76 @@ +getContainer(); + + /** @var Kernel $kernel */ + $kernel = $container->get('kernel'); + $rootPath = $kernel->getProjectDir(); + $doctrine = $container->get('doctrine'); + + $em = $doctrine->getManager(); + $connection = $em->getConnection(); + + $sql = "SELECT id, image, c_id FROM templates WHERE image IS NOT NULL"; + $stmt = $connection->prepare($sql); + $result = $stmt->executeQuery(); + + while ($row = $result->fetchAssociative()) { + $imagePath = $row['image']; + $templateId = $row['id']; + $courseId = $row['c_id']; + + $courseDirectorySql = "SELECT directory FROM course WHERE id = :courseId"; + $courseStmt = $connection->prepare($courseDirectorySql); + $courseResult = $courseStmt->executeQuery(['courseId' => $courseId]); + + $courseRow = $courseResult->fetchAssociative(); + + if ($courseRow) { + $directory = $courseRow['directory']; + $thumbPath = $rootPath.'/app/courses/'.$directory.'/upload/template_thumbnails/'.$imagePath; + + if (file_exists($thumbPath)) { + $mimeType = mime_content_type($thumbPath); + $fileName = basename($thumbPath); + $file = new UploadedFile($thumbPath, $fileName, $mimeType, null, true); + + $asset = new Asset(); + $asset->setCategory(Asset::TEMPLATE); + $asset->setTitle($fileName); + $asset->setFile($file); + + $em->persist($asset); + $em->flush(); + + $template = $em->getRepository(Templates::class)->find($templateId); + if ($template) { + $template->setImage($asset); + $em->persist($template); + $em->flush(); + } + } + } + } + } +} diff --git a/src/CoreBundle/Repository/TemplatesRepository.php b/src/CoreBundle/Repository/TemplatesRepository.php index fbd78015da..5560a852e8 100644 --- a/src/CoreBundle/Repository/TemplatesRepository.php +++ b/src/CoreBundle/Repository/TemplatesRepository.php @@ -55,4 +55,15 @@ class TemplatesRepository extends ServiceEntityRepository return $qb->getQuery()->getResult(); } + + public function findCourseDocumentTemplates(Course $course) + { + return $this->createQueryBuilder('t') + ->where('t.course = :course') + ->andWhere('t.refDoc IS NOT NULL') + ->andWhere('t.refDoc > 0') + ->setParameter('course', $course) + ->getQuery() + ->getResult(); + } } diff --git a/src/CourseBundle/Entity/CDocument.php b/src/CourseBundle/Entity/CDocument.php index e217f04283..b1061ec43f 100644 --- a/src/CourseBundle/Entity/CDocument.php +++ b/src/CourseBundle/Entity/CDocument.php @@ -181,6 +181,7 @@ class CDocument extends AbstractResource implements ResourceInterface, ResourceS #[ORM\Column(name: 'readonly', type: 'boolean', nullable: false)] protected bool $readonly; + #[Groups(['document:read', 'document:write'])] #[ORM\Column(name: 'template', type: 'boolean', nullable: false)] protected bool $template;