Merge remote-tracking branch 'upstream/master' into GH-1655

pull/6036/head
Christian Beeznest 7 months ago
commit d82e08c0bd
  1. 4
      README.md
  2. 13
      assets/css/app.scss
  3. 29
      assets/css/scss/atoms/_buttons.scss
  4. 44
      assets/css/scss/atoms/_rating.scss
  5. 1
      assets/css/scss/index.scss
  6. 5
      assets/css/scss/molecules/_course_tool.scss
  7. 6
      assets/css/scss/molecules/_empty_state.scss
  8. 2
      assets/css/scss/molecules/_toolbar.scss
  9. 29
      assets/vue/AppInstaller.vue
  10. 1
      assets/vue/components/Breadcrumb.vue
  11. 9
      assets/vue/components/Loading.vue
  12. 1
      assets/vue/components/admin/AdminBlock.vue
  13. 16
      assets/vue/components/basecomponents/BaseAppLink.vue
  14. 6
      assets/vue/components/basecomponents/BaseButton.vue
  15. 21
      assets/vue/components/basecomponents/BaseDropdown.vue
  16. 25
      assets/vue/components/basecomponents/BaseIconField.vue
  17. 26
      assets/vue/components/basecomponents/BaseRating.vue
  18. 3
      assets/vue/components/basecomponents/BaseRouteTabs.vue
  19. 1
      assets/vue/components/basecomponents/BaseSidebarPanelMenu.vue
  20. 1
      assets/vue/components/course/CourseCard.vue
  21. 1
      assets/vue/components/course/CourseTool.vue
  22. 1
      assets/vue/components/course/ShortCutList.vue
  23. 1
      assets/vue/components/documents/DocumentEntry.vue
  24. 59
      assets/vue/components/installer/Step1.vue
  25. 6
      assets/vue/components/installer/Step2.vue
  26. 61
      assets/vue/components/installer/Step3.vue
  27. 6
      assets/vue/components/installer/Step4.vue
  28. 7
      assets/vue/components/installer/Step5.vue
  29. 7
      assets/vue/components/installer/Step6.vue
  30. 35
      assets/vue/components/installer/Step7.vue
  31. 6
      assets/vue/components/layout/PlatformLogo.vue
  32. 2
      assets/vue/components/layout/TopbarLoggedIn.vue
  33. 1
      assets/vue/components/login/LoginExternalButtons.vue
  34. 1
      assets/vue/components/resource_links/ShowLinks.vue
  35. 1
      assets/vue/components/social/SocialGroupMenu.vue
  36. 1
      assets/vue/components/social/SocialSideMenu.vue
  37. 1
      assets/vue/components/social/SocialWallPost.vue
  38. 2
      assets/vue/composables/document/documentActionButtons.js
  39. 2
      assets/vue/main.js
  40. 17
      assets/vue/router/catalogue.js
  41. 6
      assets/vue/router/cataloguecourses.js
  42. 6
      assets/vue/router/cataloguesessions.js
  43. 6
      assets/vue/router/index.js
  44. 7
      assets/vue/views/course/CourseHome.vue
  45. 1
      assets/vue/views/message/MessageList.vue
  46. 67
      assets/vue/views/user/courses/List.vue
  47. 31
      assets/vue/views/user/sessions/SessionsCurrent.vue
  48. 3
      composer.json
  49. 1
      config/packages/doctrine.yaml
  50. 1
      package.json
  51. 10
      public/main/admin/course_list.php
  52. 162
      public/main/admin/questions.php
  53. 153
      public/main/admin/statistics/index.php
  54. 1
      public/main/admin/user_add.php
  55. 6
      public/main/admin/user_edit.php
  56. 1
      public/main/auth/inscription.php
  57. 9
      public/main/auth/reset.php
  58. 2
      public/main/exercise/exercise_show.php
  59. 6
      public/main/exercise/exercise_submit.php
  60. 8
      public/main/exercise/question.class.php
  61. 20
      public/main/exercise/question_list_admin.inc.php
  62. 2
      public/main/inc/ajax/exercise.ajax.php
  63. 654
      public/main/inc/ajax/statistics.ajax.php
  64. 44
      public/main/inc/lib/api.lib.php
  65. 9
      public/main/inc/lib/course.lib.php
  66. 16
      public/main/inc/lib/exercise.lib.php
  67. 27
      public/main/inc/lib/formvalidator/Element/DatePicker.php
  68. 4
      public/main/inc/lib/formvalidator/Element/DateTimePicker.php
  69. 6
      public/main/inc/lib/formvalidator/FormValidator.class.php
  70. 55
      public/main/inc/lib/internationalization.lib.php
  71. 6
      public/main/inc/lib/javascript/record_audio/record_audio.js
  72. 33
      public/main/inc/lib/pear/HTML/QuickForm/password.php
  73. 2
      public/main/inc/lib/pear/HTML/QuickForm/text.php
  74. 2
      public/main/inc/lib/promotion.lib.php
  75. 30
      public/main/inc/lib/sessionmanager.lib.php
  76. 215
      public/main/inc/lib/statistics.lib.php
  77. 55
      public/main/inc/lib/tracking.lib.php
  78. 139
      public/main/inc/lib/usermanager.lib.php
  79. 11
      public/main/install/index.php
  80. 7
      public/main/install/profiles/prod.json
  81. 2
      public/main/lp/lp_add_audio.php
  82. 4
      public/main/search/search.php
  83. 52
      public/main/session/session_list.php
  84. 2
      public/main/skills/assign.php
  85. 4
      public/main/skills/issued.php
  86. 4
      public/main/skills/issued_all.php
  87. 40
      public/main/template/default/admin/questions.html.twig
  88. 10
      public/plugin/dashboard/block_global_info/block_global_info.class.php
  89. 2
      src/CoreBundle/Component/Utils/ActionIcon.php
  90. 7
      src/CoreBundle/Controller/AccountController.php
  91. 1
      src/CoreBundle/Controller/PlatformConfigurationController.php
  92. 10
      src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php
  93. 13
      src/CoreBundle/Entity/User.php
  94. 4
      src/CoreBundle/EventListener/LoginSuccessHandler.php
  95. 32
      src/CoreBundle/Form/ProfileType.php
  96. 2
      src/CoreBundle/Migrations/Schema/V200/Version20170627122900.php
  97. 10
      src/CoreBundle/Repository/LanguageRepository.php
  98. 2
      src/CoreBundle/Settings/CourseSettingsSchema.php
  99. 3
      src/CoreBundle/Settings/DocumentSettingsSchema.php
  100. 2
      src/CoreBundle/Settings/SessionSettingsSchema.php
  101. Some files were not shown because too many files have changed in this diff Show More

@ -34,7 +34,7 @@ On Ubuntu 24.04+, the following should take care of most dependencies.
~~~~
sudo apt update
sudo apt -y upgrade
sudo apt install apache2 libapache2-mod-php mariadb-client mariadb-server redis php-pear php-{apcu,bcmath,cli,curl,dev,gd,intl,mbstring,mysql,redis,soap,xml,zip} git unzip
sudo apt install apache2 libapache2-mod-php mariadb-client mariadb-server redis php-pear php-{apcu,bcmath,cli,curl,dev,gd,intl,mbstring,mysql,redis,soap,xml,zip} git unzip curl
sudo mysql
mysql> GRANT ALL PRIVILEGES ON chamilo.* TO chamilo@localhost IDENTIFIED BY '{password}';
mysql> exit
@ -49,7 +49,7 @@ sudo apt -y upgrade
sudo apt -y install ca-certificates curl gnupg software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install apache2 libapache2-mod-php8.3 mariadb-client mariadb-server redis php-pear php8.3-{apcu,bcmath,cli,curl,dev,gd,intl,mbstring,mysql,redis,soap,xml,zip} git unzip
sudo apt install apache2 libapache2-mod-php8.3 mariadb-client mariadb-server redis php-pear php8.3-{apcu,bcmath,cli,curl,dev,gd,intl,mbstring,mysql,redis,soap,xml,zip} git unzip curl
~~~~
(replace 'chamilo' by the database name and user you want, and '{password}' by a more secure password)

@ -292,6 +292,10 @@
}
}
.mdi {
@apply font-normal text-base leading-none;
}
.field > small.p-error {
@apply text-error;
}
@ -799,6 +803,15 @@ form .field {
}
}
/* Loader */
.loader {
@apply w-10 h-10 border-4 border-gray-20 border-l-primary rounded-full animate-spin;
}
.loader-overlay {
@apply absolute inset-0 flex items-center justify-center bg-white bg-opacity-40 backdrop-blur-sm;
}
//@import "~jquery-ui-timepicker-addon/dist/jquery-ui-timepicker-addon.css";
@import "~@fancyapps/fancybox/dist/jquery.fancybox.css";
@import "~timepicker/jquery.timepicker.min.css";

@ -1,6 +1,9 @@
.btn {
@apply cursor-default font-semibold gap-2 inline-flex justify-center px-6 py-2 rounded-md transition flex-none;
font-size: 16px;
@apply cursor-default font-semibold gap-2 inline-flex justify-center px-6 py-2 rounded-md transition flex-none text-base;
.mdi {
@apply text-base;
}
&--primary {
@apply bg-primary text-white;
@ -231,8 +234,6 @@ $border-color_12: #9333EA;
@include filled-style('primary', 'support-4');
@apply cursor-pointer font-semibold gap-2 inline-flex justify-center items-center px-4 py-2 rounded-md transition;
font-size: 16px;
&:focus {
@apply outline-none;
}
@ -259,16 +260,28 @@ $border-color_12: #9333EA;
&.p-button-sm {
@apply px-2 py-1;
font-size: 13px;
.p-button-icon,
.p-button-label {
font-size: 13px;
}
}
&.p-button-lg {
@apply px-8 py-4;
font-size: 18px;
.p-button-icon,
.p-button-label {
font-size: 18px;
}
}
.p-button-icon {
@apply text-base;
}
.p-button-label {
@apply align-middle font-semibold;
@apply align-middle font-semibold text-base;
}
&.p-button-icon-only {
@ -289,7 +302,7 @@ $border-color_12: #9333EA;
@apply bg-gray-10;
.p-button-label {
text-decoration: underline;
@apply underline;
}
}
}

@ -0,0 +1,44 @@
.p-rating {
@apply relative flex items-center gap-1;
&-item {
@apply inline-flex items-center cursor-pointer outline-none rounded-lg
hover:outline-none hover:drop-shadow-lg;
.p-rating-icon {
@apply transition-none text-gray-50;
font-size: 1rem;
&.p-icon {
@apply w-4 h-4;
&.p-rating-cancel {
@apply text-danger;
}
}
}
&.p-focus {
@apply outline-none drop-shadow-lg;
}
&.p-rating-item-active {
& .p-rating-icon {
@apply text-warning;
}
}
}
&.p-readonly &-item {
@apply cursor-default;
}
&:not(.p-disabled):not(.p-readonly) &-item:hover &-icon {
@apply text-warning;
}
&:not(.p-disabled):not(.p-readonly) &-item:hover &-icon.p-rating-cancel {
@apply text-danger;
}
}

@ -47,6 +47,7 @@
@include meta.load-css("atoms/platform_logo");
@include meta.load-css("atoms/progressbar");
@include meta.load-css("atoms/radio");
@include meta.load-css("atoms/rating");
@include meta.load-css("atoms/skeleton");
@include meta.load-css("atoms/tags");
@include meta.load-css("atoms/toast");

@ -13,9 +13,8 @@
&__icon {
@apply text-transparent bg-clip-text bg-gradient-to-br from-primary to-primary-gradient leading-none;
font-size: 52px;
&::before {
&.mdi {
font-size: 52px;
}
}

@ -7,7 +7,11 @@
}
&__icon {
@apply mb-4 text-9xl text-transparent bg-clip-text bg-gradient-to-br from-primary to-primary-gradient w-32 h-32;
@apply mb-4 text-transparent bg-clip-text bg-gradient-to-br from-primary to-primary-gradient w-32 h-32;
&.mdi {
font-size: 8rem;
}
}
&__summary {

@ -3,7 +3,7 @@
&-group-left,
&-group-right {
@apply flex flex-row flex-wrap gap-2;
@apply flex flex-row flex-wrap gap-2 items-center;
}
&-separator {

@ -20,18 +20,17 @@
{{ stepTitle }}
</li>
</ol>
<div id="note">
<a
class="p-button p-component p-button-info p-button-outlined"
href="../../documentation/installation_guide.html"
target="_blank"
>
<span
aria-hidden="true"
class="p-button-icon p-button-icon-left mdi mdi-text-box-search-outline"
<div
id="note"
class="text-center"
>
<BaseAppLink url="../../documentation/installation_guide.html">
<BaseButton
type="primary"
icon="courses"
:label="t('Read the installation guide')"
/>
<span class="p-button-text">{{ t("Read the installation guide") }}</span>
</a>
</BaseAppLink>
</div>
</aside>
@ -39,19 +38,19 @@
<h1
v-if="'new' === installerData.installType"
v-t="'New installation'"
class="mb-4"
class="mb-4 text-center"
/>
<h1
v-else-if="'update' === installerData.installType"
v-t="{
path: 'Update from Chamilo ' + installerData.upgradeFromVersion.join(' | '),
}"
class="mb-4"
class="mb-4 text-center"
/>
<h1
v-else
v-t="'Chamilo\'s installation wizard'"
class="mb-8"
class="mb-8 text-center"
/>
<form
@ -235,6 +234,8 @@
import { useI18n } from "vue-i18n"
import { onMounted, provide, ref } from "vue"
import BaseAppLink from "./components/basecomponents/BaseAppLink.vue"
import BaseButton from "./components/basecomponents/BaseButton.vue"
import Step1 from "./components/installer/Step1"
import Step2 from "./components/installer/Step2"
import Step3 from "./components/installer/Step3"

@ -31,7 +31,6 @@ import { useI18n } from "vue-i18n"
import Breadcrumb from "primevue/breadcrumb"
import { useCidReqStore } from "../store/cidReq"
import { storeToRefs } from "pinia"
import BaseAppLink from "./basecomponents/BaseAppLink.vue"
const legacyItems = ref(window.breadcrumb)

@ -1,13 +1,10 @@
<template>
<div
v-if="visible"
class="w-full h-full fixed block top-0 left-0 bg-white opacity-60 text-center"
style="z-index: 9999"
class="absolute inset-0 flex items-center justify-center bg-white bg-opacity-50 backdrop-blur-md"
style="z-index: 10"
>
<div
class="spinner-border text-success opacity-75 top-1/2 my-0 mx-auto block relative w-0 h-0"
role="status"
>
<div class="loader" role="status">
<span class="sr-only">Loading</span>
</div>
</div>

@ -76,7 +76,6 @@ import { useI18n } from "vue-i18n"
import BaseInputGroup from "../basecomponents/BaseInputGroup.vue"
import BaseIcon from "../basecomponents/BaseIcon.vue"
import AdminBlockExtraContent from "./AdminBlockExtraContent.vue"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const { t } = useI18n()

@ -2,10 +2,6 @@
import { RouterLink } from "vue-router"
import { computed } from "vue"
defineOptions({
inheritAttrs: false,
})
const props = defineProps({
...RouterLink.props,
url: {
@ -28,16 +24,8 @@ const isAnchor = computed(() => !!props.url)
</a>
<router-link
v-else
v-slot="{ href, navigate }"
custom
v-bind="$props"
v-bind="props"
>
<a
:href="href"
v-bind="$attrs"
@click="navigate"
>
<slot />
</a>
<slot />
</router-link>
</template>

@ -13,6 +13,7 @@
:text="onlyIcon"
:title="tooltip || (onlyIcon ? label : undefined)"
:type="isSubmit ? 'submit' : 'button'"
:name="name"
class="cursor-pointer"
@click="$emit('click', $event)"
/>
@ -70,6 +71,11 @@ const props = defineProps({
type: String,
default: "", // This ensures that popupIdentifier is still present
},
name: {
type: String || undefined,
required: false,
default: undefined,
},
})
defineEmits(["click"])

@ -2,15 +2,14 @@
<div class="field">
<div class="p-float-label">
<Dropdown
v-model="modelValue"
:class="{ 'p-invalid': isInvalid }"
:input-id="inputId"
:model-value="modelValue"
:name="name"
:option-label="optionLabel"
:option-value="optionValue"
:options="options"
:placeholder="placeholder"
@update:model-value="$emit('update:modelValue', $event)"
/>
<label
:for="inputId"
@ -33,17 +32,15 @@
<script setup>
import Dropdown from "primevue/dropdown"
const modelValue = defineModel({
type: String || Number || Object,
})
defineProps({
name: {
type: String,
required: true,
},
// type null allow all kind of values, like prime vue does
modelValue: {
type: null,
required: true,
default: () => {},
},
options: {
type: Array,
required: true,
@ -84,8 +81,10 @@ defineProps({
required: false,
default: false,
},
helpText: String,
helpText: {
type: String || undefined,
required: false,
default: undefined,
},
})
defineEmits(["update:modelValue"])
</script>

@ -0,0 +1,25 @@
<script setup>
import { useI18n } from "vue-i18n"
import IconField from "primevue/iconfield"
import InputIcon from "primevue/inputicon"
import InputText from "primevue/inputtext"
import { chamiloIconToClass } from "./ChamiloIcons"
const modelValue = defineModel({
type: String,
})
const { t } = useI18n()
</script>
<template>
<IconField icon-position="left">
<InputIcon :class="chamiloIconToClass['search']" />
<InputText
v-model="modelValue"
:placeholder="t('Search')"
/>
</IconField>
</template>

@ -0,0 +1,26 @@
<script setup>
import Rating from "primevue/rating"
const model = defineModel({
type: Number,
})
defineProps({
stars: {
type: Number,
default: 5,
require: false,
},
})
const emit = defineEmits(["change"])
</script>
<template>
<Rating
v-model="model"
:cancel="false"
:stars="stars"
@change="emit('change', $event, model)"
/>
</template>

@ -23,9 +23,6 @@
* Component that will render a tab interface WITHOUT content. Every tab should be a router link. So, when user
* change tab the route of the url will change
*/
import BaseAppLink from "./BaseAppLink.vue"
defineProps({
tabs: {
type: Array,

@ -1,5 +1,4 @@
<script setup>
import BaseAppLink from "./BaseAppLink.vue"
import PanelMenu from "primevue/panelmenu"
import BaseIcon from "./BaseIcon.vue"

@ -62,7 +62,6 @@ import BaseAvatarList from "../basecomponents/BaseAvatarList.vue"
import { computed } from "vue"
import { isEmpty } from "lodash"
import { useFormatDate } from "../../composables/formatDate"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
import { usePlatformConfig } from "../../store/platformConfig"
const { abbreviatedDatetime } = useFormatDate()

@ -65,7 +65,6 @@ import { useSecurityStore } from "../../store/securityStore"
import { usePlatformConfig } from "../../store/platformConfig"
import { storeToRefs } from "pinia"
import { useCidReqStore } from "../../store/cidReq"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const securityStore = useSecurityStore()
const platformConfigStore = usePlatformConfig()

@ -22,7 +22,6 @@
<script setup>
import { computed } from "vue"
import { storeToRefs } from "pinia"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
import { useCidReqStore } from "../../store/cidReq"
const cidReqStore = useCidReqStore()

@ -49,7 +49,6 @@ import ResourceIcon from "./ResourceIcon.vue"
import { computed } from "vue"
import { useCidReq } from "../../composables/cidReq"
import { useFileUtils } from "../../composables/fileUtils"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const props = defineProps({
data: {

@ -5,30 +5,22 @@
class="install-icon w-36 mx-auto mb-4"
src="/main/install/chamilo-install.svg"
/>
<h2
class="install-title mb-8"
v-text="t('Step 1 - Installation Language')"
/>
<SectionHeader :title="t('Step 1 - Installation Language')" />
<div class="field">
<div class="p-float-label">
<Dropdown
v-model="installerData.langIso"
:filter="true"
:options="availableLanguages"
input-id="language_list"
option-label="english_name"
option-value="isocode"
/>
<label
v-t="'Please select installation language'"
for="language_list"
/>
</div>
<small
v-t="'Cannot find your language in the list? Contact us at info@chamilo.org to contribute as a translator.'"
/>
</div>
<BaseDropdown
v-model="installerData.langIso"
:help-text="
t('Cannot find your language in the list? Contact us at {0} to contribute as a translator.', [
'info@chamilo.org',
])
"
:label="t('Please select installation language')"
:options="availableLanguages"
input-id="language_list"
name="language_list_alt"
option-label="english_name"
option-value="isocode"
/>
<input
v-model="installerData.langIso"
@ -51,9 +43,10 @@
:closable="false"
severity="warn"
>
<p class="update-message-text">
{{ t("An update is available. Click the button below to proceed with the update.") }}
</p>
<p
class="update-message-text"
v-t="'An update is available. Click the button below to proceed with the update.'"
/>
<p>{{ installerData.checkMigrationStatus.message }}</p>
<p v-if="installerData.checkMigrationStatus.current_migration">
Current Migration: {{ installerData.checkMigrationStatus.current_migration }}
@ -63,12 +56,12 @@
</p>
<hr />
</Message>
<Button
:class="[installerData.isUpdateAvailable ? 'p-button-secondary' : 'p-button-success']"
<BaseButton
:label="t('Next')"
:name="'step1'"
icon="mdi mdi-page-next"
type="submit"
:type="installerData.isUpdateAvailable ? 'secondary' : 'success'"
icon="next"
is-submit
/>
<input
id="is_executable"
@ -84,8 +77,10 @@
import { inject } from "vue"
import { useI18n } from "vue-i18n"
import Dropdown from "primevue/dropdown"
import Button from "primevue/button"
import Message from "primevue/message"
import BaseDropdown from "../basecomponents/BaseDropdown.vue"
import BaseButton from "../basecomponents/BaseButton.vue"
import SectionHeader from "../layout/SectionHeader.vue"
import languages from "../../utils/languages"

@ -1,9 +1,6 @@
<template>
<div class="install-step">
<h2
v-t="'Step 2 - Requirements'"
class="install-title mb-8"
/>
<SectionHeader :title="t('Step 2 - Requirements')" />
<p class="RequirementText mb-4">
<strong v-text="t('Please read the following requirements thoroughly.')" />
@ -358,6 +355,7 @@ import Message from "primevue/message"
import Tag from "primevue/tag"
import InputText from "primevue/inputtext"
import Button from "primevue/button"
import SectionHeader from "../layout/SectionHeader.vue"
const { t } = useI18n()

@ -1,9 +1,6 @@
<template>
<div class="install-step">
<h2
v-t="'Step 3 - License'"
class="install-title mb-8"
/>
<SectionHeader :title="t('Step 3 - License')" />
<p
v-t="'Chamilo is free software distributed under the GNU General Public licence (GPL).'"
@ -45,36 +42,31 @@
:toggleable="true"
class="mt-4"
>
<p
v-t="'Dear user'"
class="mb-3"
/>
<p
v-t="
'You are about to start using one of the best open-source e-learning platform on the market. Like many other open-source project, this project is backed up by a large community of students, teachers, developers and content creators who would like to promote the project better.'
"
class="mb-3"
/>
<p
v-t="
'By knowing a little bit more about you, one of our most important users, who will manage this e-learning system, we will be able to let people know that our software is used and let you know when we organize events that might be relevant to you.'
"
class="mb-3"
/>
<p
v-t="
'By filling this form, you accept that the Chamilo association or its members might send you information by e-mail about important events or updates in the Chamilo software or community. This will help the community grow as an organized entity where information flow, with a permanent respect of your time and your privacy.'
"
class="mb-3"
/>
<p
class="mb-3"
v-html="
t(
'Please note that you are <b>not required</b> to fill this form. If you want to remain anonymous, we will loose the opportunity to offer you all the privileges of being a registered portal administrator, but we will respect your decision. Simply leave this form empty and click Next',
)
"
/>
<div class="space-y-3 mb-3">
<p v-t="'Dear user'" />
<p
v-t="
'You are about to start using one of the best open-source e-learning platform on the market. Like many other open-source project, this project is backed up by a large community of students, teachers, developers and content creators who would like to promote the project better.'
"
/>
<p
v-t="
'By knowing a little bit more about you, one of our most important users, who will manage this e-learning system, we will be able to let people know that our software is used and let you know when we organize events that might be relevant to you.'
"
/>
<p
v-t="
'By filling this form, you accept that the Chamilo association or its members might send you information by e-mail about important events or updates in the Chamilo software or community. This will help the community grow as an organized entity where information flow, with a permanent respect of your time and your privacy.'
"
/>
<p
v-html="
t(
'Please note that you are <b>not required</b> to fill this form. If you want to remain anonymous, we will loose the opportunity to offer you all the privileges of being a registered portal administrator, but we will respect your decision. Simply leave this form empty and click Next',
)
"
/>
</div>
<BaseInputText
id="person_name"
@ -223,6 +215,7 @@ import RadioButton from "primevue/radiobutton"
import Button from "primevue/button"
import BaseInputText from "../basecomponents/BaseInputText.vue"
import BaseDropdown from "../basecomponents/BaseDropdown.vue"
import SectionHeader from "../layout/SectionHeader.vue"
const { t } = useI18n()

@ -1,9 +1,6 @@
<template>
<div class="install-step">
<h2
v-t="'Step 4 - Database settings'"
class="RequirementHeading mb-8"
/>
<SectionHeader :title="t('Step 4 - Database settings')" />
<p
v-if="'update' === installerData.installType"
@ -226,6 +223,7 @@ import InputText from "primevue/inputtext"
import Password from "primevue/password"
import Button from "primevue/button"
import Message from "primevue/message"
import SectionHeader from "../layout/SectionHeader.vue"
const { t } = useI18n()

@ -1,8 +1,8 @@
<template>
<div class="install-step">
<h2
v-t="'Step 5 - Configuration settings'"
class="RequirementHeading mb-8"
<SectionHeader
:title="t('Step 5 - Configuration settings')"
class="RequirementHeading"
/>
<div v-if="'update' === installerData.installType">
@ -511,6 +511,7 @@ import Password from "primevue/password"
import Dropdown from "primevue/dropdown"
import Button from "primevue/button"
import RadioButton from "primevue/radiobutton"
import SectionHeader from "../layout/SectionHeader.vue"
import languages from "../../utils/languages"

@ -3,9 +3,9 @@
v-show="!loading"
class="install-step"
>
<h2
v-t="'Step 6 - Last check before install'"
class="RequirementHeading mb-8"
<SectionHeader
:title="t('Step 6 - Last check before install')"
class="RequirementHeading"
/>
<p
@ -380,6 +380,7 @@ import Message from "primevue/message"
import Button from "primevue/button"
import ProgressBar from "primevue/progressbar"
import Dialog from "primevue/dialog"
import SectionHeader from "../layout/SectionHeader.vue"
const { t } = useI18n()

@ -1,14 +1,12 @@
<template>
<div class="install-step">
<h2
v-if="'update' !== installerData.installType"
v-t="'Step 7 - Installation process execution'"
class="RequirementHeading mb-8"
/>
<h2
v-else
v-t="'Step 7 - Update process execution'"
class="RequirementHeading mb-8"
<SectionHeader
:title="
'update' !== installerData.installType
? t('Step 7 - Installation process execution')
: t('Step 7 - Update process execution')
"
class="RequirementHeading"
/>
<p
@ -42,12 +40,13 @@
<div class="formgroup-inline">
<div class="field">
<Button
:label="t('Go to your newly created portal.')"
class="p-button-success"
type="button"
@click="btnFinishOnClick"
/>
<BaseAppLink url="../../">
<Button
:label="t('Go to your newly created portal.')"
class="p-button-success"
type="button"
/>
</BaseAppLink>
</div>
</div>
</div>
@ -60,12 +59,10 @@ import { useI18n } from "vue-i18n"
import Message from "primevue/message"
import Button from "primevue/button"
import SectionHeader from "../layout/SectionHeader.vue"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const { t } = useI18n()
const installerData = inject("installerData")
function btnFinishOnClick() {
window.location = "../../"
}
</script>

@ -1,10 +1,12 @@
<script setup>
import { ref } from "vue"
import { usePlatformConfig } from "../../store/platformConfig"
import { useSecurityStore } from "../../store/securityStore"
import { useVisualTheme } from "../../composables/theme"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const platformConfigStore = usePlatformConfig()
const securityStore = useSecurityStore()
const { getThemeAssetUrl } = useVisualTheme()
const siteName = platformConfigStore.getSetting("platform.site_name")
@ -26,7 +28,7 @@ const onError = () => {
<template>
<div class="platform-logo">
<BaseAppLink :to="{ name: 'Index' }">
<BaseAppLink :to="securityStore.user ? { name: 'Home' } : { name: 'Index' }">
<img
:alt="siteName"
:src="currentSrc"

@ -63,13 +63,11 @@ import { useMessageRelUserStore } from "../../store/messageRelUserStore"
import { useNotification } from "../../composables/notification"
import { useI18n } from "vue-i18n"
import PlatformLogo from "./PlatformLogo.vue"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
import BaseIcon from "../basecomponents/BaseIcon.vue"
import { useCidReqStore } from "../../store/cidReq"
const { t } = useI18n()
// eslint-disable-next-line no-undef
const props = defineProps({
currentUser: {
required: true,

@ -1,5 +1,4 @@
<script setup>
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
import BaseDivider from "../basecomponents/BaseDivider.vue"
import { useI18n } from "vue-i18n"
import { usePlatformConfig } from "../../store/platformConfig"

@ -64,7 +64,6 @@
<script setup>
import { RESOURCE_LINK_DRAFT, RESOURCE_LINK_PUBLISHED } from "../../constants/entity/resourcelink"
import { useI18n } from "vue-i18n"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const { t } = useI18n()

@ -72,7 +72,6 @@ import { useSecurityStore } from "../../store/securityStore"
import axios from "axios"
import { useNotification } from "../../composables/notification"
import { useSocialInfo } from "../../composables/useSocialInfo"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const { t } = useI18n()
const route = useRoute()

@ -152,7 +152,6 @@ import { computed, inject, onMounted, ref, watchEffect } from "vue"
import { useSecurityStore } from "../../store/securityStore"
import axios from "axios"
import { usePlatformConfig } from "../../store/platformConfig"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const { t } = useI18n()
const route = useRoute()

@ -102,7 +102,6 @@ import BaseCard from "../basecomponents/BaseCard.vue"
import { SOCIAL_TYPE_PROMOTED_MESSAGE } from "./constants"
import { useFormatDate } from "../../composables/formatDate"
import { useSecurityStore } from "../../store/securityStore"
import BaseAppLink from "../basecomponents/BaseAppLink.vue"
const props = defineProps({
post: {

@ -29,7 +29,7 @@ export function useDocumentActionButtons() {
if (isAllowedToEdit) {
if (!isCertificateMode.value) {
showNewDocumentButton.value = true
showRecordAudioButton.value = "true" === platformConfigStore.getSetting("course.enable_record_audio")
showRecordAudioButton.value = true
showUploadButton.value = true
showNewFolderButton.value = true
showNewCloudFileButton.value = true // enable_add_file_link ?

@ -49,6 +49,7 @@ import Column from "primevue/column"
import ColumnGroup from "primevue/columngroup"
import ToastService from "primevue/toastservice"
import ConfirmationService from "primevue/confirmationservice"
import BaseAppLink from "./components/basecomponents/BaseAppLink.vue"
import "primevue/resources/primevue.min.css"
// import 'primeflex/primeflex.css';
@ -192,6 +193,7 @@ app.component("ColumnGroup", ColumnGroup)
app.component("Toolbar", Toolbar)
app.component("DashboardLayout", DashboardLayout)
app.component("EmptyLayout", EmptyLayout)
app.component("BaseAppLink", BaseAppLink)
app.config.globalProperties.axios = axios
app.config.globalProperties.window = window

@ -0,0 +1,17 @@
export default {
path: "/catalogue",
meta: { requiresAdmin: true, requiresSessionAdmin: true },
component: () => import("../components/layout/SimpleRouterViewLayout.vue"),
children: [
{
path: "courses",
name: "CatalogueCourses",
component: () => import("../views/course/CatalogueCourses.vue"),
},
{
path: "sessions",
name: "CatalogueSessions",
component: () => import("../views/course/CatalogueSessions.vue"),
},
],
}

@ -1,6 +0,0 @@
export default {
path: "/catalogue/courses",
name: "CatalogueCourses",
meta: { requiresAdmin: true, requiresSessionAdmin: true },
component: () => import("../views/course/CatalogueCourses.vue"),
}

@ -1,6 +0,0 @@
export default {
path: "/catalogue/sessions",
name: "CatalogueSessions",
meta: { requiresAdmin: true, requiresSessionAdmin: true },
component: () => import("../views/course/CatalogueSessions.vue"),
}

@ -21,6 +21,7 @@ import documents from "./documents"
import assignments from "./assignments"
import links from "./links"
import glossary from "./glossary"
import catalogue from "./catalogue"
import { useSecurityStore } from "../store/securityStore"
import MyCourseList from "../views/user/courses/List.vue"
import MySessionList from "../views/user/sessions/SessionsCurrent.vue"
@ -41,8 +42,6 @@ import Demo from "../pages/Demo.vue"
import { useCidReqStore } from "../store/cidReq"
import courseService from "../services/courseService"
import catalogueCourses from "./cataloguecourses"
import catalogueSessions from "./cataloguesessions"
import { customVueTemplateEnabled } from "../config/env"
import { useCourseSettings } from "../store/courseSettingStore"
import { checkIsAllowedToEdit, useUserSessionSubscription } from "../composables/userPermissions"
@ -227,8 +226,7 @@ const router = createRouter({
fileManagerRoutes,
termsRoutes,
socialNetworkRoutes,
catalogueCourses,
catalogueSessions,
catalogue,
adminRoutes,
courseRoutes,
//courseCategoryRoutes,

@ -423,6 +423,13 @@ onMounted(async () => {
const onStudentViewChanged = async () => {
isAllowedToEdit.value = await checkIsAllowedToEdit()
courseService.loadCTools(course.value.id, session.value?.id).then((cTools) => {
tools.value = cTools.map((element) => ({
...element,
isEnabled: element.resourceNode?.resourceLinks[0]?.visibility === 2,
}))
})
}
const allowEditToolVisibilityInSession = computed(() => {

@ -307,7 +307,6 @@ import { useSecurityStore } from "../../store/securityStore"
import SectionHeader from "../../components/layout/SectionHeader.vue"
import InputGroup from "primevue/inputgroup"
import InputText from "primevue/inputtext"
import BaseAppLink from "../../components/basecomponents/BaseAppLink.vue"
import messageRelUserService from "../../services/messagereluser"
import { useMessageReceiverFormatter } from "../../composables/message/messageFormatter"

@ -3,38 +3,41 @@
<hr />
<div
v-if="isLoading && courses.length === 0"
class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<Skeleton height="16rem" />
<Skeleton
class="hidden md:block"
height="16rem"
<div class="relative min-h-[300px]">
<Loading :visible="!isFullyLoaded" />
<div
v-if="isLoading && courses.length === 0"
class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<Skeleton height="16rem" />
<Skeleton
class="hidden md:block"
height="16rem"
/>
<Skeleton
class="hidden lg:block"
height="16rem"
/>
<Skeleton
class="hidden xl:block"
height="16rem"
/>
</div>
<div
v-if="courses.length > 0"
class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<CourseCardList :courses="courses" />
<div ref="lastCourseRef"></div>
</div>
<EmptyState
v-else-if="!isLoading && courses.length === 0"
:detail="t('Go to Explore to find a topic of interest, or wait for someone to subscribe you')"
:summary="t('You don\'t have any course yet.')"
icon="courses"
/>
<Skeleton
class="hidden lg:block"
height="16rem"
/>
<Skeleton
class="hidden xl:block"
height="16rem"
/>
</div>
<div
v-if="courses.length > 0"
class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
<CourseCardList :courses="courses" />
<div ref="lastCourseRef"></div>
</div>
<EmptyState
v-else-if="!isLoading && 0 === courses.length"
:detail="t('Go to Explore to find a topic of interest, or wait for someone to subscribe you')"
:summary="t('You don\'t have any course yet.')"
icon="courses"
/>
</template>
<script setup>
@ -47,12 +50,14 @@ import StickyCourses from "../../../views/user/courses/StickyCourses.vue"
import CourseCardList from "../../../components/course/CourseCardList.vue"
import EmptyState from "../../../components/EmptyState"
import { useSecurityStore } from "../../../store/securityStore"
import Loading from "../../../components/Loading.vue"
const securityStore = useSecurityStore()
const { t } = useI18n()
const courses = ref([])
const isLoading = ref(false)
const isFullyLoaded = ref(false)
const endCursor = ref(null)
const hasMore = ref(true)
const lastCourseRef = ref(null)
@ -82,6 +87,7 @@ watch(result, (newResult) => {
})
}
isLoading.value = false
isFullyLoaded.value = true
})
const loadMoreCourses = () => {
@ -134,6 +140,7 @@ onMounted(() => {
endCursor.value = null
hasMore.value = true
isLoading.value = false
isFullyLoaded.value = false
if (observer) observer.disconnect()
observer = new IntersectionObserver(

@ -1,25 +1,38 @@
<template>
<StickyCourses />
<SessionTabs class="mb-4" />
<SessionsLoading :is-loading="isLoading" />
<!-- All sessions -->
<!-- <SessionListWrapper :sessions="sessionList"/>-->
<div class="relative min-h-[300px]">
<Loading :visible="!isFullyLoaded" />
<SessionCategoryView
v-if="!isLoading"
:categories="categories"
:categories-with-sessions="categoriesWithSessions"
:uncategorized-sessions="uncategorizedSessions"
/>
<SessionsLoading :is-loading="isLoading" />
<SessionCategoryView
v-if="!isLoading"
:categories="categories"
:categories-with-sessions="categoriesWithSessions"
:uncategorized-sessions="uncategorizedSessions"
/>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from "vue"
import SessionTabs from "../../../components/session/SessionTabs.vue"
import StickyCourses from "../../../views/user/courses/StickyCourses.vue"
import SessionCategoryView from "../../../components/session/SessionCategoryView"
import { useSession } from "./session"
import SessionsLoading from "./SessionsLoading.vue"
import Loading from "../../../components/Loading.vue"
const { isLoading, uncategorizedSessions, categories, categoriesWithSessions } = useSession("current")
const isFullyLoaded = ref(false)
watch(isLoading, async (newVal) => {
if (!newVal) {
await new Promise((resolve) => setTimeout(resolve, 500))
await nextTick()
isFullyLoaded.value = true
}
})
</script>

@ -181,7 +181,8 @@
"symfony/flex": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"symfony/runtime": true
}
},
"process-timeout": 900
},
"require-dev": {
"behat/behat": "^3.10",

@ -5,6 +5,7 @@ doctrine:
user: '%env(DATABASE_USER)%'
password: '%env(DATABASE_PASSWORD)%'
host: '%env(DATABASE_HOST)%'
port: '%env(DATABASE_PORT)%'
driver: 'pdo_mysql'
charset: utf8mb4
options:

@ -77,7 +77,6 @@
"moment": "^2.30.1",
"multiselect-two-sides": "^2.5.7",
"mxgraph": "^4.2.2",
"optimize-css-assets-webpack-plugin": "^6.0.1",
"path": "^0.12.7",
"pinia": "^2.3.0",
"pretty-bytes": "^5.6.0",

@ -12,6 +12,7 @@ declare(strict_types=1);
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
use Chamilo\CoreBundle\Component\Utils\StateIcon;
use Chamilo\CoreBundle\Component\Utils\ToolIcon;
use Chamilo\CoreBundle\Framework\Container;
$cidReset = true;
@ -416,9 +417,12 @@ if (isset($_GET['search']) && 'advanced' === $_GET['search']) {
['id' => 'course-search-keyword', 'aria-label' => get_lang('Search courses')]
);
$form->addButtonSearch(get_lang('Search courses'));
$advanced = '<a class="btn btn--plain" href="'.api_get_path(WEB_CODE_PATH).'admin/course_list.php?search=advanced">
<em class="pi pi-search"></em> '.
get_lang('Advanced search').'</a>';
$advanced = Display::toolbarButton(
get_lang('Advanced search'),
Container::getRouter()->generate('legacy_main', ['name' => 'admin/course_list.php', 'search' => 'advanced']),
ActionIcon::SEARCH,
'plain'
);
// Create a filter by session
$sessionFilter = new FormValidator(

@ -20,14 +20,86 @@ Session::erase('objExercise');
Session::erase('objQuestion');
Session::erase('objAnswer');
$interbreadcrumb[] = ['url' => '/admin', 'name' => get_lang('Administration')];
$interbreadcrumb[] = ['url' => Container::getRouter()->generate('admin'), 'name' => get_lang('Administration')];
$action = $_REQUEST['action'] ?? '';
$id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : '';
$description = $_REQUEST['description'] ?? '';
$title = $_REQUEST['title'] ?? '';
$page = !empty($_GET['page']) ? (int) $_GET['page'] : 1;
// Prepare lists for form
// Courses list
$selectedCourse = isset($_GET['selected_course']) ? (int) $_GET['selected_course'] : null;
$courseList = CourseManager::get_courses_list(0, 0, 'title');
$courseSelectionList = ['-1' => get_lang('Select')];
foreach ($courseList as $item) {
$course = api_get_course_entity($item['real_id']);
$courseSelectionList[$course->getId()] = '';
if ($course->getId() == api_get_course_int_id()) {
$courseSelectionList[$course->getId()] = '>&nbsp;&nbsp;&nbsp;&nbsp;';
}
$courseSelectionList[$course->getId()] .= $course->getTitle();
}
// Difficulty list (only from 0 to 5)
$questionLevel = isset($_REQUEST['question_level']) ? (int) $_REQUEST['question_level'] : -1;
$levels = [
-1 => get_lang('All'),
0 => 0,
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 5,
];
// Answer type
$answerType = isset($_REQUEST['answer_type']) ? (int) $_REQUEST['answer_type'] : null;
$questionList = Question::getQuestionTypeList();
$questionTypesList = [];
$questionTypesList['-1'] = get_lang('All');
foreach ($questionList as $key => $item) {
$questionTypesList[$key] = get_lang($item[1]);
}
$form = new FormValidator('admin_questions', 'get');
$form->addHeader(get_lang('Questions'));
$form->addText('id', get_lang('Id'), false);
$form->addText('title', get_lang('Title'), false);
$form->addText('description', get_lang('Description'), false);
$form
->addSelect(
'selected_course',
[get_lang('Course'), get_lang('Course in which the question was initially created.')],
$courseSelectionList,
['id' => 'selected_course']
)
->setSelected($selectedCourse)
;
$form
->addSelect(
'question_level',
get_lang('Difficulty'),
$levels,
['id' => 'question_level']
)
->setSelected($questionLevel)
;
$form
->addSelect(
'answer_type',
get_lang('Answer type'),
$questionTypesList,
['id' => 'answer_type']
)
->setSelected($answerType)
;
$form->addHidden('form_sent', 1);
$form->addHidden('course_id_changed', '0');
$form->addButtonSearch(get_lang('Search'));
$questions = [];
@ -37,12 +109,19 @@ $length = 20;
$questionCount = 0;
$start = 0;
$end = 0;
$pdfContent = '';
$params = [
'id' => $id,
'title' => Security::remove_XSS($title),
'description' => Security::remove_XSS($description),
'selected_course' => $selectedCourse,
'question_level' => $questionLevel,
'answer_type' => $answerType,
];
if ($formSent) {
$id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : '';
$description = isset($_REQUEST['description']) ? $_REQUEST['description'] : '';
$title = isset($_REQUEST['title']) ? $_REQUEST['title'] : '';
$page = isset($_GET['page']) && !empty($_GET['page']) ? (int) $_GET['page'] : 1;
$params['form_sent'] = 1;
$em = Database::getManager();
$repo = $em->getRepository(CQuizQuestion::class);
@ -61,23 +140,27 @@ if ($formSent) {
$criteria->orWhere($criteria->expr()->contains('question', "%$title%"));
}
$questions = $repo->matching($criteria);
// if (-1 !== $selectedCourse) {
// $criteria->andWhere($criteria->expr()->eq('cId', $selectedCourse));
// }
if (empty($id)) {
$id = '';
if (-1 !== $questionLevel) {
$criteria->andWhere($criteria->expr()->eq('level', $questionLevel));
}
if (-1 !== $answerType) {
$criteria->andWhere($criteria->expr()->eq('type', $answerType));
}
$params = [
'id' => $id,
'title' => Security::remove_XSS($title),
'description' => Security::remove_XSS($description),
'form_sent' => 1,
];
$url = api_get_self().'?'.http_build_query($params);
$form->setDefaults($params);
$questions = $repo->matching($criteria);
$url = api_get_self().'?'.http_build_query($params);
$form->setDefaults($params);
$questionCount = count($questions);
if ('export_pdf' === $action) {
$length = $questionCount;
}
$paginator = new Paginator(Container::$container->get('event_dispatcher'));
$pagination = $paginator->paginate($questions, $page, $length);
$pagination->setItemNumberPerPage($length);
@ -142,6 +225,14 @@ if ($formSent) {
);
$question->questionData = ob_get_contents();
if ('export_pdf' === $action) {
$pdfContent .= '<span style="color:#000; font-weight:bold; font-size:x-large;">#'.$question->getIid().'. '.$question->getQuestion().'</span><br />';
$pdfContent .= '<span style="color:#444;">('.$questionTypesList[$question->getType()].') ['.get_lang('Source').': '.$courseCode.']</span><br />';
$pdfContent .= $question->getDescription().'<br />';
$pdfContent .= $question->questionData;
continue;
}
$deleteUrl = $url.'&'.http_build_query([
'courseId' => $question->getCId(),
'questionId' => $question->getId(),
@ -222,11 +313,20 @@ if ($formSent) {
$formContent = $form->returnForm();
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
switch ($action) {
case 'export_pdf':
$pdfContent = Security::remove_XSS($pdfContent);
$pdfParams = [
'filename' => 'questions-export-'.api_get_local_time(),
'pdf_date' => api_get_local_time(),
'orientation' => 'P',
];
$pdf = new PDF('A4', $pdfParams['orientation'], $pdfParams);
$pdf->html_to_pdf_with_template($pdfContent, false, false, true);
exit;
case 'delete':
$questionId = isset($_REQUEST['questionId']) ? $_REQUEST['questionId'] : '';
$courseId = isset($_REQUEST['courseId']) ? $_REQUEST['courseId'] : '';
$questionId = $_REQUEST['questionId'] ?? '';
$courseId = $_REQUEST['courseId'] ?? '';
$courseInfo = api_get_course_info_by_id($courseId);
if (!empty($courseInfo)) {
@ -245,16 +345,36 @@ switch ($action) {
header("Location: $url");
exit;
break;
}
$actionsLeft = Display::url(
Display::return_icon('back.png', get_lang('Administration'), [], ICON_SIZE_MEDIUM),
Container::getRouter()->generate('admin'),
);
$exportUrl = Container::getRouter()->generate(
'legacy_main',
['name' => 'admin/questions.php', 'action' => 'export_pdf', ...$params]
);
$actionsRight = Display::url(
Display::return_icon('pdf.png', get_lang('Export to PDF'), [], ICON_SIZE_MEDIUM),
$exportUrl
);
$toolbar = Display::toolbarAction(
'toolbar-admin-questions',
[$actionsLeft, $actionsRight]
);
$tpl = new Template(get_lang('Questions'));
$tpl->assign('form', $formContent);
$tpl->assign('toolbar', $toolbar);
$tpl->assign('pagination', $pagination);
$tpl->assign('pagination_length', $length);
$tpl->assign('start', $start);
$tpl->assign('end', $end);
$tpl->assign('question_count', $questionCount);
$layout = $tpl->get_template('admin/questions.tpl');
$layout = $tpl->get_template('admin/questions.html.twig');
$tpl->display($layout);

@ -352,16 +352,17 @@ $tools = [
'report=new_user_registrations' => get_lang('New users registrations'),
],
get_lang('System') => [
'report=activities' => get_lang('ImportantActivities'),
'report=user_session' => get_lang('PortalUserSessionStats'),
'report=activities' => get_lang('Important activities'),
'report=user_session' => get_lang('Portal user session stats'),
'report=quarterly_report' => get_lang('Quarterly report'),
],
get_lang('Social') => [
'report=messagereceived' => get_lang('MessagesReceived'),
'report=messagesent' => get_lang('MessagesSent'),
'report=friends' => get_lang('CountFriends'),
'report=messagereceived' => get_lang('Number of messages received'),
'report=messagesent' => get_lang('Number of messages sent'),
'report=friends' => get_lang('Contacts count'),
],
get_lang('Session') => [
'report=session_by_date' => get_lang('SessionsByDate'),
'report=session_by_date' => get_lang('Sessions by date'),
],
];
@ -1665,6 +1666,146 @@ switch ($report) {
case 'logins_by_date':
$content .= Statistics::printLoginsByDate();
break;
case 'quarterly_report':
global $htmlHeadXtra;
$ajaxPath = api_get_path(WEB_AJAX_PATH);
$waitIcon = Display::getMdiIcon('clock-time-four', 'ch-tool-icon-disabled', null, ICON_SIZE_SMALL, false);
$htmlHeadXtra[] .= '<script>
function loadReportQuarterlyUsers () {
$("#tracking-report-quarterly-users")
.html(\'<p>'.$waitIcon.'</p>\')
.load("'.$ajaxPath.'statistics.ajax.php?a=report_quarterly_users'.'");
}</script>';
$htmlHeadXtra[] .= '<script>
function loadReportQuarterlyCourses () {
$("#tracking-report-quarterly-courses")
.html(\'<p>'.$waitIcon.'</p>\')
.load("'.$ajaxPath.'statistics.ajax.php?a=report_quarterly_courses'.'");
}</script>';
$htmlHeadXtra[] .= '<script>
function loadReportQuarterlyHoursOfTraining () {
$("#tracking-report-quarterly-hours-of-training")
.html(\'<p>'.$waitIcon.'</p>\')
.load("'.$ajaxPath.'statistics.ajax.php?a=report_quarterly_hours_of_training'.'");
}</script>';
$htmlHeadXtra[] .= '<script>
function loadReportQuarterlyCertificatesGenerated () {
$("#tracking-report-quarterly-number-of-certificates-generated")
.html(\'<p>'.$waitIcon.'</p>\')
.load("'.$ajaxPath.'statistics.ajax.php?a=report_quarterly_number_of_certificates_generated'.'");
}</script>';
$htmlHeadXtra[] .= '<script>
function loadReportQuarterlySessionsByDuration () {
$("#tracking-report-quarterly-sessions-by-duration")
.html(\'<p>'.$waitIcon.'</p>\')
.load("'.$ajaxPath.'statistics.ajax.php?a=report_quarterly_sessions_by_duration'.'");
}</script>';
$htmlHeadXtra[] .= '<script>
function loadReportQuarterlyCoursesAndSessions () {
$("#tracking-report-quarterly-courses-and-sessions")
.html(\'<p>'.$waitIcon.'</p>\')
.load("'.$ajaxPath.'statistics.ajax.php?a=report_quarterly_courses_and_sessions'.'");
}</script>';
if (api_get_current_access_url_id() === 1) {
$htmlHeadXtra[] .= '<script>
function loadReportQuarterlyTotalDiskUsage () {
$("#tracking-report-quarterly-total-disk-usage")
.html(\'<p>'.$waitIcon.'</p>\')
.load("'.$ajaxPath.'statistics.ajax.php?a=report_quarterly_total_disk_usage'.'");
}</script>';
}
$content .= Display::tag('H4', get_lang('Number of users registered and connected'), ['style' => 'margin-bottom: 25px;']);
$content .= Display::url(
get_lang('Show'),
'javascript://',
['onclick' => 'loadReportQuarterlyUsers();', 'class' => 'btn btn-default']
);
$content .= Display::div('', ['id' => 'tracking-report-quarterly-users', 'style' => 'margin: 30px;']);
$content .= Display::tag('H4', get_lang('Number of existing and available courses'), ['style' => 'margin-bottom: 25px;']);
$content .= Display::url(
get_lang('Show'),
'javascript://',
['onclick' => 'loadReportQuarterlyCourses();', 'class' => 'btn btn-default']
);
$content .= Display::div('', ['id' => 'tracking-report-quarterly-courses', 'style' => 'margin: 30px;']);
$content .= Display::tag('H4', get_lang('Hours of training'), ['style' => 'margin-bottom: 25px;']);
$content .= Display::url(
get_lang('Show'),
'javascript://',
['onclick' => 'loadReportQuarterlyHoursOfTraining();', 'class' => 'btn btn-default']
);
$content .= Display::div(
'',
[
'id' => 'tracking-report-quarterly-hours-of-training',
'style' => 'margin: 30px;',
]
);
$content .= Display::tag(
'H4',
get_lang('Number of certificates generated'),
['style' => 'margin-bottom: 25px;']
);
$content .= Display::url(
get_lang('Show'),
'javascript://',
['onclick' => 'loadReportQuarterlyCertificatesGenerated();', 'class' => 'btn btn-default']
);
$content .= Display::div(
'',
['id' => 'tracking-report-quarterly-number-of-certificates-generated', 'style' => 'margin: 30px;']
);
$content .= Display::tag(
'H4',
get_lang('Number of sessions per duration'),
['style' => 'margin-bottom: 25px;']
);
$content .= Display::url(
get_lang('Show'),
'javascript://',
['onclick' => 'loadReportQuarterlySessionsByDuration();', 'class' => 'btn btn-default']
);
$content .= Display::div(
'',
['id' => 'tracking-report-quarterly-sessions-by-duration', 'style' => 'margin: 30px;']
);
$content .= Display::tag(
'H4',
get_lang('Number of courses, sessions and subscribed users'),
['style' => 'margin-bottom: 25px;']
);
$content .= Display::url(
get_lang('Show'),
'javascript://',
['onclick' => 'loadReportQuarterlyCoursesAndSessions();', 'class' => 'btn btn-default']
);
$content .= Display::div(
'',
[
'id' => 'tracking-report-quarterly-courses-and-sessions',
'style' => 'margin: 30px;',
]
);
if (api_get_current_access_url_id() === 1) {
$content .= Display::tag(
'H4',
get_lang('Total disk usage'),
['style' => 'margin-bottom: 25px;']
);
$content .= Display::url(
get_lang('Show'),
'javascript://',
['onclick' => 'loadReportQuarterlyTotalDiskUsage();', 'class' => 'btn btn-default']
);
$content .= Display::div(
'',
[
'id' => 'tracking-report-quarterly-total-disk-usage',
'style' => 'margin: 30px;',
]
);
}
break;
}
Display::display_header($tool_name);

@ -218,6 +218,7 @@ $group[] = $form->createElement(
'id' => 'password',
'autocomplete' => 'off',
'onkeydown' => 'javascript: password_switch_radio_button();',
'show_hide' => true,
//'required' => 'required'
]
);

@ -229,7 +229,11 @@ $group[] = $form->createElement(
'password',
'password',
null,
['onkeydown' => 'javascript: password_switch_radio_button();']
[
'id' => 'password',
'onkeydown' => 'javascript: password_switch_radio_button();',
'show_hide' => true,
]
);
$form->addGroup($group, 'password', null, null, false);

@ -1089,6 +1089,7 @@ if ($form->validate()) {
$cond_array = explode(':', $values['legal_accept_type']);
if (!empty($cond_array[0]) && !empty($cond_array[1])) {
$time = time();
// legal_accept is stored as version_id:language_id:timestamp
$conditionToSave = (int) $cond_array[0].':'.(int) $cond_array[1].':'.$time;
UserManager::update_extra_field_value(
$userId,

@ -13,7 +13,14 @@ if (!ctype_alnum($token)) {
$form = new FormValidator('reset', 'POST', api_get_self().'?token='.$token);
$form->addElement('header', get_lang('Reset password'));
$form->addHidden('token', $token);
$form->addElement('password', 'pass1', get_lang('Password'));
$form->addElement(
'password',
'pass1',
get_lang('Password'),
[
'show_hide' => true,
]
);
$form->addElement(
'password',
'pass2',

@ -141,7 +141,7 @@ if (!$is_allowedToEdit) {
}
}
$allowRecordAudio = 'true' === api_get_setting('enable_record_audio');
$allowRecordAudio = true;
$allowTeacherCommentAudio = ('true' === api_get_setting('exercise.allow_teacher_comment_audio'));
//$js = '<script>'.api_get_language_translate_html().'</script>';

@ -77,10 +77,8 @@ if ('true' === api_get_setting('exercise.quiz_prevent_copy_paste')) {
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'jquery.nocopypaste.js"></script>';
}
if ('true' === api_get_setting('enable_record_audio')) {
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'rtc/RecordRTC.js"></script>';
$htmlHeadXtra[] = api_get_js('record_audio/record_audio.js');
}
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'rtc/RecordRTC.js"></script>';
$htmlHeadXtra[] = api_get_js('record_audio/record_audio.js');
$zoomOptions = api_get_setting('exercise.quiz_image_zoom', true);
if (isset($zoomOptions['options']) && !in_array($origin, ['embeddable', 'mobileapp'])) {

@ -1097,10 +1097,6 @@ abstract class Question
*/
public static function get_question_type($type)
{
if (ORAL_EXPRESSION == $type && 'true' !== api_get_setting('enable_record_audio')) {
return null;
}
return self::$questionTypes[$type];
}
@ -1109,10 +1105,6 @@ abstract class Question
*/
public static function getQuestionTypeList()
{
if ('true' !== api_get_setting('enable_record_audio')) {
self::$questionTypes[ORAL_EXPRESSION] = null;
unset(self::$questionTypes[ORAL_EXPRESSION]);
}
if ('true' !== api_get_setting('enable_quiz_scenario')) {
self::$questionTypes[HOT_SPOT_DELINEATION] = null;
unset(self::$questionTypes[HOT_SPOT_DELINEATION]);

@ -144,11 +144,11 @@ if (isset($exerciseId) && $exerciseId > 0) {
// In building mode show all questions not render by teacher order.
$objExercise->questionSelectionType = EX_Q_SELECTION_ORDERED;
$allowQuestionOrdering = true;
$showPagination = api_get_setting('exercise.show_question_pagination');
$length = api_get_setting('exercise.question_pagination_length');
$showPagination = 'true' === api_get_setting('exercise.show_question_pagination');
$length = (int) api_get_setting('exercise.question_pagination_length') ?: 30;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if (!empty($showPagination) && $nbrQuestions > $showPagination) {
if ($showPagination && $nbrQuestions > $length) {
$allowQuestionOrdering = false;
$start = ($page - 1) * $length;
$questionList = $objExercise->selectQuestionList(true, true);
@ -310,13 +310,15 @@ if (isset($exerciseId) && $exerciseId > 0) {
echo '</div>'; //question list div
// Pagination navigation
$totalPages = ceil($nbrQuestions / $length);
echo '<div class="pagination flex justify-center mt-4">';
for ($i = 1; $i <= $totalPages; $i++) {
$isActive = ($i == $page) ? 'bg-primary text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-200';
echo '<a href="?'.http_build_query(array_merge($_GET, ['page' => $i])).'" class="mx-1 px-4 py-2 border '.$isActive.' rounded">'.$i.'</a>';
if ($showPagination && $nbrQuestions > $length) {
$totalPages = ceil($nbrQuestions / $length);
echo '<div class="pagination flex justify-center mt-4">';
for ($i = 1; $i <= $totalPages; $i++) {
$isActive = ($i == $page) ? 'bg-primary text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-200';
echo '<a href="?' . http_build_query(array_merge($_GET, ['page' => $i])) . '" class="mx-1 px-4 py-2 border ' . $isActive . ' rounded">' . $i . '</a>';
}
echo '</div>';
}
echo '</div>';
} else {
echo Display::return_message(get_lang('Questions list (there is no question so far).'), 'warning');
}

@ -8,7 +8,7 @@ use ChamiloSession as Session;
require_once __DIR__.'/../global.inc.php';
$current_course_tool = TOOL_QUIZ;
$debug = true;
$debug = false;
api_protect_course_script(true);
$action = $_REQUEST['a'];
$course_id = api_get_course_int_id();

@ -644,7 +644,7 @@ switch ($action) {
$all = [];
while ($row = Database::fetch_array($result)) {
$categoryData = SessionManager::get_session_category($row['session_category_id']);
$label = get_lang('NoCategory');
$label = get_lang('Without category');
if ($categoryData) {
$label = $categoryData['name'];
}
@ -759,4 +759,656 @@ switch ($action) {
header('Content-type: application/json');
echo json_encode($list);
break;
case 'report_quarterly_users':
$currentQuarterDates = getQuarterDates();
$pre1QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-3 month')
->format('Y-m-d')
);
$pre2QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-6 month')
->format('Y-m-d')
);
$pre3QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-9 month')
->format('Y-m-d')
);
$pre4QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-12 month')
->format('Y-m-d')
);
$pre5QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-15 month')
->format('Y-m-d')
);
// Make de headers for the table
$headers = [
'',
$pre5QuarterDates['quarter_title'],
$pre4QuarterDates['quarter_title'],
$pre3QuarterDates['quarter_title'],
$pre2QuarterDates['quarter_title'],
$pre1QuarterDates['quarter_title'],
get_lang('YoY'),
$currentQuarterDates['quarter_title'].'*',
];
// Get the data for the number of user registered row (2)
$countUsersTotal = UserManager::get_number_of_users(
null,
null,
null
);
$countUsersPre1Quarter = UserManager::get_number_of_users(
null,
null,
null,
null,
$pre1QuarterDates['quarter_end']
);
$countUsersPre2Quarter = UserManager::get_number_of_users(
null,
null,
null,
null,
$pre2QuarterDates['quarter_end']
);
$countUsersPre3Quarter = UserManager::get_number_of_users(
null,
null,
null,
null,
$pre3QuarterDates['quarter_end']
);
$countUsersPre4Quarter = UserManager::get_number_of_users(
null,
null,
null,
null,
$pre4QuarterDates['quarter_end']
);
$countUsersPre5Quarter = UserManager::get_number_of_users(
null,
null,
null,
null,
$pre5QuarterDates['quarter_end']
);
// Calculate percent for first row
$percentIncrementUsersRegistered = api_calculate_increment_percent(
$countUsersPre1Quarter,
$countUsersPre5Quarter
);
// Get the data for number of users connected row (3)
$countUsersConnectedCurrentQuarter = count(
Statistics::getLoginsByDate(
$currentQuarterDates['quarter_start'],
$currentQuarterDates['quarter_end']
)
);
$countUsersConnectedPre1Quarter = count(
Statistics::getLoginsByDate(
$pre1QuarterDates['quarter_start'],
$pre1QuarterDates['quarter_end']
)
);
$countUsersConnectedPre2Quarter = count(
Statistics::getLoginsByDate(
$pre2QuarterDates['quarter_start'],
$pre2QuarterDates['quarter_end']
)
);
$countUsersConnectedPre3Quarter = count(
Statistics::getLoginsByDate(
$pre3QuarterDates['quarter_start'],
$pre3QuarterDates['quarter_end']
)
);
$countUsersConnectedPre4Quarter = count(
Statistics::getLoginsByDate(
$pre4QuarterDates['quarter_start'],
$pre4QuarterDates['quarter_end']
)
);
$countUsersConnectedPre5Quarter = count(
Statistics::getLoginsByDate(
$pre5QuarterDates['quarter_start'],
$pre5QuarterDates['quarter_end']
)
);
// Calculate percent for second row
$percentIncrementUsersConnected = api_calculate_increment_percent(
$countUsersConnectedPre1Quarter,
$countUsersConnectedPre5Quarter
);
//Make de rows with the recollected data
$rows = [];
$rows[] = [
get_lang('Number of users registered (total)'),
$countUsersPre5Quarter,
$countUsersPre4Quarter,
$countUsersPre3Quarter,
$countUsersPre2Quarter,
$countUsersPre1Quarter,
$percentIncrementUsersRegistered,
$countUsersTotal,
];
//todo comprobacion + -
$rows[] = [
get_lang('Number of users registered (new vs previous quarter)'),
'-',
'+'.($countUsersPre1Quarter - $countUsersPre2Quarter),
'+'.($countUsersPre2Quarter - $countUsersPre3Quarter),
'+'.($countUsersPre3Quarter - $countUsersPre4Quarter),
'+'.($countUsersPre4Quarter - $countUsersPre5Quarter),
'-',
'+'.($countUsersTotal - $countUsersPre1Quarter),
];
$rows[] = [
get_lang('Number of users who connected'),
$countUsersConnectedPre5Quarter,
$countUsersConnectedPre4Quarter,
$countUsersConnectedPre3Quarter,
$countUsersConnectedPre2Quarter,
$countUsersConnectedPre1Quarter,
$percentIncrementUsersConnected,
$countUsersConnectedCurrentQuarter,
];
echo Display::table($headers, $rows, []);
echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning');
break;
case 'report_quarterly_courses':
$currentQuarterDates = getQuarterDates();
$pre1QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-3 month')
->format('Y-m-d')
);
$pre2QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-6 month')
->format('Y-m-d')
);
$pre3QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-9 month')
->format('Y-m-d')
);
$pre4QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-12 month')
->format('Y-m-d')
);
$pre5QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-15 month')
->format('Y-m-d')
);
// Make the headers for the table
$headers = [
'',
$pre5QuarterDates['quarter_title'],
$pre4QuarterDates['quarter_title'],
$pre3QuarterDates['quarter_title'],
$pre2QuarterDates['quarter_title'],
$pre1QuarterDates['quarter_title'],
get_lang('YoY'),
$currentQuarterDates['quarter_title'].'*',
];
// Get the data for the rows
$countCoursesCurrentQuarter = Statistics::countCourses(null, null, null);
$countCoursesPre1Quarter = Statistics::countCourses(null, null, $pre1QuarterDates['quarter_end']);
$countCoursesPre2Quarter = Statistics::countCourses(null, null, $pre2QuarterDates['quarter_end']);
$countCoursesPre3Quarter = Statistics::countCourses(null, null, $pre3QuarterDates['quarter_end']);
$countCoursesPre4Quarter = Statistics::countCourses(null, null, $pre4QuarterDates['quarter_end']);
$countCoursesPre5Quarter = Statistics::countCourses(null, null, $pre5QuarterDates['quarter_end']);
$auxArrayVisibilities = [
COURSE_VISIBILITY_OPEN_WORLD,
COURSE_VISIBILITY_OPEN_PLATFORM,
COURSE_VISIBILITY_REGISTERED,
];
$countCoursesAvailableCurrentQuarter = Statistics::countCoursesByVisibility($auxArrayVisibilities);
$countCoursesAvailablePre1Quarter = Statistics::countCoursesByVisibility(
$auxArrayVisibilities,
null,
$pre1QuarterDates['quarter_end']
);
$countCoursesAvailablePre2Quarter = Statistics::countCoursesByVisibility(
$auxArrayVisibilities,
null,
$pre2QuarterDates['quarter_end']
);
$countCoursesAvailablePre3Quarter = Statistics::countCoursesByVisibility(
$auxArrayVisibilities,
null,
$pre3QuarterDates['quarter_end']
);
$countCoursesAvailablePre4Quarter = Statistics::countCoursesByVisibility(
$auxArrayVisibilities,
null,
$pre4QuarterDates['quarter_end']
);
$countCoursesAvailablePre5Quarter = Statistics::countCoursesByVisibility(
$auxArrayVisibilities,
null,
$pre5QuarterDates['quarter_end']
);
// Calculate percents for first row
$percentIncrementCourses = api_calculate_increment_percent(
$countCoursesPre1Quarter,
$countCoursesPre5Quarter
);
// Calculate percents for second row
$percentIncrementUsersRegistered = api_calculate_increment_percent(
$countCoursesAvailablePre1Quarter,
$countCoursesAvailablePre5Quarter
);
//Make the rows with the recollected data
$rows = [];
$rows[] = [
get_lang('Number of existing courses (total)'),
$countCoursesPre5Quarter,
$countCoursesPre4Quarter,
$countCoursesPre3Quarter,
$countCoursesPre2Quarter,
$countCoursesPre1Quarter,
$percentIncrementCourses,
$countCoursesCurrentQuarter,
];
$rows[] = [
get_lang('Number of available courses (not closed or hidden, total)'),
$countCoursesAvailablePre5Quarter,
$countCoursesAvailablePre4Quarter,
$countCoursesAvailablePre3Quarter,
$countCoursesAvailablePre2Quarter,
$countCoursesAvailablePre1Quarter,
$percentIncrementUsersRegistered,
$countCoursesAvailableCurrentQuarter,
];
echo Display::table($headers, $rows, []);
echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning');
break;
case 'report_quarterly_hours_of_training':
$currentQuarterDates = getQuarterDates();
$pre1QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-3 month')
->format('Y-m-d')
);
$pre2QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-6 month')
->format('Y-m-d')
);
$pre3QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-9 month')
->format('Y-m-d')
);
$pre4QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-12 month')
->format('Y-m-d')
);
$pre5QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-15 month')
->format('Y-m-d')
);
// Make the headers for the table
$headers = [
'',
$pre5QuarterDates['quarter_title'],
$pre4QuarterDates['quarter_title'],
$pre3QuarterDates['quarter_title'],
$pre2QuarterDates['quarter_title'],
$pre1QuarterDates['quarter_title'],
get_lang('YoY'),
$currentQuarterDates['quarter_title'].'*',
];
// Get data for the row
$timeSpentCoursesCurrentQuarter = Tracking::getTotalTimeSpentInCourses(
$currentQuarterDates['quarter_start'],
$currentQuarterDates['quarter_end']
);
$timeSpentCourses1PreQuarter = Tracking::getTotalTimeSpentInCourses(
$pre1QuarterDates['quarter_start'],
$pre1QuarterDates['quarter_end']
);
$timeSpentCourses2PreQuarter = Tracking::getTotalTimeSpentInCourses(
$pre2QuarterDates['quarter_start'],
$pre2QuarterDates['quarter_end']
);
$timeSpentCourses3PreQuarter = Tracking::getTotalTimeSpentInCourses(
$pre3QuarterDates['quarter_start'],
$pre3QuarterDates['quarter_end']
);
$timeSpentCourses4PreQuarter = Tracking::getTotalTimeSpentInCourses(
$pre4QuarterDates['quarter_start'],
$pre4QuarterDates['quarter_end']
);
$timeSpentCourses5PreQuarter = Tracking::getTotalTimeSpentInCourses(
$pre5QuarterDates['quarter_start'],
$pre5QuarterDates['quarter_end']
);
// Calculate percent for the row
$percentIncrementTimeSpent = api_calculate_increment_percent(
$timeSpentCourses1PreQuarter,
$timeSpentCourses5PreQuarter
);
//Make the row with the recollected data
$rows = [];
$rows[] = [
get_lang('Number of hours of training followed (total)'),
$timeSpentCourses5PreQuarter,
$timeSpentCourses4PreQuarter,
$timeSpentCourses3PreQuarter,
$timeSpentCourses2PreQuarter,
$timeSpentCourses1PreQuarter,
$percentIncrementTimeSpent,
$timeSpentCoursesCurrentQuarter,
];
echo Display::table($headers, $rows, []);
echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning');
break;
case 'report_quarterly_number_of_certificates_generated':
$currentQuarterDates = getQuarterDates();
$pre1QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-3 month')
->format('Y-m-d')
);
$pre2QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-6 month')
->format('Y-m-d')
);
$pre3QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-9 month')
->format('Y-m-d')
);
$pre4QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-12 month')
->format('Y-m-d')
);
$pre5QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-15 month')
->format('Y-m-d')
);
// Make the headers for the table
$headers = [
'',
$pre5QuarterDates['quarter_title'],
$pre4QuarterDates['quarter_title'],
$pre3QuarterDates['quarter_title'],
$pre2QuarterDates['quarter_title'],
$pre1QuarterDates['quarter_title'],
get_lang('YoY'),
$currentQuarterDates['quarter_title'].'*',
];
// Get data for the row
$certificateGeneratedCurrentQuarter = Statistics::countCertificatesByQuarter(
null,
$currentQuarterDates['quarter_end']
);
$certificateGenerated1PreQuarter = Statistics::countCertificatesByQuarter(
null,
$pre1QuarterDates['quarter_end']
);
$certificateGenerated2PreQuarter = Statistics::countCertificatesByQuarter(
null,
$pre2QuarterDates['quarter_end']
);
$certificateGenerated3PreQuarter = Statistics::countCertificatesByQuarter(
null,
$pre3QuarterDates['quarter_end']
);
$certificateGenerated4PreQuarter = Statistics::countCertificatesByQuarter(
null,
$pre4QuarterDates['quarter_end']
);
$certificateGenerated5PreQuarter = Statistics::countCertificatesByQuarter(
null,
$pre5QuarterDates['quarter_end']
);
// Calculate percent for the row
$percentIncrementCertificateGenerated = api_calculate_increment_percent(
$certificateGenerated1PreQuarter,
$certificateGenerated5PreQuarter
);
//Make the row with the recollected data
$rows = [];
$rows[] = [
get_lang('Number of certificates generated'),
$certificateGenerated5PreQuarter,
$certificateGenerated4PreQuarter,
$certificateGenerated3PreQuarter,
$certificateGenerated2PreQuarter,
$certificateGenerated1PreQuarter,
$percentIncrementCertificateGenerated,
$certificateGeneratedCurrentQuarter,
];
echo Display::table($headers, $rows, []);
echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning');
break;
case "report_quarterly_sessions_by_duration":
$currentQuarterDates = getQuarterDates();
$pre1QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-3 month')
->format('Y-m-d')
);
$pre2QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-6 month')
->format('Y-m-d')
);
$pre3QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-9 month')
->format('Y-m-d')
);
$pre4QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-12 month')
->format('Y-m-d')
);
$pre5QuarterDates = getQuarterDates(
date_create($currentQuarterDates['quarter_start'])
->modify('-15 month')
->format('Y-m-d')
);
// Make the headers for the table
$headers = [
get_lang('Sessions per duration (by quarter)'),
$pre5QuarterDates['quarter_title'],
$pre4QuarterDates['quarter_title'],
$pre3QuarterDates['quarter_title'],
$pre2QuarterDates['quarter_title'],
$pre1QuarterDates['quarter_title'],
get_lang('YoY'),
$currentQuarterDates['quarter_title'].'*',
];
// Get the data for the rows
$sessionsDurationCurrentQuarter = Statistics::getSessionsByDuration(
$currentQuarterDates['quarter_start'],
$currentQuarterDates['quarter_end']
);
$sessionsDuration1PreQuarter = Statistics::getSessionsByDuration(
$pre1QuarterDates['quarter_start'],
$pre1QuarterDates['quarter_end']
);
$sessionsDuration2PreQuarter = Statistics::getSessionsByDuration(
$pre2QuarterDates['quarter_start'],
$pre2QuarterDates['quarter_end']
);
$sessionsDuration3PreQuarter = Statistics::getSessionsByDuration(
$pre3QuarterDates['quarter_start'],
$pre3QuarterDates['quarter_end']
);
$sessionsDuration4PreQuarter = Statistics::getSessionsByDuration(
$pre4QuarterDates['quarter_start'],
$pre4QuarterDates['quarter_end']
);
$sessionsDuration5PreQuarter = Statistics::getSessionsByDuration(
$pre5QuarterDates['quarter_start'],
$pre5QuarterDates['quarter_end']
);
// Calculate percent for the rows
$percentIncrementSessionDuration0 = api_calculate_increment_percent(
$sessionsDuration1PreQuarter['0'],
$sessionsDuration5PreQuarter['0']
);
$percentIncrementSessionDuration5 = api_calculate_increment_percent(
$sessionsDuration1PreQuarter['5'],
$sessionsDuration5PreQuarter['5']
);
$percentIncrementSessionDuration10 = api_calculate_increment_percent(
$sessionsDuration1PreQuarter['10'],
$sessionsDuration5PreQuarter['10']
);
$percentIncrementSessionDuration15 = api_calculate_increment_percent(
$sessionsDuration1PreQuarter['15'],
$sessionsDuration5PreQuarter['15']
);
$percentIncrementSessionDuration30 = api_calculate_increment_percent(
$sessionsDuration1PreQuarter['30'],
$sessionsDuration5PreQuarter['30']
);
$percentIncrementSessionDuration60 = api_calculate_increment_percent(
$sessionsDuration1PreQuarter['60'],
$sessionsDuration5PreQuarter['60']
);
//Make the rows with the recollected data
$rows = [];
$rows[] = [
'0-5&#8242;',
$sessionsDuration5PreQuarter['0'],
$sessionsDuration4PreQuarter['0'],
$sessionsDuration3PreQuarter['0'],
$sessionsDuration2PreQuarter['0'],
$sessionsDuration1PreQuarter['0'],
$percentIncrementSessionDuration0,
$sessionsDurationCurrentQuarter['0'],
];
$rows[] = [
'6-10&#8242;',
$sessionsDuration5PreQuarter['5'],
$sessionsDuration4PreQuarter['5'],
$sessionsDuration3PreQuarter['5'],
$sessionsDuration2PreQuarter['5'],
$sessionsDuration1PreQuarter['5'],
$percentIncrementSessionDuration5,
$sessionsDurationCurrentQuarter['5'],
];
$rows[] = [
'11-15&#8242;',
$sessionsDuration5PreQuarter['10'],
$sessionsDuration4PreQuarter['10'],
$sessionsDuration3PreQuarter['10'],
$sessionsDuration2PreQuarter['10'],
$sessionsDuration1PreQuarter['10'],
$percentIncrementSessionDuration10,
$sessionsDurationCurrentQuarter['10'],
];
$rows[] = [
'16-30&#8242;',
$sessionsDuration5PreQuarter['15'],
$sessionsDuration4PreQuarter['15'],
$sessionsDuration3PreQuarter['15'],
$sessionsDuration2PreQuarter['15'],
$sessionsDuration1PreQuarter['15'],
$percentIncrementSessionDuration15,
$sessionsDurationCurrentQuarter['15'],
];
$rows[] = [
'31-60&#8242;',
$sessionsDuration5PreQuarter['30'],
$sessionsDuration4PreQuarter['30'],
$sessionsDuration3PreQuarter['30'],
$sessionsDuration2PreQuarter['30'],
$sessionsDuration1PreQuarter['30'],
$percentIncrementSessionDuration30,
$sessionsDurationCurrentQuarter['30'],
];
$rows[] = [
'60-&#8734;&#8242;',
$sessionsDuration5PreQuarter['60'],
$sessionsDuration4PreQuarter['60'],
$sessionsDuration3PreQuarter['60'],
$sessionsDuration2PreQuarter['60'],
$sessionsDuration1PreQuarter['60'],
$percentIncrementSessionDuration60,
$sessionsDurationCurrentQuarter['60'],
];
echo Display::table($headers, $rows, []);
echo Display::label(get_lang('*: Current quarter, incomplete data'), 'warning');
break;
case "report_quarterly_courses_and_sessions":
// Make the headers for the tables
$headers = [
[
get_lang('List of course codes'),
get_lang('Number of subscribed users').'*',
get_lang('Number of users who finished the course (as defined in gradebook)'),
],
[
get_lang('List of course codes and sessions'),
get_lang('Number of subscribed users').'*',
get_lang('Number of users who finished the course (as defined in gradebook)'),
],
];
// Get the data fot the first table
$courses = UserManager::countUsersWhoFinishedCourses();
//Make the rows for first table
$rows = [];
foreach ($courses as $course => $data) {
$course_url = api_get_path(WEB_CODE_PATH).'course_home/course_home.php?cidReq='.$course;
$rows[] = [
Display::url($course, $course_url, ['target' => SESSION_LINK_TARGET]),
$data['subscribed'],
$data['finished'],
];
}
echo Display::table($headers[0], $rows, []);
//Get the data for the second table (with sessions)
$courses = UserManager::countUsersWhoFinishedCoursesInSessions();
//Make the rows for second table
$rows = [];
foreach ($courses as $course => $data) {
$rows[] = [
$course,
$data['subscribed'],
$data['finished'],
];
}
echo Display::tag('br', '', ['style' => 'margin-top: 25px;']);
echo Display::table($headers[1], $rows, []);
echo Display::tag('br', '', ['style' => 'margin-top: 25px;']);
echo Display::label(get_lang('*: All users, including inactive, are included'), 'warning');
break;
case "report_quarterly_total_disk_usage":
$accessUrlId = api_get_current_access_url_id();
if (api_is_windows_os()) {
$message = get_lang('The space used on disk cannot be measured properly on Windows-based systems.');
} else {
$dir = api_get_path(SYS_PATH);
$du = exec('du -sh '.$dir, $err);
list($size, $none) = explode("\t", $du);
unset($none);
$limit = 0;
if (isset($_configuration[$accessUrlId]['hosting_limit_disk_space'])) {
$limit = $_configuration[$accessUrlId]['hosting_limit_disk_space'];
}
$message = sprintf(get_lang('Total space used by portal %s limit is %s MB'), $size, $limit);
}
echo Display::tag('H5', $message, ['style' => 'margin-bottom: 25px;']);
break;
}

@ -4082,14 +4082,16 @@ function copy_folder_course_session(
$session_id,
$course_info,
$document,
$source_course_id
$source_course_id,
array $originalFolderNameList = [],
string $originalBaseName = ''
) {
$table = Database::get_course_table(TABLE_DOCUMENT);
$session_id = intval($session_id);
$source_course_id = intval($source_course_id);
// Check whether directory already exists.
if (is_dir($pathname) || empty($pathname)) {
if (empty($pathname) || is_dir($pathname)) {
return true;
}
@ -4100,12 +4102,20 @@ function copy_folder_course_session(
return false;
}
$baseNoDocument = str_replace('document', '', $originalBaseName);
$folderTitles = explode('/', $baseNoDocument);
$folderTitles = array_filter($folderTitles);
$table = Database::get_course_table(TABLE_DOCUMENT);
$session_id = (int) $session_id;
$source_course_id = (int) $source_course_id;
$course_id = $course_info['real_id'];
$folders = explode(DIRECTORY_SEPARATOR, str_replace($base_path_document.DIRECTORY_SEPARATOR, '', $pathname));
$new_pathname = $base_path_document;
$path = '';
foreach ($folders as $folder) {
foreach ($folders as $index => $folder) {
$new_pathname .= DIRECTORY_SEPARATOR.$folder;
$path .= DIRECTORY_SEPARATOR.$folder;
@ -4123,13 +4133,22 @@ function copy_folder_course_session(
if (0 == $num_rows) {
mkdir($new_pathname, api_get_permissions_for_new_directories());
$title = basename($new_pathname);
if (isset($folderTitles[$index + 1])) {
$checkPath = $folderTitles[$index +1];
if (isset($originalFolderNameList[$checkPath])) {
$title = $originalFolderNameList[$checkPath];
}
}
// Insert new folder with destination session_id.
$params = [
'c_id' => $course_id,
'path' => $path,
'comment' => $document->comment,
'title' => basename($new_pathname),
'title' => $title,
'filetype' => 'folder',
'size' => '0',
'session_id' => $session_id,
@ -7534,3 +7553,20 @@ function api_get_permission(string $permissionSlug, array $roles): bool
return $permissionService->hasPermission($permissionSlug, $roles);
}
/**
* Calculate the percentage of change between two numbers.
*
* @param int $newValue
* @param int $oldValue
* @return string
*/
function api_calculate_increment_percent(int $newValue, int $oldValue): string
{
if ($oldValue <= 0) {
$result = " - ";
} else {
$result = ' '.round(100 * (($newValue / $oldValue) - 1), 2).' %';
}
return $result;
}

@ -4220,7 +4220,8 @@ class CourseManager
$destination_course_code,
$destination_session_id,
$params = [],
$withBaseContent = true
bool $withBaseContent = true,
bool $copySessionContent = false
) {
$course_info = api_get_course_info($source_course_code);
@ -4228,6 +4229,7 @@ class CourseManager
$cb = new CourseBuilder('', $course_info);
$course = $cb->build($source_session_id, $source_course_code, $withBaseContent);
$restorer = new CourseRestorer($course);
$restorer->copySessionContent = $copySessionContent;
$restorer->skip_content = $params;
$restorer->restore(
$destination_course_code,
@ -4260,7 +4262,7 @@ class CourseManager
$source_session_id = 0,
$destination_session_id = 0,
$params = [],
$copySessionContent = false
bool $copySessionContent = false
) {
$source_course_info = api_get_course_info($source_course_code);
if (!empty($source_course_info)) {
@ -4278,7 +4280,8 @@ class CourseManager
$newCourse->getCode(),
$destination_session_id,
$params,
true
true,
$copySessionContent
);
if ($result) {
return $newCourse;

@ -201,15 +201,13 @@ class ExerciseLib
break;
case ORAL_EXPRESSION:
// Add nanog
if ('true' === api_get_setting('enable_record_audio')) {
//@todo pass this as a parameter
global $exercise_stat_info;
if (!empty($exercise_stat_info)) {
echo $objQuestionTmp->returnRecorder((int) $exercise_stat_info['exe_id']);
$generatedFile = self::getOralFileAudio($exercise_stat_info['exe_id'], $questionId);
if (!empty($generatedFile)) {
echo $generatedFile;
}
//@todo pass this as a parameter
global $exercise_stat_info;
if (!empty($exercise_stat_info)) {
echo $objQuestionTmp->returnRecorder((int) $exercise_stat_info['exe_id']);
$generatedFile = self::getOralFileAudio($exercise_stat_info['exe_id'], $questionId);
if (!empty($generatedFile)) {
echo $generatedFile;
}
}

@ -2,7 +2,8 @@
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Utils\ToolIcon;
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
use Chamilo\CoreBundle\Component\Utils\ObjectIcon;
use Chamilo\CoreBundle\Framework\Container;
/**
@ -20,7 +21,7 @@ class DatePicker extends HTML_QuickForm_text
if (!isset($attributes['id'])) {
$attributes['id'] = $elementName;
}
$attributes['class'] = 'form-control border flex-grow';
$attributes['class'] = 'p-component p-inputtext p-filled';
parent::__construct($elementName, $elementLabel, $attributes);
$this->_appendName = true;
@ -57,20 +58,18 @@ class DatePicker extends HTML_QuickForm_text
$requiredSymbol = '<span class="form_required">*</span>';
}
$attrs = $this->_attributes;
unset($attrs['id']);
$this->setAttribute('placeholder', get_lang('Select date'));
return '
<div>'.$requiredSymbol.$label.'</div>
<div id="'.$id.'" class="flex items-center mt-1 flatpickr-wrapper" data-wrap="true">
<input '.$this->_getAttrString($this->_attributes).'
value="'.$value.'" placeholder="'.get_lang('Select date').'" data-input>
<label>'.$requiredSymbol.$label.'</label>
<div id="'.$id.'_container" class="flex items-center mt-1 flatpickr-wrapper" data-wrap="true">
<input '.$this->_getAttrString($this->_attributes).' value="'.$value.'" data-input>
<div class="flex space-x-1 ml-2" id="button-addon3">
<button class="btn btn--secondary-outline mr-2" type="button" data-toggle>
<i class="pi pi-calendar pi-lg"></i>
'.Display::getMdiIcon(ObjectIcon::AGENDA).'
</button>
<button class="btn btn--secondary-outline" type="button" data-clear>
<i class="pi pi-times pi-lg"></i>
'.Display::getMdiIcon(ActionIcon::CLOSE).'
</button>
</div>
</div>
@ -109,7 +108,7 @@ class DatePicker extends HTML_QuickForm_text
return "<script>
document.addEventListener('DOMContentLoaded', function () {
function initializeFlatpickr() {
const fp = flatpickr('#{$id}', {
const fp = flatpickr('#{$id}_container', {
locale: '{$localeCode}',
altInput: true,
altFormat: '{$altFormat}',
@ -119,11 +118,7 @@ class DatePicker extends HTML_QuickForm_text
wrap: true
});
if ($('label[for=\"".$id."\"]').length > 0) {
$('label[for=\"".$id."\"]').hide();
}
document.querySelector('label[for=\"' + '{$id}' + '\"]').classList.add('datepicker-label');
$('label[for=\"".$id."\"]').hide().addClass('datepicker-label');
}
function loadLocale() {

@ -19,7 +19,7 @@ class DateTimePicker extends HTML_QuickForm_text
if (!isset($attributes['id'])) {
$attributes['id'] = $elementName;
}
$attributes['class'] = 'p-component p-inputtext';
$attributes['class'] = 'p-component p-inputtext p-filled';
parent::__construct($elementName, $elementLabel, $attributes);
$this->_appendName = true;
}
@ -98,7 +98,7 @@ class DateTimePicker extends HTML_QuickForm_text
}
});
document.querySelector('label[for=\"' + '{$id}' + '\"]').classList.add('datepicker-label');
document.querySelector('label[for=\"' + '{$id}' + '\"]')?.classList.add('datepicker-label');
}
function loadLocaleAndInitialize() {

@ -50,10 +50,8 @@ class FormValidator extends HTML_QuickForm
switch ($layout) {
case self::LAYOUT_BOX_SEARCH:
$attributes['class'] = 'form--search';
break;
case self::LAYOUT_INLINE:
$attributes['class'] = 'flex flex-row gap-3 ';
$attributes['class'] = 'flex flex-row gap-3 items-center ';
break;
case self::LAYOUT_BOX:
$attributes['class'] = 'ch flex gap-1 ';
@ -1750,7 +1748,7 @@ EOT;
$(function() {
var defaultValue = '$defaultId';
$('#$typeNoDots').val(defaultValue);
$('#$typeNoDots').selectpicker('render');
//$('#$typeNoDots').selectpicker('render');
if (defaultValue != '') {
var selected = $('#$typeNoDots option:selected').val();
$.ajax({

@ -2013,3 +2013,58 @@ function api_get_human_date_time($date, $showTime = true, $humanForm = false)
}
}
}
/**
* Return an array with the start and end dates of a quarter (as in 3 months period).
* If no DateTime is not sent, use the current date.
*
* @param string|null $date (optional) The date or null.
*
* @return array E.G.: ['quarter_start' => '2022-10-11',
* 'quarter_end' => '2022-12-31',
* 'quarter_title' => 'Q4 2022']
*/
function getQuarterDates(string $date = null): array
{
if (empty($date)) {
$date = api_get_utc_datetime();
}
if (strlen($date > 10)) {
$date = substr($date, 0, 10);
}
$month = substr($date, 5, 2);
$year = substr($date, 0, 4);
switch ($month) {
case $month >= 1 && $month <= 3:
$start = "$year-01-01";
$end = "$year-03-31";
$quarter = 1;
break;
case $month >= 4 && $month <= 6:
$start = "$year-04-01";
$end = "$year-06-30";
$quarter = 2;
break;
case $month >= 7 && $month <= 9:
$start = "$year-07-01";
$end = "$year-09-30";
$quarter = 3;
break;
case $month >= 10 && $month <= 12:
$start = "$year-10-01";
$end = "$year-12-31";
$quarter = 4;
break;
default:
// Should never happen
$start = "$year-01-01";
$end = "$year-03-31";
$quarter = 1;
break;
}
return [
'quarter_start' => $start,
'quarter_end' => $end,
'quarter_title' => sprintf(get_lang('Q%s %s'), $quarter, $year),
];
}

@ -87,6 +87,8 @@ window.RecordAudio = (function () {
if (btnSave) {
btnSave.prop('disabled', true).text(btnSave.data('loadingtext'));
}
$('.exercise_save_now_button button, .exercise_actions button').prop('disabled', true);
}
}).done(function (response) {
$(response.message).insertAfter($(rtcInfo.blockId).find('.well'));
@ -97,6 +99,8 @@ window.RecordAudio = (function () {
btnStop.prop('disabled', true).addClass('hidden');
btnPause.prop('disabled', true).addClass('hidden');
btnStart.prop('disabled', false).removeClass('hidden');
$('.exercise_save_now_button button, .exercise_actions button').prop('disabled', false);
});
}
@ -127,7 +131,7 @@ window.RecordAudio = (function () {
alert(error);
}
if(!!(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia)) {
if(navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia) {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
navigator.getUserMedia(mediaConstraints, successCallback, errorCallback);
return;

@ -21,6 +21,8 @@
* @link http://pear.php.net/package/HTML_QuickForm
*/
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
/**
* HTML class for a password type field
*
@ -47,7 +49,7 @@ class HTML_QuickForm_password extends HTML_QuickForm_text
*/
public function __construct($elementName = null, $elementLabel = null, $attributes = null)
{
$attributes['class'] = isset($attributes['class']) ? $attributes['class'] : 'form-control';
$attributes['class'] = $attributes['class'] ?? '';
parent::__construct($elementName, $elementLabel, $attributes);
$this->setType('password');
}
@ -95,4 +97,33 @@ class HTML_QuickForm_password extends HTML_QuickForm_text
return ('' != $value ? '**********' : '&nbsp;').
$this->_getPersistantData();
}
public function toHtml(): string
{
if (parent::isFrozen()) {
return parent::getFrozenHtml();
}
$attributes = $this->getAttributes();
$this->removeAttribute('show_hide');
$this->_attributes['class'] = ($attributes['class'] ?? '').' p-password-input ';
$input = parent::toHtml();
if (empty($attributes['show_hide'])) {
return $input;
}
$id = $attributes['id'] ?? '';
return '<div class="p-password p-component p-inputwrapper p-inputwrapper-filled p-icon-field p-icon-field-right">
'.$input.'
<span class="p-icon p-input-icon mdi mdi-'.ActionIcon::VISIBLE->value.'"></span>
</div>'
."<script>$('input#$id + .p-icon.mdi').click(() => {
var txtPasswd = $('input#$id')
txtPasswd.attr('type', txtPasswd.attr('type') === 'password' ? 'text' : 'password');
})</script>";
}
}

@ -134,6 +134,8 @@ class HTML_QuickForm_text extends HTML_QuickForm_input
return $this->getFrozenHtml();
}
$attributes = $this->getAttributes();
$this->_attributes['class'] = ($attributes['class'] ?? '').' p-inputtext p-component ';
if (FormValidator::LAYOUT_HORIZONTAL === $this->getLayout()) {

@ -104,7 +104,7 @@ class Promotion extends Model
foreach ($session_list as $item) {
$sid = SessionManager::copy(
$item['id'],
(int) $item['id'],
true,
false,
false,

@ -4592,6 +4592,7 @@ class SessionManager
* @param bool $copyTeachersAndDrh
* @param bool $create_new_courses New courses will be created
* @param bool $set_exercises_lp_invisible Set exercises and LPs in the new session to invisible by default
* @param bool $copyWithSessionContent Copy course session content into the courses
*
* @return int The new session ID on success, 0 otherwise
*
@ -4600,11 +4601,12 @@ class SessionManager
* @todo make sure the extra session fields are copied too
*/
public static function copy(
$id,
$copy_courses = true,
$copyTeachersAndDrh = true,
$create_new_courses = false,
$set_exercises_lp_invisible = false
int $id,
bool $copy_courses = true,
bool $copyTeachersAndDrh = true,
bool $create_new_courses = false,
bool $set_exercises_lp_invisible = false,
bool $copyWithSessionContent = false,
) {
$id = (int) $id;
$s = self::fetch($id);
@ -4714,9 +4716,7 @@ class SessionManager
foreach ($short_courses as $course_data) {
$course = CourseManager::copy_course_simple(
$course_data['title'].' '.get_lang(
'Copy'
),
$course_data['title'].' '.get_lang('Copy'),
$course_data['course_code'],
$id,
$sid,
@ -4765,6 +4765,20 @@ class SessionManager
$short_courses = $new_short_courses;
self::add_courses_to_session($sid, $short_courses, true);
if ($copyWithSessionContent) {
foreach ($courses as $course) {
CourseManager::copy_course(
$course['code'],
$id,
$course['code'],
$sid,
[],
false,
true
);
}
}
if (false === $create_new_courses && $copyTeachersAndDrh) {
foreach ($short_courses as $courseItemId) {
$coachList = self::getCoachesByCourseSession($id, $courseItemId);

@ -5,6 +5,7 @@ use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Chamilo\CoreBundle\Entity\MessageRelUser;
use Chamilo\CoreBundle\Entity\UserRelUser;
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
/**
* This class provides some functions for statistics.
@ -37,42 +38,44 @@ class Statistics
/**
* Count courses.
*
* @param string $categoryCode Code of a course category.
* Default: count all courses.
* @param string|null $categoryCode Code of a course category.
* Default: count all courses.
* @param string|null $dateFrom dateFrom
* @param string|null $dateUntil dateUntil
*
* @return int Number of courses counted
* @throws \Doctrine\DBAL\Exception
*/
public static function countCourses($categoryCode = null)
public static function countCourses(string $categoryCode = null, string $dateFrom = null, string $dateUntil = null): int
{
$course_table = Database::get_main_table(TABLE_MAIN_COURSE);
$tblCourseCategory = Database::get_main_table(TABLE_MAIN_CATEGORY);
$access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
$courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
$accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
$urlId = api_get_current_access_url_id();
$categoryJoin = '';
$categoryCondition = '';
if (!empty($categoryCode)) {
//$categoryJoin = " LEFT JOIN $tblCourseCategory course_category ON course.category_id = course_category.id ";
//$categoryCondition = " course_category.code = '".Database::escape_string($categoryCode)."' ";
}
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT COUNT(*) AS number
FROM ".$course_table." as c, $access_url_rel_course_table as u
$categoryJoin
WHERE u.c_id = c.id AND access_url_id='".$urlId."'";
FROM ".$courseTable." AS c, $accessUrlRelCourseTable AS u
WHERE u.c_id = c.id AND $accessUrlRelCourseTable='".$urlId."'";
if (isset($categoryCode)) {
$sql .= " AND $categoryCondition";
$sql .= " AND category_code = '".Database::escape_string($categoryCode)."'";
}
} else {
$sql = "SELECT COUNT(*) AS number
FROM $course_table $categoryJoin";
FROM $courseTable AS c
WHERE 1 = 1";
if (isset($categoryCode)) {
$sql .= " WHERE $categoryCondition";
$sql .= " WHERE c.category_code = '".Database::escape_string($categoryCode)."'";
}
}
if (!empty($dateFrom)) {
$dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
$sql .= " AND c.creation_date >= '$dateFrom' ";
}
if (!empty($dateUntil)) {
$dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
$sql .= " AND c.creation_date <= '$dateUntil' ";
}
$res = Database::query($sql);
$obj = Database::fetch_object($res);
@ -82,30 +85,52 @@ class Statistics
/**
* Count courses by visibility.
*
* @param int $visibility visibility (0 = closed, 1 = private, 2 = open, 3 = public) all courses
* @param array|null $visibility visibility (0 = closed, 1 = private, 2 = open, 3 = public) all courses
* @param string|null $dateFrom dateFrom
* @param string|null $dateUntil dateUntil
*
* @return int Number of courses counted
* @throws \Doctrine\DBAL\Exception
*/
public static function countCoursesByVisibility($visibility = null)
public static function countCoursesByVisibility(
array $visibility = null,
string $dateFrom = null,
string $dateUntil = null
): int
{
if (!isset($visibility)) {
if (empty($visibility)) {
return 0;
} else {
$visibilityString = '';
$auxArrayVisibility = [];
if (!is_array($visibility)) {
$visibility = [$visibility];
}
foreach ($visibility as $item) {
$auxArrayVisibility[] = (int) $item;
}
$visibilityString = implode(',', $auxArrayVisibility);
}
$course_table = Database::get_main_table(TABLE_MAIN_COURSE);
$access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
$courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
$accessUrlRelCourseTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
$urlId = api_get_current_access_url_id();
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT COUNT(*) AS number
FROM $course_table as c, $access_url_rel_course_table as u
WHERE u.c_id = c.id AND access_url_id='".$urlId."'";
if (isset($visibility)) {
$sql .= " AND visibility = ".intval($visibility);
}
FROM $courseTable AS c, $accessUrlRelCourseTable AS u
WHERE u.c_id = c.id AND u.access_url_id='".$urlId."'";
} else {
$sql = "SELECT COUNT(*) AS number FROM $course_table ";
if (isset($visibility)) {
$sql .= " WHERE visibility = ".intval($visibility);
}
$sql = "SELECT COUNT(*) AS number
FROM $courseTable AS c
WHERE 1 = 1";
}
$sql .= " AND visibility IN ($visibilityString) ";
if (!empty($dateFrom)) {
$dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
$sql .= " AND c.creation_date >= '$dateFrom' ";
}
if (!empty($dateUntil)) {
$dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
$sql .= " AND c.creation_date <= '$dateUntil' ";
}
$res = Database::query($sql);
$obj = Database::fetch_object($res);
@ -149,7 +174,7 @@ class Statistics
$where = implode(' AND ', $conditions);
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT COUNT(DISTINCT(u.id)) AS number
FROM $user_table as u
INNER JOIN $access_url_rel_user_table as url ON u.id = url.user_id
@ -203,7 +228,7 @@ class Statistics
$urlId = api_get_current_access_url_id();
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT DISTINCT(t.c_id) FROM $table t , $access_url_rel_course_table a
WHERE
t.c_id = a.c_id AND
@ -232,7 +257,7 @@ class Statistics
$table_user = Database::get_main_table(TABLE_MAIN_USER);
$access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$urlId = api_get_current_access_url_id();
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT count(default_id) AS total_number_of_items
FROM $track_e_default, $table_user user, $access_url_rel_user_table url
WHERE user.active <> ".USER_SOFT_DELETED." AND
@ -298,7 +323,7 @@ class Statistics
$direction = 'DESC';
}
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT
default_event_type as col0,
default_value_type as col1,
@ -522,7 +547,7 @@ class Statistics
$where_url = null;
$now = api_get_utc_datetime();
$where_url_last = ' WHERE login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)';
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$table_url = ", $access_url_rel_user_table";
$where_url = " WHERE login_user_id=user_id AND access_url_id='".$urlId."'";
$where_url_last = ' AND login_date > DATE_SUB("'.$now.'",INTERVAL 1 %s)';
@ -622,7 +647,7 @@ class Statistics
$urlId = api_get_current_access_url_id();
$table_url = '';
$where_url = '';
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$table_url = ", $access_url_rel_user_table";
$where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'";
}
@ -712,7 +737,7 @@ class Statistics
$urlId = api_get_current_access_url_id();
$table_url = '';
$where_url = '';
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$table_url = ", $access_url_rel_user_table";
$where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'";
}
@ -778,7 +803,7 @@ class Statistics
foreach ($tools as $tool) {
$tool_names[$tool] = get_lang(ucfirst($tool), '');
}
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT access_tool, count( access_id ) AS number_of_logins
FROM $table t , $access_url_rel_course_table a
WHERE
@ -827,7 +852,7 @@ class Statistics
$table = Database::get_main_table(TABLE_MAIN_COURSE);
$access_url_rel_course_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
$urlId = api_get_current_access_url_id();
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT course_language, count( c.code ) AS number_of_courses
FROM $table as c, $access_url_rel_course_table as u
WHERE u.c_id = c.id AND access_url_id='".$urlId."'
@ -858,7 +883,7 @@ class Statistics
$url_condition = null;
$url_condition2 = null;
$table = null;
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$url_condition = ", $access_url_rel_user_table as url WHERE url.user_id=u.id AND access_url_id='".$urlId."'";
$url_condition2 = " AND url.user_id=u.id AND access_url_id='".$urlId."'";
$table = ", $access_url_rel_user_table as url ";
@ -971,7 +996,7 @@ class Statistics
$values = $form->exportValues();
$date_diff = $values['date_diff'];
$table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LASTACCESS);
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT * FROM $table t , $access_url_rel_course_table a
WHERE
c_id = a.c_id AND
@ -1050,7 +1075,7 @@ class Statistics
break;
}
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT u.lastname, u.firstname, u.username, COUNT(DISTINCT m.id) AS count_message
FROM $messageTable m
INNER JOIN $messageRelUserTable mru ON $joinCondition
@ -1095,7 +1120,7 @@ class Statistics
$access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$urlId = api_get_current_access_url_id();
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT lastname, firstname, username, COUNT(friend_user_id) AS count_friend
FROM $access_url_rel_user_table as url, $user_friend_table uf
LEFT JOIN $user_table u
@ -1135,7 +1160,7 @@ class Statistics
$access_url_rel_user_table = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$urlId = api_get_current_access_url_id();
$total = self::countUsers();
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$table_url = ", $access_url_rel_user_table";
$where_url = " AND login_user_id=user_id AND access_url_id='".$urlId."'";
} else {
@ -1516,7 +1541,7 @@ class Statistics
*
* @return array
*/
private static function getLoginsByDate($startDate, $endDate)
public static function getLoginsByDate(string $startDate, string $endDate): array
{
$startDate = api_get_utc_datetime("$startDate 00:00:00");
$endDate = api_get_utc_datetime("$endDate 23:59:59");
@ -1530,7 +1555,7 @@ class Statistics
$urlJoin = '';
$urlWhere = '';
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$tblUrlUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$urlJoin = "INNER JOIN $tblUrlUser au ON u.id = au.user_id";
@ -1665,4 +1690,88 @@ class Statistics
return $groupedData;
}
/**
* Return de number of certificates generated.
* This function is resource intensive.
* @throws \Doctrine\DBAL\Exception
* @throws Exception
*/
public static function countCertificatesByQuarter(string $dateFrom = null, string $dateUntil = null): int
{
$tableGradebookCertificate = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
$condition = "";
if (!empty($dateFrom) && !empty($dateUntil)) {
$dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
$dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
$condition = "WHERE (created_at BETWEEN '$dateFrom' AND '$dateUntil')";
} elseif (!empty($dateFrom)) {
$dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
$condition = "WHERE created_at >= '$dateFrom'";
} elseif (!empty($dateUntil)) {
$dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
$condition = "WHERE created_at <= '$dateUntil'";
}
$sql = "
SELECT count(*) AS count
FROM $tableGradebookCertificate
$condition
";
$response = Database::query($sql);
$obj = Database::fetch_object($response);
return $obj->count;
}
/**
* Get the number of logins by dates.
* This function is resource intensive.
* @throws Exception
*/
public static function getSessionsByDuration(string $dateFrom, string $dateUntil): array
{
$results = [
'0' => 0,
'5' => 0,
'10' => 0,
'15' => 0,
'30' => 0,
'60' => 0,
];
if (!empty($dateFrom) && !empty($dateUntil)) {
$table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
$accessUrlRelUserTable = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$urlId = api_get_current_access_url_id();
$tableUrl = '';
$whereUrl = '';
$dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
$dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
if (AccessUrlHelper::isMultiple()) {
$tableUrl = ", $accessUrlRelUserTable";
$whereUrl = " AND login_user_id = user_id AND access_url_id = $urlId";
}
$sql = "SELECT login_id, TIMESTAMPDIFF(SECOND, login_date, logout_date) AS duration
FROM $table $tableUrl
WHERE login_date >= '$dateFrom'
AND logout_date <= '$dateUntil'
$whereUrl
";
$res = Database::query($sql);
while ($session = Database::fetch_array($res)) {
if ($session['duration'] > 3600) {
$results['60']++;
} elseif ($session['duration'] > 1800) {
$results['30']++;
} elseif ($session['duration'] > 900) {
$results['15']++;
} elseif ($session['duration'] > 600) {
$results['10']++;
} elseif ($session['duration'] > 300) {
$results['5']++;
} else {
$results['0']++;
}
}
}
return $results;
}
}

@ -20,6 +20,7 @@ use CpChart\Image as pImage;
use ExtraField as ExtraFieldModel;
use Chamilo\CoreBundle\Component\Utils\ActionIcon;
use Chamilo\CoreBundle\Component\Utils\StateIcon;
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
/**
* Class Tracking.
@ -1745,7 +1746,7 @@ class Tracking
$url_condition = null;
$tbl_url_rel_user = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$url_table = null;
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$access_url_id = api_get_current_access_url_id();
$url_table = ", $tbl_url_rel_user as url_users";
$url_condition = " AND u.login_user_id = url_users.user_id AND access_url_id='$access_url_id'";
@ -1827,7 +1828,7 @@ class Tracking
$url_table = null;
$url_condition = null;
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$access_url_id = api_get_current_access_url_id();
$url_table = ", ".$tbl_url_rel_user." as url_users";
$url_condition = " AND u.login_user_id = url_users.user_id AND access_url_id='$access_url_id'";
@ -3599,7 +3600,7 @@ class Tracking
$tbl_session_user = Database::get_main_table(TABLE_MAIN_SESSION_USER);
$tbl_session = Database::get_main_table(TABLE_MAIN_SESSION);
$accessUrlEnabled = api_is_multiple_url_enabled();
$accessUrlEnabled = AccessUrlHelper::isMultiple();
$access_url_id = $accessUrlEnabled ? api_get_current_access_url_id() : -1;
$students = [];
@ -3754,7 +3755,7 @@ class Tracking
ON (c.id = sc.c_id)
WHERE sc.user_id = '.$coach_id.' AND sc.status = '.SessionEntity::COURSE_COACH;
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$access_url_id = api_get_current_access_url_id();
if (-1 != $access_url_id) {
$sql = 'SELECT DISTINCT c.code
@ -3792,7 +3793,7 @@ class Tracking
INNER JOIN $tbl_course as course
ON course.id = session_course.c_id";
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$tbl_course_rel_access_url = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
$access_url_id = api_get_current_access_url_id();
if (-1 != $access_url_id) {
@ -3815,11 +3816,11 @@ class Tracking
if (!empty($sessionId)) {
$sql .= ' WHERE session_course.session_id='.$sessionId;
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql .= ' AND access_url_id = '.$access_url_id;
}
} else {
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql .= ' WHERE access_url_id = '.$access_url_id;
}
}
@ -4596,7 +4597,7 @@ class Tracking
$session_id = (int) $session_id;
$urlId = api_get_current_access_url_id();
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT c.id, c.code, title
FROM $tbl_course_user cu
INNER JOIN $tbl_course c
@ -4643,7 +4644,7 @@ class Tracking
}
// Get the list of sessions where the user is subscribed as student
if (api_is_multiple_url_enabled()) {
if (AccessUrlHelper::isMultiple()) {
$sql = "SELECT DISTINCT c.code, s.id as session_id, s.title
FROM $tbl_session_course_user cu
INNER JOIN $tbl_access_rel_session a
@ -8134,4 +8135,40 @@ class Tracking
return $exeDate;
}
/**
* Return the total time spent in courses (no the total in platform).
*
* @return int
* @throws \Doctrine\DBAL\Exception
*/
public static function getTotalTimeSpentInCourses(
string $dateFrom = '',
string $dateUntil = ''
): int {
$tableTrackLogin = Database::get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
$tableUrlRelUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
$tableUrl = null;
$urlCondition = null;
$conditionTime = null;
if (AccessUrlHelper::isMultiple()) {
$accessUrlId = api_get_current_access_url_id();
$tableUrl = ", ".$tableUrlRelUser." as url_users";
$urlCondition = " AND u.user_id = url_users.user_id AND access_url_id = $accessUrlId";
}
if (!empty($dateFrom) && !empty($dateUntil)) {
$dateFrom = Database::escape_string($dateFrom);
$dateUntil = Database::escape_string($dateUntil);
$conditionTime = " (login_course_date >= '$dateFrom' AND logout_course_date <= '$dateUntil' ) ";
}
$sql = "SELECT SUM(TIMESTAMPDIFF(HOUR, login_course_date, logout_course_date)) diff
FROM $tableTrackLogin u $tableUrl
WHERE $conditionTime $urlCondition";
$rs = Database::query($sql);
$row = Database::fetch_array($rs, 'ASSOC');
$diff = $row['diff'];
if ($diff >= 0 and !empty($diff)) {
return $diff;
}
return 0;
}
}

@ -3368,27 +3368,33 @@ class UserManager
/**
* Get the total count of users.
*
* @param int $status Status of users to be counted
* @param int $access_url_id Access URL ID (optional)
* @param int $active
* @param ?int $status Status of users to be counted
* @param ?int $access_url_id Access URL ID (optional)
* @param ?int $active
*
* @return mixed Number of users or false on error
* @throws \Doctrine\DBAL\Exception
*/
public static function get_number_of_users($status = 0, $access_url_id = 1, $active = null)
{
$t_u = Database::get_main_table(TABLE_MAIN_USER);
$t_a = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
public static function get_number_of_users(
?int $status = 0,
?int $access_url_id = 1,
?int $active = null,
?string $dateFrom = null,
?string $dateUntil = null
): mixed {
$tableUser = Database::get_main_table(TABLE_MAIN_USER);
$tableAccessUrlRelUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
if (api_is_multiple_url_enabled()) {
$sql = "SELECT count(u.id)
FROM $t_u u
INNER JOIN $t_a url_user
FROM $tableUser u
INNER JOIN $tableAccessUrlRelUser url_user
ON (u.id = url_user.user_id)
WHERE url_user.access_url_id = $access_url_id
";
} else {
$sql = "SELECT count(u.id)
FROM $t_u u
FROM $tableUser u
WHERE 1 = 1 ";
}
@ -3397,11 +3403,20 @@ class UserManager
$sql .= " AND u.status = $status ";
}
if (null !== $active) {
if (isset($active)) {
$active = (int) $active;
$sql .= " AND u.active = $active ";
}
if (!empty($dateFrom)) {
$dateFrom = api_get_utc_datetime("$dateFrom 00:00:00");
$sql .= " AND u.registration_date >= '$dateFrom' ";
}
if (!empty($dateUntil)) {
$dateUntil = api_get_utc_datetime("$dateUntil 23:59:59");
$sql .= " AND u.registration_date <= '$dateUntil' ";
}
$res = Database::query($sql);
if (1 === Database::num_rows($res)) {
return (int) Database::result($res, 0, 0);
@ -6066,4 +6081,106 @@ SQL;
return $url;
}
/**
* Count users in courses and if they have certificate.
* This function is resource intensive.
*
* @return array
* @throws Exception
* @throws \Doctrine\DBAL\Exception
*/
public static function countUsersWhoFinishedCourses()
{
$courses = [];
$currentAccessUrlId = api_get_current_access_url_id();
$sql = "SELECT course.code, cru.user_id
FROM course_rel_user cru
JOIN course ON cru.c_id = course.id
JOIN access_url_rel_user auru on cru.user_id = auru.user_id
JOIN access_url_rel_course ON course.id = access_url_rel_course.c_id
WHERE access_url_rel_course.access_url_id = $currentAccessUrlId
ORDER BY course.code
";
$res = Database::query($sql);
if (Database::num_rows($res) > 0) {
while ($row = Database::fetch_array($res)) {
if (!isset($courses[$row['code']])) {
$courses[$row['code']] = [
'subscribed' => 0,
'finished' => 0,
];
}
$courses[$row['code']]['subscribed']++;
$entityManager = Database::getManager();
$repository = $entityManager->getRepository('ChamiloCoreBundle:GradebookCategory');
//todo check when have more than 1 gradebook
/** @var \Chamilo\CoreBundle\Entity\GradebookCategory $gradebook */
$gradebook = $repository->findOneBy(['courseCode' => $row['code']]);
if (!empty($gradebook)) {
$finished = 0;
$gb = Category::createCategoryObjectFromEntity($gradebook);
$finished = $gb->is_certificate_available($row['user_id']);
if (!empty($finished)) {
$courses[$row['code']]['finished']++;
}
}
}
}
return $courses;
}
/**
* Count users in sessions and if they have certificate.
* This function is resource intensive.
*
* @return array
* @throws Exception
* @throws \Doctrine\DBAL\Exception
*/
public static function countUsersWhoFinishedCoursesInSessions()
{
$coursesInSessions = [];
$currentAccessUrlId = api_get_current_access_url_id();
$sql = "SELECT course.code, srcru.session_id, srcru.user_id, session.title
FROM session_rel_course_rel_user srcru
JOIN course ON srcru.c_id = course.id
JOIN access_url_rel_session aurs on srcru.session_id = aurs.session_id
JOIN session ON srcru.session_id = session.id
WHERE aurs.access_url_id = $currentAccessUrlId
ORDER BY course.code, session.title
";
$res = Database::query($sql);
if (Database::num_rows($res) > 0) {
while ($row = Database::fetch_array($res)) {
$index = $row['code'].' ('.$row['title'].')';
if (!isset($coursesInSessions[$index])) {
$coursesInSessions[$index] = [
'subscribed' => 0,
'finished' => 0,
];
}
$coursesInSessions[$index]['subscribed']++;
$entityManager = Database::getManager();
$repository = $entityManager->getRepository('ChamiloCoreBundle:GradebookCategory');
/** @var \Chamilo\CoreBundle\Entity\GradebookCategory $gradebook */
$gradebook = $repository->findOneBy(
[
'courseCode' => $row['code'],
'sessionId' => $row['session_id'],
]
);
if (!empty($gradebook)) {
$finished = 0;
$gb = Category::createCategoryObjectFromEntity($gradebook);
$finished = $gb->is_certificate_available($row['user_id']);
if (!empty($finished)) {
$coursesInSessions[$index]['finished']++;
}
}
}
}
return $coursesInSessions;
}
}

@ -9,6 +9,7 @@ use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Dotenv\Dotenv;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session as HttpSession;
use Symfony\Component\Translation\Loader\PoFileLoader;
use Symfony\Component\Translation\Translator;
@ -71,12 +72,16 @@ Container::$session = new HttpSession();
require_once 'install.lib.php';
$installationLanguage = 'en_US';
if (!empty($_POST['language_list']) && !ChamiloSession::has('install_language')) {
$httpRequest = Request::createFromGlobals();
if ($httpRequest->request->get('language_list')) {
$search = ['../', '\\0'];
$installationLanguage = str_replace($search, '', urldecode($_POST['language_list']));
$installationLanguage = str_replace($search, '', urldecode($httpRequest->request->get('language_list')));
ChamiloSession::write('install_language', $installationLanguage);
} elseif (ChamiloSession::has('install_language')) {
$installationLanguage = ChamiloSession::read('install_language');
} else {
$installationLanguage = $httpRequest->getPreferredLanguage();
}
// Set translation
@ -254,7 +259,7 @@ $total_steps = 7;
$current_step = 1;
if (!$_POST) {
$current_step = 1;
} elseif (!empty($_POST['language_list']) || !empty($_POST['step1']) || ((!empty($_POST['step2_update_8']) || (!empty($_POST['step2_update_6']))) && ($emptyUpdatePath || $badUpdatePath))) {
} elseif ($httpRequest->request->get('language_list') || !empty($_POST['step1']) || ((!empty($_POST['step2_update_8']) || (!empty($_POST['step2_update_6']))) && ($emptyUpdatePath || $badUpdatePath))) {
$current_step = 2;
} elseif (!empty($_POST['step2']) || (!empty($_POST['step2_update_8']) || (!empty($_POST['step2_update_6'])))) {
$current_step = 3;

@ -43,11 +43,6 @@
"subkey": "",
"selected_value": "true"
},
{
"variable": "enabled_text2audio",
"subkey": "",
"selected_value": "true"
},
{
"variable": "gradebook_score_display_coloring",
"subkey": "",
@ -59,4 +54,4 @@
"selected_value": "true"
}
]
}
}

@ -132,7 +132,7 @@ $tpl = new Template(get_lang('Add'));
$tpl->assign('unique_file_id', api_get_unique_id());
$tpl->assign('course_code', api_get_course_id());
$tpl->assign('filename', $lp_item->get_title().'_nano.wav');
$tpl->assign('enable_record_audio', 'true' === api_get_setting('enable_record_audio'));
$tpl->assign('enable_record_audio', true);
$tpl->assign('cur_dir_path', '/audio');
$tpl->assign('lp_item_id', $lp_item_id);
//$tpl->assign('lp_dir', api_remove_trailing_slash($lpPathInfo['dir']));

@ -12,7 +12,7 @@ require_once __DIR__.'/../inc/global.inc.php';
api_block_anonymous_users();
if ('false' === api_get_setting('session.allow_search_diagnostic')) {
if ('false' !== api_get_setting('session.allow_search_diagnostic')) {
api_not_allowed();
}
@ -89,7 +89,7 @@ $htmlHeadXtra[] = '
const diapoButton = document.querySelector("#card_"+targetBlockWithoutHash+" a");
setTimeout(function() {
diapoButton.click();
diapoButton?.click();
}, 500);
});
</script>

@ -20,7 +20,12 @@ $action = $_REQUEST['action'] ?? null;
$idChecked = $_REQUEST['idChecked'] ?? null;
$idMultiple = $_REQUEST['id'] ?? null;
$listType = isset($_REQUEST['list_type']) ? Security::remove_XSS($_REQUEST['list_type']) : SessionManager::getDefaultSessionTab();
$copySessionContent = isset($_REQUEST['copy_session_content']) ? true : false;
$copySessionContent = isset($_REQUEST['copy_session_content']);
$addSessionContent = 'true' === api_get_setting('session.duplicate_specific_session_content_on_session_copy');
if (!$addSessionContent) {
$copySessionContent = false;
}
switch ($action) {
case 'delete_multiple':
@ -50,7 +55,14 @@ switch ($action) {
header('Location: '.$url);
exit();
case 'copy':
$result = SessionManager::copy($idChecked);
$result = SessionManager::copy(
(int) $idChecked,
true,
true,
false,
false,
$copySessionContent
);
if ($result) {
Display::addFlash(Display::return_message(get_lang('ItemCopied')));
} else {
@ -65,7 +77,7 @@ switch ($action) {
case 'copy_multiple':
$sessionList = explode(',', $idMultiple);
foreach ($sessionList as $id) {
$sessionIdCopied = SessionManager::copy($id);
$sessionIdCopied = SessionManager::copy((int) $id);
if ($sessionIdCopied) {
$sessionInfo = api_get_session_info($sessionIdCopied);
Display::addFlash(Display::return_message(get_lang('ItemCopied').' - '.$sessionInfo['name']));
@ -152,14 +164,38 @@ if (isset($_REQUEST['keyword'])) {
$filter->groupOp = 'OR';
$filter = json_encode($filter);
$url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions&_force_search=true&rows=20&page=1&sidx=&sord=asc&filters='.$filter.'&searchField=s.title&searchString='.Security::remove_XSS($_REQUEST['keyword']).'&searchOper=in';
$url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?'
.http_build_query([
'a' => 'get_sessions',
'_force_search' => 'true',
'rows' => 20,
'page' => 1,
'sidx' => '',
'sord' => 'asc',
'filters' => $filter,
'searchField' => 's.title',
'searchString' => Security::remove_XSS($_REQUEST['keyword']),
'searchOper' => 'in',
]);
}
if (isset($_REQUEST['id_category'])) {
$sessionCategory = SessionManager::get_session_category($_REQUEST['id_category']);
if (!empty($sessionCategory)) {
//Begin with see the searchOper param
$url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions&_force_search=true&rows=20&page=1&sidx=&sord=asc&filters=&searchField=sc.title&searchString='.Security::remove_XSS($sessionCategory['title']).'&searchOper=in';
$url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?'
.http_build_query([
'a' => 'get_sessions',
'_force_search' => 'true',
'rows' => 20,
'page' => 1,
'sidx' => '',
'sord' => 'asc',
'filters' => '',
'searchField' => 'sc.title',
'searchString' => Security::remove_XSS($sessionCategory['title']),
'searchOper' => 'in',
]);
}
}
@ -187,6 +223,11 @@ if (!isset($_GET['keyword'])) {
}
$hideSearch = ('true' === api_get_setting('session.hide_search_form_in_session_list'));
$copySessionContentLink = '';
if ($addSessionContent) {
$copySessionContentLink = ' <a onclick="javascript:if(!confirm('."\'".addslashes(api_htmlentities(get_lang("ConfirmYourChoice"), ENT_QUOTES))."\'".')) return false;" href="session_list.php?copy_session_content=1&list_type='.$listType.'&action=copy&idChecked=\'+options.rowId+\'">'.
Display::return_icon('copy.png', get_lang('CopyWithSessionContent'), '', ICON_SIZE_SMALL).'</a>';
}
//With this function we can add actions to the jgrid (edit, delete, etc)
$action_links = 'function action_formatter(cellvalue, options, rowObject) {
@ -194,6 +235,7 @@ $action_links = 'function action_formatter(cellvalue, options, rowObject) {
'&nbsp;<a href="add_users_to_session.php?page=session_list.php&id_session=\'+options.rowId+\'">'.Display::getMdiIcon('account-multiple-plus', 'ch-tool-icon', null, 22, get_lang('Subscribe users to this session')).'</a>'.
'&nbsp;<a href="add_courses_to_session.php?page=session_list.php&id_session=\'+options.rowId+\'">'.Display::getMdiIcon('book-open-page-variant', 'ch-tool-icon', null, 22, get_lang('Add courses to this session')).'</a>'.
'&nbsp;<a onclick="javascript:if(!confirm('."\'".addslashes(api_htmlentities(get_lang("Please confirm your choice"), ENT_QUOTES))."\'".')) return false;" href="session_list.php?action=copy&idChecked=\'+options.rowId+\'">'.Display::getMdiIcon('text-box-plus', 'ch-tool-icon', null, 22, get_lang('Copy')).'</a>'.
$copySessionContentLink.
'<button type="button" title="'.get_lang('Delete').'" onclick="if(confirm('."\'".addslashes(api_htmlentities(get_lang("Please confirm your choice"), ENT_QUOTES))."\'".')) window.location = '."\'session_list.php?action=delete&idChecked=\' + ".'\' + options.rowId +\';">'.Display::getMdiIcon('delete', 'ch-tool-icon', null, 22, get_lang('Delete')).'</button>'.
'\';
}';

@ -198,7 +198,7 @@ if (!empty($skillIdFromGet)) {
foreach ($subSkillList as $subSkillId) {
$children = $skillManager->getChildren($subSkillId);
if (isset($subSkillList[$counter - 1])) {
if (isset($subSkillList[$counter - 1]) && isset($subSkillList[$counter])) {
$oldSkill = $skillRepo->find($subSkillList[$counter]);
}
$skillsOptions = [];

@ -150,7 +150,7 @@ foreach ($skillRelUserComments as $comment) {
}
$acquiredLevel = [];
$profile = $skillRepo->find($skillId)->getProfile();
$profile = $skillRepo->find($skillId)->getLevelProfile();
if (!$profile) {
$skillRelSkill = new SkillRelSkillModel();
@ -160,7 +160,7 @@ if (!$profile) {
foreach ($parents as $parent) {
$skillParentId = $parent['skill_id'];
$profile = $skillRepo->find($skillParentId)->getProfile();
$profile = $skillRepo->find($skillParentId)->getLevelProfile();
if ($profile) {
break;

@ -108,7 +108,7 @@ foreach ($userSkills as $index => $skillRelUser) {
}
$acquiredLevel = [];
$profile = $skillRepo->find($skillId)->getProfile();
$profile = $skillRepo->find($skillId)->getLevelProfile();
if (!$profile) {
$skillRelSkill = new SkillRelSkillModel();
@ -118,7 +118,7 @@ foreach ($userSkills as $index => $skillRelUser) {
foreach ($parents as $parent) {
$skillParentId = $parent['skill_id'];
$profile = $skillRepo->find($skillParentId)->getProfile();
$profile = $skillRepo->find($skillParentId)->getLevelProfile();
if ($profile) {
break;

@ -2,24 +2,26 @@
{% import '@ChamiloCore/Macros/box.html.twig' as display %}
{% block content %}
{{ toolbar }}
{{ form }}
{# {% for question in pagination %}#}
{% for i in start..end %}
{% if pagination[i] is defined %}
{% set question = pagination[i] %}
{{ display.collapse(
question.iid,
'#' ~ question.courseCode ~'-'~ question.iid ~ ' - ' ~ question.question,
question.questionData,
false,
false
)
}}
{% endif %}
{% endfor %}
{% autoescape false %}
{{ toolbar }}
{{ form }}
{# {% for question in pagination %}#}
{% for i in start..end %}
{% if pagination[i] is defined %}
{% set question = pagination[i] %}
{{ display.collapse(
question.iid,
'#' ~ question.courseCode ~'-'~ question.iid ~ ' - ' ~ question.question,
question.questionData,
false,
false
)
}}
{% endif %}
{% endfor %}
{% if question_count > pagination_length %}
{{ pagination }}
{% endif %}
{% if question_count > pagination_length %}
{{ pagination }}
{% endif %}
{% endautoescape %}
{% endblock %}

@ -122,11 +122,11 @@ class BlockGlobalInfo extends Block
[get_lang('Number of active users'), '<a href="'.$path.'admin/user_list.php?keyword_firstname=&amp;keyword_lastname=&amp;keyword_username=&amp;keyword_email=&amp;keyword_officialcode=&amp;keyword_status=%25&amp;keyword_active=1&amp;submit=&amp;_qf__advanced_search=">'.Statistics::countUsers(null, null, null, true).'</a>'],
// Check number of courses
[get_lang('Total number of courses'), '<a href="'.$path.'admin/course_list.php">'.Statistics::countCourses().'</a>'],
[get_lang('Number of public courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_OPEN_WORLD.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_OPEN_WORLD).'</a>'],
[get_lang('Number of open courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_OPEN_PLATFORM.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_OPEN_PLATFORM).'</a>'],
[get_lang('Number of private courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_REGISTERED.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_REGISTERED).'</a>'],
[get_lang('Number of closed courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_CLOSED.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_CLOSED).'</a>'],
[get_lang('Number of hidden courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_HIDDEN.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility(COURSE_VISIBILITY_HIDDEN).'</a>'],
[get_lang('Number of public courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_OPEN_WORLD.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_OPEN_WORLD]).'</a>'],
[get_lang('Number of open courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_OPEN_PLATFORM.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_OPEN_PLATFORM]).'</a>'],
[get_lang('Number of private courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_REGISTERED.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_REGISTERED]).'</a>'],
[get_lang('Number of closed courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_CLOSED.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_CLOSED]).'</a>'],
[get_lang('Number of hidden courses'), '<a href="'.$path.'admin/course_list.php?keyword_code=&amp;keyword_title=&amp;keyword_language=%25&amp;keyword_category=&amp;keyword_visibility='.COURSE_VISIBILITY_HIDDEN.'&amp;keyword_subscribe=%25&amp;keyword_unsubscribe=%25&amp;submit=&amp;_qf__advanced_course_search=">'.Statistics::countCoursesByVisibility([COURSE_VISIBILITY_HIDDEN]).'</a>'],
];
}
}

@ -164,4 +164,6 @@ enum ActionIcon: string
case SWAP_FILE = 'file-swap';
case ADD_FILE_VARIATION = 'file-replace';
case CLOSE = 'close';
}

@ -61,6 +61,13 @@ class AccountController extends BaseController
}
}
if ($form->has('password')) {
$password = $form['password']->getData();
if ($password) {
$user->setPlainPassword($password);
}
}
$showTermsIfProfileCompleted = ('true' === $settingsManager->getSetting('show_terms_if_profile_completed'));
$user->setProfileCompleted($showTermsIfProfileCompleted);

@ -76,7 +76,6 @@ class PlatformConfigurationController extends AbstractController
'course.course_validation',
'course.student_view_enabled',
'course.allow_edit_tool_visibility_in_session',
'course.enable_record_audio',
'session.limit_session_admin_role',
'session.allow_session_admin_read_careers',
'session.limit_session_admin_list_users',

@ -363,11 +363,6 @@ class SettingsCurrentFixtures extends Fixture implements FixtureGroupInterface
'title' => 'Courses custom icons',
'comment' => 'Use course images as the course icon in courses lists (instead of the default green blackboard icon).',
],
[
'name' => 'enable_record_audio',
'title' => 'Enable audio recorder',
'comment' => 'Enables the WebRTC (flashless) audio recorder at several locations inside Chamilo',
],
[
'name' => 'hide_scorm_copy_link',
'title' => 'Hide SCORM Copy',
@ -816,11 +811,6 @@ class SettingsCurrentFixtures extends Fixture implements FixtureGroupInterface
'title' => 'Enable Webcam Clip',
'comment' => 'Webcam Clip allow to users capture images from his webcam and send them to server in JPEG (.jpg or .jpeg) format',
],
[
'name' => 'enabled_text2audio',
'title' => 'Enable online services for text to speech conversion',
'comment' => 'Online tool to convert text to speech. Uses speech synthesis technology to generate audio files saved into your course.',
],
[
'name' => 'pdf_export_watermark_by_course',
'title' => 'Enable watermark definition by course',

@ -1220,7 +1220,7 @@ class User implements UserInterface, EquatableInterface, ResourceInterface, Reso
return $this->plainPassword;
}
public function setPlainPassword(string $password): self
public function setPlainPassword(?string $password): self
{
$this->plainPassword = $password;
// forces the object to look "dirty" to Doctrine. Avoids
@ -2210,6 +2210,17 @@ class User implements UserInterface, EquatableInterface, ResourceInterface, Reso
return $this;
}
public function getLogin(): string
{
return $this->username;
}
public function setLogin(string $login): self
{
$this->username = $login;
return $this;
}
/**
* @return Collection<int, TrackELogin>
*/

@ -144,6 +144,10 @@ class LoginSuccessHandler
$trackELoginRepository->createLoginRecord($user, new DateTime(), $userIp);
$trackEOnlineRepository->createOnlineSession($user, $userIp);
$user->setLastLogin(new DateTime());
$this->entityManager->persist($user);
$this->entityManager->flush();
// Log of connection attempts
$trackELoginRecordRepository->addTrackLogin($user->getUsername(), $userIp, true);
$this->loginAttemptLogger->logAttempt(true, $user->getUsername(), $userIp);

@ -11,8 +11,9 @@ use Chamilo\CoreBundle\Form\Type\IllustrationType;
use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Settings\SettingsManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
use Symfony\Component\Form\FormBuilderInterface;
@ -23,20 +24,16 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
class ProfileType extends AbstractType
{
private LanguageRepository $languageRepository;
public function __construct(
LanguageRepository $languageRepository,
private readonly SettingsManager $settingsManager
) {
$this->languageRepository = $languageRepository;
}
private readonly LanguageRepository $languageRepository,
private readonly SettingsManager $settingsManager,
) {}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$changeableOptions = $this->settingsManager->getSetting('profile.changeable_options') ?? [];
$visibleOptions = $this->settingsManager->getSetting('profile.visible_options') ?? [];
$languages = array_flip($this->languageRepository->getAllAvailableToArray());
$changeableOptions = $this->settingsManager->getSetting('profile.changeable_options', true) ?? [];
$visibleOptions = $this->settingsManager->getSetting('profile.visible_options', true) ?? [];
$languages = array_flip($this->languageRepository->getAllAvailableToArray(true));
$fieldsMap = [
'name' => ['field' => 'firstname', 'type' => TextType::class, 'label' => 'Firstname'],
@ -49,10 +46,16 @@ class ProfileType extends AbstractType
'mapped' => false,
],
'login' => ['field' => 'login', 'type' => TextType::class, 'label' => 'Login'],
'password' => ['field' => 'password', 'type' => TextType::class, 'label' => 'Password'],
'password' => [
'field' => 'password',
'type' => PasswordType::class,
'label' => 'Password',
'mapped' => false,
'required' => false,
],
'language' => [
'field' => 'locale',
'type' => LocaleType::class,
'type' => ChoiceType::class,
'label' => 'Language',
'choices' => $languages,
],
@ -69,7 +72,8 @@ class ProfileType extends AbstractType
array_merge(
[
'label' => $fieldConfig['label'],
'required' => false,
'required' => $fieldConfig['required'] ?? false,
'mapped' => $fieldConfig['mapped'] ?? true,
'attr' => !$isEditable ? ['readonly' => true] : [],
],
isset($fieldConfig['choices']) ? ['choices' => $fieldConfig['choices']] : []

@ -205,7 +205,6 @@ class Version20170627122900 extends AbstractMigrationChamilo
'server_type' => 'platform',
'show_official_code_whoisonline' => 'profile',
'show_terms_if_profile_completed' => 'ticket',
'enable_record_audio' => 'course',
'add_users_by_coach' => 'session',
'allow_captcha' => 'security',
'allow_coach_to_edit_course_session' => 'session',
@ -236,7 +235,6 @@ class Version20170627122900 extends AbstractMigrationChamilo
'enable_webcam_clip' => 'document',
'enabled_support_pixlr' => 'editor',
'enabled_support_svg' => 'editor',
'enabled_text2audio' => 'document',
'extend_rights_for_coach' => 'session',
'extend_rights_for_coach_on_survey' => 'survey',
'hide_course_group_if_no_tools_available' => 'group',

@ -44,9 +44,15 @@ class LanguageRepository extends ServiceEntityRepository
return $qb;
}
public function getAllAvailableToArray(): array
public function getAllAvailableToArray(bool $onlyActive = false): array
{
$languages = $this->getAllAvailable()->getQuery()->getResult();
$queryBuilder = $this->getAllAvailable();
if (!$onlyActive) {
$queryBuilder->resetDQLPart('where');
}
$languages = $queryBuilder->getQuery()->getResult();
$list = [];

@ -85,7 +85,6 @@ class CourseSettingsSchema extends AbstractSettingsSchema
'course_images_in_courses_list' => 'true',
'teacher_can_select_course_template' => 'true',
'show_toolshortcuts' => '',
'enable_record_audio' => 'false',
'lp_show_reduced_report' => 'false',
'course_creation_splash_screen' => 'true',
'block_registered_users_access_to_open_course_contents' => 'false',
@ -246,7 +245,6 @@ class CourseSettingsSchema extends AbstractSettingsSchema
->add('course_images_in_courses_list', YesNoType::class)
->add('teacher_can_select_course_template', YesNoType::class)
->add('show_toolshortcuts', YesNoType::class)
->add('enable_record_audio', YesNoType::class)
->add('lp_show_reduced_report', YesNoType::class)
->add('course_creation_splash_screen', YesNoType::class)
->add('block_registered_users_access_to_open_course_contents', YesNoType::class)

@ -39,8 +39,6 @@ class DocumentSettingsSchema extends AbstractSettingsSchema
'students_export2pdf' => 'true',
'show_users_folders' => 'true',
'show_default_folders' => 'true',
'enabled_text2audio' => 'false',
// 'enable_nanogong' => 'false',
'show_documents_preview' => 'false',
'enable_webcam_clip' => 'false',
'tool_visible_by_default_at_creation' => [
@ -121,7 +119,6 @@ class DocumentSettingsSchema extends AbstractSettingsSchema
->add('students_export2pdf', YesNoType::class)
->add('show_users_folders', YesNoType::class)
->add('show_default_folders', YesNoType::class)
->add('enabled_text2audio', YesNoType::class)
// ->add('enable_nanogong', YesNoType::class)
->add('show_documents_preview', YesNoType::class)
->add('enable_webcam_clip', YesNoType::class)

@ -79,6 +79,7 @@ class SessionSettingsSchema extends AbstractSettingsSchema
'session_creation_user_course_extra_field_relation_to_prefill' => '',
'session_creation_form_set_extra_fields_mandatory' => '',
'session_model_list_field_ordered_by_id' => 'false',
'duplicate_specific_session_content_on_session_copy' => 'false',
]
)
;
@ -217,6 +218,7 @@ class SessionSettingsSchema extends AbstractSettingsSchema
]
)
->add('session_model_list_field_ordered_by_id', YesNoType::class)
->add('duplicate_specific_session_content_on_session_copy', YesNoType::class)
;
$this->updateFormFieldsFromSettingsInfo($builder);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save