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

pull/5274/head
Yannick Warnier 2 years ago
commit 5bd96c08b0
  1. 19
      assets/css/scss/_calendar.scss
  2. 2
      assets/css/scss/index.scss
  3. 2
      assets/vue/App.vue
  4. 25
      assets/vue/components/ccalendarevent/CCalendarEventInfo.vue
  5. 32
      assets/vue/components/ccalendarevent/CalendarEventInvitationsInfo.vue
  6. 69
      assets/vue/components/ccalendarevent/CalendarEventSubscriptionsInfo.vue
  7. 12
      assets/vue/components/layout/DashboardLayout.vue
  8. 39
      assets/vue/components/layout/Sidebar.vue
  9. 38
      assets/vue/composables/calendar/calendarEvent.js
  10. 6
      assets/vue/services/baseService.js
  11. 5
      assets/vue/services/resourceLinkService.js
  12. 10
      assets/vue/views/admin/AdminConfigureColors.vue
  13. 41
      assets/vue/views/ccalendarevent/CCalendarEventList.vue
  14. 2
      public/main/inc/lib/api.lib.php
  15. 4
      public/main/lp/lp_view.php
  16. 2
      src/CoreBundle/ApiResource/CalendarEvent.php
  17. 20
      src/CoreBundle/Controller/ThemeController.php
  18. 9
      src/CoreBundle/DataTransformer/CalendarEventTransformer.php
  19. 137
      src/CoreBundle/Entity/AgendaEventInvitation.php
  20. 65
      src/CoreBundle/Entity/AgendaEventInvitee.php
  21. 12
      src/CoreBundle/Entity/AgendaEventSubscriber.php
  22. 32
      src/CoreBundle/Entity/AgendaEventSubscription.php
  23. 49
      src/CoreBundle/Entity/ColorTheme.php
  24. 1
      src/CoreBundle/Entity/ResourceLink.php
  25. 6
      src/CoreBundle/Entity/ResourceNode.php
  26. 6
      src/CoreBundle/EventListener/CourseListener.php
  27. 6
      src/CoreBundle/EventListener/ExceptionListener.php
  28. 23
      src/CoreBundle/EventListener/TwigListener.php
  29. 9
      src/CoreBundle/Exception/NotAllowedException.php
  30. 7
      src/CoreBundle/Migrations/AbstractMigrationChamilo.php
  31. 9
      src/CoreBundle/Migrations/Schema/V200/Version20200821224242.php
  32. 8
      src/CoreBundle/Migrations/Schema/V200/Version20211005153900.php
  33. 25
      src/CoreBundle/Migrations/Schema/V200/Version20230904173400.php
  34. 34
      src/CoreBundle/Migrations/Schema/V200/Version20230904173401.php
  35. 6
      src/CoreBundle/Migrations/Schema/V200/Version20240112131200.php
  36. 12
      src/CoreBundle/Migrations/Schema/V200/Version20240122221400.php
  37. 2
      src/CoreBundle/Migrations/Schema/V200/Version20240318105600.php
  38. 40
      src/CoreBundle/Repository/ColorThemeRepository.php
  39. 20
      src/CoreBundle/Resources/views/Index/vue.html.twig
  40. 6
      src/CoreBundle/Resources/views/Layout/head.html.twig
  41. 5
      src/CoreBundle/Resources/views/Layout/no_layout.html.twig
  42. 15
      src/CoreBundle/State/ColorThemeProcessor.php

@ -0,0 +1,19 @@
.calendar-event-info {
@apply flex flex-col space-y-4;
.invitations-info {
@apply space-y-2;
&__title {
@apply text-gray-50 mb-3;
}
&__item {
@apply flex text-body-2 flex-row justify-between;
p {
@apply first:font-semibold;
}
}
}
}

@ -59,7 +59,7 @@
@import 'layout/main_container'; @import 'layout/main_container';
@import "admin_index"; @import "admin_index";
@import "calendar";
@import "course_home"; @import "course_home";
@import "documents"; @import "documents";

@ -79,7 +79,7 @@ const layout = computed(() => {
const queryParams = new URLSearchParams(window.location.search) const queryParams = new URLSearchParams(window.location.search)
if (queryParams.has("lp") || (queryParams.has("origin") && "learnpath" === queryParams.get("origin"))) { if (queryParams.has("lp_id") || (queryParams.has("origin") && "learnpath" === queryParams.get("origin"))) {
return "EmptyLayout" return "EmptyLayout"
} }

@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col space-y-4"> <div class="calendar-event-info">
<h5 v-text="event.title" /> <h5 v-text="event.title" />
<p v-text="abbreviatedDatetime(event.startDate)" /> <p v-text="abbreviatedDatetime(event.startDate)" />
@ -13,14 +13,19 @@
<div v-html="event.content" /> <div v-html="event.content" />
<div v-if="allowCollectiveInvitations && type.invitation === event.invitationType"> <CalendarEventSubscriptionsInfo
<h6 v-t="'Invitees'" /> v-if="type.subscription === event.invitationType"
:event="event"
<ShowLinks />
:item="event" <CalendarEventInvitationsInfo
:show-status="false" v-else-if="type.invitation === event.invitationType"
/> :event="event"
</div> />
<ShowLinks
v-else
:item="event"
:show-status="false"
/>
</div> </div>
</template> </template>
@ -29,6 +34,8 @@ import { useFormatDate } from "../../composables/formatDate"
import ShowLinks from "../resource_links/ShowLinks" import ShowLinks from "../resource_links/ShowLinks"
import { useCalendarInvitations } from "../../composables/calendar/calendarInvitations" import { useCalendarInvitations } from "../../composables/calendar/calendarInvitations"
import { type } from "../../constants/entity/ccalendarevent" import { type } from "../../constants/entity/ccalendarevent"
import CalendarEventSubscriptionsInfo from "./CalendarEventSubscriptionsInfo.vue"
import CalendarEventInvitationsInfo from "./CalendarEventInvitationsInfo.vue"
const { abbreviatedDatetime } = useFormatDate() const { abbreviatedDatetime } = useFormatDate()
const { allowCollectiveInvitations } = useCalendarInvitations() const { allowCollectiveInvitations } = useCalendarInvitations()

@ -0,0 +1,32 @@
<script setup>
import ShowLinks from "../resource_links/ShowLinks.vue"
defineProps({
event: {
type: Object,
required: true,
},
})
</script>
<template>
<div class="invitations-info">
<h6
v-t="'Invitations'"
class="invitations-info__title"
/>
<div
v-if="event.resourceLinkListFromEntity.length"
class="invitations-info__item"
>
<p v-t="'Invitees'" />
<div>
<ShowLinks
:item="event"
:show-status="false"
/>
</div>
</div>
</div>
</template>

@ -0,0 +1,69 @@
<script setup>
import ShowLinks from "../resource_links/ShowLinks.vue"
import { subscriptionVisibility } from "../../constants/entity/ccalendarevent"
defineProps({
event: {
type: Object,
required: true,
},
})
</script>
<template>
<div class="invitations-info">
<h6
v-t="'Subscriptions'"
class="invitations-info__title"
/>
<div class="invitations-info__item">
<p v-t="'Allow subscriptions'" />
<p
v-if="subscriptionVisibility.no === event.subscriptionVisibility"
v-text="'No'"
/>
<p
v-else-if="subscriptionVisibility.all === event.subscriptionVisibility"
v-text="'All system users'"
/>
<p
v-else-if="subscriptionVisibility.class === event.subscriptionVisibility"
v-text="'Users inside the class'"
/>
<p
v-if="subscriptionVisibility.class === event.subscriptionVisibility"
v-text="event.subscriptionItemTitle"
/>
</div>
<div
v-if="event.maxAttendees"
class="invitations-info__item"
>
<p v-t="'Maximum number of subscriptions'" />
<p v-text="event.maxAttendees" />
</div>
<div
v-if="event.maxAttendees"
class="invitations-info__item"
>
<p v-t="'Subscriptions count'" />
<p v-text="event.resourceLinkListFromEntity.length" />
</div>
<div
v-if="event.resourceLinkListFromEntity.length"
class="invitations-info__item"
>
<p v-t="'Subscribers'" />
<div>
<ShowLinks
:item="event"
:show-status="false"
/>
</div>
</div>
</div>
</template>

@ -1,7 +1,10 @@
<template> <template>
<Topbar v-if="!hideInterface" /> <Topbar />
<Sidebar v-if="!hideInterface && securityStore.isAuthenticated" /> <Sidebar v-if="securityStore.isAuthenticated" />
<div v-if="!hideInterface" class="app-main" :class="{ 'app-main--no-sidebar': !securityStore.isAuthenticated }"> <div
class="app-main"
:class="{ 'app-main--no-sidebar': !securityStore.isAuthenticated }"
>
<Breadcrumb v-if="showBreadcrumb" /> <Breadcrumb v-if="showBreadcrumb" />
<slot /> <slot />
<router-view /> <router-view />
@ -9,7 +12,6 @@
</template> </template>
<script setup> <script setup>
import { ref } from "vue"
import Breadcrumb from "../../components/Breadcrumb.vue" import Breadcrumb from "../../components/Breadcrumb.vue"
import Topbar from "../../components/layout/Topbar.vue" import Topbar from "../../components/layout/Topbar.vue"
import Sidebar from "../../components/layout/Sidebar.vue" import Sidebar from "../../components/layout/Sidebar.vue"
@ -23,6 +25,4 @@ defineProps({
}) })
const securityStore = useSecurityStore() const securityStore = useSecurityStore()
const chamiloAppSettings = window.ChamiloAppSettings || {}
const hideInterface = ref(!!chamiloAppSettings.hideInterface)
</script> </script>

@ -4,7 +4,7 @@
<h3 class="app-sidebar__top"> <h3 class="app-sidebar__top">
{{ t("Menu") }} {{ t("Menu") }}
</h3> </h3>
<div class="app-sidebar__panel"> <div class="app-sidebar__panel" @click="handlePanelHeaderClick">
<PanelMenu :model="menuItems" /> <PanelMenu :model="menuItems" />
</div> </div>
<div class="app-sidebar__bottom"> <div class="app-sidebar__bottom">
@ -55,6 +55,7 @@ const securityStore = useSecurityStore()
const { menuItems } = useSidebarMenu() const { menuItems } = useSidebarMenu()
const sidebarIsOpen = ref(window.localStorage.getItem("sidebarIsOpen") === "true") const sidebarIsOpen = ref(window.localStorage.getItem("sidebarIsOpen") === "true")
const expandingDueToPanelClick = ref(false)
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
@ -62,13 +63,35 @@ watch(
sidebarIsOpen, sidebarIsOpen,
(newValue) => { (newValue) => {
const appEl = document.querySelector("#app") const appEl = document.querySelector("#app")
window.localStorage.setItem("sidebarIsOpen", newValue.toString()) window.localStorage.setItem("sidebarIsOpen", newValue.toString())
appEl.classList.toggle("app--sidebar-inactive", !newValue) appEl.classList.toggle("app--sidebar-inactive", !newValue)
},
{ if (!newValue) {
immediate: true, if (!expandingDueToPanelClick.value) {
}, const expandedHeaders = document.querySelectorAll('.p-panelmenu-header.p-highlight')
) expandedHeaders.forEach(header => {
header.click()
})
sidebarIsOpen.value = false
window.localStorage.setItem("sidebarIsOpen", 'false')
}
}
expandingDueToPanelClick.value = false
}, { immediate: true })
const handlePanelHeaderClick = (event) => {
const header = event.target.closest('.p-panelmenu-header')
if (!header) return
const contentId = header.getAttribute('aria-controls')
const contentPanel = document.getElementById(contentId)
if (contentPanel && contentPanel.querySelector('.p-toggleable-content')) {
if (!sidebarIsOpen.value) {
expandingDueToPanelClick.value = true
sidebarIsOpen.value = true
window.localStorage.setItem("sidebarIsOpen", 'true')
}
}
}
</script> </script>

@ -1,9 +1,13 @@
import { type } from "../../constants/entity/ccalendarevent" import { type, subscriptionVisibility } from "../../constants/entity/ccalendarevent"
export function useCalendarEvent() { export function useCalendarEvent() {
return { return {
findUserLink, findUserLink,
isEditableByUser, isEditableByUser,
isSubscribeable,
canSubscribeToEvent,
allowSubscribeToEvent,
allowUnsubscribeToEvent,
} }
} }
@ -36,3 +40,35 @@ function isEditableByUser(event, userId) {
return false return false
} }
function isSubscribeable(event) {
if (type.subscription !== event.invitationType) {
return false
}
return subscriptionVisibility.no !== event.subscriptionVisibility
}
/**
* @param {Object} event
* @returns {boolean}
*/
function canSubscribeToEvent(event) {
return event.resourceLinkListFromEntity.length < event.maxAttendees || 0 === event.maxAttendees
}
function allowSubscribeToEvent(event) {
if (!isSubscribeable(event)) {
return false
}
return canSubscribeToEvent(event)
}
function allowUnsubscribeToEvent(event, userId) {
if (!isSubscribeable(event)) {
return false
}
return !!findUserLink(event, userId)
}

@ -8,6 +8,8 @@ async function find(iri) {
return await api.get(iri) return await api.get(iri)
} }
export { async function post(params) {
find return await api.post("/api/resource_links", params)
} }
export { find, post }

@ -0,0 +1,5 @@
import { post } from "./baseService"
export default {
post,
}

@ -40,7 +40,12 @@
/> />
</div> </div>
<div class="flex flex-wrap mb-4"> <div class="flex flex-wrap mb-4 gap-4">
<BaseInputText
v-model="themeTitle"
:label="t('Title')"
/>
<BaseButton <BaseButton
type="primary" type="primary"
icon="send" icon="send"
@ -182,6 +187,8 @@ import axios from "axios"
const { t } = useI18n() const { t } = useI18n()
const { getColorTheme, getColors } = useTheme() const { getColorTheme, getColors } = useTheme()
const themeTitle = ref()
let primaryColor = getColorTheme("--color-primary-base") let primaryColor = getColorTheme("--color-primary-base")
let primaryColorGradient = getColorTheme("--color-primary-gradient") let primaryColorGradient = getColorTheme("--color-primary-gradient")
let secondaryColor = getColorTheme("--color-secondary-base") let secondaryColor = getColorTheme("--color-secondary-base")
@ -196,6 +203,7 @@ const saveColors = async () => {
let colors = getColors() let colors = getColors()
// TODO send colors to backend, then notify if was correct or incorrect // TODO send colors to backend, then notify if was correct or incorrect
await axios.post("/api/color_themes", { await axios.post("/api/color_themes", {
title: themeTitle.value,
variables: colors, variables: colors,
}) })
} }

@ -44,6 +44,7 @@
v-model:visible="dialogShow" v-model:visible="dialogShow"
:header="t('Event')" :header="t('Event')"
:modal="true" :modal="true"
:style="{ width: '30rem' }"
> >
<CCalendarEventInfo :event="item" /> <CCalendarEventInfo :event="item" />
@ -54,6 +55,22 @@
type="black" type="black"
@click="dialogShow = false" @click="dialogShow = false"
/> />
<BaseButton
v-if="allowToUnsubscribe"
:label="t('Unsubscribe')"
type="black"
icon="join-group"
@click="unsubscribeToEvent"
/>
<BaseButton
v-else-if="allowToSubscribe"
:label="t('Subscribe')"
type="black"
icon="join-group"
@click="subscribeToEvent"
/>
<BaseButton <BaseButton
:label="t('Delete')" :label="t('Delete')"
icon="delete" icon="delete"
@ -130,6 +147,7 @@ import { storeToRefs } from "pinia"
import CalendarSectionHeader from "../../components/ccalendarevent/CalendarSectionHeader.vue" import CalendarSectionHeader from "../../components/ccalendarevent/CalendarSectionHeader.vue"
import { useCalendarActionButtons } from "../../composables/calendar/calendarActionButtons" import { useCalendarActionButtons } from "../../composables/calendar/calendarActionButtons"
import { useCalendarEvent } from "../../composables/calendar/calendarEvent" import { useCalendarEvent } from "../../composables/calendar/calendarEvent"
import resourceLinkService from "../../services/resourceLinkService"
const store = useStore() const store = useStore()
const confirm = useConfirm() const confirm = useConfirm()
@ -141,12 +159,14 @@ const { abbreviatedDatetime } = useFormatDate()
const { showAddButton } = useCalendarActionButtons() const { showAddButton } = useCalendarActionButtons()
const { isEditableByUser } = useCalendarEvent() const { isEditableByUser, allowSubscribeToEvent, allowUnsubscribeToEvent } = useCalendarEvent()
const item = ref({}) const item = ref({})
const dialog = ref(false) const dialog = ref(false)
const dialogShow = ref(false) const dialogShow = ref(false)
const allowToEdit = ref(false) const allowToEdit = ref(false)
const allowToSubscribe = ref(false)
const allowToUnsubscribe = ref(false)
const currentUser = computed(() => store.getters["security/getUser"]) const currentUser = computed(() => store.getters["security/getUser"])
const { t } = useI18n() const { t } = useI18n()
@ -248,6 +268,8 @@ const calendarOptions = ref({
item.value["parentResourceNodeId"] = event.extendedProps.resourceNode.creator.id item.value["parentResourceNodeId"] = event.extendedProps.resourceNode.creator.id
allowToEdit.value = isEditableByUser(item.value, currentUser.value.id) allowToEdit.value = isEditableByUser(item.value, currentUser.value.id)
allowToSubscribe.value = !allowToEdit.value && allowSubscribeToEvent(item.value)
allowToUnsubscribe.value = !allowToEdit.value && allowUnsubscribeToEvent(item.value, currentUser.value.id)
dialogShow.value = true dialogShow.value = true
}, },
@ -309,6 +331,23 @@ function confirmDelete() {
}) })
} }
async function subscribeToEvent() {
try {
await resourceLinkService.post({
resourceNode: item.value.resourceNode["@id"],
user: currentUser.value["@id"],
visibility: RESOURCE_LINK_PUBLISHED,
})
allowToSubscribe.value = false
allowToUnsubscribe.value = true
} catch (e) {
console.error(e)
}
}
async function unsubscribeToEvent() {}
const isLoading = computed(() => store.getters["ccalendarevent/isLoading"]) const isLoading = computed(() => store.getters["ccalendarevent/isLoading"])
const createForm = ref(null) const createForm = ref(null)

@ -3517,7 +3517,7 @@ function api_not_allowed(
$message = null, $message = null,
$responseCode = 0 $responseCode = 0
): never { ): never {
throw new NotAllowedException($message ?: 'You are not allowed', $responseCode); throw new NotAllowedException($message ?: 'You are not allowed', null, $responseCode);
} }
/** /**

@ -197,7 +197,7 @@ if (!isset($src)) {
switch ($lpType) { switch ($lpType) {
case CLp::LP_TYPE: case CLp::LP_TYPE:
$oLP->stop_previous_item(); $oLP->stop_previous_item();
$htmlHeadXtra[] = '<script src="scorm_api.php"></script>'; $htmlHeadXtra[] = '<script src="scorm_api.php?'.api_get_cidreq().'"></script>';
$preReqCheck = $oLP->prerequisites_match($lp_item_id); $preReqCheck = $oLP->prerequisites_match($lp_item_id);
if (true === $preReqCheck) { if (true === $preReqCheck) {
@ -239,7 +239,7 @@ if (!isset($src)) {
case CLp::SCORM_TYPE: case CLp::SCORM_TYPE:
// save old if asset // save old if asset
$oLP->stop_previous_item(); // save status manually if asset $oLP->stop_previous_item(); // save status manually if asset
$htmlHeadXtra[] = '<script src="scorm_api.php"></script>'; $htmlHeadXtra[] = '<script src="scorm_api.php?'.api_get_cidreq().'"></script>';
$preReqCheck = $oLP->prerequisites_match($lp_item_id); $preReqCheck = $oLP->prerequisites_match($lp_item_id);
if (true === $preReqCheck) { if (true === $preReqCheck) {

@ -37,6 +37,8 @@ class CalendarEvent extends AbstractResource
#[Groups(['calendar_event:read'])] #[Groups(['calendar_event:read'])]
public ?int $subscriptionItemId = null, public ?int $subscriptionItemId = null,
#[Groups(['calendar_event:read'])] #[Groups(['calendar_event:read'])]
public ?string $subscriptionItemTitle = null,
#[Groups(['calendar_event:read'])]
public int $maxAttendees = 0, public int $maxAttendees = 0,
#[Groups(['calendar_event:read'])] #[Groups(['calendar_event:read'])]
public ?ResourceNode $resourceNode = null, public ?ResourceNode $resourceNode = null,

@ -6,6 +6,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller; namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Repository\ColorThemeRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
@ -16,18 +17,23 @@ class ThemeController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly ParameterBagInterface $parameterBag, private readonly ParameterBagInterface $parameterBag,
private readonly ColorThemeRepository $colorThemeRepository,
) {} ) {}
#[Route('/theme/colors.css', name: 'chamilo_color_theme', methods: ['GET'])] #[Route('/theme/colors.css', name: 'chamilo_color_theme', methods: ['GET'])]
public function colorTehemeAction(): Response public function colorThemeAction(): Response
{ {
$fs = new Filesystem(); $response = new Response('');
$path = $this->parameterBag->get('kernel.project_dir').'/var/theme/colors.css';
if ($fs->exists($path)) { $colorTheme = $this->colorThemeRepository->getActiveOne();
$response = $this->file($path);
} else { if ($colorTheme) {
$response = new Response(''); $fs = new Filesystem();
$path = $this->parameterBag->get('kernel.project_dir')."/var/theme/{$colorTheme->getSlug()}/colors.css";
if ($fs->exists($path)) {
$response = $this->file($path);
}
} }
$response->headers->add(['Content-Type' => 'text/css']); $response->headers->add(['Content-Type' => 'text/css']);

@ -10,6 +10,7 @@ use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use Chamilo\CoreBundle\ApiResource\CalendarEvent; use Chamilo\CoreBundle\ApiResource\CalendarEvent;
use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\SessionRelCourse; use Chamilo\CoreBundle\Entity\SessionRelCourse;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Chamilo\CourseBundle\Entity\CCalendarEvent; use Chamilo\CourseBundle\Entity\CCalendarEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Routing\RouterInterface;
@ -18,6 +19,7 @@ class CalendarEventTransformer implements DataTransformerInterface
{ {
public function __construct( public function __construct(
private readonly RouterInterface $router, private readonly RouterInterface $router,
private readonly UsergroupRepository $usergroupRepository,
) {} ) {}
public function transform($object, string $to, array $context = []): object public function transform($object, string $to, array $context = []): object
@ -40,6 +42,12 @@ class CalendarEventTransformer implements DataTransformerInterface
$object->setResourceLinkListFromEntity(); $object->setResourceLinkListFromEntity();
$subscriptionItemTitle = null;
if (CCalendarEvent::SUBSCRIPTION_VISIBILITY_CLASS == $object->getSubscriptionVisibility()) {
$subscriptionItemTitle = $this->usergroupRepository->find($object->getSubscriptionItemId())?->getTitle();
}
return new CalendarEvent( return new CalendarEvent(
'calendar_event_'.$object->getIid(), 'calendar_event_'.$object->getIid(),
$object->getTitle(), $object->getTitle(),
@ -52,6 +60,7 @@ class CalendarEventTransformer implements DataTransformerInterface
$object->isCollective(), $object->isCollective(),
$object->getSubscriptionVisibility(), $object->getSubscriptionVisibility(),
$object->getSubscriptionItemId(), $object->getSubscriptionItemId(),
$subscriptionItemTitle,
$object->getMaxAttendees(), $object->getMaxAttendees(),
$object->getResourceNode(), $object->getResourceNode(),
$object->getResourceLinkListFromEntity(), $object->getResourceLinkListFromEntity(),

@ -1,137 +0,0 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'agenda_event_invitation')]
#[ORM\Entity()]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap([
'invitation' => 'Chamilo\CoreBundle\Entity\AgendaEventInvitation',
'subscription' => 'Chamilo\CoreBundle\Entity\AgendaEventSubscription',
])]
class AgendaEventInvitation
{
use TimestampableTypedEntity;
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue(strategy: 'AUTO')]
protected ?int $id = null;
#[ORM\OneToMany(
mappedBy: 'invitation',
targetEntity: AgendaEventInvitee::class,
cascade: ['persist', 'remove'],
orphanRemoval: true
)]
protected Collection $invitees;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'resourceNodes')]
#[ORM\JoinColumn(name: 'creator_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
protected User $creator;
public function __construct()
{
$this->invitees = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getInvitees(): Collection
{
return $this->invitees;
}
public function setInvitees(Collection $invitees): self
{
$this->invitees = $invitees;
return $this;
}
public function addInvitee(AgendaEventInvitee $invitee): self
{
$invitee->setInvitation($this);
$this->invitees->add($invitee);
return $this;
}
public function removeInviteeUser(User $user): self
{
/** @var AgendaEventInvitee $invitee */
$invitee = $this
->invitees
->filter(function (AgendaEventInvitee $invitee) use ($user) {
return $invitee->getUser() === $user;
})
->first()
;
if ($invitee) {
$this->invitees->removeElement($invitee);
$invitee->setInvitation(null);
}
return $this;
}
public function removeInvitees(): self
{
$this->invitees = new ArrayCollection();
return $this;
}
public function getCreator(): User
{
return $this->creator;
}
public function setCreator(User $creator): self
{
$this->creator = $creator;
return $this;
}
public function hasUserAsInvitee(User $user): bool
{
return $this->invitees->exists(
function (int $key, AgendaEventInvitee $invitee) use ($user) {
return $invitee->getUser() === $user;
}
);
}
public function removeInviteesNotInIdList(array $idList): self
{
$toRemove = [];
/** @var AgendaEventInvitee $invitee */
foreach ($this->invitees as $key => $invitee) {
if (!\in_array($invitee->getUser()->getId(), $idList, true)) {
$toRemove[] = $key;
}
}
foreach ($toRemove as $key) {
$this->invitees->remove($key);
}
return $this;
}
}

@ -1,65 +0,0 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'agenda_event_invitee')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap([
'invitee' => AgendaEventInvitee::class,
'subscriber' => AgendaEventSubscriber::class,
])]
class AgendaEventInvitee
{
use TimestampableTypedEntity;
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
private int $id;
#[ORM\ManyToOne(targetEntity: AgendaEventInvitation::class, inversedBy: 'invitees')]
#[ORM\JoinColumn(name: 'invitation_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
private ?AgendaEventInvitation $invitation;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
private ?User $user;
public function getId(): int
{
return $this->id;
}
public function getInvitation(): ?AgendaEventInvitation
{
return $this->invitation;
}
public function setInvitation(?AgendaEventInvitation $invitation): self
{
$this->invitation = $invitation;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
}

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class AgendaEventSubscriber extends AgendaEventInvitee {}

@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class AgendaEventSubscription extends AgendaEventInvitation
{
public const SUBSCRIPTION_NO = 0;
public const SUBSCRIPTION_ALL = 1;
public const SUBSCRIPTION_CLASS = 2;
#[ORM\Column(name: 'max_attendees', type: 'integer', nullable: false, options: ['default' => 0])]
protected int $maxAttendees = 0;
public function getMaxAttendees(): int
{
return $this->maxAttendees;
}
public function setMaxAttendees(int $maxAttendees): self
{
$this->maxAttendees = $maxAttendees;
return $this;
}
}

@ -11,6 +11,7 @@ use ApiPlatform\Metadata\Post;
use Chamilo\CoreBundle\State\ColorThemeProcessor; use Chamilo\CoreBundle\State\ColorThemeProcessor;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity; use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity] #[ORM\Entity]
@ -34,6 +35,10 @@ class ColorTheme
#[ORM\Column] #[ORM\Column]
private ?int $id = null; private ?int $id = null;
#[Groups(['color_theme:write'])]
#[ORM\Column(length: 255)]
private ?string $title = null;
/** /**
* @var array<string, mixed> * @var array<string, mixed>
*/ */
@ -41,11 +46,31 @@ class ColorTheme
#[ORM\Column] #[ORM\Column]
private array $variables = []; private array $variables = [];
#[Gedmo\Slug(fields: ['title'])]
#[ORM\Column(length: 255)]
private ?string $slug = null;
#[Groups(['color_theme:write'])]
#[ORM\Column]
private ?bool $active = null;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
} }
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
public function getVariables(): array public function getVariables(): array
{ {
return $this->variables; return $this->variables;
@ -57,4 +82,28 @@ class ColorTheme
return $this; return $this;
} }
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): static
{
$this->slug = $slug;
return $this;
}
public function isActive(): ?bool
{
return $this->active;
}
public function setActive(bool $active): static
{
$this->active = $active;
return $this;
}
} }

@ -259,6 +259,7 @@ class ResourceLink implements Stringable
public function setResourceNode(ResourceNode $resourceNode): self public function setResourceNode(ResourceNode $resourceNode): self
{ {
$this->resourceNode = $resourceNode; $this->resourceNode = $resourceNode;
$this->resourceTypeGroup = $resourceNode->getResourceType()->getId();
return $this; return $this;
} }

@ -469,10 +469,8 @@ class ResourceNode implements Stringable
public function addResourceLink(ResourceLink $link): self public function addResourceLink(ResourceLink $link): self
{ {
$link $link->setResourceNode($this);
->setResourceNode($this)
->setResourceTypeGroup($this->resourceType->getId())
;
$this->resourceLinks->add($link); $this->resourceLinks->add($link);
return $this; return $this;

@ -73,7 +73,7 @@ class CourseListener
} }
if (true === $cidReset) { if (true === $cidReset) {
$this->removeCourseFromSession($request); $this->cleanSessionHandler($request);
return; return;
} }
@ -190,6 +190,8 @@ class CourseListener
$courseParams = $this->generateCourseUrl($course, $sessionId, $groupId, $origin); $courseParams = $this->generateCourseUrl($course, $sessionId, $groupId, $origin);
$sessionHandler->set('course_url_params', $courseParams); $sessionHandler->set('course_url_params', $courseParams);
$twig->addGlobal('course_url_params', $courseParams); $twig->addGlobal('course_url_params', $courseParams);
} else {
$this->cleanSessionHandler($request);
} }
} }
@ -268,7 +270,7 @@ class CourseListener
} }
} }
public function removeCourseFromSession(Request $request): void public function cleanSessionHandler(Request $request): void
{ {
$sessionHandler = $request->getSession(); $sessionHandler = $request->getSession();
$alreadyVisited = $sessionHandler->get('course_already_visited'); $alreadyVisited = $sessionHandler->get('course_already_visited');

@ -6,8 +6,8 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\EventListener; namespace Chamilo\CoreBundle\EventListener;
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Chamilo\CoreBundle\Exception\NotAllowedException; use Chamilo\CoreBundle\Exception\NotAllowedException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
@ -44,7 +44,9 @@ class ExceptionListener
$redirectUrl = $baseUrl.$path.($query ? '?'.$query : ''); $redirectUrl = $baseUrl.$path.($query ? '?'.$query : '');
$loginUrl = $this->router->generate('login', ['redirect' => $redirectUrl], UrlGeneratorInterface::ABSOLUTE_URL); $loginUrl = $this->router->generate('login', ['redirect' => $redirectUrl], UrlGeneratorInterface::ABSOLUTE_URL);
ChamiloApi::redirectTo($loginUrl); $event->setResponse(new RedirectResponse($loginUrl));
return;
} }
} }

@ -6,9 +6,11 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\EventListener; namespace Chamilo\CoreBundle\EventListener;
use Chamilo\CoreBundle\Repository\ColorThemeRepository;
use Chamilo\CoreBundle\Repository\LanguageRepository; use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Settings\SettingsManager; use Chamilo\CoreBundle\Settings\SettingsManager;
use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
@ -31,7 +33,9 @@ class TwigListener
SerializerInterface $serializer, SerializerInterface $serializer,
TokenStorageInterface $tokenStorage, TokenStorageInterface $tokenStorage,
SettingsManager $settingsManager, SettingsManager $settingsManager,
LanguageRepository $languageRepository LanguageRepository $languageRepository,
private readonly ColorThemeRepository $colorThemeRepository,
private readonly RouterInterface $router,
) { ) {
$this->twig = $twig; $this->twig = $twig;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
@ -99,5 +103,22 @@ class TwigListener
$this->twig->addGlobal('access_url_id', $request->getSession()->get('access_url_id')); $this->twig->addGlobal('access_url_id', $request->getSession()->get('access_url_id'));
$this->twig->addGlobal('config_json', json_encode($config)); $this->twig->addGlobal('config_json', json_encode($config));
$this->twig->addGlobal('languages_json', json_encode($languages)); $this->twig->addGlobal('languages_json', json_encode($languages));
$this->loadColorTheme();
}
private function loadColorTheme(): void
{
$link = null;
$colorTheme = $this->colorThemeRepository->getActiveOne();
if ($colorTheme) {
$path = $this->router->generate('chamilo_color_theme');
$link = '<link rel="stylesheet" href="'.$path.'">';
}
$this->twig->addGlobal('color_theme_link', $link);
} }
} }

@ -6,12 +6,13 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Exception; namespace Chamilo\CoreBundle\Exception;
use Exception; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
class NotAllowedException extends Exception class NotAllowedException extends HttpException
{ {
public function __construct($message = 'Not allowed', $code = 0, ?Exception $previous = null) public function __construct(?string $message = 'Not allowed', ?\Throwable $previous = null, int $code = 0, array $headers = [])
{ {
parent::__construct($message, $code, $previous); parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous, $headers);
} }
} }

@ -264,8 +264,11 @@ abstract class AbstractMigrationChamilo extends AbstractMigration implements Con
$userId = (int) $item['insert_user_id']; $userId = (int) $item['insert_user_id'];
$sessionId = $item['session_id'] ?? 0; $sessionId = $item['session_id'] ?? 0;
$groupId = $item['to_group_id'] ?? 0; $groupId = $item['to_group_id'] ?? 0;
$lastUpdatedAt = new DateTime($item['lastedit_date'], new DateTimeZone('UTC')); if (empty($item['lastedit_date'])) {
$lastUpdatedAt = new DateTime('now', new DateTimeZone('UTC'));
} else {
$lastUpdatedAt = new DateTime($item['lastedit_date'], new DateTimeZone('UTC'));
}
$newVisibility = ResourceLink::VISIBILITY_DRAFT; $newVisibility = ResourceLink::VISIBILITY_DRAFT;
// Old 1.11.x visibility (item property) is based in this switch: // Old 1.11.x visibility (item property) is based in this switch:

@ -19,6 +19,12 @@ final class Version20200821224242 extends AbstractMigrationChamilo
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
$connection = $this->getEntityManager()->getConnection(); $connection = $this->getEntityManager()->getConnection();
if ($schema->hasTable('message_feedback')) {
$table = $schema->getTable('message_feedback');
if ($table->hasForeignKey('FK_DB0F8049537A1329')) {
$this->addSql('ALTER TABLE message_feedback DROP FOREIGN KEY FK_DB0F8049537A1329');
}
}
$table = $schema->getTable('message'); $table = $schema->getTable('message');
$this->addSql('ALTER TABLE message CHANGE id id INT AUTO_INCREMENT NOT NULL, CHANGE parent_id parent_id INT DEFAULT NULL;'); $this->addSql('ALTER TABLE message CHANGE id id INT AUTO_INCREMENT NOT NULL, CHANGE parent_id parent_id INT DEFAULT NULL;');
@ -207,9 +213,6 @@ final class Version20200821224242 extends AbstractMigrationChamilo
$table = $schema->getTable('message_feedback'); $table = $schema->getTable('message_feedback');
if ($table->hasForeignKey('FK_DB0F8049537A1329')) {
$this->addSql('ALTER TABLE message_feedback DROP FOREIGN KEY FK_DB0F8049537A1329');
}
if ($table->hasIndex('IDX_DB0F8049537A1329')) { if ($table->hasIndex('IDX_DB0F8049537A1329')) {
$this->addSql('DROP INDEX IDX_DB0F8049537A1329 ON message_feedback'); $this->addSql('DROP INDEX IDX_DB0F8049537A1329 ON message_feedback');
} }

@ -46,6 +46,14 @@ class Version20211005153900 extends AbstractMigrationChamilo
$this->addSql('CREATE INDEX IDX_EDE2C7686219A7B7 ON ticket_ticket (assigned_last_user)'); $this->addSql('CREATE INDEX IDX_EDE2C7686219A7B7 ON ticket_ticket (assigned_last_user)');
} }
if (!$table->hasColumn('exercise_id')) {
$this->addSql('ALTER TABLE ticket_ticket ADD exercise_id INT DEFAULT NULL');
}
if (!$table->hasColumn('lp_id')) {
$this->addSql('ALTER TABLE ticket_ticket ADD lp_id INT DEFAULT NULL');
}
$table = $schema->getTable('ticket_assigned_log'); $table = $schema->getTable('ticket_assigned_log');
if (!$table->hasForeignKey('FK_54B65868700047D2')) { if (!$table->hasForeignKey('FK_54B65868700047D2')) {
$this->addSql('ALTER TABLE ticket_assigned_log ADD CONSTRAINT FK_54B65868700047D2 FOREIGN KEY (ticket_id) REFERENCES ticket_ticket (id) ON DELETE CASCADE;'); $this->addSql('ALTER TABLE ticket_assigned_log ADD CONSTRAINT FK_54B65868700047D2 FOREIGN KEY (ticket_id) REFERENCES ticket_ticket (id) ON DELETE CASCADE;');

@ -72,31 +72,36 @@ class Version20230904173400 extends AbstractMigrationChamilo
$em->persist($calendarEvent); $em->persist($calendarEvent);
if ($collectiveInvitationsEnabled) { if ($collectiveInvitationsEnabled) {
$calendarEvent->setCollective((bool) $personalAgenda['collective']);
$hasSubscriptions = false;
$invitationsOrSubscriptionsInfo = []; $invitationsOrSubscriptionsInfo = [];
if ($subscriptionsEnabled) { if ($subscriptionsEnabled) {
$subscriptionsInfo = $this->getSubscriptions((int) $personalAgenda['id']); $subscriptionsInfo = $this->getSubscriptions((int) $personalAgenda['id']);
if (\count($subscriptionsInfo) > 0) { if (\count($subscriptionsInfo) > 0
$hasSubscriptions = true; && 0 !== $personalAgenda['subscription_visibility']
) {
$invitationsOrSubscriptionsInfo = $subscriptionsInfo; $invitationsOrSubscriptionsInfo = $subscriptionsInfo;
} }
} }
if ($hasSubscriptions) { if ($invitationsOrSubscriptionsInfo) {
$calendarEvent $calendarEvent
->setInvitationType(CCalendarEvent::TYPE_SUBSCRIPTION) ->setInvitationType(CCalendarEvent::TYPE_SUBSCRIPTION)
->setSubscriptionVisibility($personalAgenda['subscription_visibility']) ->setSubscriptionVisibility($personalAgenda['subscription_visibility'])
->setSubscriptionItemId($personalAgenda['subscription_item_id']) ->setSubscriptionItemId($personalAgenda['subscription_item_id'])
->setMaxAttendees($invitationsOrSubscriptionsInfo[0]['max_attendees'])
; ;
} else { } else {
$calendarEvent->setInvitationType(CCalendarEvent::TYPE_INVITATION); $invitationsInfo = $this->getInvitations($subscriptionsEnabled, (int) $personalAgenda['id']);
if (\count($invitationsInfo) > 0) {
$calendarEvent
->setCollective((bool) $personalAgenda['collective'])
->setInvitationType(CCalendarEvent::TYPE_INVITATION)
;
$invitationsOrSubscriptionsInfo = $this->getInvitations($subscriptionsEnabled, (int) $personalAgenda['id']); $invitationsOrSubscriptionsInfo = $invitationsInfo;
}
} }
foreach ($invitationsOrSubscriptionsInfo as $invitationOrSubscriptionInfo) { foreach ($invitationsOrSubscriptionsInfo as $invitationOrSubscriptionInfo) {
@ -197,7 +202,7 @@ class Version20230904173400 extends AbstractMigrationChamilo
private function getSubscriptions(int $personalAgendaId): array private function getSubscriptions(int $personalAgendaId): array
{ {
$sql = "SELECT i.id, i.creator_id, i.created_at, i.updated_at $sql = "SELECT i.id, i.creator_id, i.created_at, i.updated_at, i.max_attendees
FROM agenda_event_invitation i FROM agenda_event_invitation i
INNER JOIN personal_agenda pa ON i.id = pa.agenda_event_invitation_id INNER JOIN personal_agenda pa ON i.id = pa.agenda_event_invitation_id
WHERE pa.id = $personalAgendaId WHERE pa.id = $personalAgendaId

@ -0,0 +1,34 @@
<?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;
class Version20230904173401 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Calendar: Cleanup about invitations/subscriptions';
}
public function up(Schema $schema): void
{
if ($schema->hasTable('agenda_event_invitation')) {
$this->addSql('ALTER TABLE personal_agenda DROP FOREIGN KEY FK_D8612460AF68C6B');
$this->addSql('DROP INDEX UNIQ_D8612460AF68C6B ON personal_agenda');
$this->addSql('ALTER TABLE personal_agenda DROP agenda_event_invitation_id, DROP collective, DROP subscription_visibility, DROP subscription_item_id');
$this->addSql('ALTER TABLE agenda_event_invitation DROP FOREIGN KEY FK_52A2D5E161220EA6');
$this->addSql('DROP TABLE agenda_event_invitation');
$this->addSql('ALTER TABLE agenda_event_invitee DROP FOREIGN KEY FK_4F5757FEA76ED395');
$this->addSql('DROP TABLE agenda_event_invitee');
}
}
}

@ -75,12 +75,6 @@ final class Version20240112131200 extends AbstractMigrationChamilo
$this->addSql('ALTER TABLE resource_tag CHANGE id id INT AUTO_INCREMENT NOT NULL;'); $this->addSql('ALTER TABLE resource_tag CHANGE id id INT AUTO_INCREMENT NOT NULL;');
} }
if ($schema->hasTable('agenda_event_invitation')) {
error_log('Perform the changes in the agenda_event_invitation table');
$this->addSql('ALTER TABLE personal_agenda DROP FOREIGN KEY FK_D8612460AF68C6B');
$this->addSql('ALTER TABLE agenda_event_invitation CHANGE id id INT AUTO_INCREMENT NOT NULL;');
}
if ($schema->hasTable('gradebook_comment')) { if ($schema->hasTable('gradebook_comment')) {
error_log('Perform the changes in the gradebook_comment table'); error_log('Perform the changes in the gradebook_comment table');
$this->addSql('ALTER TABLE gradebook_comment CHANGE id id INT AUTO_INCREMENT NOT NULL;'); $this->addSql('ALTER TABLE gradebook_comment CHANGE id id INT AUTO_INCREMENT NOT NULL;');

@ -88,9 +88,9 @@ final class Version20240122221400 extends AbstractMigrationChamilo
$kernel = $container->get('kernel'); $kernel = $container->get('kernel');
$rootPath = $kernel->getProjectDir(); $rootPath = $kernel->getProjectDir();
$langPath = $rootPath.'/public/main/lang/'.$englishName.'/trad4all.inc.php'; $langPath = $rootPath.'/var/translations/import/'.$englishName.'/trad4all.inc.php';
$destinationFilePath = $rootPath.'/var/translations/messages.'.$isocode.'.po'; $destinationFilePath = $rootPath.'/var/translations/messages.'.$isocode.'.po';
$originalFile = $rootPath.'/public/main/lang/english/trad4all.inc.php'; $originalFile = $rootPath.'/var/translations/import/english/trad4all.inc.php';
if (!file_exists($langPath)) { if (!file_exists($langPath)) {
error_log("Original file not found: $langPath"); error_log("Original file not found: $langPath");
@ -118,6 +118,8 @@ final class Version20240122221400 extends AbstractMigrationChamilo
$langPath, $langPath,
true true
); );
$termsInLanguage = [];
foreach ($originalTermsInLanguage as $id => $content) { foreach ($originalTermsInLanguage as $id => $content) {
if (!isset($termsInLanguage[$id])) { if (!isset($termsInLanguage[$id])) {
$termsInLanguage[$id] = trim(rtrim($content, ';'), '"'); $termsInLanguage[$id] = trim(rtrim($content, ';'), '"');
@ -131,10 +133,8 @@ final class Version20240122221400 extends AbstractMigrationChamilo
continue; continue;
} }
$doneTranslations[$englishTranslation] = true; $doneTranslations[$englishTranslation] = true;
$translatedTerm = ''; $translatedTerm = $termsInLanguage[$term] ?? '';
if (!empty($termsInLanguage[$term])) {
$translatedTerm = $termsInLanguage[$term];
}
// Here we apply a little correction to avoid unterminated strings // Here we apply a little correction to avoid unterminated strings
// when a string ends with a \" // when a string ends with a \"
if (preg_match('/\\\$/', $englishTranslation)) { if (preg_match('/\\\$/', $englishTranslation)) {

@ -13,6 +13,6 @@ class Version20240318105600 extends AbstractMigrationChamilo
{ {
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
$this->addSql("CREATE TABLE color_theme (id INT AUTO_INCREMENT NOT NULL, variables LONGTEXT NOT NULL COMMENT '(DC2Type:json)', 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"); $this->addSql("CREATE TABLE color_theme (id INT AUTO_INCREMENT NOT NULL, title VARCHAR(255) NOT NULL, variables LONGTEXT NOT NULL COMMENT '(DC2Type:json)', slug VARCHAR(255) NOT NULL, active 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");
} }
} }

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Chamilo\CoreBundle\Repository;
use Chamilo\CoreBundle\Entity\ColorTheme;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class ColorThemeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ColorTheme::class);
}
public function deactivateAll(): void
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->update(ColorTheme::class, 'ct')
->set('ct.active', ':inactive')
->where(
$qb->expr()->eq('ct.active', ':active')
)
->setParameters(['active' => true, 'inactive' => false])
->getQuery()
->execute()
;
}
public function getActiveOne(): ?ColorTheme
{
return $this->findOneBy(
['active' => true],
['createdAt' => 'DESC']
);
}
}

@ -1,18 +1,6 @@
{% extends "@ChamiloCore/Layout/base-layout.html.twig" %} {% extends "@ChamiloCore/Layout/no_layout.html.twig" %}
{% block chamilo_wrap %}
{%- autoescape %}
{% if not from_vue %}
<div id="app" data-flashes="{{ app.flashes()|json_encode }}"></div>
{% endif %}
{% endautoescape -%}
{% autoescape false %}
<section id="sectionMainContent" class="section-content">
{%- block content %}
{% include '@ChamiloCore/Layout/vue_setup.html.twig' %}
{% endblock -%}
</section>
{% endautoescape %}
{% endblock %}
{% block chamilo_footer %} {%- block content %}
{% include '@ChamiloCore/Layout/vue_setup.html.twig' %}
{# {{ encore_entry_script_tags('vue') }}#}
{% endblock %} {% endblock %}

@ -30,7 +30,11 @@
{{ encore_entry_link_tags('legacy_document') }} {{ encore_entry_link_tags('legacy_document') }}
{{ encore_entry_link_tags('vue') }} {{ encore_entry_link_tags('vue') }}
{{ encore_entry_link_tags('app') }} {{ encore_entry_link_tags('app') }}
<link rel="stylesheet" href="{{ path('chamilo_color_theme') }}">
{% if color_theme_link %}
{{ color_theme_link|raw }}
{% endif %}
{# Files app.css is generated from "assets/css/app.scss" file using the file webpack.config.js #} {# Files app.css is generated from "assets/css/app.scss" file using the file webpack.config.js #}
{# {{ encore_entry_link_tags('app') }} #} {# {{ encore_entry_link_tags('app') }} #}
{% if theme is defined %} {% if theme is defined %}

@ -13,11 +13,6 @@
{% endblock -%} {% endblock -%}
</section> </section>
{% endautoescape %} {% endautoescape %}
<script>
window.ChamiloAppSettings = {
hideInterface: {{ not from_vue ? 'true' : 'false' }},
};
</script>
{% endblock %} {% endblock %}
{% block chamilo_footer %} {% block chamilo_footer %}

@ -9,6 +9,7 @@ namespace Chamilo\CoreBundle\State;
use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProcessorInterface;
use Chamilo\CoreBundle\Entity\ColorTheme; use Chamilo\CoreBundle\Entity\ColorTheme;
use Chamilo\CoreBundle\Repository\ColorThemeRepository;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
@ -19,12 +20,18 @@ class ColorThemeProcessor implements ProcessorInterface
public function __construct( public function __construct(
private readonly ProcessorInterface $persistProcessor, private readonly ProcessorInterface $persistProcessor,
private readonly ParameterBagInterface $parameterBag, private readonly ParameterBagInterface $parameterBag,
private readonly ColorThemeRepository $colorThemeRepository,
) {} ) {}
public function process($data, Operation $operation, array $uriVariables = [], array $context = []) public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{ {
\assert($data instanceof ColorTheme); \assert($data instanceof ColorTheme);
$this->colorThemeRepository->deactivateAll();
$data->setActive(true);
/** @var ColorTheme $colorTheme */
$colorTheme = $this->persistProcessor->process($data, $operation, $uriVariables, $context); $colorTheme = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
if ($colorTheme) { if ($colorTheme) {
@ -33,16 +40,18 @@ class ColorThemeProcessor implements ProcessorInterface
$contentParts = []; $contentParts = [];
$contentParts[] = ':root {'; $contentParts[] = ':root {';
foreach ($data->getVariables() as $variable => $value) { foreach ($colorTheme->getVariables() as $variable => $value) {
$contentParts[] = " $variable: $value;"; $contentParts[] = " $variable: $value;";
} }
$contentParts[] = '}'; $contentParts[] = '}';
$dirName = $projectDir."/var/theme/{$colorTheme->getSlug()}";
$fs = new Filesystem(); $fs = new Filesystem();
$fs->mkdir($projectDir.'/var/theme'); $fs->mkdir($dirName);
$fs->dumpFile( $fs->dumpFile(
$projectDir.'/var/theme/colors.css', $dirName.'/colors.css',
implode(PHP_EOL, $contentParts) implode(PHP_EOL, $contentParts)
); );
} }

Loading…
Cancel
Save