Merge pull request #4783 from christianbeeznest/GH-4768

Glossary: Change from legacy to Vue interface and backend adaptation - refs #4768
pull/4788/head
Nicolas Ducoulombier 1 year ago committed by GitHub
commit b5437404e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 95
      assets/vue/components/glossary/GlossaryExportForm.vue
  2. 154
      assets/vue/components/glossary/GlossaryForm.vue
  3. 127
      assets/vue/components/glossary/GlossaryImportForm.vue
  4. 9
      assets/vue/components/glossary/GlossaryLayout.vue
  5. 34
      assets/vue/router/glossary.js
  6. 2
      assets/vue/router/index.js
  7. 35
      assets/vue/views/glossary/CreateTerm.vue
  8. 41
      assets/vue/views/glossary/ExportGlossary.vue
  9. 258
      assets/vue/views/glossary/GlossaryList.vue
  10. 35
      assets/vue/views/glossary/ImportGlossary.vue
  11. 40
      assets/vue/views/glossary/UpdateTerm.vue
  12. 64
      src/CoreBundle/Controller/Api/CreateCGlossaryAction.php
  13. 131
      src/CoreBundle/Controller/Api/ExportCGlossaryAction.php
  14. 97
      src/CoreBundle/Controller/Api/ExportGlossaryToDocumentsAction.php
  15. 58
      src/CoreBundle/Controller/Api/GetGlossaryCollectionController.php
  16. 135
      src/CoreBundle/Controller/Api/ImportCGlossaryAction.php
  17. 62
      src/CoreBundle/Controller/Api/UpdateCGlossaryAction.php
  18. 2
      src/CoreBundle/Tool/Glossary.php
  19. 172
      src/CourseBundle/Entity/CGlossary.php

@ -0,0 +1,95 @@
<template>
<form @submit.prevent="submitForm" class="export-form">
<div class="form-field">
<label for="export-format">Export Format:</label>
<select id="export-format" v-model="selectedFormat">
<option value="csv">CSV</option>
<option value="xls">Excel</option>
<option value="pdf">PDF</option>
</select>
</div>
<button type="submit" class="btn btn--primary">Export</button>
</form>
</template>
<script>
import axios from "axios";
import { ENTRYPOINT } from "../../config/entrypoint";
import { RESOURCE_LINK_PUBLISHED } from "../resource_links/visibility";
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from "vue-i18n";
import { ref, onMounted } from "vue";
export default {
setup() {
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const selectedFormat = ref('csv');
const parentResourceNodeId = ref(Number(route.params.node));
const resourceLinkList = ref(
JSON.stringify([
{
sid: route.query.sid,
cid: route.query.cid,
visibility: RESOURCE_LINK_PUBLISHED, // visible by default
},
])
);
const submitForm = () => {
const format = selectedFormat.value;
const formData = new FormData();
formData.append('format', format);
formData.append("sid", route.query.sid);
formData.append("cid", route.query.cid);
const endpoint = `${ENTRYPOINT}glossaries/export`;
axios.post(endpoint, formData, { responseType: 'blob' })
.then(response => {
const fileUrl = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = fileUrl;
link.setAttribute('download', `glossary.${format}`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch(error => {
console.error('Error exporting glossary:', error);
});
};
return {
selectedFormat,
submitForm,
};
},
};
</script>
<style scoped>
.export-form {
max-width: 400px;
margin: 0 auto;
}
.form-field {
margin-bottom: 10px;
}
label {
font-weight: bold;
}
.btn--primary {
background-color: #007bff;
color: #ffffff;
padding: 10px 20px;
border: none;
cursor: pointer;
}
</style>

@ -0,0 +1,154 @@
<template>
<div>
<form @submit.prevent="submitGlossaryForm" name="glossary" id="glossary">
<div class="field">
<div class="p-float-label">
<input v-model="formData.name" id="glossary_title" name="name" type="text" class="p-inputtext p-component p-filled" />
<label for="glossary_title">
<span class="form_required">*</span>
Term
</label>
</div>
</div>
<div class="field">
<div class="p-float-label">
<textarea v-model="formData.description" id="description" name="description"></textarea>
<label for="description">
<span class="form_required">*</span>
Term definition
</label>
</div>
</div>
<div class="field 2">
<div class="8">
<label for="glossary_SubmitGlossary" class="h-4 ">
</label>
<button class="btn btn--primary" name="SubmitGlossary" type="submit" id="glossary_SubmitGlossary">
<em class="mdi mdi-plus"></em> Save term
</button>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<span class="form_required">*</span>
<small>Required field</small>
</div>
</div>
<input name="_qf__glossary" type="hidden" value="" id="glossary__qf__glossary" />
<input name="sec_token" type="hidden" value="1e7d47c276bfdfe308a79e1b71d58089" id="glossary_sec_token" />
</form>
</div>
</template>
<script>
import axios from "axios";
import { ENTRYPOINT } from "../../config/entrypoint";
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from "vue-i18n";
import { ref, onMounted } from "vue";
import {RESOURCE_LINK_PUBLISHED} from "../resource_links/visibility";
export default {
props: {
termId: {
type: Number,
default: null
}
},
setup(props) {
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const parentResourceNodeId = ref(Number(route.params.node));
const resourceLinkList = ref(
JSON.stringify([
{
sid: route.query.sid,
cid: route.query.cid,
visibility: RESOURCE_LINK_PUBLISHED, // visible by default
},
])
);
const formData = ref({
name: '',
description: '',
});
const fetchTerm = () => {
if (props.termId) {
axios.get(ENTRYPOINT + 'glossaries/' + props.termId)
.then(response => {
const glossary = response.data;
formData.value.name = glossary.name;
formData.value.description = glossary.description;
})
.catch(error => {
console.error('Error fetching link:', error);
});
}
};
onMounted(() => {
fetchTerm();
});
const submitGlossaryForm = () => {
const postData = {
name: formData.value.name,
description: formData.value.description,
parentResourceNodeId: parentResourceNodeId.value,
resourceLinkList: resourceLinkList.value,
sid: route.query.sid,
cid: route.query.cid,
};
if (props.termId) {
const endpoint = `${ENTRYPOINT}glossaries/${props.termId}`;
axios.put(endpoint, postData)
.then(response => {
console.log('Glossary updated:', response.data);
router.push({
name: "GlossaryList",
query: route.query,
});
})
.catch(error => {
console.error('Error updating Glossary:', error);
});
} else {
const endpoint = `${ENTRYPOINT}glossaries`;
axios.post(endpoint, postData)
.then(response => {
console.log('Glossary created:', response.data);
router.push({
name: "GlossaryList",
query: route.query,
});
})
.catch(error => {
console.error('Error creating Glossary:', error);
});
}
};
return {
formData,
submitGlossaryForm,
};
},
};
</script>

@ -0,0 +1,127 @@
<template>
<form @submit.prevent="submitForm">
<div class="field 2">
<div class="8">
<label for="glossary_file" class="h-4">
File
</label>
<input class="mt-1" :ref="fileInputRef" name="file" type="file" id="glossary_file" />
</div>
</div>
<div class="field">
<label>File type</label>
<div class="field-radiobutton">
<input name="file_type" value="csv" type="radio" id="qf_85f94d" v-model="fileType" checked="checked" />
<label for="qf_85f94d" class="">CSV</label>
</div>
<div class="field-radiobutton">
<input name="file_type" value="xls" type="radio" id="qf_bff468" v-model="fileType" />
<label for="qf_bff468" class="">XLS</label>
</div>
</div>
<div class="field 2">
<div class="8">
<div id="replace" class="field-checkbox">
<input class="appearance-none checked:bg-support-4 outline-none" name="replace" type="checkbox" value="1" id="qf_5b8df0" v-model="replace" />
<label for="qf_5b8df0" class="">
Delete all terms before import.
</label>
</div>
</div>
</div>
<div class="field 2">
<div class="8">
<div id="update" class="field-checkbox">
<input class="appearance-none checked:bg-support-4 outline-none" name="update" type="checkbox" value="1" id="qf_594e6e" v-model="update" />
<label for="qf_594e6e" class="">
Update existing terms.
</label>
</div>
</div>
</div>
<div class="field 2">
<div class="8">
<button class="btn btn--primary" name="SubmitImport" type="submit" id="glossary_SubmitImport">
<em class="mdi mdi-check"></em> Import
</button>
</div>
</div>
</form>
</template>
<script>
import axios from 'axios';
import { ENTRYPOINT } from "../../config/entrypoint";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { ref } from "vue";
import { RESOURCE_LINK_PUBLISHED } from "../resource_links/visibility";
export default {
setup() {
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const fileInputRef = ref(null);
const fileType = ref("csv");
const replace = ref(false);
const update = ref(false);
const parentResourceNodeId = ref(Number(route.params.node));
const resourceLinkList = ref(
JSON.stringify([
{
sid: route.query.sid,
cid: route.query.cid,
visibility: RESOURCE_LINK_PUBLISHED, // visible by default
},
])
);
const submitForm = async () => {
const fileInput = document.getElementById('glossary_file');
const file = fileInput.files[0];
const formData = new FormData();
formData.append("file", file);
formData.append("file_type", fileType.value);
formData.append("replace", replace.value);
formData.append("update", update.value);
formData.append("sid", route.query.sid);
formData.append("cid", route.query.cid);
formData.append("parentResourceNodeId", parentResourceNodeId.value);
formData.append("resourceLinkList", resourceLinkList.value);
console.log('formData', formData);
console.log(ENTRYPOINT + 'glossaries/import');
try {
// eslint-disable-next-line no-unused-vars
const response = await axios.post(ENTRYPOINT + 'glossaries/import', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
router.push({
name: "GlossaryList",
query: route.query,
});
} catch (error) {
fileInputRef.value = null;
fileType.value = "csv";
replace.value = false;
update.value = false;
}
};
return {
fileInputRef,
fileType,
replace,
update,
submitForm,
};
},
};
</script>

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'GlossaryLayout'
}
</script>

@ -0,0 +1,34 @@
export default {
path: '/resources/glossary/:node/',
meta: { requiresAuth: true, showBreadcrumb: true },
name: 'glossary',
component: () => import('../components/glossary/GlossaryLayout.vue'),
redirect: { name: 'GlossaryList' },
children: [
{
name: 'GlossaryList',
path: '',
component: () => import('../views/glossary/GlossaryList.vue')
},
{
name: 'CreateTerm',
path: 'create',
component: () => import('../views/glossary/CreateTerm.vue')
},
{
name: 'UpdateTerm',
path: 'edit/:id',
component: () => import('../views/glossary/UpdateTerm.vue')
},
{
name: 'ImportGlossary',
path: '',
component: () => import('../views/glossary/ImportGlossary.vue')
},
{
name: 'ExportGlossary',
path: '',
component: () => import('../views/glossary/ExportGlossary.vue')
},
]
};

@ -15,6 +15,7 @@ import socialNetworkRoutes from './social';
//import courseCategoryRoutes from './coursecategory';
import documents from './documents';
import links from './links';
import glossary from './glossary';
import store from '../store';
import MyCourseList from '../views/user/courses/List.vue';
import MySessionList from '../views/user/sessions/SessionsCurrent.vue';
@ -123,6 +124,7 @@ const router = createRouter({
//courseCategoryRoutes,
documents,
links,
glossary,
accountRoutes,
personalFileRoutes,
messageRoutes,

@ -0,0 +1,35 @@
<template>
<div>
<div class="mb-4">
<button class="btn btn--secondary" @click="goBack">Back</button>
</div>
<h1 class="text-h3 font-small text-gray-800 mb-4">Add new glossary term<hr /></h1>
<GlossaryForm />
</div>
</template>
<script>
import GlossaryForm from "../../components/glossary/GlossaryForm.vue";
import { useRouter, useRoute } from 'vue-router';
export default {
components: {
GlossaryForm,
},
setup() {
const router = useRouter();
const route = useRoute();
const goBack = () => {
router.push({
name: "GlossaryList",
query: route.query,
});
};
return {
goBack,
};
},
};
</script>

@ -0,0 +1,41 @@
<template>
<div class="export-glossary">
<div class="mb-4">
<button class="btn btn--secondary" @click="goBack">Back</button>
</div>
<h2>Export Glossary</h2>
<GlossaryExportForm />
</div>
</template>
<script>
import GlossaryExportForm from '../../components/glossary/GlossaryExportForm.vue';
import {useRoute, useRouter} from "vue-router";
export default {
components: {
GlossaryExportForm
},
setup() {
const router = useRouter();
const route = useRoute();
const goBack = () => {
router.push({
name: "GlossaryList",
query: route.query,
});
};
return {
goBack,
};
},
};
</script>
<style scoped>
.export-glossary {
margin: 20px;
}
</style>

@ -0,0 +1,258 @@
<template>
<div>
<input
type="text"
v-model="searchTerm"
placeholder="Search term..."
/>
<ButtonToolbar v-if="isAuthenticated && isCurrentTeacher">
<BaseButton
label="Add new glossary term"
icon="new_glossary_term"
class="mr-2 mb-2"
type="black"
@click="addNewTerm"
/>
<BaseButton
label="Import glossary"
icon="import"
class="mr-2 mb-2"
type="black"
@click="importGlossary"
/>
<BaseButton
label="Export"
icon="save"
class="mr-2 mb-2"
type="black"
@click="exportGlossary"
/>
<BaseButton
:label="view === 'table' ? 'List view' : 'Table view'"
icon="view_text"
class="mr-2 mb-2"
type="black"
@click="changeView(view)"
/>
<BaseButton
label="Export to Documents"
icon="export_to_documents"
class="mr-2 mb-2"
type="black"
@click="exportToDocuments"
/>
</ButtonToolbar>
<div v-if="glossaries.length === 0">
<!-- Render the image and create button -->
<EmptyState
icon="mdi mdi-alphabetical"
summary="Add your first term glossary to this course"
>
<BaseButton
label="Add Glossary"
class="mt-4"
icon="plus"
type="primary"
@click="addNewTerm"
/>
</EmptyState>
</div>
<div v-if="glossaries">
<div v-if="view === 'list'">
<ul>
<li v-for="glossary in glossaries" :key="glossary.id">
<span>{{ glossary.name }} - {{ glossary.description }}</span>
<BaseButton
label="Edit"
class="mr-2"
icon="edit"
type="black"
@click="editTerm(glossary.iid)"
/>
<BaseButton
label="Delete"
class="mr-2"
icon="delete"
type="black"
@click="deleteTerm(glossary.iid)"
/>
</li>
</ul>
</div>
<table v-else>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="glossary in glossaries" :key="glossary.id">
<td>{{ glossary.name }}</td>
<td>{{ glossary.description }}</td>
<td>
<BaseButton
label="Edit"
class="mr-2"
icon="edit"
type="black"
@click="editTerm(glossary.iid)"
/>
<BaseButton
label="Delete"
class="mr-2"
icon="delete"
type="black"
@click="deleteTerm(glossary.iid)"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import EmptyState from "../../components/EmptyState.vue";
import BaseButton from "../../components/basecomponents/BaseButton.vue";
import ButtonToolbar from "../../components/basecomponents/ButtonToolbar.vue";
import {computed, onMounted, ref, watch} from "vue";
import { useStore } from "vuex";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import axios from "axios";
import { ENTRYPOINT } from "../../config/entrypoint";
import {RESOURCE_LINK_PUBLISHED} from "../../components/resource_links/visibility";
const store = useStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const searchTerm = ref('');
const parentResourceNodeId = ref(Number(route.params.node));
const resourceLinkList = ref(
JSON.stringify([
{
sid: route.query.sid,
cid: route.query.cid,
visibility: RESOURCE_LINK_PUBLISHED, // visible by default
},
])
);
const isAuthenticated = computed(() => store.getters["security/isAuthenticated"]);
const isCurrentTeacher = computed(() => store.getters["security/isCurrentTeacher"]);
const glossaries = ref([]);
const view = ref('list');
function addNewTerm() {
router.push({
name: "CreateTerm",
query: route.query,
});
}
function editTerm(termId) {
console.log('termId ', termId);
router.push({
name: 'UpdateTerm',
params: { id: termId},
query: route.query,
});
}
function deleteTerm(termId) {
if (confirm('¿Delete?')) {
axios
.delete(`${ENTRYPOINT}glossaries/${termId}`)
.then(response => {
console.log('Term deleted:', response.data);
fetchGlossaries();
})
.catch(error => {
console.error('Error deleting term:', error);
});
}
}
function importGlossary() {
router.push({
name: "ImportGlossary",
query: route.query,
});
}
function exportGlossary() {
router.push({
name: "ExportGlossary",
query: route.query,
});
}
function changeView(newView) {
// Handle changing the view (e.g., table view)
view.value = newView === 'table' ? 'list' : 'table';
}
function exportToDocuments() {
const postData = {
parentResourceNodeId: parentResourceNodeId.value,
resourceLinkList: resourceLinkList.value,
};
const endpoint = `${ENTRYPOINT}glossaries/export_to_documents`;
axios.post(endpoint, postData)
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error(error);
});
}
function fetchGlossaries() {
const params = {
'resourceNode.parent': route.query.parent || null,
'cid': route.query.cid || null,
'sid': route.query.sid || null,
'q': searchTerm.value
};
axios
.get(ENTRYPOINT + 'glossaries', { params })
.then(response => {
console.log('responsedata:', response.data);
glossaries.value = response.data;
console.log('en fetch glossaries.value', glossaries.value);
})
.catch(error => {
console.error('Error fetching links:', error);
});
}
watch(searchTerm, () => {
fetchGlossaries();
});
onMounted(() => {
fetchGlossaries();
});
</script>

@ -0,0 +1,35 @@
<template>
<div>
<div class="mb-4">
<button class="btn btn--secondary" @click="goBack">Back</button>
</div>
<h1 class="text-h3 font-small text-gray-800 mb-4">Import glossary<hr /></h1>
<GlossaryImportForm />
</div>
</template>
<script>
import GlossaryImportForm from "../../components/glossary/GlossaryImportForm.vue";
import {useRoute, useRouter} from "vue-router";
export default {
components: {
GlossaryImportForm,
},
setup() {
const router = useRouter();
const route = useRoute();
const goBack = () => {
router.push({
name: "GlossaryList",
query: route.query,
});
};
return {
goBack,
};
},
};
</script>

@ -0,0 +1,40 @@
<template>
<div>
<div class="mb-4">
<button class="btn btn--secondary" @click="goBack">Back</button>
</div>
<h1>Edit term <hr /></h1>
<GlossaryForm :termId="termId" />
</div>
</template>
<script>
import GlossaryForm from "../../components/glossary/GlossaryForm.vue";
import {useRoute, useRouter} from "vue-router";
export default {
components: {
GlossaryForm,
},
setup() {
const router = useRouter();
const route = useRoute();
const goBack = () => {
router.push({
name: "GlossaryList",
query: route.query,
});
};
return {
goBack,
};
},
computed: {
termId() {
return this.$route.params.id;
},
},
};
</script>

@ -0,0 +1,64 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CGlossary;
use Chamilo\CourseBundle\Repository\CGlossaryRepository;
use DateTime;
use Doctrine\ORM\EntityManager;
use Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class CreateCGlossaryAction extends BaseResourceFileAction
{
public function __invoke(Request $request, CGlossaryRepository $repo, EntityManager $em): CGlossary
{
$data = json_decode($request->getContent(), true);
$title = $data['name'];
$description = $data['description'];
$parentResourceNodeId = $data['parentResourceNodeId'];
$resourceLinkList = json_decode($data['resourceLinkList'], true);
$sid = (int) $data['sid'];
$cid = (int) $data['cid'];
$course = null;
$session = null;
if (0 !== $cid) {
$course = $em->getRepository(Course::class)->find($cid);
}
if (0 !== $sid) {
$session = $em->getRepository(Session::class)->find($sid);
}
// Check if the term already exists
$qb = $repo->getResourcesByCourse($course, $session)
->andWhere('resource.name = :name')
->setParameter('name', $title);
$existingGlossaryTerm = $qb->getQuery()->getOneOrNullResult();
if ($existingGlossaryTerm !== null) {
throw new BadRequestHttpException('The glossary term already exists.');
}
$glossary = (new CGlossary())
->setName($title)
->setDescription($description)
;
if (!empty($parentResourceNodeId)) {
$glossary->setParentResourceNode($parentResourceNodeId);
}
if (!empty($resourceLinkList)) {
$glossary->setResourceLinkArray($resourceLinkList);
}
return $glossary;
}
}

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CGlossary;
use Chamilo\CourseBundle\Repository\CGlossaryRepository;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Exception\NotSupported;
use Mpdf\Mpdf;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Pdf\Tcpdf;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ExportCGlossaryAction
{
public function __invoke(Request $request, CGlossaryRepository $repo, EntityManager $em, KernelInterface $kernel, TranslatorInterface $translator): Response
{
$format = $request->get('format');
$cid = $request->request->get('cid');
$sid = $request->request->get('sid');
if (!in_array($format, ['csv', 'xls', 'pdf'])) {
throw new BadRequestHttpException('Invalid export format');
}
$exportPath = $kernel->getCacheDir();
$course = null;
$session = null;
if (0 !== $cid) {
$course = $em->getRepository(Course::class)->find($cid);
}
if (0 !== $sid) {
$session = $em->getRepository(Session::class)->find($sid);
}
$qb = $repo->getResourcesByCourse($course, $session);
$glossaryItems = $qb->getQuery()->getResult();
$exportFilePath = $this->generateExportFile($glossaryItems, $format, $exportPath, $translator);
$file = new File($exportFilePath);
$response = new Response($file->getContent());
$response->headers->set('Content-Type', $file->getMimeType());
$response->headers->set('Content-Disposition', 'attachment; filename="glossary.'.$format.'"');
unlink($exportFilePath);
return $response;
}
private function generateExportFile(array $glossaryItems, string $format, string $exportPath, TranslatorInterface $translator): string
{
switch ($format) {
case 'csv':
return $this->generateCsvFile($glossaryItems, $exportPath);
case 'xls':
return $this->generateExcelFile($glossaryItems, $exportPath);
case 'pdf':
return $this->generatePdfFile($glossaryItems, $exportPath, $translator);
default:
throw new NotSupported('Export format not supported');
}
}
private function generateCsvFile(array $glossaryItems, string $exportPath): string
{
$csvFilePath = $exportPath.'/glossary.csv';
$csvContent = '';
/* @var CGlossary $item */
foreach ($glossaryItems as $item) {
$csvContent .= $item->getName() . ',' . $item->getDescription() . "\n";
}
file_put_contents($csvFilePath, $csvContent);
return $csvFilePath;
}
private function generateExcelFile(array $glossaryItems, string $exportPath): string
{
$excelFilePath = $exportPath.'/glossary.xlsx';
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
/* @var CGlossary $item */
foreach ($glossaryItems as $index => $item) {
$row = $index + 1;
$sheet->setCellValue('A' . $row, $item->getName());
$sheet->setCellValue('B' . $row, $item->getDescription());
}
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$writer->save($excelFilePath);
return $excelFilePath;
}
private function generatePdfFile(array $glossaryItems, string $exportPath, TranslatorInterface $translator): string
{
$pdfFilePath = $exportPath . '/glossary.pdf';
$mpdf = new Mpdf();
$html = '<h1>'.$translator->trans('Glossary').'</h1>';
$html .= '<table>';
$html .= '<tr><th>'.$translator->trans('Term').'</th><th>'.$translator->trans('Definition').'</th></tr>';
/* @var CGlossary $item */
foreach ($glossaryItems as $item) {
$html .= '<tr>';
$html .= '<td>' . $item->getName(). '</td>';
$html .= '<td>' . $item->getDescription() . '</td>';
$html .= '</tr>';
}
$html .= '</table>';
$mpdf->WriteHTML($html);
$mpdf->Output($pdfFilePath, 'F');
return $pdfFilePath;
}
}

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CDocument;
use Chamilo\CourseBundle\Entity\CGlossary;
use Chamilo\CourseBundle\Repository\CGlossaryRepository;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Exception\NotSupported;
use Mpdf\Mpdf;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Pdf\Tcpdf;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class ExportGlossaryToDocumentsAction
{
public function __invoke(Request $request, CGlossaryRepository $repo, EntityManager $em, KernelInterface $kernel, TranslatorInterface $translator): String
{
$data = json_decode($request->getContent(), true);
$parentResourceNodeId = $data['parentResourceNodeId'];
$resourceLinkList = json_decode($data['resourceLinkList'], true);
$exportPath = $kernel->getCacheDir();
$glossaryItems = $repo->findAll();
$pdfFilePath = $this->generatePdfFile($glossaryItems, $exportPath, $translator);
if ($pdfFilePath) {
$fileName = basename($pdfFilePath);
$uploadFile = new UploadedFile(
$pdfFilePath,
$fileName
);
$document = new CDocument();
$document->setTitle($fileName);
$document->setUploadFile($uploadFile);
$document->setFiletype('file');
if (!empty($parentResourceNodeId)) {
$document->setParentResourceNode($parentResourceNodeId);
}
if (!empty($resourceLinkList)) {
$document->setResourceLinkArray($resourceLinkList);
}
// Save the CDocument entity to the database
$em->persist($document);
$em->flush();
unlink($pdfFilePath);
}
return $pdfFilePath;
}
private function generatePdfFile(array $glossaryItems, string $exportPath, TranslatorInterface $translator): string
{
$date = date('Y-m-d');
$pdfFileName = 'glossary_' . $date . '.pdf';
$pdfFilePath = $exportPath . '/' . $pdfFileName;
$mpdf = new Mpdf();
$html = '<h1>'.$translator->trans('Glossary').'</h1>';
$html .= '<table>';
$html .= '<tr><th>'.$translator->trans('Term').'</th><th>'.$translator->trans('Definition').'</th></tr>';
/* @var CGlossary $item */
foreach ($glossaryItems as $item) {
$html .= '<tr>';
$html .= '<td>' . $item->getName(). '</td>';
$html .= '<td>' . $item->getDescription() . '</td>';
$html .= '</tr>';
}
$html .= '</table>';
$mpdf->WriteHTML($html);
$mpdf->Output($pdfFilePath, 'F');
return $pdfFilePath;
}
}

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CGlossary;
use Chamilo\CourseBundle\Repository\CGlossaryRepository;
use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class GetGlossaryCollectionController extends BaseResourceFileAction
{
public function __invoke(Request $request, CGlossaryRepository $repo, EntityManager $em): Response
{
$cid = $request->query->getInt('cid');
$sid = $request->query->getInt('sid');
$q = $request->query->get('q');
$course = null;
$session = null;
if ($cid) {
$course = $em->getRepository(Course::class)->find($cid);
}
if ($sid) {
$session = $em->getRepository(Session::class)->find($sid);
}
$qb = $repo->getResourcesByCourse($course, $session);
if ($q) {
$qb->andWhere($qb->expr()->like('resource.name', ':name'))
->setParameter('name', '%' . $q . '%');
}
$glossaries = $qb->getQuery()->getResult();
$dataResponse = [];
if ($glossaries) {
/* @var CGlossary $item */
foreach ($glossaries as $item) {
$dataResponse[] =
[
'iid' => $item->getIid(),
'id' => $item->getIid(),
'name' => $item->getName(),
'description' => $item->getDescription(),
];
}
}
return new JsonResponse($dataResponse);
}
}

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CGlossary;
use Chamilo\CourseBundle\Repository\CGlossaryRepository;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Exception\NotSupported;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpFoundation\Response;
class ImportCGlossaryAction
{
public function __invoke(Request $request, CGlossaryRepository $repo, EntityManager $em): Response
{
$file = $request->files->get('file');
$fileType = $request->request->get('file_type');
$replace = $request->request->get('replace');
$update = $request->request->get('update');
$cid = $request->request->get('cid');
$sid = $request->request->get('sid');
$course = null;
$session = null;
if (0 !== $cid) {
$course = $em->getRepository(Course::class)->find($cid);
}
if (0 !== $sid) {
$session = $em->getRepository(Session::class)->find($sid);
}
if (!$file instanceof UploadedFile || !$file->isValid()) {
throw new BadRequestHttpException('Invalid file');
}
$data = [];
if ($fileType === 'csv') {
if (($handle = fopen($file->getPathname(), 'r')) !== false) {
$header = fgetcsv($handle, 0, ';');
while (($row = fgetcsv($handle, 0, ';')) !== false) {
$term = isset($row[0]) ? trim($row[0]) : '';
$definition = isset($row[1]) ? trim($row[1]) : '';
$data[$term] = $definition;
}
fclose($handle);
}
} elseif ($fileType === 'xls') {
$spreadsheet = IOFactory::load($file->getPathname());
$sheet = $spreadsheet->getActiveSheet();
$firstRow = true;
foreach ($sheet->getRowIterator() as $row) {
if ($firstRow) {
$firstRow = false;
continue;
}
$cellIterator = $row->getCellIterator();
$cellIterator->setIterateOnlyExistingCells(false);
$rowData = [];
foreach ($cellIterator as $cell) {
$rowData[] = $cell->getValue();
}
$term = isset($rowData[0]) ? utf8_decode(trim($rowData[0])) : '';
$definition = isset($rowData[1]) ? utf8_decode(trim($rowData[1])) : '';
$data[$term] = $definition;
}
} else {
throw new BadRequestHttpException('Invalid file type');
}
if (empty($data)) {
throw new BadRequestHttpException('Invalid data');
}
if ('true' === $replace) {
$qb = $repo->getResourcesByCourse($course, $session);
$allGlossaries = $qb->getQuery()->getResult();
if ($allGlossaries) {
/* @var CGlossary $item */
foreach ($allGlossaries as $item) {
$termToDelete = $repo->find($item->getIid());
if (null !== $termToDelete) {
$repo->delete($termToDelete);
}
}
}
}
if ('true' === $update) {
foreach ($data as $termToUpdate => $descriptionToUpdate) {
// Check if the term already exists
$qb = $repo->getResourcesByCourse($course, $session)
->andWhere('resource.name = :name')
->setParameter('name', $termToUpdate);
/* @var CGlossary $existingGlossaryTerm */
$existingGlossaryTerm = $qb->getQuery()->getOneOrNullResult();
if ($existingGlossaryTerm !== null) {
$existingGlossaryTerm->setDescription($descriptionToUpdate);
$repo->update($existingGlossaryTerm);
unset($data[$termToUpdate]);
}
}
}
foreach ($data as $term => $description) {
$qb = $repo->getResourcesByCourse($course, $session)
->andWhere('resource.name = :name')
->setParameter('name', $term);
/* @var CGlossary $existingNewGlossaryTerm */
$existingNewGlossaryTerm = $qb->getQuery()->getOneOrNullResult();
if (!$existingNewGlossaryTerm) {
$newGlossary = (new CGlossary())
->setName($term)
->setDescription($description)
->setParent($course)
->addCourseLink($course, $session)
;
$repo->create($newGlossary);
}
}
$response = new Response(json_encode($data), Response::HTTP_OK, ['Content-Type' => 'application/json']);
return $response;
}
}

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CourseBundle\Entity\CGlossary;
use Chamilo\CourseBundle\Repository\CGlossaryRepository;
use DateTime;
use Doctrine\ORM\EntityManager;
use Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class UpdateCGlossaryAction extends BaseResourceFileAction
{
public function __invoke(CGlossary $glossary, Request $request, CGlossaryRepository $repo, EntityManager $em): CGlossary
{
$data = json_decode($request->getContent(), true);
$title = $data['name'];
$description = $data['description'];
$parentResourceNodeId = $data['parentResourceNodeId'];
$resourceLinkList = json_decode($data['resourceLinkList'], true);
$sid = (int) $data['sid'];
$cid = (int) $data['cid'];
$course = null;
$session = null;
if (0 !== $cid) {
$course = $em->getRepository(Course::class)->find($cid);
}
if (0 !== $sid) {
$session = $em->getRepository(Session::class)->find($sid);
}
// Check if the term already exists
$qb = $repo->getResourcesByCourse($course, $session)
->andWhere('resource.name = :name')
->setParameter('name', $title);
$existingGlossaryTerm = $qb->getQuery()->getOneOrNullResult();
if ($existingGlossaryTerm !== null && $existingGlossaryTerm->getIid() !== $glossary->getIid()) {
throw new BadRequestHttpException('The glossary term already exists.');
}
$glossary->setName($title);
$glossary->setDescription($description);
if (!empty($parentResourceNodeId)) {
$glossary->setParentResourceNode($parentResourceNodeId);
}
if (!empty($resourceLinkList)) {
$glossary->setResourceLinkArray($resourceLinkList);
}
return $glossary;
}
}

@ -17,7 +17,7 @@ class Glossary extends AbstractTool implements ToolInterface
public function getLink(): string
{
return '/main/glossary/index.php';
return '/resources/glossary/:nodeId/';
}
public function getIcon(): string

@ -6,31 +6,197 @@ declare(strict_types=1);
namespace Chamilo\CourseBundle\Entity;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Chamilo\CoreBundle\Controller\Api\CreateCGlossaryAction;
use Chamilo\CoreBundle\Controller\Api\ExportCGlossaryAction;
use Chamilo\CoreBundle\Controller\Api\ExportGlossaryToDocumentsAction;
use Chamilo\CoreBundle\Controller\Api\GetGlossaryCollectionController;
use Chamilo\CoreBundle\Controller\Api\ImportCGlossaryAction;
use Chamilo\CoreBundle\Controller\Api\UpdateCGlossaryAction;
use Chamilo\CoreBundle\Entity\AbstractResource;
use Chamilo\CoreBundle\Entity\ResourceInterface;
use Chamilo\CourseBundle\Repository\CGlossaryRepository;
use Doctrine\ORM\Mapping as ORM;
use Stringable;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Course glossary.
*/
#[ApiResource(
shortName: 'Glossary',
operations: [
new Put(
controller: UpdateCGlossaryAction::class,
security: "is_granted('EDIT', object.resourceNode)",
validationContext: [
'groups' => ['media_object_create', 'glossary:write']
],
deserialize: false
),
new Get(security: "is_granted('VIEW', object.resourceNode)"),
new Delete(security: "is_granted('DELETE', object.resourceNode)"),
new Post(
controller: CreateCGlossaryAction::class,
openapiContext: [
'requestBody' => [
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'parentResourceNodeId' => ['type' => 'integer'],
'resourceLinkList' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'visibility' => ['type' => 'integer'],
'cid' => ['type' => 'integer'],
'gid' => ['type' => 'integer'],
'sid' => ['type' => 'integer']
]
]
]
],
'required' => ['name'],
],
],
],
],
],
security: "is_granted('ROLE_CURRENT_COURSE_TEACHER') or is_granted('ROLE_CURRENT_COURSE_SESSION_TEACHER')",
validationContext: ['groups' => ['Default', 'media_object_create', 'glossary:write']],
deserialize: false
),
new GetCollection(
controller: GetGlossaryCollectionController::class,
openapiContext: [
'parameters' => [
[
'name' => 'resourceNode.parent',
'in' => 'query',
'required' => true,
'description' => 'Resource node Parent',
'schema' => ['type' => 'integer']
],
[
'name' => 'cid',
'in' => 'query',
'required' => true,
'description' => 'Course id',
'schema' => [
'type' => 'integer'
]
],
[
'name' => 'sid',
'in' => 'query',
'required' => false,
'description' => 'Session id',
'schema' => [
'type' => 'integer'
]
],
[
'name' => 'q',
'in' => 'query',
'required' => false,
'description' => 'Search term',
'schema' => [
'type' => 'string'
]
]
]
]
),
new Post(
uriTemplate: '/glossaries/import',
controller: ImportCGlossaryAction::class,
openapiContext: [
'summary' => 'Import a glossary',
'requestBody' => [
'content' => [
'multipart/form-data' => [
'schema' => [
'type' => 'object',
'properties' => [
'file' => [
'type' => 'string',
'format' => 'binary',
],
],
],
],
],
],
'responses' => [
'200' => [
'description' => 'Glossaries imported successfully',
],
],
],
security: "is_granted('ROLE_CURRENT_COURSE_TEACHER') or is_granted('ROLE_CURRENT_COURSE_SESSION_TEACHER')",
validationContext: ['groups' => ['Default', 'media_object_create', 'glossary:write']],
deserialize: false
),
new Post(
uriTemplate: '/glossaries/export',
controller: ExportCGlossaryAction::class,
security: "is_granted('ROLE_CURRENT_COURSE_TEACHER') or is_granted('ROLE_CURRENT_COURSE_SESSION_TEACHER')",
validationContext: ['groups' => ['Default', 'media_object_create', 'glossary:write']],
deserialize: false
),
new Post(
uriTemplate: '/glossaries/export_to_documents',
controller: ExportGlossaryToDocumentsAction::class,
security: "is_granted('ROLE_CURRENT_COURSE_TEACHER') or is_granted('ROLE_CURRENT_COURSE_SESSION_TEACHER')",
validationContext: ['groups' => ['Default', 'media_object_create', 'glossary:write']],
deserialize: false
),
],
normalizationContext: [
'groups' => ['glossary:read', 'resource_node:read'],
],
denormalizationContext: [
'groups' => ['glossary:write'],
],
)]
#[ApiFilter(SearchFilter::class, properties: ['name' => 'partial'])]
#[ApiFilter(OrderFilter::class, properties: ['iid', 'name', 'createdAt', 'updatedAt'])]
#[ORM\Table(name: 'c_glossary')]
#[ORM\Entity(repositoryClass: \Chamilo\CourseBundle\Repository\CGlossaryRepository::class)]
#[ORM\Entity(repositoryClass: CGlossaryRepository::class)]
class CGlossary extends AbstractResource implements ResourceInterface, Stringable
{
#[ApiProperty(identifier: true)]
#[Groups(['glossary:read'])]
#[ORM\Column(name: 'iid', type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue]
protected int $iid;
#[Groups(['glossary:read', 'glossary:write'])]
#[Assert\NotBlank]
#[ORM\Column(name: 'name', type: 'text', nullable: false)]
protected string $name;
#[Groups(['glossary:read', 'glossary:write'])]
#[ORM\Column(name: 'description', type: 'text', nullable: false)]
protected ?string $description = null;
#[Groups(['glossary:read', 'glossary:write'])]
#[ORM\Column(name: 'display_order', type: 'integer', nullable: true)]
protected ?int $displayOrder = null;
@ -63,7 +229,7 @@ class CGlossary extends AbstractResource implements ResourceInterface, Stringabl
*
* @return string
*/
public function getDescription()
public function getDescription(): ?string
{
return $this->description;
}
@ -80,7 +246,7 @@ class CGlossary extends AbstractResource implements ResourceInterface, Stringabl
*
* @return int
*/
public function getDisplayOrder()
public function getDisplayOrder(): ?int
{
return $this->displayOrder;
}

Loading…
Cancel
Save