Merge remote-tracking branch 'origin/master'

pull/5126/head
Angel Fernando Quiroz Campos 2 years ago
commit 747ed877c2
  1. 2
      assets/vue/components/basecomponents/ChamiloIcons.js
  2. 7
      assets/vue/composables/fileUtils.js
  3. 7
      assets/vue/views/documents/CreateFile.vue
  4. 144
      assets/vue/views/documents/DocumentsList.vue
  5. 4
      assets/vue/views/documents/UpdateFile.vue
  6. 21
      public/main/admin/settings.lib.php
  7. 154
      src/CoreBundle/Controller/TemplateController.php
  8. 1
      src/CoreBundle/Entity/Asset.php
  9. 26
      src/CoreBundle/Entity/Templates.php
  10. 32
      src/CoreBundle/Migrations/Schema/V200/Version20240129225700.php
  11. 76
      src/CoreBundle/Migrations/Schema/V200/Version20240130161800.php
  12. 11
      src/CoreBundle/Repository/TemplatesRepository.php
  13. 1
      src/CourseBundle/Entity/CDocument.php

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

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

@ -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(){

@ -191,6 +191,13 @@
type="black"
@click="selectAsDefaultCertificate(slotProps.data)"
/>
<BaseButton
v-if="securityStore.isAuthenticated && isCurrentTeacher && isHtmlFile(slotProps.data)"
:icon="getTemplateIcon(slotProps.data.iid)"
size="small"
type="black"
@click="openTemplateForm(slotProps.data.iid)"
/>
</div>
</template>
</Column>
@ -305,6 +312,41 @@
@document-not-saved="recordedAudioNotSaved"
/>
</BaseDialog>
<BaseDialogConfirmCancel
v-model:is-visible="showTemplateFormModal"
:cancel-label="t('Cancel')"
:confirm-label="t('Save')"
:title="t('Add as a template')"
@confirm-clicked="submitTemplateForm"
@cancel-clicked="showTemplateFormModal = false"
>
<form @submit.prevent="submitTemplateForm">
<div class="p-float-label">
<InputText
id="templateTitle"
v-model.trim="templateFormData.title"
class="form-control"
required
/>
<label
v-t="'Name'"
for="templateTitle"
/>
</div>
<small
v-if="submitted && !templateFormData.title"
v-t="'Title is required'"
class="p-error"
/>
<BaseFileUpload
id="post-file"
:label="t('File upload')"
accept="image"
size="small"
@file-selected="selectedFile = $event"
model-value=""/>
</form>
</BaseDialogConfirmCancel>
</template>
<script setup>
@ -331,6 +373,7 @@ import { useNotification } from "../../composables/notification"
import { useSecurityStore } from "../../store/securityStore"
import prettyBytes from "pretty-bytes"
import { ENTRYPOINT } from "../../config/entrypoint"
import BaseFileUpload from "../../components/basecomponents/BaseFileUpload.vue"
const store = useStore()
const route = useRoute()
@ -341,7 +384,7 @@ const { t } = useI18n()
const { filters, options, onUpdateOptions, deleteItem } = useDatatableList("Documents")
const notification = useNotification()
const { cid, sid, gid } = useCidReq()
const { isImage } = useFileUtils()
const { isImage, isHtml } = useFileUtils();
const { relativeDatetime } = useFormatDate()
@ -379,6 +422,10 @@ const isCertificateMode = computed(() => {
const defaultCertificateId = ref(null);
const isHtmlFile = (fileData) => {
return isHtml(fileData);
};
onMounted(() => {
filters.value.loadNode = 1
@ -643,10 +690,10 @@ async function selectAsDefaultCertificate(certificate) {
if (response.status === 200) {
loadDefaultCertificate()
onUpdateOptions(options.value)
notification.showSuccessNotification("Certificate set as default successfully");
notification.showSuccessNotification(t("Certificate set as default successfully"));
}
} catch (error) {
notification.showErrorNotification("Error setting certificate as default");
notification.showErrorNotification(t("Error setting certificate as default"));
}
}
@ -655,7 +702,96 @@ async function loadDefaultCertificate() {
const response = await axios.get(`/gradebook/default_certificate/${cid}`);
defaultCertificateId.value = response.data.certificateId;
} catch (error) {
console.error('Error to laod certificate', error);
if (error.response && error.response.status === 404) {
console.error('Default certificate not found.');
defaultCertificateId.value = null;
} else {
console.error('Error loading the certificate', error);
}
}
}
const showTemplateFormModal = ref(false);
const selectedFile = ref(null);
const templateFormData = ref({
title: '',
thumbnail: null,
});
const currentDocumentId = ref(null);
const isDocumentTemplate = async (documentId) => {
try {
const response = await axios.get(`/template/document-templates/${documentId}/is-template`);
return response.data.isTemplate;
} catch (error) {
console.error('Error verifying the template status:', error);
return false;
}
};
const deleteDocumentTemplate = async (documentId) => {
try {
await axios.post(`/template/document-templates/${documentId}/delete`);
onUpdateOptions(options.value);
notification.showSuccessNotification(t('Template successfully deteled.'));
} catch (error) {
console.error('Error deleting the template:', error);
notification.showErrorNotification(t('Error deleting the template.'));
}
};
const getTemplateIcon = (documentId) => {
const document = items.value.find(doc => doc.iid === documentId);
return document && document.template ? 'template-selected' : 'template-not-selected';
};
const openTemplateForm = async (documentId) => {
const isTemplate = await isDocumentTemplate(documentId);
if (isTemplate) {
await deleteDocumentTemplate(documentId);
onUpdateOptions(listaoptions.value);
} else {
currentDocumentId.value = documentId;
showTemplateFormModal.value = true;
}
};
const submitTemplateForm = async () => {
submitted.value = true;
if (!templateFormData.value.title || !selectedFile.value) {
notification.showErrorNotification(t('The title and thumbnail are required.'));
return;
}
try {
const formData = new FormData();
formData.append('title', templateFormData.value.title);
formData.append('thumbnail', selectedFile.value);
formData.append('refDoc', currentDocumentId.value);
formData.append('cid', cid);
const response = await axios.post('/template/document-templates/create', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
if (response.status === 200 || response.status === 201) {
notification.showSuccessNotification(t('Template created successfully.'));
templateFormData.value.title = '';
selectedFile.value = null;
showTemplateFormModal.value = false;
onUpdateOptions(options.value);
} else {
notification.showErrorNotification(t('Error creating the template.'));
}
} catch (error) {
console.error('Error submitting the form:', error);
notification.showErrorNotification(t('Error submitting the form.'));
}
};
</script>

@ -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);

@ -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 <patrick.cool@UGent.be>, 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 '<img src="'.$imageUrl.'" alt="'.get_lang('Template preview').'"/>';
} else {
return '<img src="'.api_get_path(WEB_PUBLIC_PATH).'img/template_thumb/noimage.gif" alt="'.get_lang('NoTemplate preview').'"/>';

@ -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);
}
}

@ -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]

@ -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;
}
/**

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
final class Version20240129225700 extends AbstractMigrationChamilo
{
public function getDescription() : string
{
return 'Add image_id field to templates table to link with asset table';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Entity\Asset;
use Chamilo\CoreBundle\Entity\Templates;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Chamilo\Kernel;
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class Version20240130161800 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Migrate template images to Asset entities.';
}
public function up(Schema $schema): void
{
$container = $this->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();
}
}
}
}
}
}

@ -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();
}
}

@ -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;

Loading…
Cancel
Save