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

pull/5264/head
Yannick Warnier 2 years ago
commit 066aa1256e
  1. 204
      assets/css/scss/_lp.scss
  2. 26
      assets/js/legacy/lp.js
  3. 13
      assets/vue/components/layout/DashboardLayout.vue
  4. 14
      assets/vue/composables/theme.js
  5. 200
      assets/vue/views/admin/AdminConfigureColors.vue
  6. 4
      config/services.yaml
  7. 8
      public/main/inc/lib/display.lib.php
  8. 5
      public/main/lp/learnpath.class.php
  9. 16
      public/main/lp/lp_list.php
  10. 1
      src/CoreBundle/Component/Utils/ChamiloApi.php
  11. 8
      src/CoreBundle/Controller/SocialController.php
  12. 37
      src/CoreBundle/Controller/ThemeController.php
  13. 60
      src/CoreBundle/Entity/ColorTheme.php
  14. 2
      src/CoreBundle/Entity/Language.php
  15. 5
      src/CoreBundle/EventListener/ExceptionListener.php
  16. 3
      src/CoreBundle/Exception/NotAllowedException.php
  17. 8
      src/CoreBundle/Migrations/Schema/V200/Version20240310160200.php
  18. 18
      src/CoreBundle/Migrations/Schema/V200/Version20240318105600.php
  19. 16
      src/CoreBundle/Repository/Node/UserRepository.php
  20. 20
      src/CoreBundle/Resources/views/Index/vue.html.twig
  21. 1
      src/CoreBundle/Resources/views/Layout/head.html.twig
  22. 5
      src/CoreBundle/Resources/views/Layout/no_layout.html.twig
  23. 12
      src/CoreBundle/Resources/views/LearnPath/list.html.twig
  24. 52
      src/CoreBundle/State/ColorThemeProcessor.php
  25. 6
      src/CourseBundle/Entity/CAttendanceResultComment.php
  26. 3
      src/CourseBundle/Entity/CLpCategory.php

@ -7,33 +7,6 @@
padding: 0;
}
#lp_item_list li {
//border-bottom: 1px solid #dddddd;
//width: 100%;
//margin-bottom: 5px;
//margin-top: 5px;
}
#lp_item_list, #lp_item_list li {
/* list-style-type: none;*/
}
#lp_item_list .active {
border: 2px dotted #BDB76B;
}
#lp_item_list li:last-child {
//border-bottom: none;
}
#lp_item_list .item_data {
padding: .5em;
}
#lp_item_list .item_data span {
margin-left: 5px;
}
.item_data .button_actions {
display: none;
margin: 5px 0;
@ -67,14 +40,179 @@
font-size: 14px
}
.lp_resource li {
padding: 10px 10px 10px 20px;
}
.list-group-item-empty {
height: 50px;
}
.media {
display:none;
}
.display-panel-collapse {
display: block;
h5 {
font-size: 1.25rem;
font-weight: 500;
margin-bottom: 0;
}
a {
text-decoration: none;
color: #1f2937;
}
.card-body {
padding: 1rem;
}
}
#resource_tab {
margin-top: 15px;
}
#resource_tab .nav-tabs {
.nav-link.active {
background-color: #007bff;
border-color: #007bff;
border-radius: 0.25rem;
color: #FFF;
}
.nav-link {
display: flex;
justify-content: center;
align-items: center;
margin-right: 0.5rem;
transition: background-color 0.3s, transform 0.3s;
}
.nav-link i {
font-size: 64px;
color: #495057;
transition: color 0.3s;
height: auto !important;
padding: 2px;
}
.nav-link:hover {
background-color: #e9ecef;
transform: scale(1.05);
color: inherit;
}
.nav-link i:hover {
color: #495057 !important;
}
.nav-link.active i {
color: #ffffff;
}
}
.mdi-cursor-move {
cursor: move !important;
font-size: 16px;
width: 16px;
height: 16px;
}
#lp_item_list {
font-family: 'Arial', sans-serif;
background-color: #f9f9f9;
.list-group-item {
border: 1px solid #ddd;
margin-bottom: 5px;
padding: 10px;
background-color: white;
}
.ch-tool-icon {
color: #555;
margin-right: 10px;
cursor: pointer;
}
.ch-tool-icon:hover {
color: #000;
}
.button_actions a {
margin-right: 5px;
}
.btn-toolbar {
margin-top: 5px;
}
}
#doc_list {
.list-group-item {
padding-left: 10px;
display: block;
}
.nested-1 {
padding-left: 20px;
}
.nested-2 {
padding-left: 25px;
}
.nested-3 {
padding-left: 30px;
border: none;
}
.nested-4 {
padding-left: 35px;
border: none;
}
.nested-5 {
padding-left: 40px;
border: none;
}
.nested-6 {
padding-left: 45px;
border: none;
}
}
#dropzone {
position: relative;
overflow: hidden;
direction: ltr;
cursor: pointer;
text-align: center;
color: #333;
font-weight: bold;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
border-radius: 10px;
width: auto;
margin-left: auto;
margin-right: auto;
height: auto;
line-height: 50px;
background-color: #D4E6F0;
border: 2px dashed #bbbbbb;
font-size: 120%;
margin-bottom: 0;
}
#dropzone.hover {
background: lawngreen;
}
#upload_form .fa-plus-square-o, #upload_form .fa-minus-square-o {
cursor: pointer;
}
#upload, .description-upload {
padding-top: 15px;
}
.description-upload {
margin-bottom: 15px;
}

@ -16,3 +16,29 @@ window.frameReady = frameReady;
var hljs = require('highlight.js');
global.hljs = hljs;
document.addEventListener('DOMContentLoaded', (event) => {
var tabLinks = document.querySelectorAll('.nav-item.nav-link');
function removeActiveClasses() {
tabLinks.forEach(function(link) {
link.classList.remove('active');
var tabPanel = document.getElementById(link.getAttribute('aria-controls'));
if (tabPanel) {
tabPanel.classList.remove('active');
}
});
}
tabLinks.forEach(function(link) {
link.addEventListener('click', function() {
removeActiveClasses();
this.classList.add('active');
var tabContentId = this.getAttribute('aria-controls');
var tabContent = document.getElementById(tabContentId);
if (tabContent) {
tabContent.classList.add('active');
}
});
});
});

@ -1,10 +1,7 @@
<template>
<Topbar />
<Sidebar v-if="securityStore.isAuthenticated" />
<div
class="app-main"
:class="{ 'app-main--no-sidebar': !securityStore.isAuthenticated }"
>
<Topbar v-if="!hideInterface" />
<Sidebar v-if="!hideInterface && securityStore.isAuthenticated" />
<div v-if="!hideInterface" class="app-main" :class="{ 'app-main--no-sidebar': !securityStore.isAuthenticated }">
<Breadcrumb v-if="showBreadcrumb" />
<slot />
<router-view />
@ -12,12 +9,12 @@
</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"
import { useSecurityStore } from "../../store/securityStore"
// eslint-disable-next-line no-undef
defineProps({
showBreadcrumb: {
type: Boolean,
@ -26,4 +23,6 @@ defineProps({
})
const securityStore = useSecurityStore()
const chamiloAppSettings = window.ChamiloAppSettings || {}
const hideInterface = ref(!!chamiloAppSettings.hideInterface)
</script>

@ -1,7 +1,6 @@
import {onMounted, ref, watch} from "vue";
import { onMounted, ref, watch } from "vue"
export const useTheme = () => {
let colors = {}
onMounted(() => {
@ -16,7 +15,7 @@ export const useTheme = () => {
if (Object.hasOwn(colors, variableName)) {
return colors[variableName]
}
const colorRef = ref(getCssVariableValue(variableName))
const colorRef = ref(getCssVariableValue(variableName))
watch(colorRef, (newColor) => {
setCssVariableValue(variableName, newColor)
})
@ -25,9 +24,7 @@ export const useTheme = () => {
}
const getCssVariableValue = (variableName) => {
const colorVariable = getComputedStyle(document.body)
.getPropertyValue(variableName)
.split(", ")
const colorVariable = getComputedStyle(document.body).getPropertyValue(variableName).split(", ")
return {
r: parseInt(colorVariable[0]),
g: parseInt(colorVariable[1]),
@ -36,14 +33,13 @@ export const useTheme = () => {
}
const setCssVariableValue = (variableName, color) => {
document.documentElement.style
.setProperty(variableName, `${color.r}, ${color.g}, ${color.b}`)
document.documentElement.style.setProperty(variableName, `${color.r}, ${color.g}, ${color.b}`)
}
const getColors = () => {
let colorsPlainObject = {}
for (const [key, value] of Object.entries(colors)) {
colorsPlainObject[key] = `${value.value.r} ${value.value.g} ${value.value.b}`
colorsPlainObject[key] = `${value.value.r}, ${value.value.g}, ${value.value.b}`
}
return colorsPlainObject
}

@ -1,40 +1,108 @@
<template class="personal-theme">
<h4 class="mb-4">{{ t('Configure chamilo colors') }}</h4>
<h4 class="mb-4">{{ t("Configure chamilo colors") }}</h4>
<div class="grid grid-cols-2 gap-2 mb-8">
<BaseColorPicker v-model="primaryColor" :label="t('Pick primary color')"/>
<BaseColorPicker v-model="primaryColorGradient" :label="t('Pick primary color gradient')"/>
<BaseColorPicker v-model="secondaryColor" :label="t('Pick secondary color')"/>
<BaseColorPicker v-model="secondaryColorGradient" :label="t('Pick secondary color gradient')"/>
<BaseColorPicker v-model="tertiaryColor" :label="t('Pick tertiary color')"/>
<BaseColorPicker v-model="tertiaryColorGradient" :label="t('Pick tertiary color gradient')"/>
<BaseColorPicker v-model="successColor" :label="t('Pick success color')"/>
<BaseColorPicker v-model="successColorGradient" :label="t('Pick success color gradient')"/>
<BaseColorPicker v-model="dangerColor" :label="t('Pick danger color')"/>
<BaseColorPicker
v-model="primaryColor"
:label="t('Pick primary color')"
/>
<BaseColorPicker
v-model="primaryColorGradient"
:label="t('Pick primary color gradient')"
/>
<BaseColorPicker
v-model="secondaryColor"
:label="t('Pick secondary color')"
/>
<BaseColorPicker
v-model="secondaryColorGradient"
:label="t('Pick secondary color gradient')"
/>
<BaseColorPicker
v-model="tertiaryColor"
:label="t('Pick tertiary color')"
/>
<BaseColorPicker
v-model="tertiaryColorGradient"
:label="t('Pick tertiary color gradient')"
/>
<BaseColorPicker
v-model="successColor"
:label="t('Pick success color')"
/>
<BaseColorPicker
v-model="successColorGradient"
:label="t('Pick success color gradient')"
/>
<BaseColorPicker
v-model="dangerColor"
:label="t('Pick danger color')"
/>
</div>
<div class="flex flex-wrap mb-4">
<BaseButton type="primary" icon="send" :label="t('Save')" @click="saveColors"/>
<BaseButton
type="primary"
icon="send"
:label="t('Save')"
@click="saveColors"
/>
</div>
<hr>
<h5 class="mb-4">{{ t('You can see examples of how chamilo will look here') }}</h5>
<hr />
<h5 class="mb-4">{{ t("You can see examples of how chamilo will look here") }}</h5>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t('Buttons') }}</p>
<p class="mb-3 text-lg">{{ t("Buttons") }}</p>
<div class="flex flex-row flex-wrap">
<BaseButton class="mr-2 mb-2" :label="t('Button')" type="primary" icon="eye-on"/>
<BaseButton class="mr-2 mb-2" :label="t('Disabled')" type="primary" icon="eye-on" disabled/>
<BaseButton class="mr-2 mb-2" :label="t('Secondary')" type="secondary" icon="eye-on"/>
<BaseButton class="mr-2 mb-2" :label="t('Tertiary')" type="black" icon="eye-on"/>
<BaseButton class="mr-2 mb-2" type="primary" icon="cog" only-icon/>
<BaseButton class="mr-2 mb-2" :label="t('Success')" type="success" icon="send"/>
<BaseButton class="mr-2 mb-2" :label="t('Danger')" type="danger" icon="delete"/>
<BaseButton
class="mr-2 mb-2"
:label="t('Button')"
type="primary"
icon="eye-on"
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Disabled')"
type="primary"
icon="eye-on"
disabled
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Secondary')"
type="secondary"
icon="eye-on"
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Tertiary')"
type="black"
icon="eye-on"
/>
<BaseButton
class="mr-2 mb-2"
type="primary"
icon="cog"
only-icon
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Success')"
type="success"
icon="send"
/>
<BaseButton
class="mr-2 mb-2"
:label="t('Danger')"
type="danger"
icon="delete"
/>
</div>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t('Menu on button pressed') }}</p>
<p class="mb-3 text-lg">{{ t("Menu on button pressed") }}</p>
<BaseButton
class="mr-2 mb-2"
type="primary"
@ -43,13 +111,27 @@
only-icon
@click="toggle"
/>
<BaseMenu id="menu" ref="menu" :model="menuItems"></BaseMenu>
<BaseMenu
id="menu"
ref="menu"
:model="menuItems"
></BaseMenu>
</div>
<div class="mb-4">
<p class="mb-3 text-lg">{{ t('Checkbox and radio buttons') }}</p>
<BaseCheckbox id="check1" v-model="checkbox1" :label="t('Checkbox 1')" name="checkbox1"/>
<BaseCheckbox id="check2" v-model="checkbox2" :label="t('Checkbox 2')" name="checkbox2"/>
<p class="mb-3 text-lg">{{ t("Checkbox and radio buttons") }}</p>
<BaseCheckbox
id="check1"
v-model="checkbox1"
:label="t('Checkbox 1')"
name="checkbox1"
/>
<BaseCheckbox
id="check2"
v-model="checkbox2"
:label="t('Checkbox 2')"
name="checkbox2"
/>
<div class="mb-2"></div>
<BaseRadioButtons
v-model="radioValue"
@ -69,7 +151,12 @@
<div class="mb-4">
<p class="mb-3 text-lg">Dialogs</p>
<BaseButton :label="t('Show dialog')" type="black" icon="eye-on" @click="isDialogVisible = true"/>
<BaseButton
:label="t('Show dialog')"
type="black"
icon="eye-on"
@click="isDialogVisible = true"
/>
<BaseDialogConfirmCancel
:title="t('Dialog example')"
:is-visible="isDialogVisible"
@ -81,41 +168,40 @@
<script setup>
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import {useI18n} from "vue-i18n"
import { useI18n } from "vue-i18n"
import BaseMenu from "../../components/basecomponents/BaseMenu.vue"
import {ref} from "vue"
import { ref } from "vue"
import BaseCheckbox from "../../components/basecomponents/BaseCheckbox.vue"
import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue"
import BaseDialogConfirmCancel from "../../components/basecomponents/BaseDialogConfirmCancel.vue"
import BaseInputText from "../../components/basecomponents/BaseInputText.vue"
import BaseColorPicker from "../../components/basecomponents/BaseColorPicker.vue"
import {useTheme} from "../../composables/theme"
const {t} = useI18n()
const {getColorTheme, getColors} = useTheme()
let primaryColor = getColorTheme('--color-primary-base')
let primaryColorGradient = getColorTheme('--color-primary-gradient')
let secondaryColor = getColorTheme('--color-secondary-base')
let secondaryColorGradient = getColorTheme('--color-secondary-gradient')
let tertiaryColor = getColorTheme('--color-tertiary-base')
let tertiaryColorGradient = getColorTheme('--color-tertiary-gradient')
let successColor = getColorTheme('--color-success-base')
let successColorGradient = getColorTheme('--color-success-gradient')
let dangerColor = getColorTheme('--color-danger-base')
const saveColors = () => {
import { useTheme } from "../../composables/theme"
import axios from "axios"
const { t } = useI18n()
const { getColorTheme, getColors } = useTheme()
let primaryColor = getColorTheme("--color-primary-base")
let primaryColorGradient = getColorTheme("--color-primary-gradient")
let secondaryColor = getColorTheme("--color-secondary-base")
let secondaryColorGradient = getColorTheme("--color-secondary-gradient")
let tertiaryColor = getColorTheme("--color-tertiary-base")
let tertiaryColorGradient = getColorTheme("--color-tertiary-gradient")
let successColor = getColorTheme("--color-success-base")
let successColorGradient = getColorTheme("--color-success-gradient")
let dangerColor = getColorTheme("--color-danger-base")
const saveColors = async () => {
let colors = getColors()
// TODO send colors to backend, then notify if was correct or incorrect
console.log(colors)
await axios.post("/api/color_themes", {
variables: colors,
})
}
const menu = ref('menu')
const menuItems = [
{label: t('Item 1')},
{label: t('Item 2')},
{label: t('Item 3')},
]
const menu = ref("menu")
const menuItems = [{ label: t("Item 1") }, { label: t("Item 2") }, { label: t("Item 3") }]
const toggle = (event) => {
menu.value.toggle(event)
}
@ -124,13 +210,13 @@ const checkbox1 = ref(true)
const checkbox2 = ref(false)
const radioButtons = [
{label: t('Value 1'), value: 'value1'},
{label: t('Value 2'), value: 'value2'},
{label: t('Value 3'), value: 'value3'},
{ label: t("Value 1"), value: "value1" },
{ label: t("Value 2"), value: "value2" },
{ label: t("Value 3"), value: "value3" },
]
const radioValue = ref('value1')
const radioValue = ref("value1")
const isDialogVisible = ref(false)
const inputText = ref('')
const inputText = ref("")
</script>

@ -82,6 +82,10 @@ services:
tags:
- { name: 'api_platform.state_processor' }
Chamilo\CoreBundle\State\ColorThemeProcessor:
bind:
$persistProcessor: '@api_platform.doctrine.orm.state.persist_processor'
Chamilo\CoreBundle\EventSubscriber\AnonymousUserSubscriber:
tags:
- name: kernel.event_subscriber

@ -2455,9 +2455,9 @@ class Display
});
</script>';
$html = '
<div class="mt-4 rounded-lg bg-gray-50 p-2">
<div class="px-4 bg-gray-100 border border-gray-50" id="card_'.$idAccordion.'">
<h5>
<div class="mt-4 rounded-lg bg-gray-15 display-panel-collapse mr-4">
<div class="px-4 bg-gray-25 border border-gray-30" id="card_'.$idAccordion.'">
<h5 class="p-2">
<a role="button"
class="cursor-pointer"
data-toggle="collapse"
@ -2471,7 +2471,7 @@ class Display
</div>
<div
id="collapse_'.$idAccordion.'"
class="px-4 border border-gray-50 bg-white collapse custom-collapse '.(($open) ? 'active' : '').'"
class="px-4 border border-gray-30 bg-white collapse custom-collapse '.(($open) ? 'active' : '').'"
>
<div id="collapse_contant_'.$idAccordion.'" class="card-body ">';

@ -7237,10 +7237,9 @@ class learnpath
foreach ($lps as $lp) {
$lp->setCategory(null);
$em->persist($lp);
}
$em->persist($lp);
$course = api_get_course_entity();
$session = api_get_session_entity();
@ -7753,7 +7752,7 @@ class learnpath
$values['title']
);
$this->add_item(
0,
null,
$lastItemId,
'final_item',
$documentId,

@ -99,6 +99,14 @@ if ($allowCategory) {
foreach ($categoriesTempList as $category) {
$categorySessionId = (int) learnpath::getCategorySessionId($category->getIid());
if ($categorySessionId === $sessionId || 0 === $categorySessionId) {
if (!empty($categorySessionId)) {
$sessionImage = api_get_session_image(
$categorySessionId,
api_get_user_entity()
);
$newTitle = $category->getTitle().' '.$sessionImage;
$category->setTitle($newTitle);
}
$newCategoryFiltered[] = $category;
}
if (!empty($sessionId) && empty($firstSessionCategoryId) && $categorySessionId == $sessionId) {
@ -846,13 +854,21 @@ foreach ($categories as $category) {
}
$shortcut = false;
$canEditCategory = false;
if ($category->hasResourceNode()) {
$shortcut = $shortcutRepository->getShortcutFromResource($category);
$categorySession = 0;
$categoryLink = $category->getResourceNode()->getResourceLinks()->first();
if ($categoryLink && $categoryLink->getSession()) {
$categorySession = (int) $categoryLink->getSession()->getId();
}
$canEditCategory = api_get_session_id() === $categorySession;
}
$data[] = [
'category' => $category,
'category_visibility' => $visibility,
'can_edit_category' => $canEditCategory,
'category_is_published' => $shortcut ? 1 : 0,
'lp_list' => $listData,
];

@ -442,6 +442,7 @@ class ChamiloApi
{
if (!empty($url)) {
header("Location: $url");
exit;
}
}

@ -272,10 +272,10 @@ class SocialController extends AbstractController
UserManager::update_extra_field_value($userId, $justificationFieldToUpdate, $explanation);
$request = $requestStack->getCurrentRequest();
$baseUrl = $request->getSchemeAndHttpHost() . $request->getBasePath();
$baseUrl = $request->getSchemeAndHttpHost().$request->getBasePath();
$specificPath = '/main/admin/user_list_consent.php';
$link = $baseUrl . $specificPath;
$emailContent .= $translator->trans('Go here : '). '<a href="' . $link . '">' . $link . '</a>';
$link = $baseUrl.$specificPath;
$emailContent .= $translator->trans('Go here : ').'<a href="'.$link.'">'.$link.'</a>';
$emailOfficer = $settingsManager->getSetting('profile.data_protection_officer_email');
if (!empty($emailOfficer)) {
@ -666,7 +666,7 @@ class SocialController extends AbstractController
'status' => $isUserOnline ? 'online' : 'offline',
'url' => '/social?id='.$item['id'],
'relationType' => $relation['relationType'] ?? null,
'existingInvitations' => $existingInvitations
'existingInvitations' => $existingInvitations,
];
}
} elseif ('group' === $type) {

@ -0,0 +1,37 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ThemeController extends AbstractController
{
public function __construct(
private readonly ParameterBagInterface $parameterBag,
) {}
#[Route('/theme/colors.css', name: 'chamilo_color_theme', methods: ['GET'])]
public function colorTehemeAction(): Response
{
$fs = new Filesystem();
$path = $this->parameterBag->get('kernel.project_dir').'/var/theme/colors.css';
if ($fs->exists($path)) {
$response = $this->file($path);
} else {
$response = new Response('');
}
$response->headers->add(['Content-Type' => 'text/css']);
return $response;
}
}

@ -0,0 +1,60 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Post;
use Chamilo\CoreBundle\State\ColorThemeProcessor;
use Chamilo\CoreBundle\Traits\TimestampableTypedEntity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
#[ApiResource(
operations: [
new Post(
processor: ColorThemeProcessor::class,
),
],
denormalizationContext: [
'groups' => ['color_theme:write'],
],
security: "is_granted('ROLE_ADMIN')",
)]
class ColorTheme
{
use TimestampableTypedEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
/**
* @var array<string, mixed>
*/
#[Groups(['color_theme:write'])]
#[ORM\Column]
private array $variables = [];
public function getId(): ?int
{
return $this->id;
}
public function getVariables(): array
{
return $this->variables;
}
public function setVariables(array $variables): static
{
$this->variables = $variables;
return $this;
}
}

@ -20,7 +20,7 @@ use Symfony\Component\Validator\Constraints as Assert;
/**
* Platform languages.
*/
#[ApiResource(attributes: ["pagination_enabled" => false])]
#[ApiResource(attributes: ['pagination_enabled' => false])]
#[ApiFilter(BooleanFilter::class, properties: ['available'])]
#[ApiFilter(OrderFilter::class, properties: ['english_name' => 'DESC'])]
#[ORM\Table(name: 'language', options: ['row_format' => 'DYNAMIC'])]

@ -13,7 +13,6 @@ use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Twig\Environment;
class ExceptionListener
@ -39,10 +38,10 @@ class ExceptionListener
if (null === $this->tokenStorage->getToken()) {
$currentUrl = $request->getUri();
$parsedUrl = parse_url($currentUrl);
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
$baseUrl = $parsedUrl['scheme'].'://'.$parsedUrl['host'];
$path = rtrim($parsedUrl['path'], '/') ?: '';
$query = $parsedUrl['query'] ?? '';
$redirectUrl = $baseUrl . $path . ($query ? '?' . $query : '');
$redirectUrl = $baseUrl.$path.($query ? '?'.$query : '');
$loginUrl = $this->router->generate('login', ['redirect' => $redirectUrl], UrlGeneratorInterface::ABSOLUTE_URL);
ChamiloApi::redirectTo($loginUrl);

@ -1,4 +1,5 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
@ -9,7 +10,7 @@ use Exception;
class NotAllowedException extends Exception
{
public function __construct($message = 'Not allowed', $code = 0, Exception $previous = null)
public function __construct($message = 'Not allowed', $code = 0, ?Exception $previous = null)
{
parent::__construct($message, $code, $previous);
}

@ -1,6 +1,5 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
@ -12,6 +11,8 @@ use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
use Doctrine\DBAL\Schema\Schema;
use const PASSWORD_DEFAULT;
class Version20240310160200 extends AbstractMigrationChamilo
{
public function getDescription(): string
@ -26,7 +27,7 @@ class Version20240310160200 extends AbstractMigrationChamilo
$em = $doctrine->getManager();
/* @var UserRepository $repo*/
/** @var UserRepository $repo */
$repo = $container->get(UserRepository::class);
$plainPassword = 'fallback_user';
@ -46,7 +47,8 @@ class Version20240310160200 extends AbstractMigrationChamilo
->setPhone('0000000000')
->setLocale('en')
->setActive(User::SOFT_DELETED)
->setTimezone('UTC');
->setTimezone('UTC')
;
$em->flush();
error_log($fallbackUser->getFullname());

@ -0,0 +1,18 @@
<?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 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");
}
}

@ -29,6 +29,7 @@ use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
@ -166,8 +167,9 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
$em->flush();
$em->getConnection()->commit();
} catch (\Exception $e) {
} catch (Exception $e) {
$em->getConnection()->rollBack();
throw $e;
}
}
@ -289,17 +291,17 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
];
foreach ($relations as $relation) {
$entityClass = 'Chamilo\\' . $relation['bundle'] . '\\Entity\\' . $relation['entity'];
$entityClass = 'Chamilo\\'.$relation['bundle'].'\\Entity\\'.$relation['entity'];
$repository = $em->getRepository($entityClass);
$records = $repository->findBy([$relation['field'] => $userToDelete]);
foreach ($records as $record) {
$setter = 'set' . ucfirst($relation['field']);
if ($relation['action'] === 'delete') {
$setter = 'set'.ucfirst($relation['field']);
if ('delete' === $relation['action']) {
$em->remove($record);
} elseif (method_exists($record, $setter)) {
$valueToSet = $relation['type'] === 'object' ? $fallbackUser : $fallbackUser->getId();
$record->$setter($valueToSet);
$valueToSet = 'object' === $relation['type'] ? $fallbackUser : $fallbackUser->getId();
$record->{$setter}($valueToSet);
if (method_exists($record, 'getResourceFile') && $record->getResourceFile()) {
$resourceFile = $record->getResourceFile();
if (!$em->contains($resourceFile)) {
@ -319,7 +321,7 @@ class UserRepository extends ResourceRepository implements PasswordUpgraderInter
$fallbackUser = $this->findOneBy(['status' => User::ROLE_FALLBACK], ['id' => 'ASC']);
if (!$fallbackUser) {
throw new \Exception("User not found.");
throw new Exception('User not found.');
}
return $fallbackUser;

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

@ -30,6 +30,7 @@
{{ encore_entry_link_tags('legacy_document') }}
{{ encore_entry_link_tags('vue') }}
{{ encore_entry_link_tags('app') }}
<link rel="stylesheet" href="{{ path('chamilo_color_theme') }}">
{# 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,6 +13,11 @@
{% endblock -%}
</section>
{% endautoescape %}
<script>
window.ChamiloAppSettings = {
hideInterface: {{ not from_vue ? 'true' : 'false' }},
};
</script>
{% endblock %}
{% block chamilo_footer %}

@ -33,7 +33,7 @@
{{ lp_data.category.getTitle() | trim }}
</h6>
{% if lp_data.category.iid > 0 %}
{% if not session %}
{% if lp_data.can_edit_category %}
<a href="{{ 'lp_controller.php?' ~ course_url_params ~ '&action=add_lp_category&id=' ~ lp_data.category.iid }}"
title="{{ "Edit"|trans }}">
<i class="mdi-pencil mdi ch-tool-icon" style="font-size: 22px; width: 22px; height: 22px;" aria-hidden="true" title="{{ "Edit"|trans }}"></i>
@ -94,11 +94,11 @@
<i class="mdi-checkbox-multiple-blank mdi ch-tool-icon" style="font-size: 22px; width: 22px; height: 22px;" aria-hidden="true" title="{{ "Hide"|trans }}"></i>
</a>
{% endif %}
{% if not session %}
<a href="{{ 'lp_controller.php?' ~ course_url_params ~ '&action=delete_lp_category&id=' ~ lp_data.category.iid }}"
title="{{ "Delete"|trans }}">
<i class="mdi-delete mdi ch-tool-icon" style="font-size: 22px; width: 22px; height: 22px;" aria-hidden="true" title="{{ "Delete"|trans }}"></i>
</a>
{% if lp_data.can_edit_category %}
<a href="{{ 'lp_controller.php?' ~ course_url_params ~ '&action=delete_lp_category&id=' ~ lp_data.category.iid }}"
title="{{ "Delete"|trans }}">
<i class="mdi-delete mdi ch-tool-icon" style="font-size: 22px; width: 22px; height: 22px;" aria-hidden="true" title="{{ "Delete"|trans }}"></i>
</a>
{% endif %}
{% endif %}
{% elseif lp_data.lp_list is not empty %}

@ -0,0 +1,52 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\State;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use Chamilo\CoreBundle\Entity\ColorTheme;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Filesystem\Filesystem;
use const PHP_EOL;
class ColorThemeProcessor implements ProcessorInterface
{
public function __construct(
private readonly ProcessorInterface $persistProcessor,
private readonly ParameterBagInterface $parameterBag,
) {}
public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
{
\assert($data instanceof ColorTheme);
$colorTheme = $this->persistProcessor->process($data, $operation, $uriVariables, $context);
if ($colorTheme) {
$projectDir = $this->parameterBag->get('kernel.project_dir');
$contentParts = [];
$contentParts[] = ':root {';
foreach ($data->getVariables() as $variable => $value) {
$contentParts[] = " $variable: $value;";
}
$contentParts[] = '}';
$fs = new Filesystem();
$fs->mkdir($projectDir.'/var/theme');
$fs->dumpFile(
$projectDir.'/var/theme/colors.css',
implode(PHP_EOL, $contentParts)
);
}
return $colorTheme;
}
}

@ -9,9 +9,9 @@ use DateTime;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: "c_attendance_result_comment")]
#[ORM\Index(columns: ["attendance_sheet_id"], name: "attendance_sheet_id")]
#[ORM\Index(columns: ["user_id"], name: "user_id")]
#[ORM\Table(name: 'c_attendance_result_comment')]
#[ORM\Index(columns: ['attendance_sheet_id'], name: 'attendance_sheet_id')]
#[ORM\Index(columns: ['user_id'], name: 'user_id')]
class CAttendanceResultComment
{
#[ORM\Column(name: 'iid', type: 'integer')]

@ -8,6 +8,7 @@ namespace Chamilo\CourseBundle\Entity;
use Chamilo\CoreBundle\Entity\AbstractResource;
use Chamilo\CoreBundle\Entity\ResourceInterface;
use Chamilo\CoreBundle\Entity\ResourceShowCourseResourcesInSessionInterface;
use Chamilo\CoreBundle\Entity\User;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
@ -22,7 +23,7 @@ use Symfony\Component\Validator\Constraints as Assert;
*/
#[ORM\Table(name: 'c_lp_category')]
#[ORM\Entity(repositoryClass: SortableRepository::class)]
class CLpCategory extends AbstractResource implements ResourceInterface, Stringable
class CLpCategory extends AbstractResource implements ResourceInterface, ResourceShowCourseResourcesInSessionInterface, Stringable
{
#[ORM\Column(name: 'iid', type: 'integer')]
#[ORM\Id]

Loading…
Cancel
Save