Merge branch 'master' of github.com:chamilo/chamilo-lms

pull/5012/head
Yannick Warnier 2 years ago
commit 9948f1d442
  1. 11
      assets/css/app.scss
  2. 71
      assets/vue/components/ccalendarevent/CCalendarEventForm.vue
  3. 16
      assets/vue/components/ccalendarevent/CCalendarEventInfo.vue
  4. 21
      assets/vue/components/documents/FormNewDocument.vue
  5. 236
      assets/vue/composables/sidebarMenu.js
  6. 10
      assets/vue/mixins/CreateMixin.js
  7. 123
      assets/vue/mixins/NotificationMixin.js
  8. 33
      assets/vue/store/enrolledStore.js
  9. 215
      assets/vue/views/ccalendarevent/CCalendarEventCreate.vue
  10. 8
      config/services.yaml
  11. 2
      public/main/exercise/exercise_result.php
  12. 42
      public/main/inc/lib/formvalidator/Element/DateTimePicker.php
  13. 9
      public/main/inc/lib/sessionmanager.lib.php
  14. 1
      public/main/lp/lp_view.php
  15. 9
      public/main/search/load_search.php
  16. 35
      public/main/session/session_add.php
  17. 6
      src/CoreBundle/Controller/ContactCategoryController.php
  18. 6
      src/CoreBundle/Controller/ContactController.php
  19. 52
      src/CoreBundle/Controller/CourseController.php
  20. 1
      src/CoreBundle/Controller/PlatformConfigurationController.php
  21. 19
      src/CoreBundle/Entity/ContactCategory.php
  22. 2
      src/CoreBundle/Entity/TrackELogin.php
  23. 83
      src/CoreBundle/EventListener/CourseListener.php
  24. 154
      src/CoreBundle/EventSubscriber/AnonymousUserSubscriber.php
  25. 4
      src/CoreBundle/Form/ContactCategoryType.php
  26. 14
      src/CoreBundle/Form/ContactType.php
  27. 18
      src/CoreBundle/Migrations/Schema/V200/Version20170625145000.php
  28. 4
      src/CoreBundle/Migrations/Schema/V200/Version20201212114910.php
  29. 3
      src/CoreBundle/Migrations/Schema/V200/Version20201212203625.php
  30. 21
      src/CoreBundle/Migrations/Schema/V200/Version20230216122950.php
  31. 178
      src/CoreBundle/Migrations/Schema/V200/Version20230904173400.php
  32. 34
      src/CoreBundle/Migrations/Schema/V200/Version20231110194300.php
  33. 30
      src/CoreBundle/Security/Authorization/Voter/AnonymousVoter.php
  34. 29
      src/CoreBundle/Settings/PlatformSettingsSchema.php
  35. 12
      src/CoreBundle/Settings/StylesheetsSettingsSchema.php
  36. 69
      src/CourseBundle/Entity/CCalendarEvent.php
  37. 0
      var/log/.gitkeep
  38. 10
      webpack.config.js

@ -451,6 +451,17 @@ table#skill_holder {
padding: 20px 0px;
}
#date_fields label {
top: 0px;
left: 0.5rem;
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
padding-left: 0.25rem;
padding-right: 0.25rem;
--tw-text-opacity: 1;
color: rgb(46 117 163 / var(--tw-text-opacity));
}
/* ****************************************************
END SKILL
**************************************************** */

@ -1,14 +1,14 @@
<template>
<form>
<BaseInputText
v-model="v$.item.title.$model"
:error-text="v$.item.title.$errors.map((error) => error.$message).join('<br>')"
:is-invalid="v$.item.title.$error"
:label="t('Title')"
/>
<div class="flex flex-col md:flex-row gap-x-5">
<div class="md:w-1/2 flex flex-col">
<div class="grid lg:grid-cols-2 md:gap-4">
<BaseInputText
v-model="v$.item.title.$model"
:error-text="v$.item.title.$errors.map((error) => error.$message).join('<br>')"
:is-invalid="v$.item.title.$error"
:label="t('Title')"
/>
<div class="grid md:grid-cols-2 md:gap-4">
<div class="field">
<div class="p-float-label">
<Calendar
@ -51,31 +51,36 @@
class="p-error"
/>
</div>
</div>
</div>
<tiny-editor
v-model="v$.item.content.$model"
:init="{
skin_url: '/build/libs/tinymce/skins/ui/oxide',
content_css: '/build/libs/tinymce/skins/content/default/content.css',
branding: false,
relative_urls: false,
height: 250,
toolbar_mode: 'sliding',
file_picker_callback: browser,
autosave_ask_before_unload: true,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste wordcount emoticons',
],
toolbar:
'undo redo | bold italic underline strikethrough | insertfile image media template link | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | pagebreak | charmap emoticons | fullscreen preview save print | code codesample | ltr rtl',
<div class="grid md:grid-cols-2 md:gap-4">
<tiny-editor
v-model="v$.item.content.$model"
:init="{
skin_url: '/build/libs/tinymce/skins/ui/oxide',
content_css: '/build/libs/tinymce/skins/content/default/content.css',
branding: false,
relative_urls: false,
height: 250,
toolbar_mode: 'sliding',
file_picker_callback: browser,
autosave_ask_before_unload: true,
plugins: [
'advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste wordcount emoticons',
],
toolbar:
'undo redo | bold italic underline strikethrough | insertfile image media template link | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | pagebreak | charmap emoticons | fullscreen preview save print | code codesample | ltr rtl',
}"
required
/>
</div>
required
/>
<div class="md:w-1/2 flex flex-col">
<div
v-if="agendaCollectiveInvitations"
class="flex flex-col"
>
<div
v-t="'Invitees'"
class="text-h6"
@ -112,8 +117,12 @@ import Calendar from "primevue/calendar"
import EditLinks from "../resource_links/EditLinks.vue"
import BaseCheckbox from "../basecomponents/BaseCheckbox.vue"
import { useI18n } from "vue-i18n"
import { usePlatformConfig } from "../../store/platformConfig"
const store = useStore()
const platformConfigStore = usePlatformConfig();
const agendaCollectiveInvitations = 'true' === platformConfigStore.getSetting('agenda.agenda_collective_invitations')
const { t } = useI18n()

@ -13,12 +13,16 @@
<div v-html="event.content" />
<h6 v-t="'Invitees'" />
<ShowLinks
:item="event"
:show-status="false"
/>
<div
v-if="event.resourceLinkListFromEntity && event.resourceLinkListFromEntity.length"
>
<h6 v-t="'Invitees'" />
<ShowLinks
:item="event"
:show-status="false"
/>
</div>
</div>
</template>

@ -1,14 +1,17 @@
<template>
<q-form>
<q-input
id="item_title"
v-model="item.title"
:error="v$.item.title.$error"
:error-message="titleErrors"
:placeholder="$t('Title')"
@blur="v$.item.title.$touch()"
@input="v$.item.title.$touch()"
/>
<div class="p-field">
<InputText
id="item_title"
v-model.trim="item.title"
:error="v$.item.title.$error"
:error-message="titleErrors"
:placeholder="$t('Title')"
@blur="v$.item.title.$touch()"
@input="v$.item.title.$touch()"
/>
</div>
<TinyEditor
v-if="

@ -1,150 +1,178 @@
import { useI18n } from "vue-i18n"
import { ref, onMounted, watch } from "vue"
import { useSecurityStore } from "../store/securityStore"
import { usePlatformConfig } from "../store/platformConfig"
import { useEnrolledStore } from "../store/enrolledStore"
export function useSidebarMenu() {
const { t } = useI18n()
const securityStore = useSecurityStore()
const platformConfigStore = usePlatformConfig()
const enrolledStore = useEnrolledStore()
const showTabsSetting = platformConfigStore.getSetting("platform.show_tabs")
const items = []
const items = ref([])
const coursesItems = ref([])
if (showTabsSetting.indexOf("campus_homepage") > -1) {
items.push({
icon: "mdi mdi-home",
label: t("Home"),
to: { name: "Home" },
})
}
const updateItems = () => {
items.value = []
if (securityStore.isAuthenticated) {
if (showTabsSetting.indexOf("campus_homepage") > -1) {
items.push({
icon: "mdi mdi-book-open-page-variant",
items.value.push({
icon: "mdi mdi-home",
label: t("Home"),
to: { name: "Home" },
})
}
if (securityStore.isAuthenticated) {
if (coursesItems.value.length > 0) {
const coursesMenu = {
icon: "mdi mdi-book-open-page-variant",
label: coursesItems.value.length > 1 ? t("Courses") : coursesItems.value[0].label,
items: coursesItems.value.length > 1 ? coursesItems.value : undefined,
to: coursesItems.value.length === 1 ? coursesItems.value[0].to : undefined,
}
items.value.push(coursesMenu)
}
if (showTabsSetting.indexOf("my_agenda") > -1) {
items.value.push({
icon: "mdi mdi-calendar-text",
label: t("Events"),
to: { name: "CCalendarEventList" },
})
}
if (showTabsSetting.indexOf("reporting") > -1) {
let subItems = []
if (securityStore.isTeacher || securityStore.isHRM || securityStore.isSessionAdmin) {
subItems.push({
label: securityStore.isHRM ? t("Course sessions") : t("Reporting"),
url: "/main/my_space/" + (securityStore.isHRM ? "session.php" : "index.php"),
})
} else if (securityStore.isStudentBoss) {
subItems.push({
label: t("Learners"),
url: "/main/my_space/student.php",
})
} else {
subItems.push({
label: t("Progress"),
url: "/main/auth/my_progress.php",
})
}
items.value.push({
icon: "mdi mdi-chart-box",
label: t("Reporting"),
items: subItems,
})
}
if (showTabsSetting.indexOf("social") > -1) {
items.value.push({
icon: "mdi mdi-sitemap-outline",
label: t("Social network"),
to: { name: "SocialWall" },
})
}
if (platformConfigStore.plugins?.bbb?.show_global_conference_link) {
items.value.push({
icon: "mdi mdi-video",
label: t("Videoconference"),
url: platformConfigStore.plugins.bbb.listingURL,
})
}
}
if (securityStore.isStudentBoss || securityStore.isStudent) {
items.value.push({
icon: "mdi mdi-text-box-search",
items: [
{
label: t("My courses"),
to: { name: "MyCourses" },
label: t("Diagnosis Management"),
url: "/main/search/load_search.php",
visible: securityStore.isStudentBoss,
},
{
label: t("My sessions"),
to: { name: "MySessions" },
label: t("Diagnostic Form"),
url: "/main/search/search.php",
},
],
label: t("Courses"),
})
}
if (showTabsSetting.indexOf("my_agenda") > -1) {
items.push({
icon: "mdi mdi-calendar-text",
label: t("Events"),
to: { name: "CCalendarEventList" },
label: t("Diagnosis"),
})
}
if (showTabsSetting.indexOf("reporting") > -1) {
let subItems = []
if (securityStore.isAdmin || securityStore.isSessionAdmin) {
const adminItems = [
{
label: t("Administration"),
to: { name: "AdminIndex" },
},
]
if (securityStore.isTeacher || securityStore.isHRM || securityStore.isSessionAdmin) {
subItems.push({
label: securityStore.isHRM ? t("Course sessions") : t("Reporting"),
url: "/main/my_space/" + (securityStore.isHRM ? "session.php" : "index.php"),
})
} else if (securityStore.isStudentBoss) {
subItems.push({
label: t("Learners"),
url: "/main/my_space/student.php",
if (securityStore.isSessionAdmin && 'true' === platformConfigStore.getSetting('session.limit_session_admin_list_users')) {
adminItems.push({
label: t("Add user"),
url: "/main/admin/user_add.php",
})
} else {
subItems.push({
label: t("Progress"),
url: "/main/auth/my_progress.php",
adminItems.push({
label: t("Users"),
url: "/main/admin/user_list.php",
})
}
items.push({
icon: "mdi mdi-chart-box",
label: t("Reporting"),
items: subItems,
})
}
if (securityStore.isAdmin) {
adminItems.push({
label: t("Courses"),
url: "/main/admin/course_list.php",
})
}
if (showTabsSetting.indexOf("social") > -1) {
items.push({
icon: "mdi mdi-sitemap-outline",
label: t("Social network"),
to: { name: "SocialWall" },
adminItems.push({
label: t("Sessions"),
url: "/main/session/session_list.php",
})
}
if (platformConfigStore.plugins?.bbb?.show_global_conference_link) {
items.push({
icon: "mdi mdi-video",
label: t("Videoconference"),
url: platformConfigStore.plugins.bbb.listingURL,
items.value.push({
icon: "mdi mdi-cog",
items: adminItems,
label: t("Administration"),
})
}
}
if (securityStore.isStudentBoss || securityStore.isStudent) {
items.push({
icon: "mdi mdi-text-box-search",
items: [
{
label: t("Diagnosis Management"),
url: "/main/search/load_search.php",
visible: securityStore.isStudentBoss,
},
{
label: t("Diagnostic Form"),
url: "/main/search/search.php",
},
],
label: t("Diagnosis"),
})
}
if (securityStore.isAdmin || securityStore.isSessionAdmin) {
const adminItems = [
{
label: t("Administration"),
to: { name: "AdminIndex" },
},
]
const updateCoursesItems = () => {
coursesItems.value = [];
if (securityStore.isSessionAdmin && 'true' === platformConfigStore.getSetting('session.limit_session_admin_list_users')) {
adminItems.push({
label: t("Add user"),
url: "/main/admin/user_add.php",
})
} else {
adminItems.push({
label: t("Users"),
url: "/main/admin/user_list.php",
if (enrolledStore.isEnrolledInCourses) {
coursesItems.value.push({
label: t("My courses"),
to: { name: "MyCourses" },
})
}
if (securityStore.isAdmin) {
adminItems.push({
label: t("Courses"),
url: "/main/admin/course_list.php",
if (enrolledStore.isEnrolledInSessions) {
coursesItems.value.push({
label: t("My sessions"),
to: { name: "MySessions" },
})
}
adminItems.push({
label: t("Sessions"),
url: "/main/session/session_list.php",
})
items.push({
icon: "mdi mdi-cog",
items: adminItems,
label: t("Administration"),
})
updateItems();
}
onMounted(async () => {
await enrolledStore.initialize();
updateCoursesItems();
})
watch(() => enrolledStore.isEnrolledInCourses, updateCoursesItems)
watch(() => enrolledStore.isEnrolledInSessions, updateCoursesItems)
return items
}

@ -8,12 +8,18 @@ export default {
methods: {
formatDateTime,
onCreated(item) {
let message;
if (item['resourceNode']) {
this.showMessage(this.$i18n.t('{resource} created', {'resource': item['resourceNode'].title}));
message = this.$i18n && this.$i18n.t
? this.$i18n.t('{resource} created', {'resource': item['resourceNode'].title})
: `${item['resourceNode'].title} created`;
} else {
this.showMessage(this.$i18n.t('{resource} created', {'resource': item.title}));
message = this.$i18n && this.$i18n.t
? this.$i18n.t('{resource} created', {'resource': item.title})
: `${item.title} created`;
}
this.showMessage(message);
let folderParams = this.$route.query;
this.$router.push({

@ -1,105 +1,28 @@
import {mapFields} from 'vuex-map-fields';
import Snackbar from "../components/Snackbar.vue";
//import { useToast } from "vue-toastification";
// inside of a Vue file
//import { useQuasar } from 'quasar'
import { mapFields } from 'vuex-map-fields';
export default {
setup() {
},
computed: {
...mapFields('notifications', ['color', 'show', 'subText', 'text', 'timeout'])
methods: {
showError(error) {
this.showMessage(error, 'error'); // Use 'error' for PrimeVue
},
methods: {
cleanState() {
/*setTimeout(() => {
this.show = false;
}, this.timeout
);*/
},
showError(error) {
this.showMessage(error, 'danger');
},
showMessage(message, type = 'success') {
/*const content = {
// Your component or JSX template
component: Snackbar,
// Props are just regular props, but these won't be reactive
props: {
message: message
},
// Listeners will listen to and execute on event emission
listeners: {
//click: () => console.log("Clicked!"),
//myEvent: myEventHandler
}
};*/
let color = 'primary';
let icon = 'info';
switch (type) {
case 'info':
break;
case 'success':
color = 'green';
break;
case 'error':
case 'danger':
color = 'red';
icon: 'error';
break;
case 'warning':
color = 'yellow';
break;
}
if ('danger' === type) {
type = 'error';
}
this.$q.notify({
position: 'top',
timeout: 10000,
message: message,
color: color,
html: true,
multiLine: true,
})
/*const toast = useToast();
console.log('toast');
console.log(message);
console.log(content);
toast(content, {
type: type,
position: 'top-center',
timeout: 10000, // 10 seconds
closeOnClick: false,
pauseOnFocusLoss: true,
pauseOnHover: true,
draggable: true,
draggablePercent: 0.6,
showCloseButtonOnHover: false,
hideProgressBar: true,
closeButton: "button",
icon: true,
rtl: false
});*/
/*this.show = true;
this.color = color;
if (typeof message === 'string') {
this.text = message;
this.cleanState();
return;
}
this.text = message.message;
if (message.response) this.subText = message.response.data.message;
this.cleanState();*/
}
showMessage(message, type = 'success') {
// Convert message type to PrimeVue's severity
let severity = type;
if (type === 'danger') {
severity = 'error'; // PrimeVue uses 'error' instead of 'danger'
}
// Use PrimeVue's ToastService
this.$toast.add({
severity: severity,
summary: message,
detail: '',
life: 5000, // Message duration in milliseconds
closable: true, // Whether the message can be closed manually
});
}
},
computed: {
...mapFields('notifications', ['color', 'show', 'subText', 'text', 'timeout'])
},
};

@ -0,0 +1,33 @@
import { defineStore } from "pinia"
import axios from "axios"
import { computed, ref } from "vue"
export const useEnrolledStore = defineStore("enrolledStore", () => {
// Reactive state to track if the user is enrolled in courses or sessions
const isEnrolledInCourses = ref(false)
const isEnrolledInSessions = ref(false)
// Function to check enrollment status
async function checkEnrollments() {
try {
const { data } = await axios.get("/course/check-enrollments")
console.log('Check enrollments data:', data)
isEnrolledInCourses.value = data.isEnrolledInCourses
isEnrolledInSessions.value = data.isEnrolledInSessions
} catch (error) {
console.error("Error verifying enrollments:", error)
}
}
// Function to initialize the store
async function initialize() {
await checkEnrollments()
}
return {
// Computed properties for reactivity
isEnrolledInCourses: computed(() => isEnrolledInCourses.value),
isEnrolledInSessions: computed(() => isEnrolledInSessions.value),
initialize
};
});

@ -1,120 +1,137 @@
<template>
<Toolbar :handle-send="onSendForm" />
<CCalendarEventForm
ref="createForm"
:errors="violations"
:values="item"
/>
<BaseButton
:label="t('Add')"
class="ml-auto"
icon="plus"
type="primary"
@click="onClickCreateEvent"
/>
<Loading :visible="isLoading" />
</template>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<script>
import { mapActions, mapGetters, useStore } from "vuex"
import { createHelpers } from "vuex-map-fields"
<script setup>
import { useStore } from "vuex"
import CCalendarEventForm from "../../components/ccalendarevent/CCalendarEventForm.vue"
import Loading from "../../components/Loading.vue"
import Toolbar from "../../components/Toolbar.vue"
import CreateMixin from "../../mixins/CreateMixin"
import { computed, onMounted, ref } from "vue"
import useVuelidate from "@vuelidate/core"
import { onMounted, ref, watch } from "vue"
import { useRoute, useRouter } from "vue-router"
import isEmpty from "lodash/isEmpty"
import { RESOURCE_LINK_PUBLISHED } from "../../components/resource_links/visibility.js"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import { useSecurityStore } from "../../store/securityStore"
import { useI18n } from "vue-i18n"
import { useNotification } from "../../composables/notification"
const servicePrefix = "Message"
const { mapFields } = createHelpers({
getterType: "ccalendarevent/getField",
mutationType: "ccalendarevent/updateField",
})
//const { DateTime } = require("luxon");
export default {
name: "CCalendarEventCreate",
servicePrefix,
mixins: [CreateMixin],
components: {
CCalendarEventForm,
Loading,
Toolbar,
},
setup() {
const users = ref([])
const isLoadingSelect = ref(false)
const item = ref({})
const store = useStore()
const route = useRoute()
const router = useRouter()
let id = route.params.id
if (isEmpty(id)) {
id = route.query.id
const item = ref({})
const store = useStore()
const securityStore = useSecurityStore();
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const createForm = ref(null)
let id = route.params.id
if (isEmpty(id)) {
id = route.query.id
}
const isLoading = ref(true);
onMounted(async () => {
isLoading.value = true
const response = await store.dispatch("message/load", id)
const currentUser = securityStore.user;
item.value = await response
isLoading.value = false;
// Remove unused properties:
delete item.value["status"]
delete item.value["msgType"]
delete item.value["@type"]
delete item.value["@context"]
delete item.value["@id"]
delete item.value["id"]
delete item.value["firstReceiver"]
//delete item.value['receivers'];
delete item.value["sendDate"]
item.value.parentResourceNodeId = currentUser.resourceNode["id"]
//item.value['startDate'] = date.now();
//item.value['endDate'] = new Date();
//item.value['originalSender'] = item.value['sender'];
// New sender.
//item.value['sender'] = currentUser['@id'];
// Set new receivers, will be loaded by onSendMessageForm()
item.value.resourceLinkListFromEntity = []
const receivers = [...item.value.receiversTo, ...item.value.receiversCc];
let itemsAdded = []
receivers.forEach((receiver) => {
// Skip current user.
if (currentUser["@id"] === receiver.receiver["@id"]) {
return
}
item.value.resourceLinkListFromEntity.push({
uid: receiver.receiver["id"],
user: { username: receiver.receiver["username"] },
visibility: RESOURCE_LINK_PUBLISHED,
})
itemsAdded.push(receiver.receiver["username"])
})
onMounted(async () => {
const response = await store.dispatch("message/load", id)
const currentUser = computed(() => store.getters["security/getUser"])
item.value = await response
// Remove unused properties:
delete item.value["status"]
delete item.value["msgType"]
delete item.value["@type"]
delete item.value["@context"]
delete item.value["@id"]
delete item.value["id"]
delete item.value["firstReceiver"]
//delete item.value['receivers'];
delete item.value["sendDate"]
item.value["parentResourceNodeId"] = currentUser.value.resourceNode["id"]
//item.value['startDate'] = date.now();
//item.value['endDate'] = new Date();
//item.value['originalSender'] = item.value['sender'];
// New sender.
//item.value['sender'] = currentUser.value['@id'];
// Set new receivers, will be loaded by onSendMessageForm()
item.value["resourceLinkListFromEntity"] = []
let itemsAdded = []
item.value["receivers"].forEach((receiver) => {
// Skip current user.
if (currentUser.value["@id"] === receiver.receiver["@id"]) {
return
}
item.value.resourceLinkListFromEntity.push({
uid: receiver.receiver["id"],
user: { username: receiver.receiver["username"] },
visibility: RESOURCE_LINK_PUBLISHED,
})
itemsAdded.push(receiver.receiver["username"])
})
// Sender is not added to the list.
if (!itemsAdded.includes(item.value["sender"]["username"])) {
// Set the sender too.
item.value["resourceLinkListFromEntity"].push({
uid: item.value["sender"]["id"],
user: { username: item.value["sender"]["username"] },
visibility: RESOURCE_LINK_PUBLISHED,
})
}
delete item.value["sender"]
// Sender is not added to the list.
if (!itemsAdded.includes(item.value["sender"]["username"])) {
// Set the sender too.
item.value["resourceLinkListFromEntity"].push({
uid: item.value["sender"]["id"],
user: { username: item.value["sender"]["username"] },
visibility: RESOURCE_LINK_PUBLISHED,
})
}
return { v$: useVuelidate(), users, isLoadingSelect, item }
},
computed: {
...mapFields(["error", "isLoading", "created", "violations"]),
...mapGetters({
isAuthenticated: "security/isAuthenticated",
currentUser: "security/getUser",
}),
},
methods: {
...mapActions("ccalendarevent", ["create", "createWithFormData", "reset"]),
},
delete item.value["sender"]
})
const onClickCreateEvent = async () => {
if (createForm.value.v$.$invalid) {
return;
}
isLoading.value = true;
const itemModel = createForm.value.v$.item.$model
try {
await store.dispatch("ccalendarevent/create", itemModel)
} catch (e) {
isLoading.value = false;
notification.showErrorNotification(e)
return;
}
await router.push({ name: 'CCalendarEventList' })
}
const notification = useNotification();
watch(
() => store.state.ccalendarevent.created,
(created) => {
notification.showSuccessNotification(
t("{resource} created", { resource: created.resourceNode.title })
)
},
)
</script>

@ -67,6 +67,14 @@ services:
bind:
$persistProcessor: '@api_platform.doctrine.orm.state.persist_processor'
Chamilo\CoreBundle\EventSubscriber\AnonymousUserSubscriber:
tags:
- name: kernel.event_subscriber
Chamilo\CoreBundle\Security\Authorization\Voter\AnonymousVoter:
tags:
- name: security.voter
cocur_slugify:
lowercase: true

@ -93,7 +93,7 @@ if (api_is_course_admin() && !in_array($origin, ['learnpath', 'embeddable'])) {
'admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id
)
.Display::url(
Display::getMdiIconn('cog', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('ModifyExercise')),
Display::getMdiIcon('cog', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('ModifyExercise')),
'exercise_admin.php?'.api_get_cidreq().'&modifyExercise=yes&exerciseId='.$objExercise->id
),
]

@ -86,48 +86,6 @@ class DateTimePicker extends HTML_QuickForm_text
}
};
$('#{$id}').flatpickr(config);
/*
var txtDateTime = $('#$id'),
inputGroup = txtDateTime.parents('.input-group'),
txtDateTimeAlt = $('#{$id}_alt'),
txtDateTimeAltText = $('#{$id}_alt_text');
txtDateTime
.hide()
.datetimepicker({
defaultDate: '".$this->getValue()."',
dateFormat: 'yy-mm-dd',
timeFormat: 'HH:mm',
altField: '#{$id}_alt',
altFormat: \"".get_lang('MM dd, yy')."\",
altTimeFormat: \"".get_lang('HH:mm')."\",
altSeparator: \" ".get_lang(' at')." \",
altFieldTimeOnly: false,
showOn: 'both',
buttonImage: '".Display::return_icon('attendance.png', null, [], ICON_SIZE_TINY, true, true)."',
buttonImageOnly: true,
buttonText: '".get_lang('Select date')."',
changeMonth: true,
changeYear: true
})
.on('change', function (e) {
txtDateTimeAltText.text(txtDateTimeAlt.val());
});
txtDateTimeAltText.on('click', function () {
txtDateTime.datepicker('show');
});
inputGroup
.find('button')
.on('click', function (e) {
e.preventDefault();
$('#$id, #{$id}_alt').val('');
$('#{$id}_alt_text').html('');
});
*/
});
</script>";

@ -569,7 +569,7 @@ class SessionManager
$rs = Database::query($sql);
if (Database::num_rows($rs) > 0) {
$fieldId = Database::result($rs, 0, 0);
$sqlInjectJoins .= " INNER JOIN $tblExtraFieldValue cfv ON (c.id = cfv.item_id AND cfv.field_id = $fieldId)";
$sqlInjectJoins .= " LEFT JOIN $tblExtraFieldValue cfv ON (c.id = cfv.item_id AND cfv.field_id = $fieldId)";
$where .= " AND (c.course_language = '$isoCode' OR cfv.field_value LIKE '%$language%')";
} else {
$where .= " AND c.course_language = '$isoCode' ";
@ -8099,7 +8099,7 @@ class SessionManager
];
$form->addSelect('access', get_lang('Access'), $options, [
'onchange' => 'accessSwitcher()',
'onchange' => 'accessSwitcher(this.value)',
'id' => 'access',
]);
@ -8122,7 +8122,7 @@ class SessionManager
// Dates
$form->addDateTimePicker(
'access_start_date',
[get_lang('Access start date'), get_lang('Date on which the session is made available to all')],
[get_lang('Access start'), get_lang('Date on which the session is made available to all')],
['id' => 'access_start_date']
);
@ -8193,9 +8193,6 @@ class SessionManager
$form->addCheckBox(
'send_subscription_notification',
[
//get_lang('Send mail notification to students to inform of subscription'),
],
get_lang('Send an email when a user being subscribed to session'),
);

@ -123,6 +123,7 @@ if (isset($zoomOptions['options']) && !in_array($origin, ['embeddable', 'noheade
$allowLpItemTip = ('false' === api_get_setting('lp.hide_accessibility_label_on_lp_item'));
if ($allowLpItemTip) {
$htmlHeadXtra[] = api_get_asset('qtip2/dist/jquery.qtip.js');
$htmlHeadXtra[] = '<script>
$(function() {
$(".scorm_item_normal").qtip({

@ -953,14 +953,15 @@ if (!empty($filterToSend)) {
}
}
break;
case 'extra_filiere':
$filterItem['data'] = $filterItem['data']['extra_filiere'];
break;
}
}
if ($deleteFiliere) {
foreach ($filterToSend['rules'] as &$filterItem) {
if (isset($filterItem['field']) && 'extra_filiere' == $filterItem['field']) {
$filterItem = [];
}
if (isset($filterItem['field']) && 'extra_filiere' == $filterItem['field']) {
$filterItem = [];
}
}
}

@ -78,12 +78,28 @@ function search_coachs($needle)
return $xajax_response;
}
$urlAction = api_get_self();
$session = null;
$fromSessionId = null;
$accessSelected = 0;
if (isset($_GET['fromSessionId'])) {
$fromSessionId = (int) $_GET['fromSessionId'];
$session = api_get_session_entity($fromSessionId);
if ($session && 0 === (int) $session->getDuration()) {
$accessSelected = 1;
}
$urlAction .= '?fromSessionId=' . $fromSessionId;
}
$xajax->processRequests();
$htmlHeadXtra[] = $xajax->getJavascript('../inc/lib/xajax/');
$htmlHeadXtra[] = "
<script>
$(function() {
accessSwitcher(0);
setTimeout(function() {
$('#access').val('".$accessSelected."').trigger('change');
accessSwitcher('".$accessSelected."');
}, 1000);
});
function fill_coach_field (username) {
@ -121,8 +137,6 @@ if (isset($_POST['formSent']) && $_POST['formSent']) {
$tool_name = get_lang('Add a training session');
$urlAction = api_get_self();
function check_session_name($name)
{
$session = SessionManager::get_session_by_name($name);
@ -130,13 +144,6 @@ function check_session_name($name)
return empty($session) ? true : false;
}
$session = null;
$fromSessionId = null;
if (isset($_GET['fromSessionId'])) {
$fromSessionId = (int) $_GET['fromSessionId'];
$session = api_get_session_entity($fromSessionId);
$urlAction .= '?fromSessionId=' . $fromSessionId;
}
$form = new FormValidator('add_session', 'post', $urlAction);
$form->addElement('header', $tool_name);
$result = SessionManager::setForm($form, null, $fromSessionId);
@ -157,7 +164,9 @@ $(function() {
function repopulateFormValues() {
var formValues = JSON.parse(sessionStorage.getItem('formValues'));
$.each(formValues, function(i, field) {
$('[name=\"' + field.name + '\"]').val(field.value);
if (field.name === 'coach_username' || field.name === 'name' || field.name === 'system_template') {
$('[name=\"' + field.name + '\"]').val(field.value);
}
});
}
@ -244,9 +253,9 @@ $form->setDefaults($formDefaults);
if ($form->validate()) {
$params = $form->getSubmitValues();
$name = $params['name'];
$startDate = $params['access_start_date'];
$startDate = $params['access_start_date2'];
$endDate = $params['access_end_date'];
$displayStartDate = $params['display_start_date'];
$displayStartDate = $params['display_start_date2'];
$displayEndDate = $params['display_end_date'];
$coachStartDate = $params['coach_access_start_date'];
if (empty($coachStartDate)) {

@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\ContactCategory;
@ -23,7 +24,8 @@ class ContactCategoryController extends AbstractController
$contactCategories = $entityManager
->getRepository(ContactCategory::class)
->findAll();
->findAll()
;
return $this->render('@ChamiloCore/ContactCategory/index.html.twig', [
'contact_categories' => $contactCategories,

@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Form\ContactType;
@ -39,7 +40,8 @@ class ContactController extends AbstractController
->text(
"Sender: {$contactData['email']}\n".
"Message: {$contactData['message']}"
);
)
;
// Send the email
$mailer->send($email);

@ -1,14 +1,16 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\SessionRelUser;
use Chamilo\CoreBundle\Entity\Tag;
use Chamilo\CoreBundle\Entity\Tool;
use Chamilo\CoreBundle\Entity\User;
@ -712,6 +714,31 @@ class CourseController extends ToolBaseController
return new JsonResponse($responseData);
}
#[Route('/check-enrollments', name: 'chamilo_core_check_enrollments', methods: ['GET'])]
public function checkEnrollments(EntityManagerInterface $em, SettingsManager $settingsManager): JsonResponse
{
/** @var User|null $user */
$user = $this->getUser();
if (!$user) {
return new JsonResponse(['error' => 'User not found'], Response::HTTP_UNAUTHORIZED);
}
$isEnrolledInCourses = $this->isUserEnrolledInAnyCourse($user, $em);
$isEnrolledInSessions = $this->isUserEnrolledInAnySession($user, $em);
if (!$isEnrolledInCourses && !$isEnrolledInSessions) {
$defaultMenuEntry = $settingsManager->getSetting('platform.default_menu_entry_for_course_or_session');
$isEnrolledInCourses = 'my_courses' === $defaultMenuEntry;
$isEnrolledInSessions = 'my_sessions' === $defaultMenuEntry;
}
return new JsonResponse([
'isEnrolledInCourses' => $isEnrolledInCourses,
'isEnrolledInSessions' => $isEnrolledInSessions,
]);
}
private function autoLaunch(): void
{
$autoLaunchWarning = '';
@ -897,4 +924,25 @@ class CourseController extends ToolBaseController
return $link.'?'.$this->getCourseUrlQuery();
}
// Implement the real logic to check course enrollment
private function isUserEnrolledInAnyCourse(User $user, EntityManagerInterface $em): bool
{
$enrollmentCount = $em
->getRepository(CourseRelUser::class)
->count(['user' => $user])
;
return $enrollmentCount > 0;
}
// Implement the real logic to check session enrollment
private function isUserEnrolledInAnySession(User $user, EntityManagerInterface $em): bool
{
$enrollmentCount = $em->getRepository(SessionRelUser::class)
->count(['user' => $user])
;
return $enrollmentCount > 0;
}
}

@ -57,6 +57,7 @@ class PlatformConfigurationController extends AbstractController
'agenda.personal_calendar_show_sessions_occupation',
//'agenda.agenda_reminders',
'agenda.agenda_collective_invitations',
'social.social_enable_messages_feedback',
'social.disable_dislike_option',

@ -1,29 +1,30 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table(name: "contact_form_contact_category")]
#[Table(name: 'contact_form_contact_category')]
class ContactCategory
{
#[Id]
#[GeneratedValue]
#[Column(type: "integer")]
#[Column(type: 'integer')]
private ?int $id = null;
#[Column(type: "string", length: 255)]
#[Column(type: 'string', length: 255)]
private string $name;
#[Column(type: "string", length: 255)]
#[Column(type: 'string', length: 255)]
private string $email;
public function getId(): ?int
@ -39,6 +40,7 @@ class ContactCategory
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
@ -50,6 +52,7 @@ class ContactCategory
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
}

@ -23,7 +23,7 @@ class TrackELogin
#[ORM\GeneratedValue]
protected int $loginId;
#[ORM\ManyToOne(targetEntity: \Chamilo\CoreBundle\Entity\User::class, inversedBy: 'logins')]
#[ORM\ManyToOne(targetEntity: \Chamilo\CoreBundle\Entity\User::class, inversedBy: 'logins', cascade: ['persist'])]
#[ORM\JoinColumn(name: 'login_user_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
protected User $user;

@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\EventListener;
use Chamilo\CoreBundle\Controller\EditorController;
@ -15,7 +15,7 @@ use Chamilo\CoreBundle\Security\Authorization\Voter\GroupVoter;
use Chamilo\CoreBundle\Security\Authorization\Voter\SessionVoter;
use Chamilo\CourseBundle\Controller\CourseControllerInterface;
use Chamilo\CourseBundle\Entity\CGroup;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
@ -25,6 +25,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
/**
@ -37,7 +38,9 @@ class CourseListener
public function __construct(
private readonly Environment $twig,
private readonly AuthorizationCheckerInterface $authorizationChecker
private readonly AuthorizationCheckerInterface $authorizationChecker,
private readonly TranslatorInterface $translator,
private readonly EntityManagerInterface $entityManager,
) {
}
@ -46,6 +49,8 @@ class CourseListener
*/
public function onKernelRequest(RequestEvent $event): void
{
global $cidReset;
if (!$event->isMainRequest()) {
// don't do anything if it's not the master request
return;
@ -67,9 +72,13 @@ class CourseListener
return;
}
if (true === $cidReset) {
$this->removeCourseFromSession($request);
return;
}
$sessionHandler = $request->getSession();
$container = $this->container;
$translator = $container->get('translator');
$twig = $this->twig;
$course = null;
@ -79,46 +88,25 @@ class CourseListener
$courseId = (int) $request->get('cid');
$checker = $this->authorizationChecker;
/** @var EntityManager $em */
$em = $container->get('doctrine')->getManager();
//dump("cid value in request: $courseId");
if (!empty($courseId)) {
$course = null;
if ($sessionHandler->has('course')) {
/** @var Course $courseFromSession */
$courseFromSession = $sessionHandler->get('course');
if ($courseId === $courseFromSession->getId()) {
$course = $courseFromSession;
$courseInfo = $sessionHandler->get('_course');
//dump("Course #$courseId loaded from Session ");
}
}
//$course = null; //force loading from database
//if (null === $course) {
$course = $em->getRepository(Course::class)->find($courseId);
if (null === $course) {
throw new NotFoundHttpException($translator->trans('Course does not exist'));
$course = $this->entityManager->find(Course::class, $courseId);
$courseInfo = api_get_course_info($course->getCode());
}
//dump("Course loaded from DB #$courseId");
$courseInfo = api_get_course_info($course->getCode());
//}
/*if (null === $course) {
throw new NotFoundHttpException($translator->trans('Course does not exist'));
}*/
}
global $cidReset;
if (true === $cidReset) {
$this->removeCourseFromSession($request);
return;
}
if (null === $course) {
throw new NotFoundHttpException($this->translator->trans('Course does not exist'));
}
if (null !== $course) {
// Setting variables in the session.
$sessionHandler->set('course', $course);
$sessionHandler->set('_real_cid', $course->getId());
@ -129,32 +117,27 @@ class CourseListener
// Setting variables for the twig templates.
$twig->addGlobal('course', $course);
if (false === $checker->isGranted(CourseVoter::VIEW, $course)) {
throw new AccessDeniedException($this->translator->trans('You\'re not allowed in this course'));
}
// Checking if sid is used.
$sessionId = (int) $request->get('sid');
$session = null;
if (empty($sessionId)) {
$sessionHandler->remove('session_name');
$sessionHandler->remove('sid');
$sessionHandler->remove('session');
// Check if user is allowed to this course
// See CourseVoter.php
//dump("Checkisgranted");
if (false === $checker->isGranted(CourseVoter::VIEW, $course)) {
throw new AccessDeniedException($translator->trans('You\'re not allowed in this course'));
}
} else {
//dump("Load chamilo session from DB");
$session = $em->getRepository(Session::class)->find($sessionId);
$session = $this->entityManager->find(Session::class, $sessionId);
if (null !== $session) {
if (!$session->hasCourse($course)) {
throw new AccessDeniedException($translator->trans('Course is not registered in the Session'));
}
//$course->setCurrentSession($session);
$session->setCurrentCourse($course);
// Check if user is allowed to this course-session
// See SessionVoter.php
if (false === $checker->isGranted(SessionVoter::VIEW, $session)) {
throw new AccessDeniedException($translator->trans('You\'re not allowed in this session'));
throw new AccessDeniedException($this->translator->trans('You\'re not allowed in this session'));
}
$sessionHandler->set('session_name', $session->getName());
$sessionHandler->set('sid', $session->getId());
@ -162,7 +145,7 @@ class CourseListener
$twig->addGlobal('session', $session);
} else {
throw new NotFoundHttpException($translator->trans('Session not found'));
throw new NotFoundHttpException($this->translator->trans('Session not found'));
}
}
@ -173,16 +156,16 @@ class CourseListener
$sessionHandler->remove('gid');
} else {
//dump('Load chamilo group from DB');
$group = $em->getRepository(CGroup::class)->find($groupId);
$group = $this->entityManager->getRepository(CGroup::class)->find($groupId);
if (null === $group) {
throw new NotFoundHttpException($translator->trans('Group not found'));
throw new NotFoundHttpException($this->translator->trans('Group not found'));
}
$group->setParent($course);
if (false === $checker->isGranted(GroupVoter::VIEW, $group)) {
throw new AccessDeniedException($translator->trans('You\'re not allowed in this group'));
throw new AccessDeniedException($this->translator->trans('You\'re not allowed in this group'));
}
$sessionHandler->set('gid', $groupId);
@ -191,11 +174,11 @@ class CourseListener
// Check if user is allowed to this course-group
// See GroupVoter.php
if (false === $checker->isGranted(GroupVoter::VIEW, $group)) {
throw new AccessDeniedException($translator->trans('Unauthorised access to group'));
throw new AccessDeniedException($this->translator->trans('Unauthorised access to group'));
}
$sessionHandler->set('gid', $groupId);
} else {
throw new AccessDeniedException($translator->trans('Group does not exist in course'));
throw new AccessDeniedException($this->translator->trans('Group does not exist in course'));
}*/
}

@ -0,0 +1,154 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\EventSubscriber;
use Chamilo\CoreBundle\Entity\TrackELogin;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Settings\SettingsManager;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
class AnonymousUserSubscriber implements EventSubscriberInterface
{
private const MAX_ANONYMOUS_USERS = 5;
private Security $security;
private EntityManagerInterface $entityManager;
private SessionInterface $session;
private SettingsManager $settingsManager;
public function __construct(Security $security, EntityManagerInterface $entityManager, SessionInterface $session, SettingsManager $settingsManager)
{
$this->security = $security;
$this->entityManager = $entityManager;
$this->session = $session;
$this->settingsManager = $settingsManager;
}
public function onKernelRequest(RequestEvent $event): void
{
if (null !== $this->security->getUser()) {
return;
}
$request = $event->getRequest();
$userIp = $request->getClientIp();
$anonymousUserId = $this->getOrCreateAnonymousUserId($userIp);
if (null !== $anonymousUserId) {
$trackLoginRepository = $this->entityManager->getRepository(TrackELogin::class);
// Check if a login record already exists for this user and IP
$existingLogin = $trackLoginRepository->findOneBy(['userIp' => $userIp, 'user' => $anonymousUserId]);
if (!$existingLogin) {
// Record the access if it does not exist
$trackLogin = new TrackELogin();
$trackLogin->setUserIp($userIp)
->setLoginDate(new DateTime())
->setUser($this->entityManager->getReference(User::class, $anonymousUserId))
;
$this->entityManager->persist($trackLogin);
$this->entityManager->flush();
}
$userRepository = $this->entityManager->getRepository(User::class);
$user = $userRepository->find($anonymousUserId);
if ($user) {
// Store user information in the session
$userInfo = [
'user_id' => $user->getId(),
'username' => $user->getUsername(),
'firstname' => $user->getFirstname(),
'lastname' => $user->getLastname(),
'firstName' => $user->getFirstname(),
'lastName' => $user->getLastname(),
'email' => $user->getEmail(),
'official_code' => $user->getOfficialCode(),
'picture_uri' => $user->getPictureUri(),
'status' => $user->getStatus(),
'active' => $user->getActive(),
'auth_source' => $user->getAuthSource(),
'theme' => $user->getTheme(),
'language' => $user->getLocale(),
'registration_date' => $user->getRegistrationDate()->format('Y-m-d H:i:s'),
'expiration_date' => $user->getExpirationDate() ? $user->getExpirationDate()->format('Y-m-d H:i:s') : null,
'last_login' => $user->getLastLogin() ? $user->getLastLogin()->format('Y-m-d H:i:s') : null,
'is_anonymous' => true,
];
$this->session->set('_user', $userInfo);
}
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => 'onKernelRequest',
];
}
private function getOrCreateAnonymousUserId(string $userIp): ?int
{
$userRepository = $this->entityManager->getRepository(User::class);
$trackLoginRepository = $this->entityManager->getRepository(TrackELogin::class);
$maxAnonymousUsers = (int) $this->settingsManager->getSetting('admin.max_anonymous_users');
if (0 === $maxAnonymousUsers) {
$maxAnonymousUsers = self::MAX_ANONYMOUS_USERS;
}
$anonymousUsers = $userRepository->findBy(['status' => User::ANONYMOUS], ['registrationDate' => 'ASC']);
// Check in TrackELogin if there is an anonymous user with the same IP
foreach ($anonymousUsers as $user) {
$loginRecord = $trackLoginRepository->findOneBy(['userIp' => $userIp, 'user' => $user]);
if ($loginRecord) {
error_log('Existing login found for user ID: '.$user->getId());
return $user->getId();
}
}
// Delete excess anonymous users
while (\count($anonymousUsers) >= $maxAnonymousUsers) {
$oldestAnonymousUser = array_shift($anonymousUsers);
if ($oldestAnonymousUser) {
error_log('Deleting oldest anonymous user: '.$oldestAnonymousUser->getId());
$this->entityManager->remove($oldestAnonymousUser);
$this->entityManager->flush();
}
}
// Create a new anonymous user
$uniqueId = uniqid();
$anonymousUser = (new User())
->setSkipResourceNode(true)
->setLastname('Joe')
->setFirstname('Anonymous')
->setUsername('anon_'.$uniqueId)
->setStatus(User::ANONYMOUS)
->setPlainPassword('anon')
->setEmail('anon_'.$uniqueId.'@localhost.local')
->setOfficialCode('anonymous')
->setCreatorId(1)
->addRole('ROLE_ANONYMOUS')
;
$this->entityManager->persist($anonymousUser);
$this->entityManager->flush();
error_log('New anonymous user created: '.$anonymousUser->getId());
return $anonymousUser->getId();
}
}

@ -1,5 +1,9 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Form;
use Chamilo\CoreBundle\Entity\ContactCategory;

@ -1,22 +1,23 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Form;
use Chamilo\CoreBundle\Entity\ContactCategory;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
use Chamilo\CoreBundle\Entity\ContactCategory;
use Symfony\Contracts\Translation\TranslatorInterface;
class ContactType extends AbstractType
{
@ -81,7 +82,8 @@ class ContactType extends AbstractType
'class' => 'btn btn--primary hover:bg-blue-700 text-white font-bold py-2 px-4 rounded cursor-pointer',
'style' => 'border: none;',
],
]);
])
;
}
public function configureOptions(OptionsResolver $resolver): void

@ -49,7 +49,23 @@ class Version20170625145000 extends AbstractMigrationChamilo
}
if (!$table->hasColumn('collective')) {
$this->addSql('ALTER TABLE c_calendar_event ADD collective TINYINT(1) NOT NULL');
$this->addSql('ALTER TABLE c_calendar_event ADD collective TINYINT(1) DEFAULT 0 NOT NULL');
}
if (!$table->hasColumn('invitation_type')) {
$this->addSql("ALTER TABLE c_calendar_event ADD invitaion_type VARCHAR(255) DEFAULT 'invitation' NOT NULL");
}
if (!$table->hasColumn('subscription_visibility')) {
$this->addSql('ALTER TABLE c_calendar_event ADD subscription_visibility INT DEFAULT 0 NOT NULL');
}
if (!$table->hasColumn('subscription_item_id')) {
$this->addSql('ALTER TABLE c_calendar_event ADD subscription_item_id INT DEFAULT NULL');
}
if (!$table->hasColumn('max_attendees')) {
$this->addSql('ALTER TABLE c_calendar_event ADD max_attendees INT DEFAULT 0 NOT NULL');
}
$table = $schema->getTable('c_calendar_event_attachment');

@ -87,6 +87,10 @@ final class Version20201212114910 extends AbstractMigrationChamilo
$userEntity->addRole('ROLE_ADMIN');
}
if ($userEntity::ANONYMOUS === $userEntity->getStatus()) {
$userEntity->addRole('ROLE_ANONYMOUS');
}
$creatorId = $userEntity->getCreatorId();
$creator = null;
if (isset($userList[$adminId])) {

@ -236,6 +236,9 @@ final class Version20201212203625 extends AbstractMigrationChamilo
if (null === $parent) {
$parent = $course;
}
if (null === $parent->getResourceNode()) {
continue;
}
$admin = $this->getAdmin();
$result = $this->fixItemProperty('document', $documentRepo, $course, $admin, $document, $parent);

@ -60,27 +60,6 @@ final class Version20230216122950 extends AbstractMigrationChamilo
}
}
if (!$schema->hasTable('agenda_event_invitee')) {
$this->addSql(
'CREATE TABLE agenda_event_invitee (id BIGINT AUTO_INCREMENT NOT NULL, invitation_id BIGINT DEFAULT NULL, user_id INT DEFAULT NULL, created_at DATETIME NOT NULL COMMENT "(DC2Type:datetime)", updated_at DATETIME NOT NULL COMMENT "(DC2Type:datetime)", type VARCHAR(255) NOT NULL, INDEX IDX_4F5757FEA35D7AF0 (invitation_id), INDEX IDX_4F5757FEA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC'
);
$this->addSql(
'ALTER TABLE agenda_event_invitee ADD CONSTRAINT FK_4F5757FEA35D7AF0 FOREIGN KEY (invitation_id) REFERENCES agenda_event_invitation (id) ON DELETE CASCADE'
);
$this->addSql(
'ALTER TABLE agenda_event_invitee ADD CONSTRAINT FK_4F5757FEA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE SET NULL'
);
}
if (!$schema->hasTable('agenda_event_invitation')) {
$this->addSql(
'CREATE TABLE agenda_event_invitation (id BIGINT AUTO_INCREMENT NOT NULL, creator_id INT DEFAULT NULL, created_at DATETIME NOT NULL COMMENT "(DC2Type:datetime)", updated_at DATETIME NOT NULL COMMENT "(DC2Type:datetime)", type VARCHAR(255) NOT NULL, max_attendees INT DEFAULT 0, INDEX IDX_52A2D5E161220EA6 (creator_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC'
);
$this->addSql(
'ALTER TABLE agenda_event_invitation ADD CONSTRAINT FK_52A2D5E161220EA6 FOREIGN KEY (creator_id) REFERENCES user (id) ON DELETE CASCADE'
);
}
if (!$schema->hasTable('agenda_reminder')) {
$this->addSql(
'CREATE TABLE agenda_reminder (id BIGINT AUTO_INCREMENT NOT NULL, type VARCHAR(255) NOT NULL, event_id INT NOT NULL, date_interval VARCHAR(255) NOT NULL COMMENT "(DC2Type:dateinterval)", sent TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT "(DC2Type:datetime)", updated_at DATETIME NOT NULL COMMENT "(DC2Type:datetime)", PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC;'

@ -30,6 +30,9 @@ class Version20230904173400 extends AbstractMigrationChamilo
*/
public function up(Schema $schema): void
{
$collectiveInvitationsEnabled = $this->getConfigurationValue('agenda_collective_invitations');
$subscriptionsEnabled = $this->getConfigurationValue('agenda_event_subscriptions');
$this->addSql("UPDATE personal_agenda SET parent_event_id = NULL WHERE parent_event_id = 0 OR parent_event_id = ''");
$this->addSql('UPDATE personal_agenda SET parent_event_id = NULL WHERE parent_event_id NOT IN (SELECT id FROM personal_agenda)');
$this->addSql('DELETE FROM personal_agenda WHERE user NOT IN (SELECT id FROM user)');
@ -40,9 +43,7 @@ class Version20230904173400 extends AbstractMigrationChamilo
$em = $this->getEntityManager();
$userRepo = $em->getRepository(User::class);
$sql = 'SELECT * FROM personal_agenda ORDER BY id';
$result = $em->getConnection()->executeQuery($sql);
$personalAgendas = $result->fetchAllAssociative();
$personalAgendas = $this->getPersonalEvents();
$utc = new DateTimeZone('UTC');
@ -50,39 +51,166 @@ class Version20230904173400 extends AbstractMigrationChamilo
foreach ($personalAgendas as $personalAgenda) {
$oldParentId = (int) $personalAgenda['parent_event_id'];
$user = $userRepo->find($personalAgenda['user']);
$title = $personalAgenda['title'] ?: '-';
$startDate = $personalAgenda['date'] ? new DateTime($personalAgenda['date'], $utc) : null;
$endDate = $personalAgenda['enddate'] ? new DateTime($personalAgenda['enddate'], $utc) : null;
$allDay = (bool) $personalAgenda['all_day'];
$calendarEvent = new CCalendarEvent();
$calendarEvent
->setTitle($title)
->setContent($personalAgenda['text'])
->setStartDate($startDate)
->setEndDate($endDate)
->setAllDay($allDay)
->setColor($personalAgenda['color'])
->setCreator($user)
->setResourceName($title)
;
$newParent = null;
if ($oldParentId && isset($map[$oldParentId])) {
$newParent = $map[$oldParentId];
$calendarEvent
->setParentEvent($newParent)
->setParentResourceNode($newParent->getResourceNode()->getId())
;
} else {
$calendarEvent->setParentResourceNode($user->getResourceNode()->getId());
}
$calendarEvent = $this->createCCalendarEvent(
$personalAgenda['title'] ?: '-',
$personalAgenda['text'],
$personalAgenda['date'] ? new DateTime($personalAgenda['date'], $utc) : null,
$personalAgenda['enddate'] ? new DateTime($personalAgenda['enddate'], $utc) : null,
(bool) $personalAgenda['all_day'],
$personalAgenda['color'],
$user,
$newParent
);
$map[$personalAgenda['id']] = $calendarEvent;
$em->persist($calendarEvent);
if ($collectiveInvitationsEnabled) {
$calendarEvent->setCollective($personalAgenda['collective']);
$hasSubscriptions = false;
$invitationsOrSubscriptionsInfo = [];
if ($subscriptionsEnabled) {
$subscriptionsInfo = $this->getSubscriptions((int) $personalAgenda['id']);
if (\count($subscriptionsInfo) > 0) {
$hasSubscriptions = true;
$invitationsOrSubscriptionsInfo = $subscriptionsInfo;
}
}
if ($hasSubscriptions) {
$calendarEvent
->setInvitaionType(CCalendarEvent::TYPE_SUBSCRIPTION)
->setSubscriptionVisibility($personalAgenda['subscription_visibility'])
->setSubscriptionItemId($personalAgenda['subscription_item_id'])
;
} else {
$calendarEvent->setInvitaionType(CCalendarEvent::TYPE_INVITATION);
$invitationsOrSubscriptionsInfo = $this->getInvitations($subscriptionsEnabled, (int) $personalAgenda['id']);
}
foreach ($invitationsOrSubscriptionsInfo as $invitationOrSubscriptionInfo) {
$inviteesOrSubscribersInfo = $this->getInviteesOrSubscribers($invitationOrSubscriptionInfo['id']);
foreach ($inviteesOrSubscribersInfo as $oldInviteeOrSubscriberInfo) {
$user = $em->find(User::class, $oldInviteeOrSubscriberInfo['user_id']);
if ($user) {
$calendarEvent->addUserLink($user);
}
}
}
}
}
$em->flush();
}
private function getPersonalEvents(): array
{
$sql = 'SELECT * FROM personal_agenda ORDER BY id';
$result = $this->connection->executeQuery($sql);
return $result->fetchAllAssociative();
}
private function createCCalendarEvent(
string $title,
string $content,
?DateTime $startDate,
?DateTime $endDate,
bool $allDay,
string $color,
User $creator,
?CCalendarEvent $parentEvent = null
): CCalendarEvent {
$calendarEvent = new CCalendarEvent();
$calendarEvent
->setTitle($title)
->setContent($content)
->setStartDate($startDate)
->setEndDate($endDate)
->setAllDay($allDay)
->setColor($color)
->setCreator($creator)
->setResourceName($title)
;
if ($parentEvent) {
$calendarEvent
->setParentEvent($parentEvent)
->setParentResourceNode($parentEvent->getResourceNode()->getId())
;
} else {
$calendarEvent->setParentResourceNode($creator->getResourceNode()->getId());
}
return $calendarEvent;
}
private function getInvitations(bool $subscriptionsEnabled, int $personalAgendaId): array
{
$sql = "SELECT i.id, i.creator_id, i.created_at, i.updated_at
FROM agenda_event_invitation i
INNER JOIN personal_agenda pa ON i.id = pa.agenda_event_invitation_id
WHERE pa.id = $personalAgendaId";
if ($subscriptionsEnabled) {
$sql .= " AND i.type = 'invitation'";
}
try {
$result = $this->connection->executeQuery($sql);
return $result->fetchAllAssociative();
} catch (\Doctrine\DBAL\Exception) {
return [];
}
}
private function getInviteesOrSubscribers(int $invitationId): array
{
$sql = "SELECT id, user_id, created_at, updated_at
FROM agenda_event_invitee
WHERE invitation_id = $invitationId
ORDER BY created_at ASC";
try {
$result = $this->connection->executeQuery($sql);
return $result->fetchAllAssociative();
} catch (\Doctrine\DBAL\Exception) {
return [];
}
}
private function getSubscriptions(int $personalAgendaId): array
{
$sql = "SELECT i.id, i.creator_id, i.created_at, i.updated_at
FROM agenda_event_invitation i
INNER JOIN personal_agenda pa ON i.id = pa.agenda_event_invitation_id
WHERE pa.id = $personalAgendaId
AND i.type = 'subscription'";
try {
$result = $this->connection->executeQuery($sql);
return $result->fetchAllAssociative();
} catch (\Doctrine\DBAL\Exception) {
return [];
}
}
}

@ -14,7 +14,7 @@ final class Version20231110194300 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return "Copy custom theme folder to assets and update webpack.config";
return 'Copy custom theme folder to assets and update webpack.config';
}
public function up(Schema $schema): void
@ -54,13 +54,12 @@ final class Version20231110194300 extends AbstractMigrationChamilo
'fruity_orange',
'medical',
'simplex',
'tasty_olive'
'tasty_olive',
];
$sourceDir = $rootPath.'/app/Resources/public/css/themes';
$destinationDir = $rootPath.'/assets/css/themes/';
$chamiloDefaultCssPath = $destinationDir . 'chamilo/default.css';
$chamiloDefaultCssPath = $destinationDir.'chamilo/default.css';
if (!file_exists($sourceDir)) {
return;
@ -72,16 +71,16 @@ final class Version20231110194300 extends AbstractMigrationChamilo
foreach ($finder as $folder) {
$folderName = $folder->getRelativePathname();
if (!in_array($folderName, $customThemesFolders, true)) {
if (!\in_array($folderName, $customThemesFolders, true)) {
$sourcePath = $folder->getRealPath();
$destinationPath = $destinationDir . $folderName;
$destinationPath = $destinationDir.$folderName;
if (!file_exists($destinationPath)) {
$this->copyDirectory($sourcePath, $destinationPath);
$newThemes[] = $folderName;
if (file_exists($chamiloDefaultCssPath)) {
$newThemeDefaultCssPath = $destinationPath . '/default.css';
$newThemeDefaultCssPath = $destinationPath.'/default.css';
copy($chamiloDefaultCssPath, $newThemeDefaultCssPath);
}
}
@ -96,11 +95,11 @@ final class Version20231110194300 extends AbstractMigrationChamilo
$dir = opendir($src);
@mkdir($dst);
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..')) {
if (is_dir($src . '/' . $file)) {
$this->copyDirectory($src . '/' . $file, $dst . '/' . $file);
if (('.' !== $file) && ('..' !== $file)) {
if (is_dir($src.'/'.$file)) {
$this->copyDirectory($src.'/'.$file, $dst.'/'.$file);
} else {
copy($src . '/' . $file, $dst . '/' . $file);
copy($src.'/'.$file, $dst.'/'.$file);
}
}
}
@ -109,7 +108,7 @@ final class Version20231110194300 extends AbstractMigrationChamilo
private function updateWebpackConfig(string $rootPath, array $newThemes): void
{
$webpackConfigPath = $rootPath . '/webpack.config.js';
$webpackConfigPath = $rootPath.'/webpack.config.js';
if (!file_exists($webpackConfigPath)) {
return;
@ -117,13 +116,14 @@ final class Version20231110194300 extends AbstractMigrationChamilo
$content = file_get_contents($webpackConfigPath);
$pattern = "/(const themes = \[\n\s*)([^\]]*?)(\s*\];)/s";
$replacement = function($matches) use ($newThemes) {
$pattern = "/(const themes = \\[\n\\s*)([^\\]]*?)(\\s*\\];)/s";
$replacement = function ($matches) use ($newThemes) {
$existingThemesString = rtrim($matches[2], ", \n");
$newThemesString = implode("',\n '", $newThemes);
$formattedNewThemesString = $existingThemesString .
(empty($existingThemesString) ? '' : ",\n '") . $newThemesString . "'";
return $matches[1] . $formattedNewThemesString . $matches[3];
$formattedNewThemesString = $existingThemesString.
(empty($existingThemesString) ? '' : ",\n '").$newThemesString."'";
return $matches[1].$formattedNewThemesString.$matches[3];
};
$newContent = preg_replace_callback($pattern, $replacement, $content);

@ -0,0 +1,30 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Security\Authorization\Voter;
use Chamilo\CoreBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class AnonymousVoter extends Voter
{
protected function supports(string $attribute, $subject): bool
{
return 'ROLE_ANONYMOUS' === $attribute;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return User::ANONYMOUS === $user->getStatus();
}
}

@ -39,10 +39,6 @@ class PlatformSettingsSchema extends AbstractSettingsSchema
'institution_url' => 'http://www.chamilo.org',
'institution_address' => '',
'site_name' => 'Chamilo site',
// 'administrator_email' => 'admin@example.org',
// 'administrator_name' => 'Jane',
// 'administrator_surname' => 'Doe',
// 'administrator_phone' => '123456',
'timezone' => 'Europe/Paris',
'theme' => 'chamilo',
'gravatar_enabled' => 'false',
@ -60,13 +56,8 @@ class PlatformSettingsSchema extends AbstractSettingsSchema
'keep_old_images_after_delete' => 'true',
'load_term_conditions_section' => 'login',
'server_type' => 'prod',
// Chamilo mode
'show_tabs' => array_values(self::$tabs),
'chamilo_database_version' => '2.0.0',
//
//('catalog_show_courses_sessions', '0', 'CatalogueShowOnlyCourses'),
//('catalog_show_courses_sessions', '1', 'CatalogueShowOnlySessions'),
//('catalog_show_courses_sessions', '2', 'CatalogueShowCoursesAndSessions'),
'theme_fallback' => 'chamilo',
'unoconv_binaries' => '/usr/bin/unoconv',
'packager' => 'chamilo',
@ -112,6 +103,7 @@ class PlatformSettingsSchema extends AbstractSettingsSchema
'disable_user_conditions_sender_id' => '0',
'portfolio_advanced_sharing' => 'false',
'redirect_index_to_url_for_logged_users' => '',
'default_menu_entry_for_course_or_session' => 'my_courses',
]
)
->setTransformer(
@ -123,15 +115,10 @@ class PlatformSettingsSchema extends AbstractSettingsSchema
'institution' => ['string'],
'institution_url' => ['string'],
'site_name' => ['string'],
// 'administrator_email' => array('string'),
// 'administrator_name' => array('string'),
// 'administrator_surname' => array('string'),
// 'administrator_phone' => array('string'),
'timezone' => ['string'],
'gravatar_enabled' => ['string'],
'gravatar_type' => ['string'],
'show_tabs' => ['array', 'null'],
//'gamification_mode' => array('string'),
];
$this->setMultipleAllowedTypes($allowedTypes, $builder);
@ -144,10 +131,6 @@ class PlatformSettingsSchema extends AbstractSettingsSchema
->add('institution_url', UrlType::class)
->add('institution_address')
->add('site_name')
// ->add('administrator_email', 'email')
// ->add('administrator_name')
// ->add('administrator_surname')
// ->add('administrator_phone')
->add('timezone', TimezoneType::class)
->add('theme')
->add('gravatar_enabled', YesNoType::class)
@ -351,6 +334,16 @@ class PlatformSettingsSchema extends AbstractSettingsSchema
->add('disable_user_conditions_sender_id', TextType::class)
->add('portfolio_advanced_sharing', TextType::class)
->add('redirect_index_to_url_for_logged_users', TextType::class)
->add(
'default_menu_entry_for_course_or_session',
ChoiceType::class,
[
'choices' => [
'My Courses' => 'my_courses',
'My Sessions' => 'my_sessions',
],
]
)
;
}

@ -1,16 +1,16 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Settings;
use Sylius\Bundle\SettingsBundle\Schema\AbstractSettingsBuilder;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Contracts\Service\Attribute\Required;
class StylesheetsSettingsSchema extends AbstractSettingsSchema
@ -30,7 +30,8 @@ class StylesheetsSettingsSchema extends AbstractSettingsSchema
[
'stylesheets' => 'chamilo',
]
);
)
;
$allowedTypes = [
'stylesheets' => ['string'],
];
@ -50,7 +51,7 @@ class StylesheetsSettingsSchema extends AbstractSettingsSchema
private function getThemeChoices(): array
{
$projectDir = $this->parameterBag->get('kernel.project_dir');
$themesDirectory = $projectDir . '/assets/css/themes/';
$themesDirectory = $projectDir.'/assets/css/themes/';
$finder = new Finder();
$choices = [];
@ -66,7 +67,6 @@ class StylesheetsSettingsSchema extends AbstractSettingsSchema
return $choices;
}
private function formatFolderName(string $name): string
{
return ucwords(str_replace('_', ' ', $name));

@ -59,6 +59,13 @@ class CCalendarEvent extends AbstractResource implements ResourceInterface, Stri
{
public const COLOR_STUDENT_PUBLICATION = '#FF8C00';
public const TYPE_INVITATION = 'invitation';
public const TYPE_SUBSCRIPTION = 'subscription';
public const SUBSCRIPTION_VISIBILITY_NO = 0;
public const SUBSCRIPTION_VISIBILITY_ALL = 1;
public const SUBSCRIPTION_VISIBILITY_CLASS = 2;
#[Groups(['calendar_event:read', 'calendar_event:write'])]
#[ORM\Column(name: 'iid', type: 'integer')]
#[ORM\Id]
@ -128,9 +135,21 @@ class CCalendarEvent extends AbstractResource implements ResourceInterface, Stri
#[Groups(['calendar_event:read', 'calendar_event:write'])]
#[Assert\NotNull]
#[ORM\Column(name: 'collective', type: 'boolean', nullable: false)]
#[ORM\Column(name: 'collective', type: 'boolean', nullable: false, options: ['default' => false])]
protected bool $collective = false;
#[ORM\Column(name: 'invitaion_type', type: 'string', options: ['default' => self::TYPE_INVITATION])]
protected string $invitaionType = self::TYPE_INVITATION;
#[ORM\Column(name: 'subscription_visibility', type: 'integer', options: ['default' => self::SUBSCRIPTION_VISIBILITY_NO])]
protected int $subscriptionVisibility = self::SUBSCRIPTION_VISIBILITY_NO;
#[ORM\Column(name: 'subscription_item_id', type: 'integer', nullable: true)]
protected ?int $subscriptionItemId = null;
#[ORM\Column(name: 'max_attendees', type: 'integer', nullable: false, options: ['default' => 0])]
protected int $maxAttendees = 0;
public function __construct()
{
$this->children = new ArrayCollection();
@ -351,4 +370,52 @@ class CCalendarEvent extends AbstractResource implements ResourceInterface, Stri
return $this;
}
public function getInvitaionType(): string
{
return $this->invitaionType;
}
public function setInvitaionType(string $invitaionType): self
{
$this->invitaionType = $invitaionType;
return $this;
}
public function getSubscriptionVisibility(): int
{
return $this->subscriptionVisibility;
}
public function setSubscriptionVisibility(int $subscriptionVisibility): self
{
$this->subscriptionVisibility = $subscriptionVisibility;
return $this;
}
public function getSubscriptionItemId(): ?int
{
return $this->subscriptionItemId;
}
public function setSubscriptionItemId(?int $subscriptionItemId): self
{
$this->subscriptionItemId = $subscriptionItemId;
return $this;
}
public function getMaxAttendees(): int
{
return $this->maxAttendees;
}
public function setMaxAttendees(int $maxAttendees): self
{
$this->maxAttendees = $maxAttendees;
return $this;
}
}

@ -88,6 +88,16 @@ Encore
pattern: /(js.cookie.js)$/,
to: 'libs/js-cookie/src/js.cookie.js'
},
{
from: './node_modules/qtip2/dist/basic',
pattern: /(jquery.qtip.js)$/,
to: 'libs/qtip2/dist/jquery.qtip.js'
},
{
from: './node_modules/qtip2/dist/basic',
pattern: /(jquery.qtip.css)$/,
to: 'libs/qtip2/dist/jquery.qtip.css'
},
//{from: './node_modules/ckeditor4/', to: 'libs/ckeditor/[path][name].[ext]', pattern: /\.(js|css)$/, includeSubdirectories: false},
//{from: './node_modules/ckeditor4/adapters', to: 'libs/ckeditor/adapters/[path][name].[ext]'},
//{from: './node_modules/ckeditor4/lang', to: 'libs/ckeditor/lang/[path][name].[ext]'},

Loading…
Cancel
Save