Internal: Add duration field to multiple tables and handle migration of extra fields - refs #5633

pull/5636/head
christianbeeznst 1 year ago
parent d9adeb85b1
commit 50e74ac5a9
  1. 7
      assets/vue/components/course/CourseCard.vue
  2. 1
      assets/vue/graphql/queries/Course.js
  3. 3
      assets/vue/graphql/queries/CourseRelUser.js
  4. 82
      assets/vue/views/course/CatalogueCourses.vue
  5. 30
      public/main/admin/course_add.php
  6. 23
      public/main/admin/course_edit.php
  7. 4
      public/main/inc/lib/add_course.lib.inc.php
  8. 4
      public/main/inc/lib/api.lib.php
  9. 1
      src/CoreBundle/Controller/PlatformConfigurationController.php
  10. 5
      src/CoreBundle/DataFixtures/SettingsCurrentFixtures.php
  11. 16
      src/CoreBundle/Entity/Course.php
  12. 42
      src/CoreBundle/Migrations/Schema/V200/Version20240704120400.php
  13. 91
      src/CoreBundle/Migrations/Schema/V200/Version20240704120500.php
  14. 2
      src/CoreBundle/Settings/CourseSettingsSchema.php
  15. 15
      src/CourseBundle/Entity/CAttendanceCalendar.php
  16. 15
      src/CourseBundle/Entity/CLp.php
  17. 15
      src/CourseBundle/Entity/CLpItem.php
  18. 15
      src/CourseBundle/Entity/CQuiz.php
  19. 15
      src/CourseBundle/Entity/CQuizQuestion.php
  20. 15
      src/CourseBundle/Entity/CStudentPublication.php
  21. 15
      src/CourseBundle/Entity/CSurvey.php

@ -26,6 +26,9 @@
v-text="session.title"
/>
{{ course.title }}
<span v-if="showCourseDuration && course.duration">
({{ (course.duration / 60 / 60).toFixed(2) }} hours)
</span>
</div>
<BaseAppLink
v-else
@ -60,6 +63,7 @@ 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()
@ -86,6 +90,9 @@ const props = defineProps({
},
})
const platformConfigStore = usePlatformConfig()
const showCourseDuration = computed(() => 'true' === platformConfigStore.getSetting("course.show_course_duration"))
const teachers = computed(() => {
if (props.session?.courseCoachesSubscriptions) {
return props.session.courseCoachesSubscriptions

@ -9,6 +9,7 @@ export const GET_STICKY_COURSES = gql`
title
illustrationUrl
sticky
duration
}
}
}

@ -8,7 +8,8 @@ export const GET_COURSE_REL_USER = gql`
course {
_id,
title,
illustrationUrl
illustrationUrl,
duration,
users(status: 1, first: 4) {
edges {
node {

@ -40,7 +40,7 @@
<template #loading>
{{ $t("Loading courses. Please wait.") }}
</template>
<Column header="">
<Column header="" style="min-width: 5rem">
<template #body="{ data }">
<img
:alt="data.title"
@ -53,43 +53,54 @@
:header="$t('Title')"
:sortable="true"
field="title"
style="min-width: 10rem"
style="min-width: 8rem; text-align: center;"
>
<template #body="{ data }">
{{ data.title }}
</template>
</Column>
<Column
v-if="showCourseDuration"
:header="$t('Course description')"
:sortable="true"
field="description"
style="min-width: 12rem"
style="min-width: 8rem; text-align: center;"
>
<template #body="{ data }">
{{ data.description }}
</template>
</Column>
<Column
:header="$t('Duration')"
:sortable="true"
field="duration"
style="min-width: 8rem; text-align: center;"
>
<template #body="{ data }">
<div v-if="data.duration" class="course-duration">
{{ (data.duration / 60 / 60).toFixed(2) }} hours
</div>
</template>
</Column>
<Column
:header="$t('Teachers')"
:sortable="true"
field="teachers"
style="min-width: 20rem"
style="min-width: 10rem; text-align: center;"
>
<template #body="{ data }">
<TeacherBar
:teachers="
data.teachers.map((teacher) => ({
...teacher.user,
}))
"
/>
<div v-if="data.teachers && data.teachers.length > 0">
{{ data.teachers.map(teacher => teacher.user.fullName).join(', ') }}
</div>
</template>
</Column>
<Column
:header="$t('Language')"
:sortable="true"
field="courseLanguage"
style="min-width: 7rem"
style="min-width: 5rem; text-align: center;"
>
<template #body="{ data }">
{{ data.courseLanguage }}
@ -99,7 +110,7 @@
:header="$t('Categories')"
:sortable="true"
field="categories"
style="min-width: 11rem"
style="min-width: 8rem; text-align: center;"
>
<template #body="{ data }">
<span
@ -116,7 +127,7 @@
:header="$t('Ranking')"
:sortable="true"
field="trackCourseRanking.realTotalScore"
style="min-width: 8rem"
style="min-width: 10rem; text-align: center;"
>
<template #body="{ data }">
<Rating
@ -131,20 +142,20 @@
<Column
field="link"
header=""
style="min-width: 8rem"
style="min-width: 10rem; text-align: center;"
>
<template #body="{ data }">
<BaseAppLink
<router-link
v-slot="{ navigate }"
:to="{ name: 'CourseHome', params: { id: data.id } }"
>
<Button
:label="$t('Go to the course')"
class="p-button-sm"
class="btn btn--primary text-white"
icon="pi pi-external-link"
@click="navigate"
/>
</BaseAppLink>
</router-link>
</template>
</Column>
<template #footer>
@ -163,12 +174,16 @@ import DataTable from "primevue/datatable"
import Column from "primevue/column"
import Rating from "primevue/rating"
import TeacherBar from "../../components/TeacherBar.vue"
import BaseAppLink from "../../components/basecomponents/BaseAppLink.vue"
import { usePlatformConfig } from "../../store/platformConfig"
const status = ref(null)
const courses = ref([])
const filters = ref(null)
const platformConfigStore = usePlatformConfig()
const showCourseDuration = 'true' === platformConfigStore.getSetting("course.show_course_duration")
const load = function () {
status.value = true
axios
@ -176,7 +191,13 @@ const load = function () {
.then((response) => {
status.value = false
if (Array.isArray(response.data)) {
response.data.forEach((course) => (course.courseLanguage = getOriginalLanguageName(course.courseLanguage)))
response.data.forEach((course) => {
course.courseLanguage = getOriginalLanguageName(course.courseLanguage)
if (course.duration) {
course.duration = course.duration
}
})
courses.value = response.data
}
})
@ -265,3 +286,24 @@ const onRatingChange = function (event, trackCourseRanking, courseId) {
load()
initFilters()
</script>
<style scoped>
.p-datatable .p-datatable-thead > tr > th {
text-align: center !important;
}
.course-image {
width: 100px;
height: auto;
}
.course-duration {
text-align: center;
font-weight: bold;
}
.btn--primary {
background-color: #007bff;
border-color: #007bff;
color: #fff;
}
</style>

@ -148,6 +148,14 @@ $form->addCheckBox('sticky', null, get_lang('Sticky'));
$obj = new GradeModel();
$obj->fill_grade_model_select_in_form($form);
if ('true' === api_get_setting('course.show_course_duration')) {
$form->addElement('text', 'duration', get_lang('Duration (in minutes)'), [
'id' => 'duration',
'maxlength' => 10,
]);
$form->addRule('duration', get_lang('This field should be numeric'), 'numeric');
}
//Extra fields
$extra_field = new ExtraField('course');
$extra = $extra_field->addElements($form);
@ -182,20 +190,24 @@ $form->setDefaults($values);
// Validate the form
if ($form->validate()) {
$course = $form->exportValues();
$courseData = $form->exportValues();
$course_teachers = isset($course['course_teachers']) ? $course['course_teachers'] : null;
$course['exemplary_content'] = empty($course['exemplary_content']) ? false : true;
$course['teachers'] = $course_teachers;
$course['wanted_code'] = $course['visual_code'];
$course['gradebook_model_id'] = isset($course['gradebook_model_id']) ? $course['gradebook_model_id'] : null;
$course_teachers = isset($courseData['course_teachers']) ? $courseData['course_teachers'] : null;
$courseData['exemplary_content'] = empty($courseData['exemplary_content']) ? false : true;
$courseData['teachers'] = $course_teachers;
$courseData['wanted_code'] = $courseData['visual_code'];
$courseData['gradebook_model_id'] = isset($courseData['gradebook_model_id']) ? $courseData['gradebook_model_id'] : null;
if (isset($courseData['duration'])) {
$courseData['duration'] = $courseData['duration'] * 60; // Convert minutes to seconds
}
if (!empty($course['course_language'])) {
if (!empty($courseData['course_language'])) {
$translator = Container::$container->get('translator');
$translator->setLocale($course['course_language']);
$translator->setLocale($courseData['course_language']);
}
$course = CourseManager::create_course($course);
$course = CourseManager::create_course($courseData);
if (null !== $course) {
Display::addFlash(
Display::return_message(

@ -252,6 +252,14 @@ $form->addRule('disk_quota', get_lang('This field should be numeric'), 'numeric'
$form->addText('video_url', get_lang('Video URL'), false);
$form->addCheckBox('sticky', null, get_lang('Sticky'));
if ('true' === api_get_setting('course.show_course_duration')) {
$form->addElement('text', 'duration', get_lang('Duration (in minutes)'), [
'id' => 'duration',
'maxlength' => 10,
]);
$form->addRule('duration', get_lang('This field should be numeric'), 'numeric');
}
// Extra fields
$extraField = new ExtraField('course');
$extra = $extraField->addElements(
@ -289,6 +297,11 @@ $courseInfo['disk_quota'] = round(DocumentManager::get_course_quota($courseInfo[
$courseInfo['real_code'] = $courseInfo['code'];
$courseInfo['add_teachers_to_sessions_courses'] = $courseInfo['add_teachers_to_sessions_courses'] ?? 0;
// Set default duration in minutes
if (isset($courseInfo['duration'])) {
$courseInfo['duration'] = $courseInfo['duration'] / 60;
}
$form->setDefaults($courseInfo);
// Validate form
@ -296,6 +309,10 @@ if ($form->validate()) {
$course = $form->getSubmitValues();
$visibility = $course['visibility'];
if (isset($course['duration'])) {
$course['duration'] = $course['duration'] * 60;
}
// @todo should be check in the CidReqListener
/*global $_configuration;
@ -362,9 +379,13 @@ if ($form->validate()) {
->setUnsubscribe($course['unsubscribe'])
->setVisibility($visibility)
->setSticky(1 === (int) ($course['sticky'] ?? 0))
->setVideoUrl($params['video_url'] ?? '')
->setVideoUrl($course['video_url'] ?? '')
;
if (isset($course['duration'])) {
$courseEntity->setDuration($course['duration']);
}
$em->persist($courseEntity);
$em->flush();

@ -842,6 +842,10 @@ class AddCourse
->setCreator(api_get_user_entity())
;
if (isset($params['duration'])) {
$course->setDuration($params['duration']);
}
if (!empty($categories)) {
if (!is_array($categories)) {
$categories = [$categories];

@ -2281,6 +2281,10 @@ function api_format_course_array(Course $course = null)
$courseData['course_image'] = $image.'?filter=course_picture_small';
$courseData['course_image_large'] = $image.'?filter=course_picture_medium';
if ('true' === api_get_setting('course.show_course_duration') && null !== $course->getDuration()) {
$courseData['duration'] = $course->getDuration();
}
return $courseData;
}

@ -82,6 +82,7 @@ class PlatformConfigurationController extends AbstractController
'forum.global_forums_course_id',
'document.students_download_folders',
'social.hide_social_groups_block',
'course.show_course_duration',
];
$user = $this->userHelper->getCurrent();

@ -1617,6 +1617,11 @@ class SettingsCurrentFixtures extends Fixture implements FixtureGroupInterface
'title' => 'View courses in a grid layout',
'comment' => 'View courses in a layout with several courses per line. Otherwise, the layout will show one course per line.',
],
[
'name' => 'show_course_duration',
'title' => 'Show courses duration',
'comment' => 'Display the course duration next to the course title in the course catalogue and the courses list.',
],
],
'certificate' => [
[

@ -342,6 +342,10 @@ class Course extends AbstractResource implements ResourceInterface, ResourceWith
#[ORM\JoinColumn(name: 'room_id', referencedColumnName: 'id')]
protected ?Room $room;
#[Groups(['course:read', 'course_rel_user:read', 'course:write'])]
#[ORM\Column(type: 'integer', nullable: true)]
private ?int $duration = null;
public function __construct()
{
$this->visibility = self::OPEN_PLATFORM;
@ -1169,6 +1173,18 @@ class Course extends AbstractResource implements ResourceInterface, ResourceWith
return '/img/session_default.svg';
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): self
{
$this->duration = $duration;
return $this;
}
public function getResourceIdentifier(): int
{
return $this->getId();

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
final class Version20240704120400 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Add duration field to multiple tables';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE course ADD duration INT DEFAULT NULL');
$this->addSql('ALTER TABLE c_survey ADD duration INT DEFAULT NULL');
$this->addSql('ALTER TABLE c_quiz ADD duration INT DEFAULT NULL');
$this->addSql('ALTER TABLE c_quiz_question ADD duration INT DEFAULT NULL');
$this->addSql('ALTER TABLE c_lp ADD duration INT DEFAULT NULL');
$this->addSql('ALTER TABLE c_lp_item ADD duration INT DEFAULT NULL');
$this->addSql('ALTER TABLE c_student_publication ADD duration INT DEFAULT NULL');
$this->addSql('ALTER TABLE c_attendance_calendar ADD duration INT DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE course DROP duration');
$this->addSql('ALTER TABLE c_survey DROP duration');
$this->addSql('ALTER TABLE c_quiz DROP duration');
$this->addSql('ALTER TABLE c_quiz_question DROP duration');
$this->addSql('ALTER TABLE c_lp DROP duration');
$this->addSql('ALTER TABLE c_lp_item DROP duration');
$this->addSql('ALTER TABLE c_student_publication DROP duration');
$this->addSql('ALTER TABLE c_attendance_calendar DROP duration');
}
}

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
final class Version20240704120500 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Migrate extra fields to duration field in multiple tables';
}
public function up(Schema $schema): void
{
$this->migrateStudentPublicationDuration();
$this->migrateAttendanceCalendarDuration();
}
public function down(Schema $schema): void
{
// Revert changes if necessary
$this->addSql('UPDATE c_student_publication SET duration = NULL WHERE duration IS NOT NULL');
$this->addSql('UPDATE c_attendance_calendar SET duration = NULL WHERE duration IS NOT NULL');
}
private function migrateStudentPublicationDuration(): void
{
$sql = 'SELECT selected_value FROM settings_current WHERE variable = "considered_working_time" AND selected_value IS NOT NULL AND selected_value != "" AND selected_value != "false"';
$selectedValue = $this->connection->fetchOne($sql);
if ($selectedValue) {
$sql = 'SELECT s.*, efv.field_value
FROM c_student_publication s
INNER JOIN extra_field_values efv ON s.iid = efv.item_id
INNER JOIN extra_field ef ON efv.field_id = ef.id
WHERE ef.variable = ? AND ef.item_type = ?';
$params = [$selectedValue, ExtraField::WORK_FIELD_TYPE];
$data = $this->connection->fetchAllAssociative($sql, $params);
foreach ($data as $item) {
$id = $item['iid'];
$workTime = (int) $item['field_value'];
$durationInSeconds = $workTime * 60;
$this->addSql("UPDATE c_student_publication SET duration = ? WHERE iid = ?", [$durationInSeconds, $id]);
}
}
}
private function migrateAttendanceCalendarDuration(): void
{
$sql = 'SELECT s.*, efv.field_value
FROM c_attendance_calendar s
INNER JOIN extra_field_values efv ON s.iid = efv.item_id
INNER JOIN extra_field ef ON efv.field_id = ef.id
WHERE ef.variable = "duration" AND ef.item_type = ?';
$params = [ExtraField::ATTENDANCE_CALENDAR_TYPE];
$data = $this->connection->fetchAllAssociative($sql, $params);
foreach ($data as $item) {
$id = $item['iid'];
$duration = $item['field_value'];
$matches = [];
$newDuration = null;
if (preg_match('/(\d+)([h:](\d+)?)?/', $duration, $matches)) {
$hours = (int)$matches[1];
$minutes = 0;
if (!empty($matches[3])) {
$minutes = (int)$matches[3];
}
$newDuration = ($hours * 3600) + ($minutes * 60);
}
if ($newDuration !== null) {
$this->addSql('UPDATE c_attendance_calendar SET duration = ? WHERE iid = ?', [$newDuration, $id]);
}
}
}
}

@ -119,6 +119,7 @@ class CourseSettingsSchema extends AbstractSettingsSchema
'course_configuration_tool_extra_fields_to_show_and_edit' => '',
'course_creation_user_course_extra_field_relation_to_prefill' => '',
'allow_edit_tool_visibility_in_session' => 'true',
'show_course_duration' => 'false',
]
)
->setTransformer(
@ -365,6 +366,7 @@ class CourseSettingsSchema extends AbstractSettingsSchema
]
)
->add('allow_edit_tool_visibility_in_session', YesNoType::class)
->add('show_course_duration', YesNoType::class)
;
$this->updateFormFieldsFromSettingsInfo($builder);

@ -43,6 +43,9 @@ class CAttendanceCalendar
)]
protected Collection $sheets;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
protected ?int $duration = null;
public function getIid(): ?int
{
return $this->iid;
@ -113,4 +116,16 @@ class CAttendanceCalendar
return $this;
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): self
{
$this->duration = $duration;
return $this;
}
}

@ -149,6 +149,9 @@ class CLp extends AbstractResource implements ResourceInterface, ResourceShowCou
#[ORM\JoinColumn(name: 'asset_id', referencedColumnName: 'id')]
protected ?Asset $asset = null;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
protected ?int $duration = null;
public function __construct()
{
$now = new DateTime();
@ -602,6 +605,18 @@ class CLp extends AbstractResource implements ResourceInterface, ResourceShowCou
return $this;
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): self
{
$this->duration = $duration;
return $this;
}
public function getResourceIdentifier(): int|Uuid
{
return $this->getIid();

@ -114,6 +114,9 @@ class CLpItem implements Stringable
#[ORM\Column(name: 'lvl', type: 'integer')]
protected ?int $lvl;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
protected ?int $duration = null;
public function __construct()
{
$this->children = new ArrayCollection();
@ -531,4 +534,16 @@ class CLpItem implements Stringable
return $this;
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): self
{
$this->duration = $duration;
return $this;
}
}

@ -152,6 +152,9 @@ class CQuiz extends AbstractResource implements ResourceInterface, ResourceShowC
#[ORM\OneToMany(mappedBy: 'quiz', targetEntity: TrackEExercise::class)]
protected Collection $attempts;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
protected ?int $duration = null;
public function __construct()
{
$this->questions = new ArrayCollection();
@ -656,6 +659,18 @@ class CQuiz extends AbstractResource implements ResourceInterface, ResourceShowC
new ArrayCollection($this->questionsCategories->toArray());
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): self
{
$this->duration = $duration;
return $this;
}
public function getResourceIdentifier(): int|Uuid
{
return $this->getIid();

@ -90,6 +90,9 @@ class CQuizQuestion extends AbstractResource implements ResourceInterface, Strin
#[ORM\Column(name: 'mandatory', type: 'integer')]
protected int $mandatory;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
protected ?int $duration = null;
public function __construct()
{
$this->categories = new ArrayCollection();
@ -352,6 +355,18 @@ class CQuizQuestion extends AbstractResource implements ResourceInterface, Strin
return $this->iid;
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): self
{
$this->duration = $duration;
return $this;
}
public function getResourceIdentifier(): int|Uuid
{
return $this->getIid();

@ -173,6 +173,9 @@ class CStudentPublication extends AbstractResource implements ResourceInterface,
#[ORM\Column(name: 'filesize', type: 'integer', nullable: true)]
protected ?int $fileSize = null;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
protected ?int $duration = null;
public function __construct()
{
$this->description = '';
@ -503,6 +506,18 @@ class CStudentPublication extends AbstractResource implements ResourceInterface,
return $this;
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): self
{
$this->duration = $duration;
return $this;
}
public function getResourceIdentifier(): int
{
return $this->getIid();

@ -155,6 +155,9 @@ class CSurvey extends AbstractResource implements ResourceInterface, Stringable
#[ORM\Column(name: 'display_question_number', type: 'boolean', options: ['default' => true])]
protected bool $displayQuestionNumber;
#[ORM\Column(name: 'duration', type: 'integer', nullable: true)]
protected ?int $duration = null;
public function __construct()
{
$this->title = '';
@ -624,6 +627,18 @@ class CSurvey extends AbstractResource implements ResourceInterface, Stringable
return $this->iid;
}
public function getDuration(): ?int
{
return $this->duration;
}
public function setDuration(?int $duration): self
{
$this->duration = $duration;
return $this;
}
public function getResourceName(): string
{
return (string) $this->getCode();

Loading…
Cancel
Save