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

pull/5274/head
Yannick Warnier 2 years ago
commit f7b5d350ce
  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 "admin_index";
@import "calendar";
@import "course_home";
@import "documents";

@ -79,7 +79,7 @@ const layout = computed(() => {
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"
}

@ -1,5 +1,5 @@
<template>
<div class="flex flex-col space-y-4">
<div class="calendar-event-info">
<h5 v-text="event.title" />
<p v-text="abbreviatedDatetime(event.startDate)" />
@ -13,14 +13,19 @@
<div v-html="event.content" />
<div v-if="allowCollectiveInvitations && type.invitation === event.invitationType">
<h6 v-t="'Invitees'" />
<ShowLinks
:item="event"
:show-status="false"
/>
</div>
<CalendarEventSubscriptionsInfo
v-if="type.subscription === event.invitationType"
:event="event"
/>
<CalendarEventInvitationsInfo
v-else-if="type.invitation === event.invitationType"
:event="event"
/>
<ShowLinks
v-else
:item="event"
:show-status="false"
/>
</div>
</template>
@ -29,6 +34,8 @@ import { useFormatDate } from "../../composables/formatDate"
import ShowLinks from "../resource_links/ShowLinks"
import { useCalendarInvitations } from "../../composables/calendar/calendarInvitations"
import { type } from "../../constants/entity/ccalendarevent"
import CalendarEventSubscriptionsInfo from "./CalendarEventSubscriptionsInfo.vue"
import CalendarEventInvitationsInfo from "./CalendarEventInvitationsInfo.vue"
const { abbreviatedDatetime } = useFormatDate()
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>
<Topbar v-if="!hideInterface" />
<Sidebar v-if="!hideInterface && securityStore.isAuthenticated" />
<div v-if="!hideInterface" class="app-main" :class="{ 'app-main--no-sidebar': !securityStore.isAuthenticated }">
<Topbar />
<Sidebar v-if="securityStore.isAuthenticated" />
<div
class="app-main"
:class="{ 'app-main--no-sidebar': !securityStore.isAuthenticated }"
>
<Breadcrumb v-if="showBreadcrumb" />
<slot />
<router-view />
@ -9,7 +12,6 @@
</template>
<script setup>
import { ref } from "vue"
import Breadcrumb from "../../components/Breadcrumb.vue"
import Topbar from "../../components/layout/Topbar.vue"
import Sidebar from "../../components/layout/Sidebar.vue"
@ -23,6 +25,4 @@ defineProps({
})
const securityStore = useSecurityStore()
const chamiloAppSettings = window.ChamiloAppSettings || {}
const hideInterface = ref(!!chamiloAppSettings.hideInterface)
</script>

@ -4,7 +4,7 @@
<h3 class="app-sidebar__top">
{{ t("Menu") }}
</h3>
<div class="app-sidebar__panel">
<div class="app-sidebar__panel" @click="handlePanelHeaderClick">
<PanelMenu :model="menuItems" />
</div>
<div class="app-sidebar__bottom">
@ -55,6 +55,7 @@ const securityStore = useSecurityStore()
const { menuItems } = useSidebarMenu()
const sidebarIsOpen = ref(window.localStorage.getItem("sidebarIsOpen") === "true")
const expandingDueToPanelClick = ref(false)
const currentYear = new Date().getFullYear();
@ -62,13 +63,35 @@ watch(
sidebarIsOpen,
(newValue) => {
const appEl = document.querySelector("#app")
window.localStorage.setItem("sidebarIsOpen", newValue.toString())
appEl.classList.toggle("app--sidebar-inactive", !newValue)
},
{
immediate: true,
},
)
if (!newValue) {
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>

@ -1,9 +1,13 @@
import { type } from "../../constants/entity/ccalendarevent"
import { type, subscriptionVisibility } from "../../constants/entity/ccalendarevent"
export function useCalendarEvent() {
return {
findUserLink,
isEditableByUser,
isSubscribeable,
canSubscribeToEvent,
allowSubscribeToEvent,
allowUnsubscribeToEvent,
}
}
@ -36,3 +40,35 @@ function isEditableByUser(event, userId) {
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)
}
export {
find
async function post(params) {
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 class="flex flex-wrap mb-4">
<div class="flex flex-wrap mb-4 gap-4">
<BaseInputText
v-model="themeTitle"
:label="t('Title')"
/>
<BaseButton
type="primary"
icon="send"
@ -182,6 +187,8 @@ import axios from "axios"
const { t } = useI18n()
const { getColorTheme, getColors } = useTheme()
const themeTitle = ref()
let primaryColor = getColorTheme("--color-primary-base")
let primaryColorGradient = getColorTheme("--color-primary-gradient")
let secondaryColor = getColorTheme("--color-secondary-base")
@ -196,6 +203,7 @@ const saveColors = async () => {
let colors = getColors()
// TODO send colors to backend, then notify if was correct or incorrect
await axios.post("/api/color_themes", {
title: themeTitle.value,
variables: colors,
})
}

@ -44,6 +44,7 @@
v-model:visible="dialogShow"
:header="t('Event')"
:modal="true"
:style="{ width: '30rem' }"
>
<CCalendarEventInfo :event="item" />
@ -54,6 +55,22 @@
type="black"
@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
:label="t('Delete')"
icon="delete"
@ -130,6 +147,7 @@ import { storeToRefs } from "pinia"
import CalendarSectionHeader from "../../components/ccalendarevent/CalendarSectionHeader.vue"
import { useCalendarActionButtons } from "../../composables/calendar/calendarActionButtons"
import { useCalendarEvent } from "../../composables/calendar/calendarEvent"
import resourceLinkService from "../../services/resourceLinkService"
const store = useStore()
const confirm = useConfirm()
@ -141,12 +159,14 @@ const { abbreviatedDatetime } = useFormatDate()
const { showAddButton } = useCalendarActionButtons()
const { isEditableByUser } = useCalendarEvent()
const { isEditableByUser, allowSubscribeToEvent, allowUnsubscribeToEvent } = useCalendarEvent()
const item = ref({})
const dialog = ref(false)
const dialogShow = ref(false)
const allowToEdit = ref(false)
const allowToSubscribe = ref(false)
const allowToUnsubscribe = ref(false)
const currentUser = computed(() => store.getters["security/getUser"])
const { t } = useI18n()
@ -248,6 +268,8 @@ const calendarOptions = ref({
item.value["parentResourceNodeId"] = event.extendedProps.resourceNode.creator.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
},
@ -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 createForm = ref(null)

@ -3517,7 +3517,7 @@ function api_not_allowed(
$message = null,
$responseCode = 0
): 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) {
case CLp::LP_TYPE:
$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);
if (true === $preReqCheck) {
@ -239,7 +239,7 @@ if (!isset($src)) {
case CLp::SCORM_TYPE:
// save old 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);
if (true === $preReqCheck) {

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

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

@ -10,6 +10,7 @@ use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use Chamilo\CoreBundle\ApiResource\CalendarEvent;
use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\SessionRelCourse;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Chamilo\CourseBundle\Entity\CCalendarEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
@ -18,6 +19,7 @@ class CalendarEventTransformer implements DataTransformerInterface
{
public function __construct(
private readonly RouterInterface $router,
private readonly UsergroupRepository $usergroupRepository,
) {}
public function transform($object, string $to, array $context = []): object
@ -40,6 +42,12 @@ class CalendarEventTransformer implements DataTransformerInterface
$object->setResourceLinkListFromEntity();
$subscriptionItemTitle = null;
if (CCalendarEvent::SUBSCRIPTION_VISIBILITY_CLASS == $object->getSubscriptionVisibility()) {
$subscriptionItemTitle = $this->usergroupRepository->find($object->getSubscriptionItemId())?->getTitle();
}
return new CalendarEvent(
'calendar_event_'.$object->getIid(),
$object->getTitle(),
@ -52,6 +60,7 @@ class CalendarEventTransformer implements DataTransformerInterface
$object->isCollective(),
$object->getSubscriptionVisibility(),
$object->getSubscriptionItemId(),
$subscriptionItemTitle,
$object->getMaxAttendees(),
$object->getResourceNode(),
$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\Traits\TimestampableTypedEntity;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
@ -34,6 +35,10 @@ class ColorTheme
#[ORM\Column]
private ?int $id = null;
#[Groups(['color_theme:write'])]
#[ORM\Column(length: 255)]
private ?string $title = null;
/**
* @var array<string, mixed>
*/
@ -41,11 +46,31 @@ class ColorTheme
#[ORM\Column]
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
{
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
{
return $this->variables;
@ -57,4 +82,28 @@ class ColorTheme
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
{
$this->resourceNode = $resourceNode;
$this->resourceTypeGroup = $resourceNode->getResourceType()->getId();
return $this;
}

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

@ -73,7 +73,7 @@ class CourseListener
}
if (true === $cidReset) {
$this->removeCourseFromSession($request);
$this->cleanSessionHandler($request);
return;
}
@ -190,6 +190,8 @@ class CourseListener
$courseParams = $this->generateCourseUrl($course, $sessionId, $groupId, $origin);
$sessionHandler->set('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();
$alreadyVisited = $sessionHandler->get('course_already_visited');

@ -6,8 +6,8 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\EventListener;
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Chamilo\CoreBundle\Exception\NotAllowedException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
@ -44,7 +44,9 @@ class ExceptionListener
$redirectUrl = $baseUrl.$path.($query ? '?'.$query : '');
$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;
use Chamilo\CoreBundle\Repository\ColorThemeRepository;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Settings\SettingsManager;
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\User\UserInterface;
use Symfony\Component\Serializer\SerializerInterface;
@ -31,7 +33,9 @@ class TwigListener
SerializerInterface $serializer,
TokenStorageInterface $tokenStorage,
SettingsManager $settingsManager,
LanguageRepository $languageRepository
LanguageRepository $languageRepository,
private readonly ColorThemeRepository $colorThemeRepository,
private readonly RouterInterface $router,
) {
$this->twig = $twig;
$this->tokenStorage = $tokenStorage;
@ -99,5 +103,22 @@ class TwigListener
$this->twig->addGlobal('access_url_id', $request->getSession()->get('access_url_id'));
$this->twig->addGlobal('config_json', json_encode($config));
$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;
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'];
$sessionId = $item['session_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;
// 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
{
$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');
$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');
if ($table->hasForeignKey('FK_DB0F8049537A1329')) {
$this->addSql('ALTER TABLE message_feedback DROP FOREIGN KEY FK_DB0F8049537A1329');
}
if ($table->hasIndex('IDX_DB0F8049537A1329')) {
$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)');
}
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');
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;');

@ -72,31 +72,36 @@ class Version20230904173400 extends AbstractMigrationChamilo
$em->persist($calendarEvent);
if ($collectiveInvitationsEnabled) {
$calendarEvent->setCollective((bool) $personalAgenda['collective']);
$hasSubscriptions = false;
$invitationsOrSubscriptionsInfo = [];
if ($subscriptionsEnabled) {
$subscriptionsInfo = $this->getSubscriptions((int) $personalAgenda['id']);
if (\count($subscriptionsInfo) > 0) {
$hasSubscriptions = true;
if (\count($subscriptionsInfo) > 0
&& 0 !== $personalAgenda['subscription_visibility']
) {
$invitationsOrSubscriptionsInfo = $subscriptionsInfo;
}
}
if ($hasSubscriptions) {
if ($invitationsOrSubscriptionsInfo) {
$calendarEvent
->setInvitationType(CCalendarEvent::TYPE_SUBSCRIPTION)
->setSubscriptionVisibility($personalAgenda['subscription_visibility'])
->setSubscriptionItemId($personalAgenda['subscription_item_id'])
->setMaxAttendees($invitationsOrSubscriptionsInfo[0]['max_attendees'])
;
} 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) {
@ -197,7 +202,7 @@ class Version20230904173400 extends AbstractMigrationChamilo
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
INNER JOIN personal_agenda pa ON i.id = pa.agenda_event_invitation_id
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;');
}
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')) {
error_log('Perform the changes in the gradebook_comment table');
$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');
$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';
$originalFile = $rootPath.'/public/main/lang/english/trad4all.inc.php';
$originalFile = $rootPath.'/var/translations/import/english/trad4all.inc.php';
if (!file_exists($langPath)) {
error_log("Original file not found: $langPath");
@ -118,6 +118,8 @@ final class Version20240122221400 extends AbstractMigrationChamilo
$langPath,
true
);
$termsInLanguage = [];
foreach ($originalTermsInLanguage as $id => $content) {
if (!isset($termsInLanguage[$id])) {
$termsInLanguage[$id] = trim(rtrim($content, ';'), '"');
@ -131,10 +133,8 @@ final class Version20240122221400 extends AbstractMigrationChamilo
continue;
}
$doneTranslations[$englishTranslation] = true;
$translatedTerm = '';
if (!empty($termsInLanguage[$term])) {
$translatedTerm = $termsInLanguage[$term];
}
$translatedTerm = $termsInLanguage[$term] ?? '';
// Here we apply a little correction to avoid unterminated strings
// when a string ends with a \"
if (preg_match('/\\\$/', $englishTranslation)) {

@ -13,6 +13,6 @@ class Version20240318105600 extends AbstractMigrationChamilo
{
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" %}
{% 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 %}
{% extends "@ChamiloCore/Layout/no_layout.html.twig" %}
{% block chamilo_footer %}
{%- block content %}
{% include '@ChamiloCore/Layout/vue_setup.html.twig' %}
{# {{ encore_entry_script_tags('vue') }}#}
{% endblock %}

@ -30,7 +30,11 @@
{{ encore_entry_link_tags('legacy_document') }}
{{ encore_entry_link_tags('vue') }}
{{ 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 #}
{# {{ encore_entry_link_tags('app') }} #}
{% if theme is defined %}

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

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

Loading…
Cancel
Save