Internal: Allow multi-format media files. Make resource_file multiple for one resource_node - refs #5600

Author: @AngelFQC
pull/5636/head
Angel Fernando Quiroz Campos 2 years ago committed by GitHub
parent b556b32841
commit 95b92f650c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      assets/vue/components/documents/FormNewDocument.vue
  2. 38
      assets/vue/components/documents/ResourceFileLink.vue
  3. 16
      assets/vue/components/documents/ResourceIcon.vue
  4. 86
      assets/vue/composables/datatableList.js
  5. 15
      assets/vue/composables/fileUtils.js
  6. 6
      assets/vue/mixins/ListMixin.js
  7. 126
      assets/vue/mixins/ListMixinVuetify.js
  8. 12
      assets/vue/views/documents/DocumentForHtmlEditor.vue
  9. 38
      assets/vue/views/documents/DocumentShow.vue
  10. 14
      assets/vue/views/documents/DocumentsList.vue
  11. 4
      assets/vue/views/documents/List.vue.vuetify
  12. 132
      assets/vue/views/filemanager/List.vue
  13. 4
      assets/vue/views/message/MessageShow.vue
  14. 130
      assets/vue/views/personalfile/List.vue
  15. 32
      assets/vue/views/personalfile/Shared.vue
  16. 18
      assets/vue/views/personalfile/Show.vue
  17. 2
      public/main/exercise/annotation_user.php
  18. 2
      public/main/exercise/hotspot_actionscript.as.php
  19. 2
      public/main/exercise/hotspot_actionscript_admin.as.php
  20. 2
      public/main/exercise/hotspot_answers.as.php
  21. 2
      public/main/gradebook/lib/be/category.class.php
  22. 8
      public/main/inc/lib/CourseChatUtils.php
  23. 14
      public/main/inc/lib/document.lib.php
  24. 6
      public/main/lp/learnpath.class.php
  25. 2
      public/main/lp/learnpathItem.class.php
  26. 7
      public/main/lp/lp_edit.php
  27. 9
      public/main/work/work.lib.php
  28. 12
      src/CoreBundle/Controller/Api/BaseResourceFileAction.php
  29. 27
      src/CoreBundle/Controller/ResourceController.php
  30. 2
      src/CoreBundle/Controller/TemplateController.php
  31. 4
      src/CoreBundle/Entity/Listener/ResourceListener.php
  32. 5
      src/CoreBundle/Entity/Listener/ResourceNodeListener.php
  33. 2
      src/CoreBundle/Entity/PersonalFile.php
  34. 27
      src/CoreBundle/Entity/ResourceFile.php
  35. 134
      src/CoreBundle/Entity/ResourceNode.php
  36. 9
      src/CoreBundle/Migrations/Schema/V200/Version20170525122900.php
  37. 2
      src/CoreBundle/Migrations/Schema/V200/Version20201215142610.php
  38. 51
      src/CoreBundle/Migrations/Schema/V200/Version20230913162700.php
  39. 2
      src/CoreBundle/Migrations/Schema/V200/Version20240112131200.php
  40. 5
      src/CoreBundle/Migrations/Schema/V200/Version20240515124400.php
  41. 61
      src/CoreBundle/Migrations/Schema/V200/Version20240702222600.php
  42. 2
      src/CoreBundle/Repository/Node/IllustrationRepository.php
  43. 9
      src/CoreBundle/Repository/Node/UserRepository.php
  44. 12
      src/CoreBundle/Repository/ResourceNodeRepository.php
  45. 40
      src/CoreBundle/Repository/ResourceRepository.php
  46. 4
      src/CoreBundle/Resources/views/Resource/preview.html.twig
  47. 4
      src/CoreBundle/Resources/views/Resource/toolbar_view_resource.html.twig
  48. 2
      src/CoreBundle/Resources/views/Work/comments.html.twig
  49. 2
      src/CoreBundle/State/MessageProcessor.php
  50. 2
      src/CourseBundle/Entity/CDocument.php
  51. 4
      tests/CoreBundle/Controller/ResourceControllerTest.php
  52. 8
      tests/CoreBundle/Repository/MessageRepositoryTest.php
  53. 2
      tests/CoreBundle/Repository/MessageTagRepositoryTest.php
  54. 2
      tests/CoreBundle/Repository/ResourceNodeRepositoryTest.php
  55. 2
      tests/CourseBundle/Repository/CForumPostRepositoryTest.php
  56. 3
      tests/scripts/migrate_item_property.php

@ -9,7 +9,8 @@
<BaseTinyEditor
v-if="
(item.resourceNode && item.resourceNode.resourceFile && item.resourceNode.resourceFile.text) || item.newDocument
(item.resourceNode && item.resourceNode.firstResourceFile && item.resourceNode.firstResourceFile.text) ||
item.newDocument
"
v-model="item.contentFile"
:title="t('Content')"

@ -1,8 +1,8 @@
<template>
<a
data-fancybox="gallery"
:href="resource.contentUrl"
:data-type="getDataType"
data-fancybox="gallery"
:href="resource.contentUrl"
:data-type="getDataType"
>
<ResourceIcon :resource-data="resource" />
{{ resource.title }}
@ -10,29 +10,29 @@
</template>
<script>
import ResourceIcon from './ResourceIcon.vue';
import ResourceIcon from "./ResourceIcon.vue"
export default {
name: 'ResourceFileLink',
name: "ResourceFileLink",
components: {
ResourceIcon
ResourceIcon,
},
props: {
resource: {
type: Object,
required: true,
},
},
computed: {
getDataType() {
if (this.resource.resourceNode.resourceFile.image) {
return 'image';
if (this.resource.resourceNode.firstResourceFile.image) {
return "image"
}
if (this.resource.resourceNode.resourceFile.video) {
return 'video';
if (this.resource.resourceNode.firstResourceFile.video) {
return "video"
}
return 'iframe';
}
},
props: {
resource: {
type: Object,
required: true,
}
return "iframe"
},
},
};
}
</script>

@ -4,19 +4,19 @@
icon="folder-generic"
/>
<BaseIcon
v-else-if="resourceData.resourceNode.resourceFile.image"
v-else-if="resourceData.resourceNode.firstResourceFile.image"
icon="file-image"
/>
<BaseIcon
v-else-if="resourceData.resourceNode.resourceFile.video"
v-else-if="resourceData.resourceNode.firstResourceFile.video"
icon="file-video"
/>
<BaseIcon
v-else-if="resourceData.resourceNode.resourceFile.text"
v-else-if="resourceData.resourceNode.firstResourceFile.text"
icon="file-text"
/>
<BaseIcon
v-else-if="'application/pdf' === resourceData.resourceNode.resourceFile.mimeType"
v-else-if="'application/pdf' === resourceData.resourceNode.firstResourceFile.mimeType"
icon="file-pdf"
/>
<BaseIcon
@ -30,15 +30,15 @@
</template>
<script setup>
import BaseIcon from "../basecomponents/BaseIcon.vue";
import {useFileUtils} from "../../composables/fileUtils";
import BaseIcon from "../basecomponents/BaseIcon.vue"
import { useFileUtils } from "../../composables/fileUtils"
const {isAudio} = useFileUtils()
const { isAudio } = useFileUtils()
defineProps({
resourceData: {
type: Object,
required: true,
},
});
})
</script>

@ -1,13 +1,13 @@
import { useStore } from 'vuex'
import { inject, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { isEmpty } from 'lodash'
import { useStore } from "vuex"
import { inject, ref } from "vue"
import { useRoute, useRouter } from "vue-router"
import { isEmpty } from "lodash"
import { useCidReq } from './cidReq'
import { useI18n } from 'vue-i18n';
import { useCidReq } from "./cidReq"
import { useI18n } from "vue-i18n"
import { useNotification } from "./notification"
export function useDatatableList (servicePrefix) {
export function useDatatableList(servicePrefix) {
const moduleName = servicePrefix.toLowerCase()
const store = useStore()
@ -30,19 +30,19 @@ export function useDatatableList (servicePrefix) {
itemsPerPage: 5,
})
function onUpdateOptions ({ page, itemsPerPage, sortBy, sortDesc }) {
function onUpdateOptions({ page, itemsPerPage, sortBy, sortDesc }) {
page = page || options.value.page
if (!isEmpty(route.query.filetype) && route.query.filetype === 'certificate') {
filters.value.filetype = 'certificate';
if (!isEmpty(route.query.filetype) && route.query.filetype === "certificate") {
filters.value.filetype = "certificate"
} else {
filters.value.filetype = ['file', 'folder'];
filters.value.filetype = ["file", "folder"]
}
let params = { ...filters.value }
if (1 === filters.value.loadNode) {
params['resourceNode.parent'] = route.params.node
params["resourceNode.parent"] = route.params.node
}
if (itemsPerPage > 0) {
@ -50,79 +50,81 @@ export function useDatatableList (servicePrefix) {
}
if (!isEmpty(sortBy)) {
params[`order[${sortBy}]`] = sortDesc ? 'desc' : 'asc'
params[`order[${sortBy}]`] = sortDesc ? "desc" : "asc"
}
let type = route.query.type
params = { ...params, cid, sid, gid, type, page }
store.dispatch(`${moduleName}/fetchAll`, params)
.then(() => options.value = { sortBy, sortDesc, itemsPerPage, page })
store
.dispatch(`${moduleName}/fetchAll`, params)
.then(() => (options.value = { sortBy, sortDesc, itemsPerPage, page }))
}
function goToAddItem () {
console.log('addHandler');
function goToAddItem() {
console.log("addHandler")
let folderParams = route.query;
let folderParams = route.query
router.push({
name: `${servicePrefix}Create`,
query: folderParams,
});
})
}
function goToEditItem (item) {
let folderParams = route.query;
folderParams['id'] = item['@id'];
function goToEditItem(item) {
let folderParams = route.query
folderParams["id"] = item["@id"]
if ('folder' === item.filetype || isEmpty(item.filetype)) {
if ("folder" === item.filetype || isEmpty(item.filetype)) {
router.push({
name: `${servicePrefix}Update`,
params: { id: item['@id'] },
query: folderParams
});
params: { id: item["@id"] },
query: folderParams,
})
}
if ('file' === item.filetype) {
folderParams['getFile'] = true;
if (item.resourceNode.resourceFile &&
item.resourceNode.resourceFile.mimeType &&
'text/html' === item.resourceNode.resourceFile.mimeType
if ("file" === item.filetype) {
folderParams["getFile"] = true
if (
item.resourceNode.firstResourceFile &&
item.resourceNode.firstResourceFile.mimeType &&
"text/html" === item.resourceNode.firstResourceFile.mimeType
) {
//folderParams['getFile'] = true;
}
this.$router.push({
name: `${servicePrefix}UpdateFile`,
params: { id: item['@id'] },
query: folderParams
});
params: { id: item["@id"] },
query: folderParams,
})
}
}
function onShowItem (item) {
console.log('listmixin showHandler', item);
function onShowItem(item) {
console.log("listmixin showHandler", item)
let folderParams = route.query;
let folderParams = route.query
if (item) {
folderParams['id'] = item['@id'];
folderParams["id"] = item["@id"]
}
router.push({
name: `${servicePrefix}Show`,
params: folderParams,
query: folderParams,
});
})
}
async function deleteItem (item) {
async function deleteItem(item) {
await store.dispatch(`${moduleName}/del`, item.value)
onUpdateOptions(options.value);
onUpdateOptions(options.value)
notification.showSuccessNotification(t('Deleted'))
notification.showSuccessNotification(t("Deleted"))
}
return {

@ -1,31 +1,28 @@
export function useFileUtils() {
const isImage = (fileData) => {
return isFile(fileData) && fileData.resourceNode.resourceFile.image
return isFile(fileData) && fileData.resourceNode.firstResourceFile.image
}
const isVideo = (fileData) => {
return isFile(fileData) && fileData.resourceNode.resourceFile.video
return isFile(fileData) && fileData.resourceNode.firstResourceFile.video
}
const isAudio = (fileData) => {
const mimeType = fileData.resourceNode.resourceFile.mimeType
const mimeType = fileData.resourceNode.firstResourceFile.mimeType
const isAudio = mimeType.split("/")[0].toLowerCase() === "audio"
return isFile(fileData) && isAudio
}
const isHtml = (fileData) => {
if (!isFile(fileData)) {
return false;
return false
}
const mimeType = fileData.resourceNode.resourceFile.mimeType
const mimeType = fileData.resourceNode.firstResourceFile.mimeType
return mimeType.split("/")[1].toLowerCase() === "html"
}
const isFile = (fileData) => {
return fileData.resourceNode && fileData.resourceNode.resourceFile
return fileData.resourceNode && fileData.resourceNode.firstResourceFile
}
return {

@ -257,9 +257,9 @@ export default {
if ("file" === item.filetype) {
folderParams["getFile"] = true
if (
item.resourceNode.resourceFile &&
item.resourceNode.resourceFile.mimeType &&
"text/html" === item.resourceNode.resourceFile.mimeType
item.resourceNode.firstResourceFile &&
item.resourceNode.firstResourceFile.mimeType &&
"text/html" === item.resourceNode.firstResourceFile.mimeType
) {
//folderParams['getFile'] = true;
}

@ -1,6 +1,6 @@
import isEmpty from 'lodash/isEmpty';
import { formatDateTime } from '../utils/dates';
import NotificationMixin from './NotificationMixin';
import isEmpty from "lodash/isEmpty"
import { formatDateTime } from "../utils/dates"
import NotificationMixin from "./NotificationMixin"
export default {
mixins: [NotificationMixin],
@ -9,106 +9,106 @@ export default {
options: {
sortBy: [],
page: 1,
itemsPerPage: 15
itemsPerPage: 15,
},
filters: {}
};
filters: {},
}
},
watch: {
$route() {
// react to route changes...
this.resetList = true;
this.onUpdateOptions(this.options);
let nodeId = this.$route.params['node'];
this.findResourceNode('/api/resource_nodes/'+ nodeId);
this.resetList = true
this.onUpdateOptions(this.options)
let nodeId = this.$route.params["node"]
this.findResourceNode("/api/resource_nodes/" + nodeId)
},
deletedItem(item) {
this.showMessage(`${item['@id']} deleted.`);
this.showMessage(`${item["@id"]} deleted.`)
},
error(message) {
message && this.showError(message);
message && this.showError(message)
},
items() {
this.options.totalItems = this.totalItems;
}
this.options.totalItems = this.totalItems
},
},
methods: {
onUpdateOptions({ page, itemsPerPage, sortBy, sortDesc, totalItems } = {}) {
let params = {
...this.filters
};
...this.filters,
}
if (itemsPerPage > 0) {
params = { ...params, itemsPerPage, page };
params = { ...params, itemsPerPage, page }
}
if (this.$route.params.node) {
params[`resourceNode.parent`] = this.$route.params.node;
params[`resourceNode.parent`] = this.$route.params.node
}
if (!isEmpty(sortBy) && !isEmpty(sortDesc)) {
params[`order[${sortBy[0]}]`] = sortDesc[0] ? 'desc' : 'asc'
params[`order[${sortBy[0]}]`] = sortDesc[0] ? "desc" : "asc"
}
this.resetList = true;
this.resetList = true
this.getPage(params).then(() => {
this.options.sortBy = sortBy;
this.options.sortDesc = sortDesc;
this.options.itemsPerPage = itemsPerPage;
this.options.totalItems = totalItems;
});
this.options.sortBy = sortBy
this.options.sortDesc = sortDesc
this.options.itemsPerPage = itemsPerPage
this.options.totalItems = totalItems
})
},
onSendFilter() {
this.resetList = true;
this.onUpdateOptions(this.options);
this.resetList = true
this.onUpdateOptions(this.options)
},
resetFilter() {
this.filters = {};
this.filters = {}
},
addHandler() {
let folderParams = this.$route.query;
this.$router.push({name: `${this.$options.servicePrefix}Create`, query: folderParams});
let folderParams = this.$route.query
this.$router.push({ name: `${this.$options.servicePrefix}Create`, query: folderParams })
},
addDocumentHandler() {
let folderParams = this.$route.query;
this.$router.push({ name: `${this.$options.servicePrefix}CreateFile` , query: folderParams});
let folderParams = this.$route.query
this.$router.push({ name: `${this.$options.servicePrefix}CreateFile`, query: folderParams })
},
uploadDocumentHandler() {
let folderParams = this.$route.query;
this.$router.push({ name: `${this.$options.servicePrefix}UploadFile` , query: folderParams});
let folderParams = this.$route.query
this.$router.push({ name: `${this.$options.servicePrefix}UploadFile`, query: folderParams })
},
showHandler(item) {
let folderParams = this.$route.query;
folderParams['id'] = item['@id'];
let folderParams = this.$route.query
folderParams["id"] = item["@id"]
this.$router.push({
name: `${this.$options.servicePrefix}Show`,
//params: { id: item['@id'] },
query: folderParams
});
query: folderParams,
})
},
handleClick(item) {
let folderParams = this.$route.query;
this.resetList = true;
this.$route.params.node = item['resourceNode']['id'];
let folderParams = this.$route.query
this.resetList = true
this.$route.params.node = item["resourceNode"]["id"]
this.$router.push({
name: `${this.$options.servicePrefix}List`,
params: {node: item['resourceNode']['id']},
params: { node: item["resourceNode"]["id"] },
query: folderParams,
});
})
/*this.$router.push({
name: `${this.$options.servicePrefix}List`,
@ -119,36 +119,38 @@ export default {
this.onUpdateOptions(this.options);*/
},
editHandler(item) {
let folderParams = this.$route.query;
folderParams['id'] = item['@id'];
let folderParams = this.$route.query
folderParams["id"] = item["@id"]
if ('folder' === item.filetype) {
if ("folder" === item.filetype) {
this.$router.push({
name: `${this.$options.servicePrefix}Update`,
params: { id: item['@id'] },
query: folderParams
});
params: { id: item["@id"] },
query: folderParams,
})
}
if ('file' === item.filetype) {
folderParams['getFile'] = false;
if ("file" === item.filetype) {
folderParams["getFile"] = false
if (item.resourceNode.resourceFile &&
item.resourceNode.resourceFile.mimeType &&
'text/html' === item.resourceNode.resourceFile.mimeType) {
folderParams['getFile'] = true;
if (
item.resourceNode.firstResourceFile &&
item.resourceNode.firstResourceFile.mimeType &&
"text/html" === item.resourceNode.firstResourceFile.mimeType
) {
folderParams["getFile"] = true
}
this.$router.push({
name: `${this.$options.servicePrefix}UpdateFile`,
params: { id: item['@id'] },
query: folderParams
});
params: { id: item["@id"] },
query: folderParams,
})
}
},
deleteHandler(item) {
this.deleteItem(item).then(() => this.onUpdateOptions(this.options));
this.deleteItem(item).then(() => this.onUpdateOptions(this.options))
},
formatDateTime
}
};
formatDateTime,
},
}

@ -45,7 +45,7 @@
field="resourceNode.title"
>
<template #body="slotProps">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.resourceFile">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.firstResourceFile">
<ResourceFileLink :resource="slotProps.data" />
</div>
<div v-else>
@ -73,10 +73,14 @@
<Column
:header="$t('Size')"
:sortable="true"
field="resourceNode.resourceFile.size"
field="resourceNode.firstResourceFile.size"
>
<template #body="slotProps">
{{ slotProps.data.resourceNode.resourceFile ? prettyBytes(slotProps.data.resourceNode.resourceFile.size) : "" }}
{{
slotProps.data.resourceNode.firstResourceFile
? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size)
: ""
}}
</template>
</Column>
@ -171,7 +175,7 @@ export default {
columns: [
{ label: t("Title"), field: "title", name: "title", sortable: true },
{ label: t("Modified"), field: "resourceNode.updatedAt", name: "updatedAt", sortable: true },
{ label: t("Size"), field: "resourceNode.resourceFile.size", name: "size", sortable: true },
{ label: t("Size"), field: "resourceNode.firstResourceFile.size", name: "size", sortable: true },
{ label: t("Actions"), name: "action", sortable: false },
],
pageOptions: [10, 20, 50, t("All")],

@ -13,34 +13,28 @@
<h6 v-text="item.title" />
</div>
<div
class="document-show__section"
>
<div class="document-show__section">
<div class="document-show__content-side">
<div
v-if="item['resourceNode']['resourceFile']"
>
<div v-if="item.resourceNode.firstResourceFile">
<img
v-if="item.resourceNode.resourceFile.image"
v-if="item.resourceNode.firstResourceFile.image"
:src="item.contentUrl + '&w=500'"
:alt="item.title"
/>
<video
v-else-if="item['resourceNode']['resourceFile']['video']"
v-else-if="item.resourceNode.firstResourceFile.video"
controls
>
<source :src="item['contentUrl']" />
</video>
<iframe
v-if="'text/html' === item['resourceNode']['resourceFile']['mimeType']"
v-if="'text/html' === item.resourceNode.firstResourceFile.mimeType"
:src="item['contentUrl']"
/>
</div>
<div
v-else
>
<div v-else>
<BaseIcon icon="folder-generic" />
</div>
</div>
@ -51,9 +45,7 @@
<th v-text="$t('Author')" />
<td v-text="item.resourceNode.creator.username" />
</tr>
<tr
v-if="item.comment"
>
<tr v-if="item.comment">
<th v-text="$t('Comment')" />
<td v-text="item.comment" />
</tr>
@ -66,18 +58,18 @@
<tr>
<th v-text="$t('Updated at')" />
<td>
{{ item["resourceNode"] ? relativeDatetime(item["resourceNode"].updatedAt) : "" }}
{{ item.resourceNode ? relativeDatetime(item.resourceNode.updatedAt) : "" }}
</td>
</tr>
<tr v-if="item['resourceNode']['resourceFile']">
<tr v-if="item.resourceNode.firstResourceFile">
<th v-text="$t('File')" />
<td>
<a
:href="item['downloadUrl']"
class="btn btn--primary"
>
<BaseIcon icon="download" /> {{ $t("Download file") }}
</a>
<a
:href="item['downloadUrl']"
class="btn btn--primary"
>
<BaseIcon icon="download" /> {{ $t("Download file") }}
</a>
</td>
</tr>
</table>

@ -143,10 +143,14 @@
<Column
:header="t('Size')"
:sortable="true"
field="resourceNode.resourceFile.size"
field="resourceNode.firstResourceFile.size"
>
<template #body="slotProps">
{{ slotProps.data.resourceNode.resourceFile ? prettyBytes(slotProps.data.resourceNode.resourceFile.size) : "" }}
{{
slotProps.data.resourceNode.firstResourceFile
? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size)
: ""
}}
</template>
</Column>
@ -654,9 +658,9 @@ function btnEditOnClick(item) {
folderParams.getFile = true
if (
item.resourceNode.resourceFile &&
item.resourceNode.resourceFile.mimeType &&
"text/html" === item.resourceNode.resourceFile.mimeType
item.resourceNode.firstResourceFile &&
item.resourceNode.firstResourceFile.mimeType &&
"text/html" === item.resourceNode.firstResourceFile.mimeType
) {
//folderParams.getFile = true;
}

@ -39,7 +39,7 @@
slot="item.resourceNode.title"
slot-scope="{ item }"
>
<div v-if="item['resourceNode']['resourceFile']">
<div v-if="item.resourceNode.firstResourceFile">
<a
data-fancybox="gallery"
:href=" item['contentUrl'] "
@ -102,7 +102,7 @@ export default {
headers: [
{text: 'Title', value: 'resourceNode.title', sortable: true},
{text: 'Modified', value: 'resourceNode.updatedAt', sortable: true},
{text: 'Size', value: 'resourceNode.resourceFile.size', sortable: true},
{text: 'Size', value: 'resourceNode.firstResourceFile.size', sortable: true},
{text: 'Actions', value: 'action', sortable: false}
],
selected: [],

@ -30,20 +30,20 @@
<DataTable
v-model:filters="filters"
v-model:selection="selectedItems"
:globalFilterFields="['resourceNode.title', 'resourceNode.updatedAt']"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="10"
:rowsPerPageOptions="[5, 10, 20, 50]"
:totalRecords="totalItems"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalItems"
:value="items"
class="p-datatable-sm"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
dataKey="iid"
filterDisplay="menu"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsiveLayout="scroll"
current-page-report-template="Showing {first} to {last} of {totalRecords}"
data-key="iid"
filter-display="menu"
paginator-template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsive-layout="scroll"
@page="onPage($event)"
@sort="sortingChanged($event)"
>
@ -53,7 +53,7 @@
field="resourceNode.title"
>
<template #body="slotProps">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.resourceFile">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.firstResourceFile">
<ResourceFileLink :resource="slotProps.data" />
<v-icon
v-if="slotProps.data.resourceLinkListFromEntity && slotProps.data.resourceLinkListFromEntity.length > 0"
@ -76,12 +76,12 @@
<Column
:header="$t('Size')"
:sortable="true"
field="resourceNode.resourceFile.size"
field="resourceNode.firstResourceFile.size"
>
<template #body="slotProps">
{{
slotProps.data.resourceNode.resourceFile
? prettyBytes(slotProps.data.resourceNode.resourceFile.size)
slotProps.data.resourceNode.firstResourceFile
? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size)
: ""
}}
</template>
@ -143,7 +143,7 @@
<small
v-if="submitted && !item.title"
class="p-error"
>$t('Title is required')</small
>$t('Title is required')</small
>
</div>
@ -163,14 +163,35 @@
</template>
</Dialog>
<Dialog v-model:visible="deleteItemDialog" :modal="true" :style="{ width: '450px' }" header="Confirm">
<Dialog
v-model:visible="deleteItemDialog"
:modal="true"
:style="{ width: '450px' }"
header="Confirm"
>
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem"></i>
<span>Are you sure you want to delete <b>{{ itemToDelete?.title }}</b>?</span>
<i
class="pi pi-exclamation-triangle p-mr-3"
style="font-size: 2rem"
></i>
<span
>Are you sure you want to delete <b>{{ itemToDelete?.title }}</b
>?</span
>
</div>
<template #footer>
<Button class="p-button-text" icon="pi pi-times" label="No" @click="deleteItemDialog = false" />
<Button class="p-button-text" icon="pi pi-check" label="Yes" @click="deleteItemButton" />
<Button
class="p-button-text"
icon="pi pi-times"
label="No"
@click="deleteItemDialog = false"
/>
<Button
class="p-button-text"
icon="pi pi-check"
label="Yes"
@click="deleteItemButton"
/>
</template>
</Dialog>
@ -185,7 +206,7 @@
class="pi pi-exclamation-triangle p-mr-3"
style="font-size: 2rem"
/>
<span v-if="item">{{ $t('Are you sure you want to delete the selected items?') }}</span>
<span v-if="item">{{ $t("Are you sure you want to delete the selected items?") }}</span>
</div>
<template #footer>
<Button
@ -203,18 +224,33 @@
</template>
</Dialog>
<Dialog v-model:visible="detailsDialogVisible" :header="selectedItem.title || 'Item Details'" :modal="true" :style="{ width: '50%' }">
<Dialog
v-model:visible="detailsDialogVisible"
:header="selectedItem.title || 'Item Details'"
:modal="true"
:style="{ width: '50%' }"
>
<div v-if="Object.keys(selectedItem).length > 0">
<p><strong>Title:</strong> {{ selectedItem.resourceNode.title }}</p>
<p><strong>Modified:</strong> {{ relativeDatetime(selectedItem.resourceNode.updatedAt) }}</p>
<p><strong>Size:</strong> {{ prettyBytes(selectedItem.resourceNode.resourceFile.size) }}</p>
<p><strong>URL:</strong> <a :href="selectedItem.contentUrl" target="_blank">Open File</a></p>
<p><strong>Size:</strong> {{ prettyBytes(selectedItem.resourceNode.firstResourceFile.size) }}</p>
<p>
<strong>URL:</strong>
<a
:href="selectedItem.contentUrl"
target="_blank"
>Open File</a
>
</p>
</div>
<template #footer>
<Button class="p-button-text" label="Close" @click="closeDetailsDialog" />
<Button
class="p-button-text"
label="Close"
@click="closeDetailsDialog"
/>
</template>
</Dialog>
</template>
<script>
@ -263,9 +299,9 @@ export default {
sortable: true,
},
{
name: "resourceNode.resourceFile.size",
name: "resourceNode.firstResourceFile.size",
label: t("Size"),
field: "resourceNode.resourceFile.size",
field: "resourceNode.firstResourceFile.size",
sortable: true,
},
{ name: "action", label: t("Actions"), field: "action", sortable: false },
@ -273,7 +309,7 @@ export default {
columns: [
{ label: t("Title"), field: "title", name: "title", sortable: true },
{ label: t("Modified"), field: "resourceNode.updatedAt", name: "updatedAt", sortable: true },
{ label: t("Size"), field: "resourceNode.resourceFile.size", name: "size", sortable: true },
{ label: t("Size"), field: "resourceNode.firstResourceFile.size", name: "size", sortable: true },
{ label: t("Actions"), name: "action", sortable: false },
],
pageOptions: [10, 20, 50, t("All")],
@ -299,7 +335,7 @@ export default {
created() {
this.resetList = true
this.onUpdateOptions(this.options)
this.isFromEditor = window.location.search.includes('editor=tinymce');
this.isFromEditor = window.location.search.includes("editor=tinymce")
},
computed: {
// From crud.js list function
@ -331,15 +367,15 @@ export default {
selectedItem: {},
itemToDelete: null,
isFromEditor: false,
};
}
},
methods: {
showHandler(item) {
this.selectedItem = item;
this.detailsDialogVisible = true;
this.selectedItem = item
this.detailsDialogVisible = true
},
closeDetailsDialog() {
this.detailsDialogVisible = false;
this.detailsDialogVisible = false
},
// prime
onPage(event) {
@ -404,7 +440,7 @@ export default {
this.itemDialog = true
},
confirmDeleteItem(item) {
console.log('confirmDeleteItem :::', item)
console.log("confirmDeleteItem :::", item)
this.item = { ...item }
this.itemToDelete = { ...item }
this.deleteItemDialog = true
@ -423,21 +459,31 @@ export default {
this.selectedItems = null
},
deleteItemButton() {
console.log("deleteItem", this.itemToDelete);
console.log("deleteItem", this.itemToDelete)
if (this.itemToDelete && this.itemToDelete.id) {
this.deleteItem(this.itemToDelete)
.then(() => {
this.$toast.add({ severity: 'success', summary: 'Success', detail: 'Item deleted successfully', life: 3000 });
this.deleteItemDialog = false;
this.itemToDelete = null;
this.onUpdateOptions(this.options);
this.$toast.add({
severity: "success",
summary: "Success",
detail: "Item deleted successfully",
life: 3000,
})
this.deleteItemDialog = false
this.itemToDelete = null
this.onUpdateOptions(this.options)
})
.catch((error) => {
console.error("Error deleting the item:", error)
this.$toast.add({
severity: "error",
summary: "Error",
detail: "An error occurred while deleting the item",
life: 3000,
})
})
.catch(error => {
console.error("Error deleting the item:", error);
this.$toast.add({ severity: 'error', summary: 'Error', detail: 'An error occurred while deleting the item', life: 3000 });
});
} else {
console.error("No item to delete or item ID is missing");
console.error("No item to delete or item ID is missing")
}
},
onRowSelected(items) {

@ -112,7 +112,7 @@
:key="index"
>
<audio
v-if="attachment.resourceNode.resourceFile.audio"
v-if="attachment.resourceNode.firstResourceFile.audio"
controls
>
<source :src="attachment.downloadUrl" />
@ -124,7 +124,7 @@
class="btn btn--plain"
>
<BaseIcon icon="attachment" />
{{ attachment.resourceNode.resourceFile.originalName }}
{{ attachment.resourceNode.firstResourceFile.originalName }}
</a>
</li>
</ul>

@ -36,20 +36,20 @@
<DataTable
v-model:filters="filters"
v-model:selection="selectedItems"
:globalFilterFields="['resourceNode.title', 'resourceNode.updatedAt']"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="10"
:rowsPerPageOptions="[5, 10, 20, 50]"
:totalRecords="totalItems"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalItems"
:value="items"
class="p-datatable-sm"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
dataKey="iid"
filterDisplay="menu"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsiveLayout="scroll"
current-page-report-template="Showing {first} to {last} of {totalRecords}"
data-key="iid"
filter-display="menu"
paginator-template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsive-layout="scroll"
@page="onPage($event)"
@sort="sortingChanged($event)"
>
@ -59,7 +59,7 @@
field="resourceNode.title"
>
<template #body="slotProps">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.resourceFile">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.firstResourceFile">
<ResourceFileLink :resource="slotProps.data" />
<v-icon
v-if="slotProps.data.resourceLinkListFromEntity && slotProps.data.resourceLinkListFromEntity.length > 0"
@ -82,12 +82,12 @@
<Column
:header="$t('Size')"
:sortable="true"
field="resourceNode.resourceFile.size"
field="resourceNode.firstResourceFile.size"
>
<template #body="slotProps">
{{
slotProps.data.resourceNode.resourceFile
? prettyBytes(slotProps.data.resourceNode.resourceFile.size)
slotProps.data.resourceNode.firstResourceFile
? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size)
: ""
}}
</template>
@ -181,14 +181,35 @@
</template>
</Dialog>
<Dialog v-model:visible="deleteItemDialog" :modal="true" :style="{ width: '450px' }" header="Confirm">
<Dialog
v-model:visible="deleteItemDialog"
:modal="true"
:style="{ width: '450px' }"
header="Confirm"
>
<div class="confirmation-content">
<i class="pi pi-exclamation-triangle p-mr-3" style="font-size: 2rem"></i>
<span>Are you sure you want to delete <b>{{ itemToDelete?.title }}</b>?</span>
<i
class="pi pi-exclamation-triangle p-mr-3"
style="font-size: 2rem"
></i>
<span
>Are you sure you want to delete <b>{{ itemToDelete?.title }}</b
>?</span
>
</div>
<template #footer>
<Button class="p-button-text" icon="pi pi-times" label="No" @click="deleteItemDialog = false" />
<Button class="p-button-text" icon="pi pi-check" label="Yes" @click="deleteItemButton" />
<Button
class="p-button-text"
icon="pi pi-times"
label="No"
@click="deleteItemDialog = false"
/>
<Button
class="p-button-text"
icon="pi pi-check"
label="Yes"
@click="deleteItemButton"
/>
</template>
</Dialog>
@ -203,7 +224,7 @@
class="pi pi-exclamation-triangle p-mr-3"
style="font-size: 2rem"
/>
<span v-if="item">{{ $t('Are you sure you want to delete the selected items?') }}</span>
<span v-if="item">{{ $t("Are you sure you want to delete the selected items?") }}</span>
</div>
<template #footer>
<Button
@ -221,18 +242,33 @@
</template>
</Dialog>
<Dialog v-model:visible="detailsDialogVisible" :header="selectedItem.title || 'Item Details'" :modal="true" :style="{ width: '50%' }">
<Dialog
v-model:visible="detailsDialogVisible"
:header="selectedItem.title || 'Item Details'"
:modal="true"
:style="{ width: '50%' }"
>
<div v-if="Object.keys(selectedItem).length > 0">
<p><strong>Title:</strong> {{ selectedItem.resourceNode.title }}</p>
<p><strong>Modified:</strong> {{ relativeDatetime(selectedItem.resourceNode.updatedAt) }}</p>
<p><strong>Size:</strong> {{ prettyBytes(selectedItem.resourceNode.resourceFile.size) }}</p>
<p><strong>URL:</strong> <a :href="selectedItem.contentUrl" target="_blank">Open File</a></p>
<p><strong>Size:</strong> {{ prettyBytes(selectedItem.resourceNode.firstResourceFile.size) }}</p>
<p>
<strong>URL:</strong>
<a
:href="selectedItem.contentUrl"
target="_blank"
>Open File</a
>
</p>
</div>
<template #footer>
<Button class="p-button-text" label="Close" @click="closeDetailsDialog" />
<Button
class="p-button-text"
label="Close"
@click="closeDetailsDialog"
/>
</template>
</Dialog>
</template>
<script>
@ -280,9 +316,9 @@ export default {
sortable: true,
},
{
name: "resourceNode.resourceFile.size",
name: "resourceNode.firstResourceFile.size",
label: t("Size"),
field: "resourceNode.resourceFile.size",
field: "resourceNode.firstResourceFile.size",
sortable: true,
},
{ name: "action", label: t("Actions"), field: "action", sortable: false },
@ -290,7 +326,7 @@ export default {
columns: [
{ label: t("Title"), field: "title", name: "title", sortable: true },
{ label: t("Modified"), field: "resourceNode.updatedAt", name: "updatedAt", sortable: true },
{ label: t("Size"), field: "resourceNode.resourceFile.size", name: "size", sortable: true },
{ label: t("Size"), field: "resourceNode.firstResourceFile.size", name: "size", sortable: true },
{ label: t("Actions"), name: "action", sortable: false },
],
pageOptions: [10, 20, 50, t("All")],
@ -316,7 +352,7 @@ export default {
created() {
this.resetList = true
this.onUpdateOptions(this.options)
this.isFromEditor = window.location.search.includes('editor=tinymce');
this.isFromEditor = window.location.search.includes("editor=tinymce")
},
computed: {
// From crud.js list function
@ -348,15 +384,15 @@ export default {
selectedItem: {},
itemToDelete: null,
isFromEditor: false,
};
}
},
methods: {
showHandler(item) {
this.selectedItem = item;
this.detailsDialogVisible = true;
this.selectedItem = item
this.detailsDialogVisible = true
},
closeDetailsDialog() {
this.detailsDialogVisible = false;
this.detailsDialogVisible = false
},
// prime
onPage(event) {
@ -421,7 +457,7 @@ export default {
this.itemDialog = true
},
confirmDeleteItem(item) {
console.log('confirmDeleteItem :::', item)
console.log("confirmDeleteItem :::", item)
this.item = { ...item }
this.itemToDelete = { ...item }
this.deleteItemDialog = true
@ -440,21 +476,31 @@ export default {
this.selectedItems = null
},
deleteItemButton() {
console.log("deleteItem", this.itemToDelete);
console.log("deleteItem", this.itemToDelete)
if (this.itemToDelete && this.itemToDelete.id) {
this.deleteItem(this.itemToDelete)
.then(() => {
this.$toast.add({ severity: 'success', summary: 'Success', detail: 'Item deleted successfully', life: 3000 });
this.deleteItemDialog = false;
this.itemToDelete = null;
this.onUpdateOptions(this.options);
this.$toast.add({
severity: "success",
summary: "Success",
detail: "Item deleted successfully",
life: 3000,
})
this.deleteItemDialog = false
this.itemToDelete = null
this.onUpdateOptions(this.options)
})
.catch((error) => {
console.error("Error deleting the item:", error)
this.$toast.add({
severity: "error",
summary: "Error",
detail: "An error occurred while deleting the item",
life: 3000,
})
})
.catch(error => {
console.error("Error deleting the item:", error);
this.$toast.add({ severity: 'error', summary: 'Error', detail: 'An error occurred while deleting the item', life: 3000 });
});
} else {
console.error("No item to delete or item ID is missing");
console.error("No item to delete or item ID is missing")
}
},
onRowSelected(items) {

@ -1,22 +1,26 @@
<template>
<Button :label="$t('Back')" icon="pi pi-chevron-left" @click="goBack" />
<Button
:label="$t('Back')"
icon="pi pi-chevron-left"
@click="goBack"
/>
<DataTable
v-model:filters="filters"
v-model:selection="selectedItems"
:globalFilterFields="['resourceNode.title', 'resourceNode.updatedAt']"
:global-filter-fields="['resourceNode.title', 'resourceNode.updatedAt']"
:lazy="true"
:loading="isLoading"
:paginator="true"
:rows="10"
:rowsPerPageOptions="[5, 10, 20, 50]"
:totalRecords="totalItems"
:rows-per-page-options="[5, 10, 20, 50]"
:total-records="totalItems"
:value="itemsShared"
class="p-datatable-sm"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
dataKey="iid"
filterDisplay="menu"
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsiveLayout="scroll"
current-page-report-template="Showing {first} to {last} of {totalRecords}"
data-key="iid"
filter-display="menu"
paginator-template="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
responsive-layout="scroll"
@page="onPage($event)"
@sort="sortingChanged($event)"
>
@ -26,7 +30,7 @@
field="resourceNode.title"
>
<template #body="slotProps">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.resourceFile">
<div v-if="slotProps.data && slotProps.data.resourceNode && slotProps.data.resourceNode.firstResourceFile">
<ResourceFileLink :resource="slotProps.data" />
</div>
<div v-else>
@ -45,12 +49,12 @@
<Column
:header="$t('Size')"
:sortable="true"
field="resourceNode.resourceFile.size"
field="resourceNode.firstResourceFile.size"
>
<template #body="slotProps">
{{
slotProps.data.resourceNode.resourceFile
? prettyBytes(slotProps.data.resourceNode.resourceFile.size)
slotProps.data.resourceNode.firstResourceFile
? prettyBytes(slotProps.data.resourceNode.firstResourceFile.size)
: ""
}}
</template>
@ -117,7 +121,7 @@ export default {
columns: [
{ label: t("Title"), field: "title", name: "title", sortable: true },
{ label: t("Modified"), field: "resourceNode.updatedAt", name: "updatedAt", sortable: true },
{ label: t("Size"), field: "resourceNode.resourceFile.size", name: "size", sortable: true },
{ label: t("Size"), field: "resourceNode.firstResourceFile.size", name: "size", sortable: true },
{ label: t("Actions"), name: "action", sortable: false },
],
pageOptions: [10, 20, 50, t("All")],

@ -1,6 +1,10 @@
<template>
<div>
<Button :label="$t('Back')" icon="pi pi-chevron-left" @click="goBack" />
<Button
:label="$t('Back')"
icon="pi pi-chevron-left"
@click="goBack"
/>
<Toolbar
v-if="item && isCurrentTeacher"
:handle-delete="del"
@ -21,16 +25,16 @@
>
<div class="w-1/2">
<div
v-if="item['resourceNode']['resourceFile']"
v-if="item.resourceNode.firstResourceFile"
class="flex justify-center"
>
<div class="w-64">
<q-img
v-if="item['resourceNode']['resourceFile']['image']"
v-if="item.resourceNode.firstResourceFile.image"
:src="item['contentUrl'] + '&w=300'"
spinner-color="primary"
/>
<span v-else-if="item['resourceNode']['resourceFile']['video']">
<span v-else-if="item.resourceNode.firstResourceFile.video">
<video controls>
<source :src="item['contentUrl']" />
</video>
@ -93,7 +97,7 @@
</td>
<td />
</tr>
<tr v-if="item['resourceNode']['resourceFile']">
<tr v-if="item.resourceNode.firstResourceFile">
<td>
<strong>{{ $t("File") }}</strong>
</td>
@ -143,6 +147,7 @@ export default {
Toolbar,
ShowLinks,
},
mixins: [ShowMixin],
data() {
const { relativeDatetime } = useFormatDate()
const securityStore = useSecurityStore()
@ -156,7 +161,6 @@ export default {
isCurrentTeacher,
}
},
mixins: [ShowMixin],
computed: {
...mapFields("personalfile", {
isLoading: "isLoading",
@ -165,7 +169,7 @@ export default {
},
methods: {
goBack() {
this.$router.go(-1);
this.$router.go(-1)
},
...mapActions("personalfile", {
deleteItem: "del",

@ -26,7 +26,7 @@ $objQuestion = $questionRepo->find($questionId);
$answer_type = $objQuestion->getType(); //very important
$resourceFile = $objQuestion->getResourceNode()->getResourceFile();
$resourceFile = $objQuestion->getResourceNode()->getResourceFiles()->first();
$pictureWidth = $resourceFile->getWidth();
$pictureHeight = $resourceFile->getHeight();
$imagePath = $questionRepo->getHotSpotImageUrl($objQuestion);

@ -36,7 +36,7 @@ $TBL_ANSWERS = Database::get_course_table(TABLE_QUIZ_ANSWER);
if (!$objQuestion->getResourceNode()->hasResourceFile()) {
api_not_allowed();
}
$resourceFile = $objQuestion->getResourceNode()->getResourceFile();
$resourceFile = $objQuestion->getResourceNode()->getResourceFiles()->first();
$pictureWidth = $resourceFile->getWidth();
$pictureHeight = $resourceFile->getHeight();
$imagePath = $questionRepo->getHotSpotImageUrl($objQuestion).'?'.api_get_cidreq();

@ -32,7 +32,7 @@ if (!$objQuestion) {
if (!$objQuestion->getResourceNode()->hasResourceFile()) {
api_not_allowed();
}
$resourceFile = $objQuestion->getResourceNode()->getResourceFile();
$resourceFile = $objQuestion->getResourceNode()->getResourceFiles()->first();
$pictureWidth = $resourceFile->getWidth();
$pictureHeight = $resourceFile->getHeight();
$imagePath = $questionRepo->getHotSpotImageUrl($objQuestion).'?'.api_get_cidreq();

@ -60,7 +60,7 @@ if (empty($objQuestion)) {
$answer_type = $objQuestion->getType(); //very important
$TBL_ANSWERS = Database::get_course_table(TABLE_QUIZ_ANSWER);
$resourceFile = $objQuestion->getResourceNode()->getResourceFile();
$resourceFile = $objQuestion->getResourceNode()->getResourceFiles()->first();
$pictureWidth = $resourceFile->getWidth();
$pictureHeight = $resourceFile->getHeight();
$imagePath = $questionRepo->getHotSpotImageUrl($objQuestion).'?'.api_get_cidreq();

@ -2077,7 +2077,7 @@ class Category implements GradebookItem
$html = [];
if (!empty($my_certificate)) {
$pathToCertificate = $category->getDocument()->getResourceNode()->getResourceFile()->getFile()->getPathname();
$pathToCertificate = $category->getDocument()->getResourceNode()->getResourceFiles()->first()->getFile()->getPathname();
$certificate_obj = new Certificate(
$my_certificate['id'],

@ -485,13 +485,7 @@ class CourseChatUtils
$resourceNode = $resource->getResourceNode();
}
if ($resourceNode->hasResourceFile()) {
//$resourceFile = $resourceNode->getResourceFile();
//$fileName = $this->getFilename($resourceFile);
return $this->repository->getResourceNodeFileContent($resourceNode);
}
return '';
return $this->repository->getResourceNodeFileContent($resourceNode);
$remove = 0;
$content = [];

@ -2214,10 +2214,10 @@ class DocumentManager
'rootOpen' => '<ul id="doc_list" class="list-group lp_resource">',
'rootClose' => '</ul>',
//'childOpen' => '<li class="doc_resource lp_resource_element ">',
'childOpen' => function ($child) {
'childOpen' => function ($child) {;
$id = $child['id'];
$disableDrag = '';
if (!$child['resourceFile']) {
if (!$child['resourceFiles']) {
$disableDrag = ' disable_drag ';
}
@ -2230,12 +2230,12 @@ class DocumentManager
'childClose' => '</li>',
'nodeDecorator' => function ($node) use ($icon, $folderIcon) {
$disableDrag = '';
if (!$node['resourceFile']) {
if (!$node['resourceFiles']) {
$disableDrag = ' disable_drag ';
}
$link = '<div class="flex flex-row gap-1 h-4 item_data '.$disableDrag.' ">';
$file = $node['resourceFile'];
$file = $node['resourceFiles'] ? current($node['resourceFiles']) : null;
$extension = '';
if ($file) {
$extension = pathinfo($file['title'], PATHINFO_EXTENSION);
@ -2243,7 +2243,7 @@ class DocumentManager
$folder = $folderIcon;
if ($node['resourceFile']) {
if ($node['resourceFiles']) {
$link .= '<a class="moved ui-sortable-handle" href="#">';
$link .= $icon;
$link .= '</a>';
@ -2272,12 +2272,12 @@ class DocumentManager
->from(ResourceNode::class, 'node')
->innerJoin('node.resourceType', 'type')
->innerJoin('node.resourceLinks', 'links')
->leftJoin('node.resourceFile', 'file')
->innerJoin('node.resourceFiles', 'files')
->addSelect('files')
->where('type = :type')
->andWhere('links.course = :course')
->setParameters(['type' => $type, 'course' => $course])
->orderBy('node.parent', 'ASC')
->addSelect('file')
;
$sessionId = api_get_session_id();

@ -8748,10 +8748,6 @@ class learnpath
/** @var CDocument $document */
$document = $repo->find($finalItem->path);
if ($document && $document->getResourceNode()->hasResourceFile()) {
return $repo->getResourceFileContent($document);
}
return '';
return $document ? $repo->getResourceFileContent($document) : '';
}
}

@ -4035,7 +4035,7 @@ class learnpathItem
);*/
if ($document) {
$name = '/audio/'.$document->getResourceNode()->getResourceFile()->getOriginalName();
$name = '/audio/'.$document->getResourceNode()->getResourceFiles()->first()->getOriginalName();
// Store the mp3 file in the lp_item table.
$table = Database::get_course_table(TABLE_LP_ITEM);
$sql = "UPDATE $table SET

@ -333,8 +333,11 @@ if ($form->validate()) {
}
if (isset($_REQUEST['remove_picture']) && $_REQUEST['remove_picture']) {
if ($lp->getResourceNode()->hasResourceFile()) {
$lp->getResourceNode()->setResourceFile(null);
$resourceFiles = $lp->getResourceNode()->getResourceFiles();
foreach ($resourceFiles as $resourceFile) {
$em->remove($resourceFile);
$em->flush();
}
}

@ -3987,7 +3987,7 @@ function getWorkComment(CStudentPublicationComment $commentEntity, array $course
$filePath = '';
$deleteUrl = api_get_path(WEB_CODE_PATH).
'work/view.php?'.api_get_cidreq().'&id='.$workId.'&action=delete_attachment&comment_id='.$id;
$fileName = $commentEntity->getResourceNode()->getResourceFile()->getTitle();
$fileName = $commentEntity->getResourceNode()->getResourceFiles()->first()->getTitle();
}
$comment['comment'] = $commentEntity->getComment();
$comment['delete_file_url'] = $deleteUrl;
@ -4016,10 +4016,7 @@ function deleteCommentFile($id, $courseInfo = [])
/** @var CStudentPublicationComment $commentEntity */
$commentEntity = $repo->findOneBy($criteria);
if ($commentEntity->getResourceNode()->hasResourceFile()) {
$file = $commentEntity->getResourceNode()->getResourceFile();
$commentEntity->getResourceNode()->setResourceFile(null);
foreach ($commentEntity->getResourceNode()->getResourceFiles() as $file) {
$em->remove($file);
$em->flush();
}
@ -5969,7 +5966,7 @@ function getFileContents($id, $courseInfo, $sessionId = 0, $correction = false,
$title = $titleCorrection = $studentPublication->getCorrection()->getTitle();
}
if ($hasFile) {
$title = $studentPublication->getResourceNode()->getResourceFile()->getTitle();
$title = $studentPublication->getResourceNode()->getResourceFiles()->first()->getTitle();
}
$title = str_replace(' ', '_', $title);

@ -409,17 +409,15 @@ class BaseResourceFileAction
$repo->setResourceName($resource, $title);
$hasFile = $resource->getResourceNode()->hasResourceFile();
$resourceNode = $resource->getResourceNode();
$hasFile = $resourceNode->hasResourceFile();
if ($hasFile && !empty($content)) {
if ($resourceNode->hasResourceFile()) {
// The content is updated by the ResourceNodeListener.php
$resourceNode->setContent($content);
$resourceNode->getResourceFile()->setSize(\strlen($content));
// The content is updated by the ResourceNodeListener.php
$resourceNode->setContent($content);
foreach ($resourceNode->getResourceFiles() as $resourceFile) {
$resourceFile->setSize(\strlen($content));
}
$resourceNode->getResourceFile()->setUpdatedAt(new DateTime());
$resource->setResourceNode($resourceNode);
}

@ -148,9 +148,9 @@ class ResourceController extends AbstractResourceController implements CourseCon
$firstResourceLink = $resourceNode->getResourceLinks()->first();
if ($firstResourceLink && $user) {
$resourceLinkId = $firstResourceLink->getId();
$url = $resourceNode->getResourceFile()->getOriginalName();
$url = $resourceNode->getResourceFiles()->first()->getOriginalName();
$downloadRepository = $entityManager->getRepository(TrackEDownloads::class);
$downloadId = $downloadRepository->saveDownload($user->getId(), $resourceLinkId, $url);
$downloadRepository->saveDownload($user->getId(), $resourceLinkId, $url);
}
$cid = (int) $request->query->get('cid');
@ -236,9 +236,9 @@ class ResourceController extends AbstractResourceController implements CourseCon
$firstResourceLink = $resourceNode->getResourceLinks()->first();
if ($firstResourceLink) {
$resourceLinkId = $firstResourceLink->getId();
$url = $resourceNode->getResourceFile()->getOriginalName();
$url = $resourceNode->getResourceFiles()->first()->getOriginalName();
$downloadRepository = $entityManager->getRepository(TrackEDownloads::class);
$downloadId = $downloadRepository->saveDownload($user->getId(), $resourceLinkId, $url);
$downloadRepository->saveDownload($user->getId(), $resourceLinkId, $url);
}
// Redirect to download single file.
@ -251,12 +251,17 @@ class ResourceController extends AbstractResourceController implements CourseCon
$type = $repo->getResourceType();
$criteria = Criteria::create()
->where(Criteria::expr()->neq('resourceFile', null)) // must have a file
->where(Criteria::expr()->neq('resourceFiles', null)) // must have a file
->andWhere(Criteria::expr()->eq('resourceType', $type)) // only download same type
;
$qb = $resourceNodeRepo->getChildrenQueryBuilder($resourceNode);
$qb->addCriteria($criteria);
$qbAlias = $qb->getRootAliases()[0];
$qb
->leftJoin(sprintf('%s.resourceFiles', $qbAlias), 'resourceFiles') // must have a file
->addCriteria($criteria)
;
/** @var ArrayCollection|ResourceNode[] $children */
$children = $qb->getQuery()->getResult();
@ -281,7 +286,7 @@ class ResourceController extends AbstractResourceController implements CourseCon
/** @var ResourceNode $node */
foreach ($children as $node) {
$stream = $repo->getResourceNodeFileStream($node);
$fileName = $node->getResourceFile()->getOriginalName();
$fileName = $node->getResourceFiles()->first()->getOriginalName();
// $fileToDisplay = basename($node->getPathForDisplay());
// $fileToDisplay = str_replace($rootNodePath, '', $node->getPathForDisplay());
// error_log($fileToDisplay);
@ -464,13 +469,13 @@ class ResourceController extends AbstractResourceController implements CourseCon
$this->trans('Unauthorised view access to resource')
);
$resourceFile = $resourceNode->getResourceFile();
$resourceFile = $resourceNode->getResourceFiles()->first();
if (null === $resourceFile) {
throw new NotFoundHttpException($this->trans('File not found for resource'));
if (!$resourceFile) {
throw $this->createNotFoundException($this->trans('File not found for resource'));
}
$fileName = $resourceNode->getResourceFile()->getOriginalName();
$fileName = $resourceFile->getOriginalName();
$mimeType = $resourceFile->getMimeType();
$resourceNodeRepo = $this->getResourceNodeRepository();

@ -156,7 +156,7 @@ class TemplateController extends AbstractController
$document = $documentRepository->find($template->getRefDoc());
$content = '';
if (null !== $document && null !== $document->getResourceNode() && null !== $document->getResourceNode()->getResourceFile()) {
if (null !== $document && null !== $document->getResourceNode() && $document->getResourceNode()->getResourceFiles()->first()) {
$content = $documentRepository->getResourceFileContent($document);
}

@ -243,8 +243,8 @@ class ResourceListener
->setOriginalName($uploadedFile->getFilename())
->setFile($uploadedFile)
;
$em->persist($resourceFile);
$resourceNode->setResourceFile($resourceFile);
$resourceNode->addResourceFile($resourceFile);
$em->persist($resourceNode);
}
}

@ -37,8 +37,9 @@ class ResourceNodeListener
*/
public function preUpdate(ResourceNode $resourceNode, PreUpdateEventArgs $event)
{
if ($resourceNode->hasResourceFile() && $resourceNode->hasEditableTextContent()) {
$fileName = $this->resourceNodeRepository->getFilename($resourceNode->getResourceFile());
if ($resourceNode->hasEditableTextContent()) {
$resourceFile = $resourceNode->getResourceFiles()->first();
$fileName = $this->resourceNodeRepository->getFilename($resourceFile);
if ($fileName) {
$content = $resourceNode->getContent();
// Skip saving null.

@ -100,7 +100,7 @@ use Symfony\Component\Validator\Constraints as Assert;
'id',
'resourceNode.title',
'resourceNode.createdAt',
'resourceNode.resourceFile.size',
'resourceNode.firstResourceFile.size',
'resourceNode.updatedAt',
]
)]

@ -124,8 +124,6 @@ class ResourceFile implements Stringable
protected ?File $file = null;
#[ORM\Column(name: 'crop', type: 'string', length: 255, nullable: true)]
protected ?string $crop = null;
#[ORM\OneToOne(mappedBy: 'resourceFile', targetEntity: ResourceNode::class)]
protected ResourceNode $resourceNode;
/**
* @var string[]
@ -149,6 +147,10 @@ class ResourceFile implements Stringable
#[Gedmo\Timestampable(on: 'update')]
#[ORM\Column(type: 'datetime')]
protected $updatedAt;
#[ORM\ManyToOne(inversedBy: 'resourceFiles')]
private ?ResourceNode $resourceNode = null;
public function __construct()
{
$this->size = 0;
@ -217,16 +219,7 @@ class ResourceFile implements Stringable
return $this;
}
public function getResourceNode(): ResourceNode
{
return $this->resourceNode;
}
public function setResourceNode(ResourceNode $resourceNode): self
{
$this->resourceNode = $resourceNode;
return $this;
}
/*public function isEnabled(): bool
{
return $this->enabled;
@ -339,4 +332,16 @@ class ResourceFile implements Stringable
return $this;
}
public function getResourceNode(): ?ResourceNode
{
return $this->resourceNode;
}
public function setResourceNode(?ResourceNode $resourceNode): static
{
$this->resourceNode = $resourceNode;
return $this;
}
}

@ -68,7 +68,7 @@ use Symfony\Component\Validator\Constraints as Assert;
],
]
)]
#[ApiFilter(filterClass: OrderFilter::class, properties: ['id', 'title', 'resourceFile', 'createdAt', 'updatedAt'])]
#[ApiFilter(filterClass: OrderFilter::class, properties: ['id', 'title', 'createdAt', 'updatedAt'])]
#[ApiFilter(filterClass: PropertyFilter::class)]
#[ApiFilter(filterClass: SearchFilter::class, properties: ['title' => 'partial'])]
class ResourceNode implements Stringable
@ -112,14 +112,6 @@ class ResourceNode implements Stringable
#[ORM\OneToMany(mappedBy: 'resourceNode', targetEntity: ResourceLink::class, cascade: ['persist', 'remove'])]
protected Collection $resourceLinks;
/**
* ResourceFile available file for this node.
*/
#[Groups(['resource_node:read', 'resource_node:write', 'document:read', 'document:write', 'message:read'])]
#[ORM\OneToOne(inversedBy: 'resourceNode', targetEntity: ResourceFile::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\JoinColumn(name: 'resource_file_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
protected ?ResourceFile $resourceFile = null;
#[Assert\NotNull]
#[Groups(['resource_node:read', 'resource_node:write', 'document:write'])]
#[ORM\ManyToOne(targetEntity: User::class, cascade: ['persist'], inversedBy: 'resourceNodes')]
@ -189,6 +181,20 @@ class ResourceNode implements Stringable
#[ORM\Column(type: 'uuid', unique: true)]
protected ?UuidV4 $uuid = null;
/**
* ResourceFile available file for this node.
*
* @var Collection<int, ResourceFile>
*/
#[Groups(['resource_node:read', 'resource_node:write', 'document:read', 'document:write', 'message:read'])]
#[ORM\OneToMany(
mappedBy: 'resourceNode',
targetEntity: ResourceFile::class,
cascade: ['persist', 'remove'],
fetch: 'EXTRA_LAZY',
)]
private Collection $resourceFiles;
public function __construct()
{
$this->public = false;
@ -198,6 +204,7 @@ class ResourceNode implements Stringable
$this->comments = new ArrayCollection();
$this->createdAt = new DateTime();
$this->fileEditableText = false;
$this->resourceFiles = new ArrayCollection();
}
public function __toString(): string
@ -478,8 +485,8 @@ class ResourceNode implements Stringable
public function hasEditableTextContent(): bool
{
if ($this->hasResourceFile()) {
$mimeType = $this->getResourceFile()->getMimeType();
if ($resourceFile = $this->resourceFiles->first()) {
$mimeType = $resourceFile->getMimeType();
if (str_contains($mimeType, 'text')) {
return true;
@ -489,26 +496,7 @@ class ResourceNode implements Stringable
return false;
}
public function hasResourceFile(): bool
{
return null !== $this->resourceFile;
}
public function getResourceFile(): ?ResourceFile
{
return $this->resourceFile;
}
public function setResourceFile(?ResourceFile $resourceFile = null): self
{
$this->resourceFile = $resourceFile;
$resourceFile?->setResourceNode($this);
return $this;
}
public function getIcon(): string
public function getIcon(?string $additionalClass = null): string
{
$class = 'fa fa-folder';
if ($this->hasResourceFile()) {
@ -521,13 +509,17 @@ class ResourceNode implements Stringable
}
}
if ($additionalClass) {
$class .= " $additionalClass";
}
return '<i class="'.$class.'"></i>';
}
public function isResourceFileAnImage(): bool
{
if ($this->hasResourceFile()) {
$mimeType = $this->getResourceFile()->getMimeType();
if ($resourceFile = $this->resourceFiles->first()) {
$mimeType = $resourceFile->getMimeType();
if (str_contains($mimeType, 'image')) {
return true;
}
@ -538,8 +530,8 @@ class ResourceNode implements Stringable
public function isResourceFileAVideo(): bool
{
if ($this->hasResourceFile()) {
$mimeType = $this->getResourceFile()->getMimeType();
if ($resourceFile = $this->resourceFiles->first()) {
$mimeType = $resourceFile->getMimeType();
if (str_contains($mimeType, 'video')) {
return true;
}
@ -550,28 +542,19 @@ class ResourceNode implements Stringable
public function getThumbnail(RouterInterface $router): string
{
$size = 'fa-3x';
$class = sprintf('fa fa-folder %s', $size);
if ($this->hasResourceFile()) {
$class = sprintf('far fa-file %s', $size);
if ($this->isResourceFileAnImage()) {
$class = sprintf('far fa-file-image %s', $size);
$params = [
'id' => $this->getId(),
'tool' => $this->getResourceType()->getTool(),
'type' => $this->getResourceType()->getTitle(),
'filter' => 'editor_thumbnail',
];
$url = $router->generate('chamilo_core_resource_view', $params);
return sprintf("<img src='%s'/>", $url);
}
if ($this->isResourceFileAVideo()) {
$class = sprintf('far fa-file-video %s', $size);
}
if ($this->isResourceFileAnImage()) {
$params = [
'id' => $this->getId(),
'tool' => $this->getResourceType()->getTool(),
'type' => $this->getResourceType()->getTitle(),
'filter' => 'editor_thumbnail',
];
$url = $router->generate('chamilo_core_resource_view', $params);
return sprintf("<img src='%s'/>", $url);
}
return '<i class="'.$class.'"></i>';
return $this->getIcon('fa-3x');
}
/**
@ -629,4 +612,45 @@ class ResourceNode implements Stringable
return $this;
}
public function hasResourceFile(): bool
{
return $this->resourceFiles->count() > 0;
}
/**
* @return Collection<int, ResourceFile>
*/
public function getResourceFiles(): Collection
{
return $this->resourceFiles;
}
public function addResourceFile(ResourceFile $resourceFile): static
{
if (!$this->resourceFiles->contains($resourceFile)) {
$this->resourceFiles->add($resourceFile);
$resourceFile->setResourceNode($this);
}
return $this;
}
public function removeResourceFile(ResourceFile $resourceFile): static
{
if ($this->resourceFiles->removeElement($resourceFile)) {
// set the owning side to null (unless already changed)
if ($resourceFile->getResourceNode() === $this) {
$resourceFile->setResourceNode(null);
}
}
return $this;
}
#[Groups(['resource_node:read', 'document:read', 'message:read'])]
public function getFirstResourceFile(): ?ResourceFile
{
return $this->resourceFiles->first() ?: null;
}
}

@ -20,13 +20,14 @@ class Version20170525122900 extends AbstractMigrationChamilo
{
if (false === $schema->hasTable('resource_file')) {
$this->addSql(
'CREATE TABLE resource_file (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, original_name LONGTEXT DEFAULT NULL, size INT NOT NULL, dimensions LONGTEXT DEFAULT NULL COMMENT "(DC2Type:simple_array)",crop VARCHAR(255) DEFAULT NULL, mime_type LONGTEXT DEFAULT NULL, description longtext DEFAULT NULL, metadata LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:array)\', created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB ROW_FORMAT = DYNAMIC'
'CREATE TABLE resource_file (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, original_name LONGTEXT DEFAULT NULL, size INT NOT NULL, dimensions LONGTEXT DEFAULT NULL COMMENT "(DC2Type:simple_array)", crop VARCHAR(255) DEFAULT NULL, mime_type LONGTEXT DEFAULT NULL, description longtext DEFAULT NULL, metadata LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:array)\', ADD resource_node_id INT DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX IDX_83BF96AA1BAD783F (resource_node_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB ROW_FORMAT = DYNAMIC'
);
$this->addSql('ALTER TABLE resource_file ADD CONSTRAINT FK_83BF96AA1BAD783F FOREIGN KEY (resource_node_id) REFERENCES resource_node (id)');
}
if (false === $schema->hasTable('resource_node')) {
$this->addSql(
'CREATE TABLE IF NOT EXISTS resource_node (id INT AUTO_INCREMENT NOT NULL, resource_type_id INT NOT NULL, resource_file_id INT DEFAULT NULL, creator_id INT NOT NULL, parent_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, level INT DEFAULT NULL, path VARCHAR(3000) DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX IDX_8A5F48FF98EC6B7B (resource_type_id), UNIQUE INDEX UNIQ_8A5F48FFCE6B9E84 (resource_file_id), INDEX IDX_8A5F48FF61220EA6 (creator_id), INDEX IDX_8A5F48FF727ACA70 (parent_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB ROW_FORMAT = DYNAMIC;'
'CREATE TABLE IF NOT EXISTS resource_node (id INT AUTO_INCREMENT NOT NULL, resource_type_id INT NOT NULL, creator_id INT NOT NULL, parent_id INT DEFAULT NULL, title VARCHAR(255) NOT NULL, level INT DEFAULT NULL, path VARCHAR(3000) DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX IDX_8A5F48FF98EC6B7B (resource_type_id), INDEX IDX_8A5F48FF61220EA6 (creator_id), INDEX IDX_8A5F48FF727ACA70 (parent_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB ROW_FORMAT = DYNAMIC;'
);
$this->addSql(
'ALTER TABLE resource_node ADD slug VARCHAR(255) NOT NULL, ADD uuid BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', CHANGE creator_id creator_id INT DEFAULT NULL, CHANGE path path LONGTEXT DEFAULT NULL'
@ -141,10 +142,6 @@ class Version20170525122900 extends AbstractMigrationChamilo
'ALTER TABLE resource_node ADD CONSTRAINT FK_8A5F48FF98EC6B7B FOREIGN KEY (resource_type_id) REFERENCES resource_type (id);'
);
$this->addSql(
'ALTER TABLE resource_node ADD CONSTRAINT FK_8A5F48FFCE6B9E84 FOREIGN KEY (resource_file_id) REFERENCES resource_file (id) ON DELETE CASCADE'
);
$this->addSql(
'ALTER TABLE resource_node ADD CONSTRAINT FK_8A5F48FF61220EA6 FOREIGN KEY (creator_id) REFERENCES user (id) ON DELETE CASCADE;'
);

@ -169,7 +169,7 @@ final class Version20201215142610 extends AbstractMigrationChamilo
/** @var CDocument $document */
$document = $documentRepo->find($pictureId);
if ($document && $document->hasResourceNode() && $document->getResourceNode()->hasResourceFile()) {
$resourceFile = $document->getResourceNode()->getResourceFile();
$resourceFile = $document->getResourceNode()->getResourceFiles()->first();
$contents = $documentRepo->getResourceFileContent($document);
$quizQuestionRepo->addFileFromString($question, $resourceFile->getOriginalName(), $resourceFile->getMimeType(), $contents);
}

@ -9,9 +9,9 @@ namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
use Chamilo\CourseBundle\Entity\CDocument;
use Chamilo\CourseBundle\Repository\CDocumentRepository;
use Chamilo\CoreBundle\Repository\ResourceNodeRepository;
use Doctrine\DBAL\Schema\Schema;
use Exception;
use RecursiveDirectoryIterator;
@ -103,27 +103,38 @@ final class Version20230913162700 extends AbstractMigrationChamilo
$items = $result->fetchAllAssociative();
foreach ($items as $item) {
/** @var CDocument $document */
$document = $documentRepo->find($item['iid']);
if ($document) {
$resourceNode = $document->getResourceNode();
if ($resourceNode && $resourceNode->hasResourceFile()) {
$resourceFile = $resourceNode->getResourceFile();
$filePath = $resourceFile->getTitle();
if ($resourceFile && $resourceFile->getMimeType() === 'text/html') {
error_log("Verifying HTML file: " . $filePath);
try {
$content = $resourceNodeRepo->getResourceNodeFileContent($resourceNode);
$updatedContent = $this->replaceOldURLsWithNew($content, $courseDirectory, $courseId, $documentRepo);
if ($content !== $updatedContent) {
$documentRepo->updateResourceFileContent($document, $updatedContent);
$documentRepo->update($document);
}
} catch (\Exception $e) {
error_log("Error processing file $filePath: " . $e->getMessage());
}
if (!$document) {
continue;
}
$resourceNode = $document->getResourceNode();
if (!$resourceNode || !$resourceNode->hasResourceFile()) {
continue;
}
$resourceFile = $resourceNode->getResourceFiles()->first();
if (!$resourceFile) {
continue;
}
$filePath = $resourceFile->getTitle();
if ('text/html' === $resourceFile->getMimeType()) {
error_log('Verifying HTML file: '.$filePath);
try {
$content = $resourceNodeRepo->getResourceNodeFileContent($resourceNode);
$updatedContent = $this->replaceOldURLsWithNew($content, $courseDirectory, $courseId, $documentRepo);
if ($content !== $updatedContent) {
$documentRepo->updateResourceFileContent($document, $updatedContent);
$documentRepo->update($document);
}
} catch (Exception $e) {
error_log("Error processing file $filePath: ".$e->getMessage());
}
}
}

@ -57,7 +57,7 @@ final class Version20240112131200 extends AbstractMigrationChamilo
if ($schema->hasTable('resource_node')) {
error_log('Perform the changes in the resource_node table');
$this->addSql('ALTER TABLE resource_node CHANGE id id INT AUTO_INCREMENT NOT NULL, CHANGE resource_file_id resource_file_id INT DEFAULT NULL, CHANGE parent_id parent_id INT DEFAULT NULL;');
$this->addSql('ALTER TABLE resource_node CHANGE id id INT AUTO_INCREMENT NOT NULL, CHANGE parent_id parent_id INT DEFAULT NULL;');
}
if ($schema->hasTable('resource_file')) {

@ -53,8 +53,7 @@ final class Version20240515124400 extends AbstractMigrationChamilo
$resourceFile = $resourceFileRepository->findOneBy(['originalName' => $fileName]);
if ($resourceFile) {
$resourceFileId = $resourceFile->getId();
$resourceNode = $resourceNodeRepository->findOneBy(['resourceFile' => $resourceFileId]);
$resourceNode = $resourceFile->getResourceNode();
if ($resourceNode) {
$downUserId = $download->getDownUserId();
@ -66,7 +65,7 @@ final class Version20240515124400 extends AbstractMigrationChamilo
$firstResourceLink = $resourceNode->getResourceLinks()->first();
if ($firstResourceLink && $user) {
$resourceLinkId = $firstResourceLink->getId();
$url = $resourceNode->getResourceFile()->getOriginalName();
$url = $resourceNode->getResourceFiles()->first()->getOriginalName();
echo "Resource link $resourceLinkId Down id {$download->getDownId()} for $url: user ".$user->getFullname()."\n";
$this->connection->executeUpdate('UPDATE track_e_downloads SET resource_link_id = ? WHERE down_id = ?', [$resourceLinkId, $download->getDownId()]);

@ -0,0 +1,61 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
final class Version20240702222600 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Migration resource_node --* resource_file (during development)';
}
public function up(Schema $schema): void
{
$tblResourceFile = $schema->getTable('resource_file');
$tblResourceNode = $schema->getTable('resource_node');
if (!$tblResourceFile->hasColumn('resource_node_id')) {
$this->addSql('ALTER TABLE resource_file ADD resource_node_id INT DEFAULT NULL');
$result = $this->connection->executeQuery("SELECT id, resource_file_id FROM resource_node");
$resourceNodeRows = $result->fetchAllAssociative();
foreach ($resourceNodeRows as $resourceNodeRow) {
$this->addSql(
sprintf(
"UPDATE resource_file SET resource_node_id = %d WHERE id = %d",
$resourceNodeRow['id'],
$resourceNodeRow['resource_file_id']
)
);
}
}
if (!$tblResourceFile->hasForeignKey('FK_83BF96AA1BAD783F')) {
$this->addSql('ALTER TABLE resource_file ADD CONSTRAINT FK_83BF96AA1BAD783F FOREIGN KEY (resource_node_id) REFERENCES resource_node (id)');
}
if (!$tblResourceFile->hasIndex('IDX_83BF96AA1BAD783F')) {
$this->addSql('CREATE INDEX IDX_83BF96AA1BAD783F ON resource_file (resource_node_id)');
}
if ($tblResourceNode->hasForeignKey('FK_8A5F48FFCE6B9E84')) {
$this->addSql('ALTER TABLE resource_node DROP FOREIGN KEY FK_8A5F48FFCE6B9E84');
}
if ($tblResourceNode->hasIndex('UNIQ_8A5F48FFCE6B9E84')) {
$this->addSql('DROP INDEX UNIQ_8A5F48FFCE6B9E84 ON resource_node');
}
if ($tblResourceNode->hasColumn('resource_file_id')) {
$this->addSql('ALTER TABLE resource_node DROP resource_file_id');
}
}
}

@ -92,7 +92,7 @@ final class IllustrationRepository extends ResourceRepository
->select('node')
->from(ResourceNode::class, 'node')
->innerJoin('node.resourceType', 'type')
->innerJoin('node.resourceFile', 'file')
->innerJoin('node.resourceFiles', 'file')
->where('node.parent = :parent')
->andWhere('type.title = :name')
->setParameters([

@ -306,10 +306,11 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
} elseif (method_exists($record, $setter)) {
$valueToSet = 'object' === $relation['type'] ? $fallbackUser : $fallbackUser->getId();
$record->{$setter}($valueToSet);
if (method_exists($record, 'getResourceFile') && $record->getResourceFile()) {
$resourceFile = $record->getResourceFile();
if (!$em->contains($resourceFile)) {
$em->persist($resourceFile);
if (method_exists($record, 'getResourceFiles')) {
foreach ($record->getResourceFiles() as $resourceFile) {
if (!$em->contains($resourceFile)) {
$em->persist($resourceFile);
}
}
}
$em->persist($record);

@ -66,8 +66,9 @@ class ResourceNodeRepository extends MaterializedPathRepository
public function getResourceNodeFileContent(ResourceNode $resourceNode): string
{
try {
if ($resourceNode->hasResourceFile()) {
$resourceFile = $resourceNode->getResourceFile();
$resourceFile = $resourceNode->getResourceFiles()->first();
if ($resourceFile) {
$fileName = $this->getFilename($resourceFile);
return $this->getFileSystem()->read($fileName);
@ -85,8 +86,9 @@ class ResourceNodeRepository extends MaterializedPathRepository
public function getResourceNodeFileStream(ResourceNode $resourceNode)
{
try {
if ($resourceNode->hasResourceFile()) {
$resourceFile = $resourceNode->getResourceFile();
$resourceFile = $resourceNode->getResourceFiles()->first();
if ($resourceFile) {
$fileName = $this->getFilename($resourceFile);
return $this->getFileSystem()->readStream($fileName);
@ -140,7 +142,7 @@ class ResourceNodeRepository extends MaterializedPathRepository
{
$qb = $this->createQueryBuilder('node')
->select('SUM(file.size) as total')
->innerJoin('node.resourceFile', 'file')
->innerJoin('node.resourceFiles', 'file')
->innerJoin('node.resourceLinks', 'l')
->where('node.resourceType = :type')
->andWhere('node.parent = :parentNode')

@ -95,8 +95,7 @@ abstract class ResourceRepository extends ServiceEntityRepository
$resourceNode = $resource->getResourceNode();
$resourceName = $resource->getResourceName();
if ($resourceNode->hasResourceFile()) {
$resourceFile = $resourceNode->getResourceFile();
foreach ($resourceNode->getResourceFiles() as $resourceFile) {
if (null !== $resourceFile) {
$originalName = $resourceFile->getOriginalName();
$originalExtension = pathinfo($originalName, PATHINFO_EXTENSION);
@ -236,20 +235,16 @@ abstract class ResourceRepository extends ServiceEntityRepository
throw new LogicException('Resource node is null');
}
$resourceFile = $resourceNode->getResourceFile();
if (null === $resourceFile) {
$resourceFile = new ResourceFile();
}
$em = $this->getEntityManager();
$resourceFile = new ResourceFile();
$resourceFile
->setFile($file)
->setDescription($description)
->setTitle($resource->getResourceName())
->setResourceNode($resourceNode)
;
$em->persist($resourceFile);
$resourceNode->setResourceFile($resourceFile);
$resourceNode->addResourceFile($resourceFile);
$em->persist($resourceNode);
if ($flush) {
@ -371,7 +366,7 @@ abstract class ResourceRepository extends ServiceEntityRepository
->innerJoin('resource.resourceNode', 'node')
->innerJoin('node.resourceLinks', 'links')
->innerJoin('node.resourceType', 'type')
->leftJoin('node.resourceFile', 'file')
->leftJoin('node.resourceFiles', 'file')
->where('type.title = :type')
->setParameter('type', $resourceTypeName, Types::STRING)
->addSelect('node')
@ -488,7 +483,6 @@ abstract class ResourceRepository extends ServiceEntityRepository
->innerJoin('resource.resourceNode', 'node')
// ->innerJoin('node.creator', 'userCreator')
->leftJoin('node.resourceLinks', 'links')
// ->leftJoin('node.resourceFile', 'file')
->where('node.id = :id')
->setParameters([
'id' => $resourceNodeId,
@ -503,8 +497,8 @@ abstract class ResourceRepository extends ServiceEntityRepository
$em = $this->getEntityManager();
$children = $resource->getResourceNode()->getChildren();
foreach ($children as $child) {
if ($child->hasResourceFile()) {
$em->remove($child->getResourceFile());
foreach ($child->getResourceFiles() as $resourceFile) {
$em->remove($resourceFile);
}
$resourceNode = $this->getResourceFromResourceNode($child->getId());
if (null !== $resourceNode) {
@ -572,7 +566,9 @@ abstract class ResourceRepository extends ServiceEntityRepository
$resourceNode = $resource->getResourceNode();
if ($resourceNode->hasResourceFile()) {
$resourceNode->setContent($content);
$resourceNode->getResourceFile()->setSize(\strlen($content));
foreach ($resourceNode->getResourceFiles() as $resourceFile) {
$resourceFile->setSize(\strlen($content));
}
return true;
}
@ -587,20 +583,6 @@ abstract class ResourceRepository extends ServiceEntityRepository
$resourceNode = $resource->getResourceNode();
$resourceNode->setTitle($title);
}
// if ($resourceNode->hasResourceFile()) {
// $resourceNode->getResourceFile()->getFile()->
// $resourceNode->getResourceFile()->setTitle($title);
// $resourceFile->setTitle($title);
/*$fileName = $this->getResourceNodeRepository()->getFilename($resourceFile);
error_log('$fileName');
error_log($fileName);
error_log($title);
$this->getResourceNodeRepository()->getFileSystem()->rename($fileName, $title);
$resourceFile->setTitle($title);
$resourceFile->setOriginalName($title);*/
// }
}
public function toggleVisibilityPublishedDraft(AbstractResource $resource): void
@ -712,7 +694,7 @@ abstract class ResourceRepository extends ServiceEntityRepository
->select('SUM(file.size) as total')
->innerJoin('resource.resourceNode', 'node')
->innerJoin('node.resourceLinks', 'l')
->innerJoin('node.resourceFile', 'file')
->innerJoin('node.resourceFiles', 'file')
->where('l.course = :course')
->andWhere('file IS NOT NULL')
->setParameters(

@ -3,7 +3,7 @@
{% block content %}
{% autoescape false %}
<h3>{{ resource }} </h3>
{% if resource.resourceNode.resourceFile %}
{% if resource.resourceNode.firstResourceFile %}
{% if resource.resourceNode.isResourceFileAnImage %}
Preview:
<br />
@ -43,7 +43,7 @@
{{ resource.resourceNode.slug }}
</a>
<br />
Size: {{ resource.resourceNode.resourceFile.size }}
Size: {{ resource.resourceNode.firstResourceFile.size }}
<br /><br />
{% endif %}
{% endautoescape %}

@ -1,6 +1,6 @@
{% autoescape false %}
{% if resource.resourceNode.resourceFile %}
{% if resource.resourceNode.firstResourceFile %}
<a class="btn btn--success"
href="{{ url('chamilo_core_resource_view', {
'id': resource.resourceNode.id,
@ -12,7 +12,7 @@
{{ 'Download' | trans}} {# {{ resource.resourceNode.slug }}#}
</a>
<br />
{{ 'Size' | trans }}: {{ resource.resourceNode.resourceFile.size | format_file_size }}
{{ 'Size' | trans }}: {{ resource.resourceNode.firstResourceFile.size | format_file_size }}
<br /><br />
{% endif %}

@ -33,7 +33,7 @@
}}"
/>
{{ comment.resourceNode.resourceFile }}
{{ comment.resourceNode.firstResourceFile }}
</a>
{# {% if is_allowed_to_edit %}#}
{# <a href="{{ comment.delete_file_url }}">#}

@ -38,7 +38,7 @@ final class MessageProcessor implements ProcessorInterface
$message = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
foreach ($message->getAttachments() as $attachment) {
$attachment->resourceNode->setResourceFile(
$attachment->resourceNode->addResourceFile(
$attachment->getResourceFileToAttach()
);

@ -135,7 +135,7 @@ use Symfony\Component\Validator\Constraints as Assert;
'filetype',
'resourceNode.title',
'resourceNode.createdAt',
'resourceNode.resourceFile.size',
'resourceNode.firstResourceFile.size',
'resourceNode.updatedAt',
]
)]

@ -44,7 +44,7 @@ class ResourceControllerTest extends WebTestCase
/** @var CDocument $document */
$document = $documentRepo->find($document->getIid());
$resourceFile = $document->getResourceNode()->getResourceFile();
$resourceFile = $document->getResourceNode()->getResourceFiles()->first();
$this->assertNotNull($resourceFile);
$nodeId = $course->getResourceNode()->getId();
@ -129,7 +129,7 @@ class ResourceControllerTest extends WebTestCase
$node = $document->getResourceNode();
$this->assertTrue($node->hasResourceFile());
$id = $document->getResourceNode()->getUuid()->toRfc4122();
$this->assertSame('text/html', $node->getResourceFile()->getMimeType());
$this->assertSame('text/html', $node->getResourceFiles()->first()->getMimeType());
// View HTML document.
$url = '/r/document/files/'.$id.'/view';

@ -346,9 +346,11 @@ class MessageRepositoryTest extends AbstractApiTest
'@type' => 'http://schema.org/MediaObject',
'resourceNode' => [
'@type' => 'ResourceNode',
'resourceFile' => [
'@type' => 'http://schema.org/MediaObject',
'@id' => $resourceFileId,
'resourceFiles' => [
[
'@type' => 'http://schema.org/MediaObject',
'@id' => $resourceFileId,
],
],
],
],

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\Tests\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\MessageTag;
use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CoreBundle\Repository\MessageTagRepository;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
use Chamilo\Tests\AbstractApiTest;
@ -19,6 +20,7 @@ class MessageTagRepositoryTest extends AbstractApiTest
public function testCreateTagAndDeleteUser(): void
{
Container::$container = self::getContainer();
$tagRepo = self::getContainer()->get(MessageTagRepository::class);
$testUser = $this->createUser('test');

@ -228,7 +228,7 @@ class ResourceNodeRepositoryTest extends AbstractApiTest
;
$em->persist($resourceFile);
$resourceNode->setContent('')->setResourceFile($resourceFile);
$resourceNode->setContent('')->addResourceFile($resourceFile);
$em->persist($resourceNode);
$em->flush();

@ -96,7 +96,7 @@ class CForumPostRepositoryTest extends AbstractApiTest
$this->assertSame($attachment->getResourceIdentifier(), $attachment->getIid());
$this->assertSame($file->getFilename(), (string) $attachment);
$this->assertNotNull($attachment->getResourceNode());
$this->assertNotNull($attachment->getResourceNode()->getResourceFile());
$this->assertNotNull($attachment->getResourceNode()->getResourceFiles()->first());
$this->getEntityManager()->clear();

@ -193,9 +193,8 @@ while ($row = Database::fetch_assoc($result)) {
$resourceFile = new ResourceFile();
$resourceFile->setMedia($media);
$resourceFile->setTitle($documentData['title']);
$node->setResourceFile($resourceFile);
$node->addResourceFile($resourceFile);
$em->persist($resourceFile);
$em->persist($node);
break;
}

Loading…
Cancel
Save