Merge remote-tracking branch 'upstream/1.11.x' into 1.11.x.contidos-dixitais-samesite-cookies

pull/4041/head
juan-cortizas-ponte 4 years ago
commit 10fda86296
  1. 23
      app/config/auth.conf.dist.php
  2. 10
      main/admin/course_edit.php
  3. 32
      main/admin/skill_rel_course.php
  4. 4
      main/attendance/attendance_add.php
  5. 3
      main/attendance/attendance_edit.php
  6. 6
      main/course_progress/thematic.php
  7. 47
      main/course_progress/thematic_advance.php
  8. 12
      main/coursecopy/copy_course.php
  9. 4
      main/coursecopy/create_backup.php
  10. 101
      main/cron/import_csv.php
  11. 6
      main/exercise/annotation_user.php
  12. 7
      main/exercise/exercise.class.php
  13. 36
      main/exercise/fill_blanks.class.php
  14. 9
      main/exercise/multiple_answer_true_false.class.php
  15. 63
      main/exercise/pending.php
  16. 4
      main/forum/editthread.php
  17. 2
      main/forum/forumfunction.inc.php
  18. 2
      main/gradebook/gradebook_add_eval.php
  19. 11
      main/gradebook/gradebook_edit_eval.php
  20. 45
      main/gradebook/lib/fe/evalform.class.php
  21. 6
      main/inc/ajax/document.ajax.php
  22. 2
      main/inc/ajax/model.ajax.php
  23. 1
      main/inc/lib/api.lib.php
  24. 163
      main/inc/lib/exercise.lib.php
  25. 10
      main/inc/lib/exercise_show_functions.lib.php
  26. 242
      main/inc/lib/javascript/annotation/js/annotation.js
  27. 3
      main/inc/lib/javascript/ckeditor/plugins/image2_chamilo/dialogs/image2_chamilo.js
  28. 3
      main/inc/lib/link.lib.php
  29. 38
      main/inc/lib/sessionmanager.lib.php
  30. 272
      main/inc/lib/skill.lib.php
  31. 461
      main/inc/lib/tracking.lib.php
  32. 84
      main/inc/lib/webservices/Rest.php
  33. 56
      main/inc/local.inc.php
  34. 7
      main/install/configuration.dist.php
  35. 43
      main/lp/learnpath.class.php
  36. 3
      main/lp/learnpathItem.class.php
  37. 2
      main/lp/lp_add.php
  38. 3
      main/lp/lp_edit.php
  39. 99
      main/mySpace/myStudents.php
  40. 4
      main/survey/create_new_survey.php
  41. 27
      main/template/default/my_space/pdf_lp_stats.tpl
  42. 23
      main/webservices/api/v2.php
  43. 3
      main/work/work.lib.php
  44. 4
      plugin/bbb/CHANGELOG.md
  45. 11
      plugin/bbb/README.md
  46. 45
      plugin/bbb/lib/bbb.lib.php
  47. 1114
      plugin/bbb/lib/bbb_api.php
  48. 16
      plugin/bbb/lib/bbb_plugin.class.php
  49. 13
      plugin/bbb/view/listing.tpl
  50. 4
      plugin/buycourses/CHANGELOG.md
  51. 2
      plugin/buycourses/src/buy_course_plugin.class.php
  52. 2
      plugin/questionoptionsevaluation/QuestionOptionsEvaluationPlugin.php
  53. 2
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/Forum.php
  54. 3
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/ForumStudent.php
  55. 2
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/LearningPathDocuments.php
  56. 2
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/Messages.php
  57. 2
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/TestAnswerFeedback.php
  58. 2
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/TestMatching.php
  59. 1
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/TestProposedAnswer.php
  60. 3
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/TestQuestionDescription.php
  61. 3
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/Work.php
  62. 10
      src/Chamilo/CoreBundle/Entity/Skill.php
  63. 153
      src/Chamilo/CourseBundle/Component/CourseCopy/CourseBuilder.php
  64. 71
      src/Chamilo/CourseBundle/Component/CourseCopy/CourseRestorer.php
  65. 10
      src/Chamilo/CourseBundle/Component/CourseCopy/CourseSelectForm.php
  66. 4
      src/Chamilo/SkillBundle/Entity/SkillRelCourse.php
  67. 483
      tests/scripts/synchronize_user_base_from_ldap.php

@ -4,6 +4,10 @@
/**
* Configuration file for all authentication methods.
* Uncomment and configure only the section(s) you need.
* For MultiURL configuration you can override the configuration
* of every variable by defining the same variable in app/config/configuration.php
* The configuration in app/config/configuration.php will replace
* the configuration in this file.
* @package chamilo.conf.auth
*/
@ -22,6 +26,10 @@
'return_url' => api_get_path(WEB_PATH).'?action=fbconnect',
);*/
$facebookConfig = api_get_configuration_value('facebook_config');
if (!empty($facebookConfig)) {
$facebook_config = $facebookConfig;
}
/**
* Shibboleth
@ -29,6 +37,11 @@
// $shibb_login = ...;
$shibbLogin = api_get_configuration_value('shibb_login');
if (!empty($shibbLogin)) {
$shibb_login = $shibbLogin;
}
/**
* LDAP
*/
@ -116,6 +129,11 @@ $extldap_user_correspondance = array(
) */
);
$ldapUserCorrespondance = api_get_configuration_value('extldap_user_correspondance');
if (!empty($ldapUserCorrespondance)) {
$extldap_user_correspondance = $ldapUserCorrespondance;
}
/**
* Example method to get whether the user is an admin or not. Please implement your logic inside.
*/
@ -146,3 +164,8 @@ $cas = [
// 'fixedServiceURL' => false, // false by default, set to either true or to the service URL string if needed
// sites might also need proxy_settings in configuration.php
];
$casConfig = api_get_configuration_value('cas');
if (!empty($casConfig)) {
$cas = $casConfig;
}

@ -328,6 +328,10 @@ if (api_get_configuration_value('multiple_access_url_show_shared_course_marker')
}
$form->addLabel('URLs', $urlToString);
}
$allowSkillRelItem = api_get_configuration_value('allow_skill_rel_items');
if ($allowSkillRelItem) {
Skill::setSkillsToCourse($form, $courseId);
}
$htmlHeadXtra[] = '
<script>
@ -341,7 +345,7 @@ $form->addButtonUpdate(get_lang('ModifyCourseInfo'));
// Set some default values
$courseInfo['disk_quota'] = round(DocumentManager::get_course_quota($courseInfo['code']) / 1024 / 1024, 1);
$courseInfo['real_code'] = $courseInfo['code'];
$courseInfo['add_teachers_to_sessions_courses'] = isset($courseInfo['add_teachers_to_sessions_courses']) ? $courseInfo['add_teachers_to_sessions_courses'] : 0;
$courseInfo['add_teachers_to_sessions_courses'] = $courseInfo['add_teachers_to_sessions_courses'] ?? 0;
$form->setDefaults($courseInfo);
// Validate form
@ -349,6 +353,10 @@ if ($form->validate()) {
$course = $form->getSubmitValues();
$visibility = $course['visibility'];
/*if ($allowSkillRelItem) {
$result = Skill::saveSkillsToCourseFromForm($form);
}*/
global $_configuration;
if (isset($_configuration[$urlId]) &&

@ -1,8 +1,6 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\SkillBundle\Entity\SkillRelCourse;
$cidReset = true;
require_once __DIR__.'/../inc/global.inc.php';
@ -33,33 +31,9 @@ if (!empty($sessionId)) {
}
$form->addHeader(get_lang('AddSkills').$sessionName);
Skill::setSkillsToCourse($form, $courseId, $sessionId);
$skillList = [];
$em = Database::getManager();
$items = $em->getRepository('ChamiloSkillBundle:SkillRelCourse')->findBy(
['course' => $courseId, 'session' => $sessionId]
);
/** @var SkillRelCourse $skillRelCourse */
foreach ($items as $skillRelCourse) {
$skillList[$skillRelCourse->getSkill()->getId()] = $skillRelCourse->getSkill()->getName();
}
$form->addHidden('course_id', $courseId);
$form->addHidden('session_id', $sessionId);
$form->addSelectAjax(
'skills',
get_lang('Skills'),
$skillList,
[
'url' => api_get_path(WEB_AJAX_PATH).'skill.ajax.php?a=search_skills',
'multiple' => 'multiple',
]
);
$form->addButtonSave(get_lang('Save'));
$form->setDefaults(['skills' => array_keys($skillList)]);
/*$form->addButtonSave(get_lang('Save'));
if ($form->validate()) {
$result = Skill::saveSkillsToCourseFromForm($form);
@ -68,7 +42,7 @@ if ($form->validate()) {
}
header('Location: '.$url);
exit;
}
}*/
$content = $form->returnForm();
$interbreadcrumb[] = [

@ -67,9 +67,7 @@ if ((api_get_session_id() != 0 && Gradebook::is_active()) || api_get_session_id(
);
$form->applyFilter('attendance_weight', 'html_filter');
$form->addElement('html', '</div>');
$skillList = Skill::addSkillsToForm($form, ITEM_TYPE_ATTENDANCE, 0);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_ATTENDANCE, 0);
$form->addElement('html', '</div>');
}
$form->addButtonCreate(get_lang('Save'));

@ -82,7 +82,7 @@ if (Gradebook::is_active()) {
);
$form->applyFilter('attendance_weight', 'html_filter');
$form->addElement('html', '</div>');
$skillList = Skill::addSkillsToForm($form, ITEM_TYPE_ATTENDANCE, $attendance_id);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_ATTENDANCE, $attendance_id);
$form->addElement('html', '</div>');
}
$form->addButtonUpdate(get_lang('Save'));
@ -92,7 +92,6 @@ $default['title'] = Security::remove_XSS($title);
$default['description'] = Security::remove_XSS($description, STUDENT);
$default['attendance_qualify_title'] = $attendance_qualify_title;
$default['attendance_weight'] = $attendance_weight;
$default['skills'] = array_keys($skillList);
$linkInfo = GradebookUtils::isResourceInCourseGradebook(
api_get_course_id(),

@ -121,13 +121,16 @@ if ($action == 'thematic_list') {
// Display thematic data
if (!empty($thematic_data)) {
// display progress
$displayOrder = 1;
$maxThematicItem = count($thematic_data);
foreach ($thematic_data as $thematic) {
$list['id'] = $thematic['id'];
$list['id_course'] = $thematic['c_id'];
$list['id_session'] = $thematic['session_id'];
$list['title'] = Security::remove_XSS($thematic['title'], STUDENT);
$list['content'] = Security::remove_XSS($thematic['content'], STUDENT);
$list['display_orden'] = $thematic['display_order'];
$thematic['display_order'] = $displayOrder;
$thematic['max_thematic_item'] = $maxThematicItem;
$list['active'] = $thematic['active'];
$my_thematic_id = $thematic['id'];
@ -214,6 +217,7 @@ if ($action == 'thematic_list') {
$listThematic[] = $list;
$tpl->assign('data', $listThematic);
$displayOrder++;
} //End for
}
$thematicLayout = $tpl->get_template('course_progress/progress.tpl');

@ -61,8 +61,8 @@ if ($action === 'thematic_advance_add' || $action === 'thematic_advance_edit') {
);
$form->addGroup($radios, null, get_lang('StartDateOptions'));
if (isset($thematic_advance_data['attendance_id']) &&
$thematic_advance_data['attendance_id'] == 0) {
if (empty($thematic_advance_data['attendance_id'])
) {
$form->addElement('html', '<div id="div_custom_datetime" style="display:block">');
} else {
$form->addElement('html', '<div id="div_custom_datetime" style="display:none">');
@ -71,8 +71,7 @@ if ($action === 'thematic_advance_add' || $action === 'thematic_advance_edit') {
$form->addElement('DateTimePicker', 'custom_start_date', get_lang('StartDate'));
$form->addElement('html', '</div>');
if (isset($thematic_advance_data['attendance_id']) &&
$thematic_advance_data['attendance_id'] == 0
if (empty($thematic_advance_data['attendance_id'])
) {
$form->addElement('html', '<div id="div_datetime_by_attendance" style="display:none">');
} else {
@ -157,30 +156,36 @@ if ($action === 'thematic_advance_add' || $action === 'thematic_advance_edit') {
}
}
$default['start_date_type'] = 1;
$default['start_date_type'] = 2;
$default['custom_start_date'] = date('Y-m-d H:i:s', api_strtotime(api_get_local_time()));
$default['duration_in_hours'] = 1;
if (!empty($thematic_advance_data)) {
// set default values
$default['content'] = isset($thematic_advance_data['content']) ? $thematic_advance_data['content'] : null;
$default['duration_in_hours'] = isset($thematic_advance_data['duration']) ? $thematic_advance_data['duration'] : 1;
if (empty($thematic_advance_data['attendance_id'])) {
$default['start_date_type'] = 1;
$default['custom_start_date'] = null;
if (isset($thematic_advance_data['start_date'])) {
$default['custom_start_date'] = date(
'Y-m-d H:i:s',
api_strtotime(api_get_local_time($thematic_advance_data['start_date']))
);
if ('thematic_advance_edit' == $action) {
$default['content'] = isset($thematic_advance_data['content']) ? $thematic_advance_data['content'] : null;
$default['duration_in_hours'] = isset($thematic_advance_data['duration']) ? $thematic_advance_data['duration'] : 1;
if (empty($thematic_advance_data['attendance_id'])) {
$default['start_date_type'] = 2;
$default['custom_start_date'] = null;
if (isset($thematic_advance_data['start_date'])) {
$default['custom_start_date'] = date(
'Y-m-d H:i:s',
api_strtotime(api_get_local_time($thematic_advance_data['start_date']))
);
}
} else {
$default['start_date_type'] = 1;
if (!empty($thematic_advance_data['start_date'])) {
$default['start_date_by_attendance'] = api_get_local_time($thematic_advance_data['start_date']);
}
$default['attendance_select'] = $thematic_advance_data['attendance_id'];
}
} else {
$default['start_date_type'] = 1;
if (!empty($thematic_advance_data['start_date'])) {
$default['start_date_by_attendance'] = api_get_local_time($thematic_advance_data['start_date']);
}
$default['attendance_select'] = $thematic_advance_data['attendance_id'];
$default['start_date_type'] = 2;
$default['custom_start_date'] = date('Y-m-d H:i:s', api_strtotime(api_get_local_time()));
$default['duration_in_hours'] = 1;
}
}
$form->setDefaults($default);

@ -45,11 +45,19 @@ if (Security::check_token('post') && (
// Clear token
Security::clear_token();
if ($action === 'course_select_form') {
$course = CourseSelectForm::get_posted_course('copy_course');
$cb = new CourseBuilder('partial');
$course = $cb->build(0, null, false, array_keys($_POST['resource']), $_POST['resource']);
$course = CourseSelectForm::get_posted_course(null, 0, '', $course);
} else {
$cb = new CourseBuilder();
$cb = new CourseBuilder('complete');
$course = $cb->build();
}
// It builds the documents and items related to the LP
$cb->exportToCourseBuildFormat();
// It builds documents added in text (quizzes, assignments)
$cb->restoreDocumentsFromList();
$cr = new CourseRestorer($course);
$cr->set_file_option($_POST['same_file_name_option']);
$cr->restore($_POST['destination_course']);

@ -55,6 +55,10 @@ if (Security::check_token('post') &&
$cb = new CourseBuilder('complete');
$course = $cb->build();
}
// It builds the documents and items related to the LP
$cb->exportToCourseBuildFormat();
// It builds documents added in text (quizzes, assignments)
$cb->restoreDocumentsFromList();
$zipFile = CourseArchiver::createBackup($course);
echo Display::return_message(get_lang('BackupCreated'), 'confirm');
echo '<br />';

@ -169,6 +169,10 @@ class ImportCsv
$method = 'importOpenSessions';
}
if ($method == 'importOpensessions') {
$method = 'importOpenSessions';
}
if ($method === 'importSessionsall') {
$method = 'importSessionsUsersCareers';
}
@ -222,6 +226,7 @@ class ImportCsv
'courseinsert-static',
'unsubscribe-static',
'care',
'skillset',
//'careers',
//'careersdiagram',
//'careersresults',
@ -524,6 +529,14 @@ class ImportCsv
''
);
// Course skill set.
CourseManager::create_course_extra_field(
'skillset',
1,
'Skill set',
''
);
CourseManager::create_course_extra_field(
'disable_import_calendar',
13,
@ -1800,6 +1813,81 @@ class ImportCsv
}
}
private function importSkillset(
$file,
$moveFile = true
) {
$this->fixCSVFile($file);
$data = Import::csvToArray($file);
if (!empty($data)) {
$this->logger->addInfo(count($data).' records found.');
$extraFieldValues = new ExtraFieldValue('skill');
$em = Database::getManager();
$repo = $em->getRepository(\Chamilo\CoreBundle\Entity\Skill::class);
$skillSetList = [];
$urlId = api_get_current_access_url_id();
foreach ($data as $row) {
$skill = $repo->findOneBy(['shortCode' => $row['Code']]);
$new = false;
if ($skill === null) {
$new = true;
$skill = new \Chamilo\CoreBundle\Entity\Skill();
$skill
->setShortCode($row['Code'])
->setDescription('')
->setAccessUrlId($urlId)
->setIcon('')
->setStatus(1)
;
}
$skill
->setName($row['Tekst'])
->setUpdatedAt(new DateTime())
;
$em->persist($skill);
$em->flush();
if ($new) {
$skillRelSkill = (new \Chamilo\CoreBundle\Entity\SkillRelSkill())
->setRelationType(0)
->setParentId(0)
->setLevel(0)
->setSkillId($skill->getId())
;
$em->persist($skillRelSkill);
$em->flush();
}
/*
$params = [
'item_id' => $skill->getId(),
'variable' => 'skillset',
'value' => $row['SkillsetID'],
];
$extraFieldValues->save($params);*/
$skillSetList[$row['SkillsetID']][] = $skill->getId();
}
//$courseRelSkills = [];
foreach ($skillSetList as $skillSetId => $skillList) {
$skillList = array_unique($skillList);
if (empty($skillList)) {
continue;
}
$sql = "SELECT id FROM course WHERE code LIKE '%$skillSetId' ";
$result = Database::query($sql);
while ($row = Database::fetch_array($result, 'ASSOC')) {
$courseId = $row['id'];
//$courseRelSkills[$courseId] = $skillList;
Skill::saveSkillsToCourse($skillList, $courseId, null);
}
}
}
}
/**
* @param string $file
* @param bool $moveFile
@ -1820,7 +1908,6 @@ class ImportCsv
foreach ($data as $row) {
$row = $this->cleanCourseRow($row);
$courseId = CourseManager::get_course_id_from_original_id(
$row['extra_'.$this->extraFieldIdNameList['course']],
$this->extraFieldIdNameList['course']
@ -1851,6 +1938,12 @@ class ImportCsv
$row['extra_'.$this->extraFieldIdNameList['course']]
);
CourseManager::update_course_extra_field_value(
$courseInfo['code'],
'skillset',
$row['extra_courseskillset']
);
$this->logger->addInfo("Courses - Course created ".$courseInfo['code']);
} else {
$this->logger->addError("Courses - Can't create course:".$row['title']);
@ -1898,6 +1991,12 @@ class ImportCsv
);
}
CourseManager::update_course_extra_field_value(
$courseInfo['code'],
'skillset',
$row['extra_courseskillset']
);
foreach ($teachers as $teacherId) {
if (isset($groupBackup['tutor'][$teacherId]) &&
isset($groupBackup['tutor'][$teacherId][$courseInfo['code']])

@ -50,15 +50,15 @@ if (!empty($questionAttempt['answer'])) {
$points[] = Geometry::decodePoint($partPoint);
}
$data['answers']['paths'][] = [
'color' => $properties[1] ?? null,
'color' => $properties[1] ?? '#FF0000',
'points' => $points,
];
break;
case 'T':
$text = [
'text' => array_shift($parts),
'color' => $properties[1] ?? null,
'fontSize' => $properties[2] ?? null,
'color' => $properties[1] ?? '#FF0000',
'fontSize' => $properties[2] ?? '20',
];
$data['answers']['texts'][] = $text + Geometry::decodePoint($parts[0]);

@ -2492,7 +2492,7 @@ class Exercise
}
}
$skillList = Skill::addSkillsToForm($form, ITEM_TYPE_EXERCISE, $this->iid);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_EXERCISE, $this->iid);
$extraField = new ExtraField('exercise');
$extraField->addElements(
@ -2634,7 +2634,6 @@ class Exercise
} else {
$defaults['enabletimercontroltotalminutes'] = 0;
}
$defaults['skills'] = array_keys($skillList);
$defaults['notifications'] = $this->getNotifications();
} else {
$defaults['exerciseType'] = 2;
@ -3955,8 +3954,8 @@ class Exercise
if ($studentChoice == $answerCorrect) {
$questionScore += $true_score;
} else {
if ($quiz_question_options[$studentChoice]['name'] === "Don't know" ||
$quiz_question_options[$studentChoice]['name'] === "DoubtScore"
if ($quiz_question_options[$studentChoice - 1]['name'] === "Don't know" ||
$quiz_question_options[$studentChoice - 1]['name'] === "DoubtScore"
) {
$questionScore += $doubt_score;
} else {

@ -543,7 +543,7 @@ class FillBlanks extends Question
$resultOptions = ['' => '--'];
foreach ($listMenu as $item) {
$resultOptions[sha1($item)] = $item;
$resultOptions[sha1($item)] = self::replaceSpecialCharsForMenuValues($item);
}
foreach ($resultOptions as $key => $value) {
@ -586,6 +586,40 @@ class FillBlanks extends Question
return $result;
}
/*
* It searchs and replaces special chars to show in menu values
*
* @param string $value The value to parse
*
* @return string
*/
public static function replaceSpecialCharsForMenuValues($value)
{
// It replaces supscript numbers
$value = preg_replace('/<sup>([0-9]+)<\/sup>/is', "&sub$1;", $value);
// It replaces subscript numbers
$value = preg_replace_callback(
"/<sub>([0-9]+)<\/sub>/is",
function ($m) {
$precode = '&#832';
$nb = $m[1];
$code = '';
if (is_numeric($nb) && strlen($nb) > 1) {
for ($i = 0; $i < strlen($nb); $i++) {
$code .= $precode.$nb[$i].';';
}
} else {
$code = $precode.$m[1].';';
}
return $code;
},
$value);
return $value;
}
/**
* Return an array with the different choices available
* when the answers between bracket show as a menu.

@ -112,14 +112,15 @@ class MultipleAnswerTrueFalse extends Question
$answer_number->freeze();
if (is_object($answer)) {
$defaults['answer['.$i.']'] = $answer->answer[$i];
$defaults['comment['.$i.']'] = $answer->comment[$i];
$correct = $answer->correct[$i];
$defaults['answer['.$i.']'] = $answer->answer[$i] ?? '';
$defaults['comment['.$i.']'] = $answer->comment[$i] ?? '';
$correct = $answer->correct[$i] ?? 0;
$defaults['correct['.$i.']'] = $correct;
$j = 1;
if (!empty($optionData)) {
foreach ($optionData as $id => $data) {
$id++;
$rdoCorrect = $form->addElement('radio', 'correct['.$i.']', null, null, $id);
if (isset($_POST['correct']) && isset($_POST['correct'][$i]) && $id == $_POST['correct'][$i]) {
@ -298,7 +299,7 @@ class MultipleAnswerTrueFalse extends Question
if (empty($options)) {
//If this is the first time that the question is created when
// change the default values from the form 1 and 2 by the correct "option id" registered
$goodAnswer = isset($sortedByPosition[$goodAnswer]) ? $sortedByPosition[$goodAnswer]['iid'] : '';
// $goodAnswer = isset($sortedByPosition[$goodAnswer]) ? $sortedByPosition[$goodAnswer]['iid'] : '';
}
$questionWeighting += $extra_values[0]; //By default 0 has the correct answers
$objAnswer->createAnswer($answer, $goodAnswer, $comment, '', $i);

@ -13,6 +13,7 @@ $filter_user = isset($_REQUEST['filter_by_user']) ? (int) $_REQUEST['filter_by_u
$courseId = isset($_REQUEST['course_id']) ? (int) $_REQUEST['course_id'] : 0;
$exerciseId = isset($_REQUEST['exercise_id']) ? (int) $_REQUEST['exercise_id'] : 0;
$statusId = isset($_REQUEST['status']) ? (int) $_REQUEST['status'] : 0;
$exportXls = isset($_REQUEST['export_xls']) && !empty($_REQUEST['export_xls']) ? (int) $_REQUEST['export_xls'] : 0;
$action = $_REQUEST['a'] ?? null;
api_block_anonymous_users();
@ -28,7 +29,8 @@ switch ($action) {
$results = ExerciseLib::get_all_exercises_for_course_id(
null,
0,
$courseId
$courseId,
false
);
if (!empty($results)) {
foreach ($results as $exercise) {
@ -111,7 +113,41 @@ if (!empty($_REQUEST['export_report']) && $_REQUEST['export_report'] == '1') {
}
}
$htmlHeadXtra[] = '<script>
$(function() {
$("#export-xls").bind("click", function(e) {
e.preventDefault();
var input = $("<input>", {
type: "hidden",
name: "export_xls",
value: "1"
});
$("#pending").append(input);
$("#pending").submit();
});
$("#pending_pendingSubmit").bind("click", function(e) {
e.preventDefault();
if ($("input[name=\"export_xls\"]").length > 0) {
$("input[name=\"export_xls\"]").remove();
}
$("#pending").submit();
});
});
</script>';
if ($exportXls) {
ExerciseLib::exportPendingAttemptsToExcel($_REQUEST);
}
Display::display_header(get_lang('PendingAttempts'));
$actions = '';
$actions .= Display::url(
Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
'#',
['id' => 'export-xls']
);
echo Display::div($actions, ['class' => 'actions']);
$token = Security::get_token();
$extra = '<script>
$(function() {
@ -198,7 +234,8 @@ $extra .= '</div>';
echo $extra;
$courses = CourseManager::get_courses_list_by_user_id($userId, false, false, false);
$showAttemptsInSessions = api_get_configuration_value('show_exercise_attempts_in_all_user_sessions');
$courses = CourseManager::get_courses_list_by_user_id($userId, $showAttemptsInSessions, false, false);
$form = new FormValidator('pending', 'GET');
$courses = array_column($courses, 'title', 'real_id');
@ -218,10 +255,12 @@ $status = [
1 => get_lang('All'),
2 => get_lang('Validated'),
3 => get_lang('NotValidated'),
4 => get_lang('Unclosed'),
5 => get_lang('Ongoing'),
];
$form->addSelect('status', get_lang('Status'), $status);
$form->addButtonSearch(get_lang('Search'));
$form->addButtonSearch(get_lang('Search'), 'pendingSubmit');
$content = $form->returnForm();
echo $content;
@ -251,7 +290,8 @@ $columns = [
get_lang('Score'),
get_lang('IP'),
get_lang('Status'),
//get_lang('ToolLearnpath'),
get_lang('Corrector'),
get_lang('CorrectionDate'),
get_lang('Actions'),
];
@ -303,7 +343,20 @@ $column_model = [
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang('NotValidated'),
],*/
],
//['name' => 'lp', 'index' => 'orig_lp_id', 'width' => '60', 'align' => 'left', 'search' => 'false'],
[
'name' => 'qualificator_fullname',
'index' => 'qualificator_fullname',
'width' => '20',
'align' => 'left',
'search' => 'true',
],
[
'name' => 'date_of_qualification',
'index' => 'date_of_qualification',
'width' => '20',
'align' => 'left',
'search' => 'true',
],
[
'name' => 'actions',
'index' => 'actions',

@ -215,7 +215,7 @@ if (api_is_allowed_to_edit(null, true)) {
$form->addElement('html', '</div>');
$skillList = Skill::addSkillsToForm($form, ITEM_TYPE_FORUM_THREAD, $threadId);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_FORUM_THREAD, $threadId);
if (!empty($threadData)) {
$defaults['thread_qualify_gradebook'] = $gradeThisThread;
@ -233,8 +233,6 @@ if (!empty($threadData)) {
$defaults['thread_peer_qualify'] = 0;
}
$defaults['skills'] = array_keys($skillList);
$form->addButtonUpdate(get_lang('ModifyThread'), 'SubmitPost');
if ($form->validate()) {

@ -3276,7 +3276,7 @@ function show_add_post_form($current_forum, $action, $form_values = [], $showPre
}
if ($action === 'newthread') {
Skill::addSkillsToForm($form, ITEM_TYPE_FORUM_THREAD, 0);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_FORUM_THREAD, 0);
}
if (api_is_allowed_to_edit(null, true) && $action == 'newthread') {

@ -68,6 +68,8 @@ if ($form->validate()) {
];
Event::registerLog($logInfo);
Skill::saveSkills($form, ITEM_TYPE_GRADEBOOK_EVALUATION, $eval->get_id());
if (null == $eval->get_course_code()) {
if (1 == $values['adduser']) {
//Disabling code when course code is null see issue #2705

@ -1,10 +1,9 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Script.
*
* @package chamilo.gradebook
*/
require_once __DIR__.'/../inc/global.inc.php';
api_block_anonymous_users();
@ -37,23 +36,23 @@ if ($form->validate()) {
$eval->set_weight($final_weight);
$eval->set_max($values['max']);
$visible = 1;
if (empty($values['visible'])) {
$visible = 0;
} else {
$visible = 1;
}
$eval->set_visible($visible);
$eval->save();
$logInfo = [
'tool' => TOOL_GRADEBOOK,
'tool_id' => 0,
'tool_id_detail' => 0,
'action' => 'edit-eval',
'action_details' => '',
];
Event::registerLog($logInfo);
Skill::saveSkills($form, ITEM_TYPE_GRADEBOOK_EVALUATION, $values['hid_id']);
header('Location: '.Category::getUrl().'editeval=&selectcat='.$eval->get_category_id());
exit;
}

@ -463,21 +463,27 @@ class EvalForm extends FormValidator
*/
protected function build_add_form()
{
$this->setDefaults(
[
'hid_user_id' => $this->evaluation_object->get_user_id(),
'hid_category_id' => $this->evaluation_object->get_category_id(),
'hid_course_code' => $this->evaluation_object->get_course_code(),
'created_at' => api_get_utc_datetime(),
]
);
$this->setDefaults([
'hid_user_id' => $this->evaluation_object->get_user_id(),
'hid_category_id' => $this->evaluation_object->get_category_id(),
'hid_course_code' => $this->evaluation_object->get_course_code(),
'created_at' => api_get_utc_datetime(),
]);
$this->build_basic_form();
if ($this->evaluation_object->get_course_code() == null) {
$this->addElement('checkbox', 'adduser', null, get_lang('AddUserToEval'));
} else {
$this->addElement('checkbox', 'addresult', null, get_lang('AddResult'));
}
$this->addButtonCreate(get_lang('AddAssessment'), 'submit');
Skill::addSkillsToForm(
$this,
api_get_course_int_id(),
api_get_session_id(),
ITEM_TYPE_GRADEBOOK_EVALUATION
);
$this->addButtonCreate(get_lang('AddAssessment'));
}
/**
@ -496,8 +502,11 @@ class EvalForm extends FormValidator
}
$weight = $weight_mask = $this->evaluation_object->get_weight();
$evaluationId = $this->evaluation_object->get_id();
$this->addHidden('hid_id', $evaluationId);
$this->setDefaults([
'hid_id' => $this->evaluation_object->get_id(),
'hid_id' => $evaluationId,
'name' => $this->evaluation_object->get_name(),
'description' => $this->evaluation_object->get_description(),
'hid_user_id' => $this->evaluation_object->get_user_id(),
@ -509,10 +518,20 @@ class EvalForm extends FormValidator
'max' => $this->evaluation_object->get_max(),
'visible' => $this->evaluation_object->is_visible(),
]);
$id_current = isset($this->id) ? $this->id : null;
$this->addElement('hidden', 'hid_id', $id_current);
$this->build_basic_form(1);
$this->addButtonSave(get_lang('ModifyEvaluation'), 'submit');
if (!empty($evaluationId)) {
Skill::addSkillsToForm(
$this,
api_get_course_int_id(),
api_get_session_id(),
ITEM_TYPE_GRADEBOOK_EVALUATION,
$evaluationId
);
}
$this->addButtonSave(get_lang('ModifyEvaluation'));
}
/**

@ -184,10 +184,11 @@ switch ($action) {
'files'
);
if ($result) {
$relativeUrl = str_replace(api_get_path(WEB_PATH), '/', $result['direct_url']);
$data = [
'uploaded' => 1,
'fileName' => $fileUpload['name'],
'url' => $result['direct_url'],
'url' => $relativeUrl,
];
}
} else {
@ -206,10 +207,11 @@ switch ($action) {
}
if (move_uploaded_file($fileUpload['tmp_name'], $syspath.$fileUploadName)) {
$url = $webpath.$fileUploadName;
$relativeUrl = str_replace(api_get_path(WEB_PATH), '/', $url);
$data = [
'uploaded' => 1,
'fileName' => $fileUploadName,
'url' => $url,
'url' => $relativeUrl,
];
}
}

@ -1588,6 +1588,8 @@ switch ($action) {
'score',
'user_ip',
'status',
'qualificator_fullname',
'date_of_qualification',
'actions',
];
$officialCodeInList = api_get_setting('show_official_code_exercise_result_list');

@ -548,6 +548,7 @@ define('ITEM_TYPE_ATTENDANCE', 8);
define('ITEM_TYPE_SURVEY', 9);
define('ITEM_TYPE_FORUM_THREAD', 10);
define('ITEM_TYPE_PORTFOLIO', 11);
define('ITEM_TYPE_GRADEBOOK_EVALUATION', 12);
// one big string with all question types, for the validator in pear/HTML/QuickForm/Rule/QuestionType
define(

@ -744,7 +744,7 @@ class ExerciseLib
}
if ($debug_mark_answer) {
if ($id == $answerCorrect) {
if ($id + 1 == $answerCorrect) {
$attributes['checked'] = 1;
$attributes['selected'] = 1;
}
@ -754,7 +754,7 @@ class ExerciseLib
Display::input(
'radio',
'choice['.$questionId.']['.$numAnswer.']',
$id,
$id + 1,
$attributes
),
['style' => '']
@ -1729,6 +1729,18 @@ HOTSPOT;
<span class="fa fa-times-rectangle fa-fw" aria-hidden="true"></span>
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-default"
title="'.get_lang('Undo').'"
id="btn-undo-'.$questionId.'">
<span class="fa fa-undo fa-fw" aria-hidden="true"></span>
</button>
<button type="button" class="btn btn-default"
title="'.get_lang('Redo').'"
id="btn-redo-'.$questionId.'">
<span class="fa fa-repeat fa-fw" aria-hidden="true"></span>
</button>
</div>
</div>
</div>
</div>
@ -2167,6 +2179,108 @@ HOTSPOT;
return $attempt;
}
/**
* Export the pending attempts to excel.
*
* @params $values
*/
public static function exportPendingAttemptsToExcel($values)
{
$headers = [
get_lang('Course'),
get_lang('Exercise'),
get_lang('FirstName'),
get_lang('LastName'),
get_lang('LoginName'),
get_lang('Duration').' ('.get_lang('MinMinute').')',
get_lang('StartDate'),
get_lang('EndDate'),
get_lang('Score'),
get_lang('IP'),
get_lang('Status'),
get_lang('Corrector'),
get_lang('CorrectionDate'),
];
$tableXls[] = $headers;
$courseId = $values['course_id'] ?? 0;
$exerciseId = $values['exercise_id'] ?? 0;
$status = $values['status'] ?? 0;
$whereCondition = '';
if (isset($_GET['filter_by_user']) && !empty($_GET['filter_by_user'])) {
$filter_user = (int) $_GET['filter_by_user'];
if (empty($whereCondition)) {
$whereCondition .= " te.exe_user_id = '$filter_user'";
} else {
$whereCondition .= " AND te.exe_user_id = '$filter_user'";
}
}
if (isset($_GET['group_id_in_toolbar']) && !empty($_GET['group_id_in_toolbar'])) {
$groupIdFromToolbar = (int) $_GET['group_id_in_toolbar'];
if (!empty($groupIdFromToolbar)) {
if (empty($whereCondition)) {
$whereCondition .= " te.group_id = '$groupIdFromToolbar'";
} else {
$whereCondition .= " AND group_id = '$groupIdFromToolbar'";
}
}
}
if (!empty($whereCondition)) {
$whereCondition = " AND $whereCondition";
}
if (!empty($courseId)) {
$whereCondition .= " AND te.c_id = $courseId";
}
$result = ExerciseLib::get_exam_results_data(
0,
10000000,
'c_id',
'asc',
$exerciseId,
$whereCondition,
false,
null,
false,
false,
[],
false,
false,
false,
true,
$status
);
if (!empty($result)) {
foreach ($result as $attempt) {
$data = [
$attempt['course'],
$attempt['exercise'],
$attempt['firstname'],
$attempt['lastname'],
$attempt['username'],
$attempt['exe_duration'],
$attempt['start_date'],
$attempt['exe_date'],
strip_tags($attempt['score']),
$attempt['user_ip'],
strip_tags($attempt['status']),
$attempt['qualificator_fullname'],
$attempt['date_of_qualification'],
];
$tableXls[] = $data;
}
}
$fileName = get_lang('PendingAttempts').'_'.api_get_local_time();
Export::arrayToXls($tableXls, $fileName);
return true;
}
/**
* Gets exercise results.
*
@ -2287,6 +2401,21 @@ HOTSPOT;
$sessionCondition = '';
}
$showAttemptsInSessions = api_get_configuration_value('show_exercise_attempts_in_all_user_sessions');
if ($showAttemptsInSessions) {
$sessions = SessionManager::get_sessions_by_general_coach(api_get_user_id());
if (!empty($sessions)) {
$sessionIds = [];
foreach ($sessions as $session) {
$sessionIds[] = $session['id'];
}
$session_id_and = " AND te.session_id IN(".implode(',', $sessionIds).")";
$sessionCondition = " AND ttte.session_id IN(".implode(',', $sessionIds).")";
} else {
return false;
}
}
$exercise_where = '';
$exerciseFilter = '';
if (!empty($exercise_id)) {
@ -2303,7 +2432,7 @@ HOTSPOT;
// sql for chamilo-type tests for teacher / tutor view
$sql_inner_join_tbl_track_exercices = "
(
SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised
SELECT DISTINCT ttte.*, if(tr.exe_id,1, 0) as revised, tr.author as corrector, tr.insert_date as correction_date
FROM $TBL_TRACK_EXERCICES ttte
LEFT JOIN $TBL_TRACK_ATTEMPT_RECORDING tr
ON (ttte.exe_id = tr.exe_id)
@ -2448,7 +2577,9 @@ HOTSPOT;
group_name,
group_id,
orig_lp_id,
te.user_ip";
te.user_ip,
corrector,
correction_date";
}
$sql = " $sql_select
@ -2614,6 +2745,21 @@ HOTSPOT;
}
}
if (4 == $status && 2 != $revised) {
// Filter by status "unclosed"
continue;
}
if (5 == $status && 3 != $revised) {
// Filter by status "ongoing"
continue;
}
if (3 == $status && in_array($revised, [1, 2, 3])) {
// Filter by status "not validated"
continue;
}
if ($from_gradebook && ($is_allowedToEdit)) {
if (in_array(
$attempt['username'].$attempt['firstname'].$attempt['lastname'],
@ -2972,6 +3118,15 @@ HOTSPOT;
$attempt['session_access_start_date'] = $sessionStartAccessDate;
$attempt['status'] = $revisedLabel;
$attempt['score'] = $score;
$attempt['qualificator_fullname'] = '';
$attempt['date_of_qualification'] = '';
if (!empty($attempt['corrector'])) {
$qualificatorAuthor = api_get_user_info($attempt['corrector']);
$attempt['qualificator_fullname'] = api_get_person_name($qualificatorAuthor['firstname'], $qualificatorAuthor['lastname']);
}
if (!empty($attempt['correction_date'])) {
$attempt['date_of_qualification'] = api_convert_and_format_date($attempt['correction_date'], DATE_TIME_FORMAT_SHORT);
}
$attempt['score_percentage'] = self::show_score(
$my_res,
$my_total,

@ -564,8 +564,8 @@ class ExerciseShowFunctions
$course_id = api_get_course_int_id();
$new_options = Question::readQuestionOption($questionId, $course_id);
// Your choice
if (isset($new_options[$studentChoice])) {
$content .= get_lang($new_options[$studentChoice]['name']);
if (isset($new_options[$studentChoice - 1])) {
$content .= get_lang($new_options[$studentChoice - 1]['name']);
} else {
$content .= '-';
}
@ -576,8 +576,8 @@ class ExerciseShowFunctions
if ($exercise->showExpectedChoiceColumn()) {
if (!$hide_expected_answer) {
$content .= '<td width="5%">';
if (isset($new_options[$answerCorrect])) {
$content .= get_lang($new_options[$answerCorrect]['name']);
if (isset($new_options[$answerCorrect - 1])) {
$content .= get_lang($new_options[$answerCorrect - 1]['name']);
} else {
$content .= '-';
}
@ -605,7 +605,7 @@ class ExerciseShowFunctions
if (EXERCISE_FEEDBACK_TYPE_EXAM != $feedbackType) {
$content .= '<td width="20%">';
$color = 'black';
if (isset($new_options[$studentChoice]) || in_array(
if (isset($new_options[$studentChoice - 1]) || in_array(
$exercise->results_disabled,
[
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,

@ -171,7 +171,6 @@
});
this.model.on('destroy', function () {
self.el.remove();
self.model = null;
});
};
@ -240,7 +239,6 @@
});
this.model.on('destroy', function () {
self.el.remove();
self.model = null;
});
var elChoice = (function () {
@ -264,18 +262,18 @@
input.type = 'text';
input.className = 'form-control';
input.disabled = self.model instanceof SvgPathModel;
input.value = self.model instanceof SvgTextModel ? self.model.get('text') : '——————————';
return input;
})();
elText.addEventListener('change', function () {
commandsHistory.add(new TextElementCommand(self.model, this.value));
self.model.set('text', this.value);
})
var txtColor = (function () {
var input = document.createElement('input');
input.type = 'color';
input.value = self.model.get('color');
input.style.border = '0 none';
input.style.padding = '0';
input.style.margin = '0';
@ -287,6 +285,8 @@
return input;
})();
txtColor.addEventListener('change', function () {
commandsHistory.add(new ColorElementCommand(self.model, this.value));
self.model.set('color', this.value);
})
@ -302,7 +302,6 @@
var txtSize = (function () {
var input = document.createElement('input');
input.type = 'number';
input.value = self.model.get('fontSize');
input.step = '1';
input.min = '15';
input.max = '30';
@ -318,6 +317,8 @@
return input;
})();
txtSize.addEventListener('change', function () {
commandsHistory.add(new SizeElementCommand(self.model, this.value));
self.model.set('fontSize', this.value);
})
@ -330,28 +331,28 @@
})();
spanAddonSize.appendChild(txtSize);
var btnRemove = (function () {
var button = document.createElement('button');
button.type = 'button';
button.className = 'btn btn-default';
button.innerHTML = '<span class="fa fa-trash text-danger" aria-hidden="true"></span>';
return button;
})();
btnRemove.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
self.model.destroy();
});
var spanGroupBtn = (function () {
var span = document.createElement('span');
span.className = 'input-group-btn';
return span;
})();
spanGroupBtn.appendChild(btnRemove);
// var btnRemove = (function () {
// var button = document.createElement('button');
// button.type = 'button';
// button.className = 'btn btn-default';
// button.innerHTML = '<span class="fa fa-trash text-danger" aria-hidden="true"></span>';
//
// return button;
// })();
// btnRemove.addEventListener('click', function (e) {
// e.preventDefault();
// e.stopPropagation();
//
// self.model.destroy();
// });
//
// var spanGroupBtn = (function () {
// var span = document.createElement('span');
// span.className = 'input-group-btn';
//
// return span;
// })();
// spanGroupBtn.appendChild(btnRemove);
this.el = (function () {
var div = document.createElement('div');
@ -365,11 +366,14 @@
this.el.appendChild(elChoice);
this.el.appendChild(spanAddonColor);
this.el.appendChild(spanAddonSize);
this.el.appendChild(spanGroupBtn);
// this.el.appendChild(spanGroupBtn);
this.render = function () {
elChoice.value = this.model.encode();
elHotspot.value = this.model.encode();
elText.value = self.model instanceof SvgTextModel ? self.model.get('text') : '——————————';
txtColor.value = this.model.get('color');
txtSize.value = this.model.get('fontSize');
return this;
}
@ -475,10 +479,14 @@
var $rdbOptions = null;
var $btnReset = null;
var $btnUndo = null;
var $btnRedo = null;
this.render = function () {
$rdbOptions = $('[name="' + this.questionId + '-options"]');
$btnReset = $('#btn-reset-' + this.questionId);
$btnUndo = $('#btn-undo-' + this.questionId);
$btnRedo = $('#btn-redo-' + this.questionId);
setEvents();
@ -503,7 +511,11 @@
var point = getPointOnImage(self.el, e.clientX, e.clientY);
elementModel = new SvgTextModel({x: point.x, y: point.y, text: ''});
elementModel.questionId = self.questionId;
commandsHistory.add(new AddElementCommand(self.elementsCollection, elementModel));
self.elementsCollection.add(elementModel);
elementModel = null;
isMoving = false;
})
@ -537,6 +549,8 @@
return;
}
commandsHistory.add(new AddElementCommand(self.elementsCollection, elementModel));
elementModel = null;
isMoving = false;
});
@ -544,11 +558,183 @@
$btnReset.on('click', function (e) {
e.preventDefault();
commandsHistory.add(new ResetCommand(self.elementsCollection));
self.elementsCollection.reset();
});
$btnUndo.on('click', function () {
commandsHistory.undo();
});
$btnRedo.on('click', function () {
commandsHistory.redo();
});
}
};
/**
* @constructor
* @abstract
*/
function Command() {}
/**
* @abstract
*/
Command.prototype.before = function () {
throw new Error('Implement');
}
/**
* @abstract
*/
Command.prototype.after = function () {
throw new Error('Implement');
}
/**
* @param {ElementsCollection} collection
* @param {SvgElementModel} model
* @constructor
*/
function AddElementCommand(collection, model) {
Command.call(this);
this.collection = collection;
this.model = model;
}
AddElementCommand.prototype = Object.create(Command.prototype);
AddElementCommand.prototype.after = function () {
this.collection.add(this.model);
};
AddElementCommand.prototype.before = function () {
this.model.destroy();
};
/**
* @param {SvgElementModel} model
* @param {string} attribute
* @param {*} newValue
* @constructor
* @abstract
* @extends Command
*/
function ElementCommand(model, attribute, newValue) {
Command.call(this);
this.model = model;
this.attribute = attribute;
this.oldValue = this.model.get(this.attribute);
this.newValue = newValue;
}
ElementCommand.prototype = Object.create(Command.prototype);
ElementCommand.prototype.after = function () {
this.model.set(this.attribute, this.newValue);
};
ElementCommand.prototype.before = function () {
this.model.set(this.attribute, this.oldValue);
};
/**
* @param {SvgElementModel} model
* @param {*} newValue
* @constructor
* @extends ElementCommand
*/
function TextElementCommand(model, newValue) {
ElementCommand.call(this, model, 'text', newValue);
}
TextElementCommand.prototype = Object.create(ElementCommand.prototype);
/**
* @param {SvgElementModel} model
* @param {*} newValue
* @constructor
* @extends ElementCommand
*/
function ColorElementCommand(model, newValue) {
ElementCommand.call(this, model, 'color', newValue);
}
ColorElementCommand.prototype = Object.create(ElementCommand.prototype);
/**
* @param {SvgElementModel} model
* @param {*} newValue
* @constructor
* @extends ElementCommand
*/
function SizeElementCommand(model, newValue) {
ElementCommand.call(this, model, 'fontSize', newValue);
}
SizeElementCommand.prototype = Object.create(ElementCommand.prototype);
/**
*
* @param {ElementsCollection} collection
* @constructor
* @extends Command
*/
function ResetCommand(collection) {
Command.call(this);
this.collection = collection;
this.oldModels = collection.models;
}
ResetCommand.prototype = Object.create(Command.prototype);
ResetCommand.prototype.after = function () {
this.collection.reset();
};
ResetCommand.prototype.before = function () {
var self = this;
this.oldModels.forEach(function (model) {
self.collection.add(model);
});
};
function CommandHistory() {
var index = -1;
/**
* @type {Command[]}
*/
var commands = [];
/**
* @param {Command} command
*/
this.add = function (command) {
if (index > -1) {
commands = commands.slice(0, index + 1);
} else {
commands = [];
}
commands.push(command);
++index;
}
this.undo = function () {
(commands, index);
if (-1 === index) {
return;
}
var command = commands[index];
command.before();
--index;
};
this.redo = function () {
if (index + 1 === commands.length) {
return;
}
++index;
var command = commands[index];
command.after();
};
};
var commandsHistory = new CommandHistory();
window.AnnotationQuestion = function (userSettings) {
$(function () {
var settings = $.extend(

@ -539,9 +539,10 @@ CKEDITOR.dialog.add( 'image2_chamilo', function( editor ) {
id: 'isResponsive',
type: 'checkbox',
label: lang.responsive,
'default' : editor.config.image_responsive != null ? editor.config.image_responsive : false,
requiredContent: features.responsive.requiredContent,
setup: function ( widget ) {
this.setValue( widget.data.isResponsive );
//this.setValue( widget.data.isResponsive );
},
commit: function ( widget ) {
var img = widget;

@ -1681,10 +1681,9 @@ class Link extends Model
}
}
$skillList = Skill::addSkillsToForm($form, ITEM_TYPE_LINK, $linkId);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_LINK, $linkId);
$form->addHidden('lp_id', $lpId);
$form->addButtonSave(get_lang('SaveLink'), 'submitLink');
$defaults['skills'] = array_keys($skillList);
$form->setDefaults($defaults);
return $form;

@ -140,13 +140,15 @@ class SessionManager
* @param mixed $coachId If int, this is the session coach id,
* if string, the coach ID will be looked for from the user table
* @param int $sessionCategoryId ID of the session category in which this session is registered
* @param int $visibility Visibility after end date (0 = read-only, 1 = invisible, 2 = accessible)
* @param int $visibility Visibility after end date (0 = read-only, 1 = invisible, 2 =
* accessible)
* @param bool $fixSessionNameIfExists
* @param string $duration
* @param string $description Optional. The session description
* @param int $showDescription Optional. Whether show the session description
* @param array $extraFields
* @param int $sessionAdminId Optional. If this sessions was created by a session admin, assign it to him
* @param int $sessionAdminId Optional. If this sessions was created by a session admin, assign it
* to him
* @param bool $sendSubscriptionNotification Optional.
* Whether send a mail notification to users being subscribed
* @param int $accessUrlId Optional.
@ -1531,8 +1533,9 @@ class SessionManager
*
* @param string $session_name
* <code>
* $wanted_code = 'curse' if there are in the DB codes like curse1 curse2 the function will return: course3
* if the course code doest not exist in the DB the same course code will be returned
* $wanted_code = 'curse' if there are in the DB codes like curse1 curse2 the function
* will return: course3 if the course code doest not exist in the DB the same course
* code will be returned
* </code>
*
* @return string wanted unused code
@ -2558,6 +2561,8 @@ class SessionManager
}
}
$em = Database::getManager();
// Pass through the courses list we want to add to the session
foreach ($courseList as $courseId) {
$courseInfo = api_get_course_info_by_id($courseId);
@ -2709,6 +2714,21 @@ class SessionManager
VALUES ($sessionId, $courseId, 0, 0)";
Database::query($sql);
if (api_get_configuration_value('allow_skill_rel_items')) {
$skillRelCourseRepo = $em->getRepository('ChamiloSkillBundle:SkillRelCourse');
$items = $skillRelCourseRepo->findBy(['course' => $courseId, 'session' => null]);
/** @var \Chamilo\SkillBundle\Entity\SkillRelCourse $item */
foreach ($items as $item) {
$exists = $skillRelCourseRepo->findOneBy(['course' => $courseId, 'session' => $session]);
if (null === $exists) {
$skillRelCourse = clone $item;
$skillRelCourse->setSession($session);
$em->persist($skillRelCourse);
}
}
$em->flush();
}
Event::addEvent(
LOG_SESSION_ADD_COURSE,
LOG_COURSE_ID,
@ -4871,11 +4891,15 @@ class SessionManager
/**
* @param string $file
* @param bool $updateSession true: if the session exists it will be updated.
* false: if session exists a new session will be created adding a counter session1, session2, etc
* false: if session exists a new session will be
* created adding a counter session1, session2, etc
* @param int $defaultUserId
* @param Logger $logger
* @param array $extraFields convert a file row to an extra field. Example in CSV file there's a SessionID
* then it will converted to extra_external_session_id if you set: array('SessionId' => 'extra_external_session_id')
* @param array $extraFields convert a file row to an extra field. Example in
* CSV file there's a SessionID then it will
* converted to extra_external_session_id if you
* set: array('SessionId' =>
* 'extra_external_session_id')
* @param string $extraFieldId
* @param int $daysCoachAccessBeforeBeginning
* @param int $daysCoachAccessAfterBeginning

@ -2492,7 +2492,7 @@ class Skill extends Model
}
$type = 'success';
if (empty($skillRelItemRelUser)) {
$type = 'danger';
$type = '';
}
$label = '';
$skill = $skillRelItem->getSkill();
@ -2585,48 +2585,50 @@ class Skill extends Model
/**
* Add skills select ajax for an item (exercise, lp).
*
* @param int $typeId see ITEM_TYPE_* constants
* @param int $courseId
* @param int $sessionId
* @param int $typeId see ITEM_TYPE_* constants
* @param int $itemId
*
* @throws Exception
*
* @return array
*/
public static function addSkillsToForm(FormValidator $form, $typeId, $itemId = 0)
public static function addSkillsToForm(FormValidator $form, $courseId, $sessionId, $typeId, $itemId = 0)
{
$allowSkillInTools = api_get_configuration_value('allow_skill_rel_items');
if (!$allowSkillInTools) {
return [];
}
$skillList = [];
if (empty($sessionId)) {
$sessionId = null;
}
$em = Database::getManager();
$skillRelCourseRepo = $em->getRepository('ChamiloSkillBundle:SkillRelCourse');
$items = $skillRelCourseRepo->findBy(['course' => $courseId, 'session' => $sessionId]);
$skills = [];
/** @var \Chamilo\SkillBundle\Entity\SkillRelCourse $skillRelCourse */
foreach ($items as $skillRelCourse) {
$skills[] = $skillRelCourse->getSkill();
}
$selectedSkills = [];
if (!empty($itemId)) {
$em = Database::getManager();
$items = $em->getRepository('ChamiloSkillBundle:SkillRelItem')->findBy(
['itemId' => $itemId, 'itemType' => $typeId]
);
/** @var SkillRelItem $skillRelItem */
foreach ($items as $skillRelItem) {
$skillList[$skillRelItem->getSkill()->getId()] = $skillRelItem->getSkill()->getName();
$selectedSkills[] = $skillRelItem->getSkill()->getId();
}
}
$courseId = api_get_course_int_id();
$sessionId = api_get_session_id();
self::skillsToCheckbox($form, $skills, $courseId, $sessionId, $selectedSkills);
$url = api_get_path(WEB_AJAX_PATH).
'skill.ajax.php?a=search_skills_in_course&course_id='.$courseId.'&session_id='.$sessionId;
$form->addSelectAjax(
'skills',
get_lang('Skills'),
$skillList,
[
'url' => $url,
'multiple' => 'multiple',
]
);
return $skillList;
return $skills;
}
/**
@ -2794,70 +2796,80 @@ class Skill extends Model
}
}
/**
* Relate skill with an item (exercise, gradebook, lp, etc).
*
* @param FormValidator $form
* @param int $typeId
* @param int $itemId
*
* @throws \Doctrine\ORM\OptimisticLockException
*/
public static function saveSkills($form, $typeId, $itemId)
public static function setSkillsToCourse(FormValidator $form, $courseId, $sessionId = 0)
{
$allowSkillInTools = api_get_configuration_value('allow_skill_rel_items');
if ($allowSkillInTools) {
$userId = api_get_user_id();
$courseId = api_get_course_int_id();
if (empty($courseId)) {
$courseId = null;
}
$sessionId = api_get_session_id();
if (empty($sessionId)) {
$sessionId = null;
}
$courseId = (int) $courseId;
$sessionId = (int) $sessionId;
$em = Database::getManager();
$skills = (array) $form->getSubmitValue('skills');
$form->addHidden('course_id', $courseId);
$form->addHidden('session_id', $sessionId);
// Delete old ones
$items = $em->getRepository('ChamiloSkillBundle:SkillRelItem')->findBy(
['itemId' => $itemId, 'itemType' => $typeId]
if (empty($sessionId)) {
$sessionId = null;
}
$em = Database::getManager();
$skillRelCourseRepo = $em->getRepository('ChamiloSkillBundle:SkillRelCourse');
$items = $skillRelCourseRepo->findBy(['course' => $courseId, 'session' => $sessionId]);
$skillsIdList = [];
$skills = [];
/** @var \Chamilo\SkillBundle\Entity\SkillRelCourse $skillRelCourse */
foreach ($items as $skillRelCourse) {
$skillId = $skillRelCourse->getSkill()->getId();
$skills[] = $skillRelCourse->getSkill();
$skillsIdList[] = $skillId;
}
$group = self::skillsToCheckbox($form, $skills, $courseId, $sessionId, $skillsIdList);
$group->freeze();
return [];
}
public static function skillsToCheckbox(FormValidator $form, $skills, $courseId, $sessionId, $selectedSkills = [])
{
$em = Database::getManager();
$skillRelItemRepo = $em->getRepository('ChamiloSkillBundle:SkillRelItem');
$skillList = [];
/** @var \Chamilo\CoreBundle\Entity\Skill $skill */
foreach ($skills as $skill) {
$skillList[$skill->getId()] = $skill->getName();
}
if (!empty($skillList)) {
asort($skillList);
}
if (empty($sessionId)) {
$sessionId = null;
}
$elements = [];
foreach ($skillList as $skillId => $skill) {
$countLabel = '';
$skillRelItemCount = $skillRelItemRepo->count(
['skill' => $skillId, 'courseId' => $courseId, 'sessionId' => $sessionId]
);
if (!empty($items)) {
/** @var SkillRelItem $skillRelItem */
foreach ($items as $skillRelItem) {
if (!in_array($skillRelItem->getSkill()->getId(), $skills)) {
$em->remove($skillRelItem);
}
}
$em->flush();
if (!empty($skillRelItemCount)) {
$countLabel = '&nbsp;'.Display::badge($skillRelItemCount, 'info');
}
// Add new one
if (!empty($skills)) {
foreach ($skills as $skillId) {
/** @var SkillEntity $skill */
$skill = $em->getRepository('ChamiloCoreBundle:Skill')->find($skillId);
if ($skill) {
if (!$skill->hasItem($typeId, $itemId)) {
$skillRelItem = new SkillRelItem();
$skillRelItem
->setItemType($typeId)
->setItemId($itemId)
->setCourseId($courseId)
->setSessionId($sessionId)
->setCreatedBy($userId)
->setUpdatedBy($userId)
;
$skill->addItem($skillRelItem);
$em->persist($skill);
$em->flush();
}
}
}
$element = $form->createElement(
'checkbox',
"skills[$skillId]",
null,
$skill.$countLabel
);
if (in_array($skillId, $selectedSkills)) {
$element->setValue(1);
}
$elements[] = $element;
}
return $form->addGroup($elements, '', get_lang('Skills'));
}
/**
@ -2869,7 +2881,11 @@ class Skill extends Model
{
$skills = (array) $form->getSubmitValue('skills');
$courseId = (int) $form->getSubmitValue('course_id');
$sessionId = $form->getSubmitValue('session_id');
$sessionId = (int) $form->getSubmitValue('session_id');
if (!empty($skills)) {
$skills = array_keys($skills);
}
return self::saveSkillsToCourse($skills, $courseId, $sessionId);
}
@ -2879,8 +2895,6 @@ class Skill extends Model
* @param int $courseId
* @param int $sessionId
*
* @throws \Doctrine\ORM\OptimisticLockException
*
* @return bool
*/
public static function saveSkillsToCourse($skills, $courseId, $sessionId)
@ -2897,6 +2911,7 @@ class Skill extends Model
if (empty($course)) {
return false;
}
$session = null;
if (!empty($sessionId)) {
$session = api_get_session_entity($sessionId);
@ -2924,9 +2939,10 @@ class Skill extends Model
// Add new one
if (!empty($skills)) {
foreach ($skills as $skillId) {
$item = new SkillRelCourse();
$item->setCourse($course);
$item->setSession($session);
$item = (new SkillRelCourse())
->setCourse($course)
->setSession($session)
;
/** @var SkillEntity $skill */
$skill = $em->getRepository('ChamiloCoreBundle:Skill')->find($skillId);
@ -2943,6 +2959,96 @@ class Skill extends Model
return true;
}
/**
* Relate skill with an item (exercise, gradebook, lp, etc).
*
* @param FormValidator $form
* @param int $typeId
* @param int $itemId
*/
public static function saveSkills($form, $typeId, $itemId)
{
$allowSkillInTools = api_get_configuration_value('allow_skill_rel_items');
if ($allowSkillInTools) {
$userId = api_get_user_id();
$courseId = api_get_course_int_id();
if (empty($courseId)) {
$courseId = null;
}
$sessionId = api_get_session_id();
if (empty($sessionId)) {
$sessionId = null;
}
$em = Database::getManager();
$skills = (array) $form->getSubmitValue('skills');
$skillRelItemRelUserRepo = $em->getRepository('ChamiloSkillBundle:SkillRelItemRelUser');
// Delete old ones
$items = $em->getRepository('ChamiloSkillBundle:SkillRelItem')->findBy(
['itemId' => $itemId, 'itemType' => $typeId]
);
if (!empty($items)) {
/** @var SkillRelItem $skillRelItem */
foreach ($items as $skillRelItem) {
$skill = $skillRelItem->getSkill();
$skillId = $skill->getId();
$skillRelItemId = $skillRelItem->getId();
if (!in_array($skillId, $skills)) {
// Check if SkillRelItemRelUser is empty
/** @var SkillRelItem[] $skillRelItemList */
$skillRelItemRelUserList = $skillRelItemRelUserRepo->findBy(['skillRelItem' => $skillRelItemId]);
if (empty($skillRelItemRelUserList)) {
$em->remove($skillRelItem);
} else {
/** @var \Chamilo\SkillBundle\Entity\SkillRelItemRelUser $skillRelItemRelUser */
foreach ($skillRelItemRelUserList as $skillRelItemRelUser) {
Display::addFlash(
Display::return_message(
get_lang('CannotDeleteSkillBlockedByUser').'<br />'.
get_lang('User').': '.UserManager::formatUserFullName($skillRelItemRelUser->getUser()).'<br />'.
get_lang('Skill').': '.$skillRelItemRelUser->getSkillRelItem()->getSkill()->getName(),
'warning',
false
)
);
}
}
}
}
$em->flush();
}
// Add new one
if (!empty($skills)) {
$skills = array_keys($skills);
$skillRepo = $em->getRepository('ChamiloCoreBundle:Skill');
foreach ($skills as $skillId) {
/** @var SkillEntity $skill */
$skill = $skillRepo->find($skillId);
if (null !== $skill) {
if (!$skill->hasItem($typeId, $itemId)) {
$skillRelItem = (new SkillRelItem())
->setItemType($typeId)
->setItemId($itemId)
->setCourseId($courseId)
->setSessionId($sessionId)
->setCreatedBy($userId)
->setUpdatedBy($userId)
;
$skill->addItem($skillRelItem);
$em->persist($skill);
$em->flush();
}
}
}
}
}
}
/**
* Get the icon (badge image) URL.
*

@ -121,6 +121,457 @@ class Tracking
return $parsedResult;
}
/**
* It gets table html of Lp stats used to export in pdf.
*
* @param $userId
* @param $courseInfo
* @param $sessionId
* @param $lpId
*
* @return string
*/
public static function getLpStatsContentToPdf(
$userId,
$courseInfo,
$sessionId,
$lpId,
$lpName
) {
$hideTime = api_get_configuration_value('hide_lp_time');
$lpId = (int) $lpId;
$userId = (int) $userId;
$sessionId = (int) $sessionId;
$courseId = $courseInfo['real_id'];
$isAllowedToEdit = api_is_allowed_to_edit(null, true);
$sessionCondition = api_get_session_condition($sessionId);
$counter = 0;
$totalTime = 0;
$h = get_lang('h');
$resultDisabledExtAll = true;
$timeHeader = '<th>'.get_lang('ScormTime').'</th>';
if ($hideTime) {
$timeHeader = '';
}
$output = '<h2 class="clearfix text-center">'.$lpName.'</h2>';
$output .= '<table class="table table-hover table-striped data_table">
<thead>
<tr>
<th>'.get_lang('ScormLessonTitle').'</th>
<th>'.get_lang('ScormStatus').'</th>
<th>'.get_lang('ScormScore').'</th>
'.$timeHeader.'
</tr>
</thead>
<tbody>
';
$tblLpItem = Database::get_course_table(TABLE_LP_ITEM);
$tblLpItemView = Database::get_course_table(TABLE_LP_ITEM_VIEW);
$tblLpView = Database::get_course_table(TABLE_LP_VIEW);
$tblQuizQuestions = Database::get_course_table(TABLE_QUIZ_QUESTION);
$tblQuiz = Database::get_course_table(TABLE_QUIZ_TEST);
$tblStatsExercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$tblStatsAttempts = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
// it gets the max view
$sql = "SELECT max(view_count) FROM $tblLpView WHERE c_id = $courseId AND lp_id = $lpId AND user_id = $userId $sessionCondition";
$res = Database::query($sql);
$view = 0;
$viewCondition = "";
if (Database::num_rows($res) > 0) {
$view = Database::result($res, 0, 0);
$viewCondition = " AND v.view_count = ".(int) $view;
}
$chapterTypes = learnpath::getChapterTypes();
$minimumAvailable = self::minimumTimeAvailable($sessionId, $courseId);
$list = learnpath::get_flat_ordered_items_list($lpId, 0, $courseId);
if (is_array($list) && count($list) > 0) {
foreach ($list as $myItemId) {
$sql = "SELECT
iv.status as mystatus,
v.view_count as mycount,
iv.score as myscore,
iv.total_time as mytime,
i.iid as myid,
i.lp_id as mylpid,
iv.lp_view_id as mylpviewid,
i.title as mytitle,
i.max_score as mymaxscore,
iv.max_score as myviewmaxscore,
i.item_type as item_type,
iv.view_count as iv_view_count,
iv.id as iv_id,
path
FROM $tblLpItem as i
INNER JOIN $tblLpItemView as iv
ON (i.iid = iv.lp_item_id AND i.c_id = iv.c_id)
INNER JOIN $tblLpView as v
ON (iv.lp_view_id = v.id AND v.c_id = iv.c_id)
WHERE
v.c_id = $courseId AND
i.iid = $myItemId AND
i.lp_id = $lpId AND
v.user_id = $userId AND
v.session_id = $sessionId
$viewCondition
ORDER BY iv.view_count";
$result = Database::query($sql);
$num = Database::num_rows($result);
$timeForTotal = 0;
if ($num > 0) {
// Not extended.
$row = Database::fetch_array($result, 'ASSOC');
$myId = $row['myid'];
$myLpId = $row['mylpid'];
$myLpViewId = $row['mylpviewid'];
$lpItemPath = (int) $row['path'];
$resultDisabledExtAll = false;
if ($row['item_type'] === 'quiz') {
// Check results_disabled in quiz table.
$sql = "SELECT results_disabled
FROM $tblQuiz
WHERE iid = $lpItemPath";
$resResultDisabled = Database::query($sql);
$rowResultDisabled = Database::fetch_row($resResultDisabled);
if (Database::num_rows($resResultDisabled) > 0 && 1 === (int) $rowResultDisabled[0]) {
$resultDisabledExtAll = true;
}
}
// Check if there are interactions below
$extendThisAttempt = 0;
$interNum = learnpath::get_interactions_count_from_db($row['iv_id'], $courseId);
$objecNum = learnpath::get_objectives_count_from_db($row['iv_id'], $courseId);
if ($interNum > 0 || $objecNum > 0) {
$extendThisAttempt = 1;
}
$lesson_status = $row['mystatus'];
$score = $row['myscore'];
$subtotalTime = $row['mytime'];
while ($tmp_row = Database::fetch_array($result)) {
$subtotalTime += $tmp_row['mytime'];
}
$title = $row['mytitle'];
// Selecting the exe_id from stats attempts tables in order to look the max score value.
$sql = 'SELECT * FROM '.$tblStatsExercises.'
WHERE
exe_exo_id="'.$row['path'].'" AND
exe_user_id="'.$userId.'" AND
orig_lp_id = "'.$lpId.'" AND
orig_lp_item_id = "'.$row['myid'].'" AND
c_id = '.$courseId.' AND
status <> "incomplete" AND
session_id = '.$sessionId.'
ORDER BY exe_date DESC
LIMIT 1';
$resultLastAttempt = Database::query($sql);
$num = Database::num_rows($resultLastAttempt);
$idLastAttempt = null;
if ($num > 0) {
while ($rowLA = Database::fetch_array($resultLastAttempt)) {
$idLastAttempt = $rowLA['exe_id'];
}
}
switch ($row['item_type']) {
case 'sco':
if (!empty($row['myviewmaxscore']) && $row['myviewmaxscore'] > 0) {
$maxscore = $row['myviewmaxscore'];
} elseif ($row['myviewmaxscore'] === '') {
$maxscore = 0;
} else {
$maxscore = $row['mymaxscore'];
}
break;
case 'quiz':
// Get score and total time from last attempt of a exercise en lp.
$sql = "SELECT iid, score
FROM $tblLpItemView
WHERE
c_id = $courseId AND
lp_item_id = '".(int) $myId."' AND
lp_view_id = '".(int) $myLpViewId."'
ORDER BY view_count DESC
LIMIT 1";
$resScore = Database::query($sql);
$rowScore = Database::fetch_array($resScore);
$sql = "SELECT SUM(total_time) as total_time
FROM $tblLpItemView
WHERE
c_id = $courseId AND
lp_item_id = '".(int) $myId."' AND
lp_view_id = '".(int) $myLpViewId."'";
$resTime = Database::query($sql);
$rowTime = Database::fetch_array($resTime);
$score = 0;
$subtotalTime = 0;
if (Database::num_rows($resScore) > 0 && Database::num_rows($resTime) > 0) {
$score = (float) $rowScore['score'];
$subtotalTime = (int) $rowTime['total_time'];
}
// Selecting the max score from an attempt.
$sql = "SELECT SUM(t.ponderation) as maxscore
FROM (
SELECT DISTINCT
question_id, marks, ponderation
FROM $tblStatsAttempts as at
INNER JOIN $tblQuizQuestions as q
ON q.iid = at.question_id
WHERE exe_id ='$idLastAttempt'
) as t";
$result = Database::query($sql);
$rowMaxScore = Database::fetch_array($result);
$maxscore = $rowMaxScore['maxscore'];
// Get duration time from track_e_exercises.exe_duration instead of lp_view_item.total_time
$sql = 'SELECT SUM(exe_duration) exe_duration
FROM '.$tblStatsExercises.'
WHERE
exe_exo_id="'.$row['path'].'" AND
exe_user_id="'.$userId.'" AND
orig_lp_id = "'.$lpId.'" AND
orig_lp_item_id = "'.$row['myid'].'" AND
c_id = '.$courseId.' AND
status <> "incomplete" AND
session_id = '.$sessionId.'
ORDER BY exe_date DESC ';
$sumScoreResult = Database::query($sql);
$durationRow = Database::fetch_array($sumScoreResult, 'ASSOC');
if (!empty($durationRow['exe_duration'])) {
$exeDuration = $durationRow['exe_duration'];
if ($exeDuration != $subtotalTime && !empty($rowScore['iid']) && !empty($exeDuration)) {
$subtotalTime = $exeDuration;
// Update c_lp_item_view.total_time
$sqlUpdate = "UPDATE $tblLpItemView SET total_time = '$exeDuration' WHERE iid = ".$rowScore['iid'];
Database::query($sqlUpdate);
}
}
break;
default:
$maxscore = $row['mymaxscore'];
break;
}
$timeForTotal = $subtotalTime;
$time = learnpathItem::getScormTimeFromParameter('js', $subtotalTime);
if (empty($title)) {
$title = learnpath::rl_get_resource_name(
$courseInfo['code'],
$lpId,
$row['myid']
);
}
if (in_array($row['item_type'], $chapterTypes)) {
$title = Security::remove_XSS($title);
$output .= '<tr>
<td colspan="4"><h4>'.$title.'</h4></td>
</tr>';
} else {
if ('quiz' === $row['item_type']) {
$sql = 'SELECT * FROM '.$tblStatsExercises.'
WHERE
exe_exo_id="'.$row['path'].'" AND
exe_user_id="'.$userId.'" AND
orig_lp_id = "'.$lpId.'" AND
orig_lp_item_id = "'.$row['myid'].'" AND
c_id = '.$courseId.' AND
status <> "incomplete" AND
session_id = '.$sessionId.'
ORDER BY exe_date DESC ';
$resultLastAttempt = Database::query($sql);
$num = Database::num_rows($resultLastAttempt);
}
$title = Security::remove_XSS($title);
if ($lpId == $myLpId && false) {
$output .= '<tr>
<td>'.$title.'</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>';
} else {
$output .= "<tr>";
$scoreItem = null;
if ($row['item_type'] === 'quiz') {
$scoreItem .= ExerciseLib::show_score($score, $maxscore, false);
} else {
$scoreItem .= $score == 0 ? '/' : ($maxscore == 0 ? $score : $score.'/'.$maxscore);
}
$timeRow = '<td>'.$time.'</td>';
if ($hideTime) {
$timeRow = '';
}
$output .= '
<td>'.$title.'</td>
<td>'.learnpathitem::humanize_status($lesson_status).'</td>
<td>'.$scoreItem.'</td>
'.$timeRow.'
';
$output .= '</tr>';
}
}
$counter++;
if ($extendThisAttempt) {
$list1 = learnpath::get_iv_interactions_array($row['iv_id'], $courseId);
foreach ($list1 as $id => $interaction) {
$timeRow = '<td>'.$interaction['time'].'</td>';
if ($hideTime) {
$timeRow = '';
}
$output .= '<tr>
<td>'.$interaction['order_id'].'</td>
<td>'.$interaction['id'].'</td>
<td>'.$interaction['type'].'</td>
<td>'.urldecode($interaction['student_response']).'</td>
<td>'.$interaction['result'].'</td>
<td>'.$interaction['latency'].'</td>
'.$timeRow.'
</tr>';
$counter++;
}
$list2 = learnpath::get_iv_objectives_array($row['iv_id'], $courseId);
foreach ($list2 as $id => $interaction) {
$output .= '<tr>
<td>'.$interaction['order_id'].'</td>
<td>'.$interaction['objective_id'].'</td>
<td>'.$interaction['status'].'</td>
<td>'.$interaction['score_raw'].'</td>
<td>'.$interaction['score_max'].'</td>
<td>'.$interaction['score_min'].'</td>
</tr>';
$counter++;
}
}
// Attempts listing by exercise.
if ($lpId == $myLpId) {
// Get attempts of a exercise.
if (!empty($lpId) && 'quiz' === $row['item_type']) {
$sql = "SELECT path FROM $tblLpItem
WHERE
c_id = $courseId AND
lp_id = '$lpId'";
$resPath = Database::query($sql);
$rowPath = Database::fetch_array($resPath);
if (Database::num_rows($resPath) > 0) {
$sql = 'SELECT * FROM '.$tblStatsExercises.'
WHERE
exe_exo_id="'.(int) $rowPath['path'].'" AND
status <> "incomplete" AND
exe_user_id="'.$userId.'" AND
orig_lp_id = "'.$lpId.'" AND
orig_lp_item_id = "'.$myItemId.'" AND
c_id = '.$courseId.' AND
session_id = '.$sessionId.'
ORDER BY exe_date';
$resAttempts = Database::query($sql);
if (Database::num_rows($resAttempts) > 0) {
$n = 1;
while ($rowAttempts = Database::fetch_array($resAttempts)) {
$myScore = $rowAttempts['exe_result'];
$myMaxscore = $rowAttempts['exe_weighting'];
$mktimeStartDate = api_strtotime($rowAttempts['start_date'], 'UTC');
$mktimeExeDate = api_strtotime($rowAttempts['exe_date'], 'UTC');
$timeAttempt = ' - ';
if ($mktimeStartDate && $mktimeExeDate) {
$timeAttempt = api_format_time($rowAttempts['exe_duration'], 'js');
}
// Show only float when need it
if ($myScore == 0) {
$viewScore = ExerciseLib::show_score(
0,
$myMaxscore,
false
);
} else {
if ($myMaxscore == 0) {
$viewScore = $myScore;
} else {
$viewScore = ExerciseLib::show_score(
$myScore,
$myMaxscore,
false
);
}
}
$myLessonStatus = $rowAttempts['status'];
if ($myLessonStatus == '') {
$myLessonStatus = learnpathitem::humanize_status('completed');
} elseif ($myLessonStatus == 'incomplete') {
$myLessonStatus = learnpathitem::humanize_status('incomplete');
}
$timeRow = '<td>'.$timeAttempt.'</td>';
if ($hideTime) {
$timeRow = '';
}
$output .= '<tr>
<td><em>'.get_lang('Attempt').' '.$n.'</em></td>
<td>'.$myLessonStatus.'</td>
<td>'.$viewScore.'</td>
'.$timeRow;
$output .= '</tr>';
$n++;
}
}
}
}
}
}
$totalTime += $timeForTotal;
}
}
$totalScore = self::get_avg_student_score(
$userId,
$courseId,
[$lpId],
$sessionId,
false,
false
);
$totalTime = learnpathItem::getScormTimeFromParameter('js', $totalTime);
$totalTime = str_replace('NaN', '00'.$h.'00\'00"', $totalTime);
if (!$isAllowedToEdit && $resultDisabledExtAll) {
$finalScore = Display::return_icon('invisible.png', get_lang('ResultsHiddenByExerciseSetting'));
} else {
if (is_numeric($totalScore)) {
$finalScore = $totalScore.'%';
} else {
$finalScore = $totalScore;
}
}
$progress = learnpath::getProgress($lpId, $userId, $courseId, $sessionId);
$timeTotal = '<th>'.$totalTime.'</th>';
if ($hideTime) {
$timeTotal = '';
}
$output .= '<tr>
<th><i>'.get_lang('AccomplishedStepsTotal').'</i></th>
<th>'.$progress.'%</th>
<th>'.$finalScore.'</th>
'.$timeTotal.'
</tr></tbody></table>';
return $output;
}
/**
* @param int $user_id
* @param array $courseInfo
@ -368,16 +819,6 @@ class Tracking
$result = Database::query($sql);
$num = Database::num_rows($result);
$time_for_total = 0;
$attemptResult = 0;
if ($timeCourse) {
if (isset($timeCourse['learnpath_detailed']) &&
isset($timeCourse['learnpath_detailed'][$lp_id]) &&
isset($timeCourse['learnpath_detailed'][$lp_id][$my_item_id])
) {
$attemptResult = $timeCourse['learnpath_detailed'][$lp_id][$my_item_id][$view];
}
}
// Extend all
if (($extend_this || $extend_all) && $num > 0) {

@ -37,8 +37,10 @@ class Rest extends WebService
const GET_USER_COURSES = 'user_courses';
const GET_USER_SESSIONS = 'user_sessions';
const VIEW_PROFILE = 'view_user_profile';
const GET_PROFILE = 'user_profile';
const VIEW_COURSE_HOME = 'view_course_home';
const GET_COURSE_INFO = 'course_info';
const GET_COURSE_DESCRIPTIONS = 'course_descriptions';
const GET_COURSE_DOCUMENTS = 'course_documents';
@ -86,8 +88,10 @@ class Rest extends WebService
const GET_USERS = 'get_users';
const USERNAME_EXIST = 'username_exist';
const SAVE_USER = 'save_user';
const SAVE_USER_GET_APIKEY = 'save_user_get_apikey';
const SAVE_USER_JSON = 'save_user_json';
const UPDATE_USER_FROM_USERNAME = 'update_user_from_username';
const UPDATE_USER_APIKEY = 'update_user_apikey';
const DELETE_USER = 'delete_user';
const GET_COURSES = 'get_courses';
@ -399,6 +403,8 @@ class Rest extends WebService
$courses = CourseManager::get_courses_list_by_user_id($userId);
$data = [];
$webCodePath = api_get_path(WEB_CODE_PATH).'webservices/api/v2.php?';
foreach ($courses as $courseInfo) {
/** @var Course $course */
$course = Database::getManager()->find('ChamiloCoreBundle:Course', $courseInfo['real_id']);
@ -414,6 +420,14 @@ class Rest extends WebService
'urlPicture' => $picturePath,
'teachers' => $teachers,
'isSpecial' => !empty($courseInfo['special_course']),
'url' => $webCodePath.http_build_query(
[
'action' => self::VIEW_COURSE_HOME,
'api_key' => $this->apiKey,
'username' => $this->user->getUsername(),
'course' => $course->getId(),
]
),
];
}
@ -1187,6 +1201,8 @@ class Rest extends WebService
$data = [];
$sessionsByCategory = UserManager::get_sessions_by_category($this->user->getId(), false);
$webCodePath = api_get_path(WEB_CODE_PATH).'webservices/api/v2.php?';
foreach ($sessionsByCategory as $category) {
$categorySessions = [];
@ -1208,6 +1224,15 @@ class Rest extends WebService
'pictureUrl' => $courseInfo['course_image_large'],
'urlPicture' => $courseInfo['course_image_large'],
'teachers' => $teachers,
'url' => $webCodePath.http_build_query(
[
'action' => self::VIEW_COURSE_HOME,
'api_key' => $this->apiKey,
'username' => $this->user->getUsername(),
'course' => $courseInfo['real_id'],
'session' => $sessions['session_id'],
]
),
];
}
@ -1612,6 +1637,45 @@ class Rest extends WebService
return [$userId];
}
/**
* @throws Exception
*/
public function addUserGetApikey(array $userParams): array
{
list($userId) = $this->addUser($userParams);
UserManager::add_api_key($userId, self::SERVICE_NAME);
$apiKey = UserManager::get_api_keys($userId, self::SERVICE_NAME);
return [
'id' => $userId,
'api_key' => current($apiKey),
];
}
/**
* @throws Exception
*/
public function updateUserApiKey(int $userId, string $oldApiKey): array
{
if (false === $currentApiKeys = UserManager::get_api_keys($userId, self::SERVICE_NAME)) {
throw new Exception(get_lang('NotAllowed'));
}
if (current($currentApiKeys) !== $oldApiKey) {
throw new Exception(get_lang('NotAllowed'));
}
UserManager::update_api_key($userId, self::SERVICE_NAME);
$apiKey = UserManager::get_api_keys($userId, self::SERVICE_NAME);
return [
'api_key' => current($apiKey),
];
}
/**
* Subscribe User to Course.
*
@ -2876,6 +2940,26 @@ class Rest extends WebService
);
}
public function viewUserProfile(int $userId)
{
$url = api_get_path(WEB_CODE_PATH).'social/profile.php';
if ($userId) {
$url .= '?'.http_build_query(['u' => $userId]);
}
header("Location: $url");
exit;
}
public function viewCourseHome()
{
$url = api_get_course_url($this->course->getCode(), $this->session ? $this->session->getId() : 0);
header("Location: $url");
exit;
}
public function viewDocumentInFrame(int $documentId)
{
$courseCode = $this->course->getCode();

@ -338,15 +338,67 @@ if (!empty($_SESSION['_user']['user_id']) && !($login || $logout)) {
break;
}
}
// $login is set and the user exists in the database
// Check if the account is active (not locked)
if ($_user['active'] == '1') {
// Check if the expiration date has not been reached
if ($_user['expiration_date'] > date('Y-m-d H:i:s')
|| empty($_user['expiration_date'])
) {
global $_configuration;
if (api_is_multiple_url_enabled()) {
// Check if user is an admin
$my_user_is_admin = UserManager::is_admin($_user['user_id']);
// This user is subscribed in these sites => $my_url_list
$my_url_list = api_get_access_url_from_user($_user['user_id']);
//Check the access_url configuration setting if
// the user is registered in the access_url_rel_user table
//Getting the current access_url_id of the platform
$current_access_url_id = api_get_current_access_url_id();
// the user have the permissions to enter at this site
if (is_array($my_url_list) &&
in_array($current_access_url_id, $my_url_list)
) {
Session::write('_user', $_user);
Event::eventLogin($_user['user_id']);
$logging_in = true;
} else {
phpCAS::logout();
$location = api_get_path(WEB_PATH)
.'index.php?loginFailed=1&error=access_url_inactive';
header('Location: '.$location);
exit;
}
}
Session::write('_user', $_user);
Event::eventLogin($_user['user_id']);
$logging_in = true;
} else {
phpCAS::logout();
header(
'Location: '.api_get_path(WEB_PATH)
.'index.php?loginFailed=1&error=account_expired'
);
exit;
}
} else {
phpCAS::logout();
header(
'Location: '.api_get_path(WEB_PATH)
.'index.php?loginFailed=1&error=account_inactive'
);
exit;
}
// update the user record from LDAP if so required by settings
if ('true' === api_get_setting("update_user_info_cas_with_ldap")) {
UserManager::updateUserFromLDAP($login);
}
Session::write('_user', $_user);
$doNotRedirectToCourse = true; // we should already be on the right page, no need to redirect
}
}

@ -906,7 +906,7 @@ ALTER TABLE skill_rel_item_rel_user ADD CONSTRAINT FK_D1133E0DA76ED395 FOREIGN K
ALTER TABLE skill_rel_item ADD CONSTRAINT FK_EB5B2A0D5585C142 FOREIGN KEY (skill_id) REFERENCES skill (id);
ALTER TABLE skill_rel_item_rel_user ADD result_id INT DEFAULT NULL;
CREATE TABLE skill_rel_course (id INT AUTO_INCREMENT NOT NULL, skill_id INT DEFAULT NULL, c_id INT NOT NULL, session_id INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX IDX_E7CEC7FA5585C142 (skill_id), INDEX IDX_E7CEC7FA91D79BD3 (c_id), INDEX IDX_E7CEC7FA613FECDF (session_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE skill_rel_course (id INT AUTO_INCREMENT NOT NULL, skill_id INT DEFAULT NULL, c_id INT NOT NULL, session_id INT, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX IDX_E7CEC7FA5585C142 (skill_id), INDEX IDX_E7CEC7FA91D79BD3 (c_id), INDEX IDX_E7CEC7FA613FECDF (session_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
ALTER TABLE skill_rel_course ADD CONSTRAINT FK_E7CEC7FA5585C142 FOREIGN KEY (skill_id) REFERENCES skill (id);
ALTER TABLE skill_rel_course ADD CONSTRAINT FK_E7CEC7FA91D79BD3 FOREIGN KEY (c_id) REFERENCES course (id);
ALTER TABLE skill_rel_course ADD CONSTRAINT FK_E7CEC7FA613FECDF FOREIGN KEY (session_id) REFERENCES session (id);
@ -1899,6 +1899,9 @@ ALTER TABLE gradebook_comment ADD CONSTRAINT FK_C3B70763AD3ED51C FOREIGN KEY (gr
// Shows exercise session attempts in the base course.
// $_configuration['show_exercise_session_attempts_in_base_course'] = false;
// Shows exercise attempts in sessions where user is general coach
// $_configuration['show_exercise_attempts_in_all_user_sessions'] = true;
// Allow coach users to always edit announcements inside active/past sessions.
// $_configuration['allow_coach_to_edit_announcements'] = false;
@ -2001,7 +2004,7 @@ VALUES (21, 13, 'send_notification_at_a_specific_date', 'Send notification at a
//$_configuration['enable_uploadimage_editor'] = false;
// Ckeditor settings.
//$_configuration['editor_settings'] = ['config' => ['youtube_responsive' => true]];
//$_configuration['editor_settings'] = ['config' => ['youtube_responsive' => true, 'image_responsive' => true]];
// Overwrites the app/config/auth.conf.php settings
//$_configuration['extldap_config'] = ['host' => '', 'port' => ''];

@ -4981,6 +4981,49 @@ class learnpath
return false;
}
/**
* Update the last progress only in case.
*/
public function updateLpProgress()
{
$debug = $this->debug;
if ($debug) {
error_log('In learnpath::updateLpProgress()', 0);
}
$sessionCondition = api_get_session_condition(
api_get_session_id(),
true,
false
);
$courseId = api_get_course_int_id();
$userId = $this->get_user_id();
$table = Database::get_course_table(TABLE_LP_VIEW);
[$progress] = $this->get_progress_bar_text('%');
if ($progress >= 0 && $progress <= 100) {
// Check database.
$progress = (int) $progress;
$sql = "UPDATE $table SET
progress = $progress
WHERE
c_id = $courseId AND
lp_id = ".$this->get_id()." AND
user_id = ".$userId." ".$sessionCondition;
// Ignore errors as some tables might not have the progress field just yet.
Database::query($sql);
if ($debug) {
error_log($sql);
}
$this->progress_db = $progress;
if (100 == $progress) {
HookLearningPathEnd::create()
->setEventData(['lp_view_id' => $this->lp_view_id])
->hookLearningPathEnd();
}
}
}
/**
* Saves the last item seen's ID only in case.
*/

@ -4070,6 +4070,9 @@ class learnpathItem
}
}
// It updates the last progress only in case.
$_SESSION['oLP']->updateLpProgress();
if ($debug) {
error_log('End of learnpathItem::write_to_db()', 0);
}

@ -171,7 +171,7 @@ if ($subscriptionSettings['allow_add_users_to_lp']) {
$extraField = new ExtraField('lp');
$extra = $extraField->addElements($form, 0, ['lp_icon']);
Skill::addSkillsToForm($form, ITEM_TYPE_LEARNPATH, 0);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_LEARNPATH, 0);
$form->addElement('html', '</div>');

@ -285,7 +285,7 @@ if ($form->hasElement('extra_authors')) {
$author->setOptions($options);
}
$skillList = Skill::addSkillsToForm($form, ITEM_TYPE_LEARNPATH, $lpId);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_LEARNPATH, $lpId);
// Submit button
$form->addButtonSave(get_lang('SaveLPSettings'));
@ -309,7 +309,6 @@ $defaults['expired_on'] = (!empty($expired_on))
? api_get_local_time($expired_on)
: date('Y-m-d 12:00:00', time() + 84600);
$defaults['subscribe_users'] = $learnPath->getSubscribeUsers();
$defaults['skills'] = array_keys($skillList);
$form->setDefaults($defaults);
Display::display_header(get_lang('CourseSettings'), 'Path');

@ -270,6 +270,93 @@ switch ($action) {
Security::clear_token();
}
break;
case 'lp_stats_to_export_pdf':
$categoriesTempList = learnpath::getCategories($courseInfo['real_id']);
$categoryTest = new CLpCategory();
$categoryTest->setId(0);
$categoryTest->setName(get_lang('WithOutCategory'));
$categoryTest->setPosition(0);
$categories = [
$categoryTest,
];
if (!empty($categoriesTempList)) {
$categories = array_merge($categories, $categoriesTempList);
}
$userEntity = api_get_user_entity($student_id);
$courseTable = '';
/** @var CLpCategory $item */
foreach ($categories as $item) {
$categoryId = $item->getId();
if (!learnpath::categoryIsVisibleForStudent($item, $userEntity, $courseInfo['real_id'], $sessionId)) {
continue;
}
$list = new LearnpathList(
$student_id,
$courseInfo,
$sessionId,
null,
false,
$categoryId,
false,
true
);
$flatList = $list->get_flat_list();
foreach ($flatList as $learnpath) {
$lpId = $learnpath['lp_old_id'];
$output = Tracking::getLpStatsContentToPdf(
$student_id,
$courseInfo,
$sessionId,
$lpId,
$learnpath['lp_name']
);
$courseTable .= $output;
}
}
$pdfTitle = get_lang('TestResult');
$sessionInfo = api_get_session_info($sessionId);
$studentInfo = api_get_user_info($student_id);
$tpl = new Template('', false, false, false, true, false, false);
$tpl->assign('title', $pdfTitle);
$tpl->assign('session_title', $sessionInfo['name']);
$tpl->assign('session_info', $sessionInfo);
$tpl->assign('table_course', $courseTable);
$content = $tpl->fetch($tpl->get_template('my_space/pdf_lp_stats.tpl'));
$params = [
'pdf_title' => $pdfTitle,
'session_info' => $sessionInfo,
'course_info' => '',
'pdf_date' => '',
'student_info' => $studentInfo,
'show_grade_generated_date' => true,
'show_real_course_teachers' => false,
'show_teacher_as_myself' => false,
'orientation' => 'P',
];
@$pdf = new PDF('A4', $params['orientation'], $params);
$pdf->setBackground($tpl->theme);
$mode = 'D';
$pdfName = $sessionInfo['name'].'_'.$studentInfo['complete_name'];
$pdf->set_footer();
$result = @$pdf->content_to_pdf(
$content,
'',
$pdfName,
null,
$mode,
false,
null,
false,
true,
false
);
break;
default:
break;
}
@ -1299,6 +1386,18 @@ if (empty($details)) {
'data-title' => get_lang('CertificateOfAchievement'),
]
);
$sessionAction .= Display::url(
Display::return_icon('pdf.png', get_lang('TestResult'), [], ICON_SIZE_MEDIUM),
api_get_path(WEB_CODE_PATH).'mySpace/myStudents.php?'
.http_build_query(
[
'action' => 'lp_stats_to_export_pdf',
'student' => $student_id,
'id_session' => $sId,
'course' => $courseInfoItem['code'],
]
)
);
}
echo $sessionAction;
} else {

@ -326,7 +326,7 @@ if ($action === 'edit' && !empty($survey_id)) {
}
}
$skillList = Skill::addSkillsToForm($form, ITEM_TYPE_SURVEY, $survey_id);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_SURVEY, $survey_id);
$form->addElement('html', '</div><br />');
@ -351,8 +351,6 @@ $form->addRule(
'lte'
);
$defaults['skills'] = array_keys($skillList);
// Setting the default values
$form->setDefaults($defaults);

@ -0,0 +1,27 @@
<div style="position: absolute; top: 40px; right: 50px;">
{{ logo }}
</div>
{% if title %}
<h1 style="color:#084B8A; text-transform: uppercase; background-color:transparent;font-size: 24px; text-align: center; font-weight: bold; padding: 5px 10px; margin-bottom: 10px;">
{{ title }}
</h1>
{% endif %}
{% if session_title %}
<h1 style="color:#084B8A; font-size: 22px; text-align: center; font-weight: bold; padding: 5px 10px; margin-bottom: 10px;">
{{ session_title }}
</h1>
{% endif %}
{% if subtitle %}
<div style="padding-bottom: 20px; margin-top: 20px; font-weight: bold;">
{{ subtitle }}
</div>
{% endif %}
{% if table_course %}
<div style="background: #f1f6ff;">
{{ table_course }}
</div>
{% endif %}

@ -167,11 +167,19 @@ try {
$restResponse->setData($courses);
break;
case Rest::VIEW_PROFILE:
$userId = isset($_GET['user_id']) ? (int) $_GET['user_id'] : 0;
$restApi->viewUserProfile($userId);
break;
case Rest::GET_PROFILE:
$userInfo = $restApi->getUserProfile();
$restResponse->setData($userInfo);
break;
case Rest::VIEW_COURSE_HOME:
$restApi->viewCourseHome();
break;
case Rest::GET_COURSE_INFO:
$courseInfo = $restApi->getCourseInfo();
$restResponse->setData($courseInfo);
@ -464,6 +472,10 @@ try {
$data = $restApi->addUser($_POST);
$restResponse->setData($data);
break;
case Rest::SAVE_USER_GET_APIKEY:
$data = $restApi->addUserGetApikey($_POST);
$restResponse->setData($data);
break;
case Rest::SAVE_USER_JSON:
if (!array_key_exists('json', $_POST)) {
throw new Exception(get_lang('NoData'));
@ -479,6 +491,17 @@ try {
$data = $restApi->updateUserFromUserName($_POST);
$restResponse->setData([$data]);
break;
case Rest::UPDATE_USER_APIKEY:
$userId = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
$currentApiKey = $_POST['current_api_key'] ?? '';
if (empty($userId) || empty($currentApiKey)) {
throw new Exception(get_lang('NotAllowed'));
}
$data = $restApi->updateUserApiKey($userId, $currentApiKey);
$restResponse->setData($data);
break;
case Rest::DELETE_USER:
$result = UserManager::delete_user($_REQUEST['user_id']);
$restResponse->setData(['status' => $result]);

@ -5626,10 +5626,9 @@ function getFormWork($form, $defaults = [], $workId = 0)
$form->addHtml('</div>');
$skillList = Skill::addSkillsToForm($form, ITEM_TYPE_STUDENT_PUBLICATION, $workId);
Skill::addSkillsToForm($form, api_get_course_int_id(), api_get_session_id(), ITEM_TYPE_STUDENT_PUBLICATION, $workId);
if (!empty($defaults)) {
$defaults['skills'] = array_keys($skillList);
$form->setDefaults($defaults);
}

@ -1,3 +1,7 @@
Version 2.10 - 2021-10
----------------------
* Add support for multiple recording formats
Version 2.9 - 2021-08
---------------------
* Remove interface option (HTML5/Flash)

@ -72,6 +72,17 @@ ALTER TABLE plugin_bbb_room DROP COLUMN interface;
ALTER TABLE plugin_bbb_meeting DROP COLUMN interface;
```
For version 2.10 (Handles multiple recording formats - Check https://github.com/chamilo/chamilo-lms/issues/3703)
```sql
CREATE TABLE plugin_bbb_meeting_format (
id int unsigned not null PRIMARY KEY AUTO_INCREMENT,
meeting_id int unsigned not null,
format_type varchar(255) not null,
resource_url text not null
)
```
## Improve access tracking in BBB
You need to configure the cron using the *cron_close_meeting.php* file.

@ -1041,18 +1041,23 @@ class bbb
continue;
}
if (!empty($record['playbackFormatUrl'])) {
if (!empty($record['playbackFormat'])) {
$this->updateMeetingVideoUrl($meetingDB['id'], $record['playbackFormatUrl']);
}
}
}
if (isset($record['playbackFormatUrl']) && !empty($record['playbackFormatUrl'])) {
$recordLink = Display::url(
$this->plugin->get_lang('ViewRecord'),
$record['playbackFormatUrl'],
['target' => '_blank', 'class' => 'btn btn-default']
);
if (isset($record['playbackFormat']) && !empty($record['playbackFormat'])) {
$recordLink = [];
foreach ($record['playbackFormat'] as $format) {
$this->insertMeetingFormat(intval($meetingDB['id']), $format->type->__toString(), $format->url->__toString());
$recordLink['record'][] = 1;
$recordLink[] = Display::url(
$this->plugin->get_lang($format->type->__toString()),
$format->url->__toString(),
['target' => '_blank', 'class' => 'btn btn-default']
);
}
} else {
$recordLink = $this->plugin->get_lang('NoRecording');
}
@ -1072,6 +1077,7 @@ class bbb
$isAdminReport
);
$item['show_links'] = $recordLink;
$item['record'] = true;
} else {
$actionLinks = $this->getActionLinks(
$meetingDB,
@ -1081,6 +1087,7 @@ class bbb
);
$item['show_links'] = $this->plugin->get_lang('NoRecording');
$item['record'] = false;
}
$item['action_links'] = implode(PHP_EOL, $actionLinks);
@ -1244,6 +1251,30 @@ class bbb
);
}
/**
* @param int $meetingId
* @param string $formatType
* @param string $resourceUrl
*
* @return bool|int
*/
public function insertMeetingFormat(int $meetingId, string $formatType, string $resourceUrl)
{
$em = Database::getManager();
$sm = $em->getConnection()->getSchemaManager();
if ($sm->tablesExist('plugin_bbb_meeting_format')) {
return Database::insert(
'plugin_bbb_meeting_format',
[
'format_type' => $formatType,
'resource_url' => $resourceUrl,
'meeting_id' => $meetingId
]
);
}
}
/**
* Force the course, session and/or group IDs
*

File diff suppressed because it is too large Load Diff

@ -45,8 +45,8 @@ class BBBPlugin extends Plugin
protected function __construct()
{
parent::__construct(
'2.9',
'Julio Montoya, Yannick Warnier, Angel Fernando Quiroz Campos, Jose Angel Ruiz',
'2.10',
'Julio Montoya, Yannick Warnier, Angel Fernando Quiroz Campos, Jose Angel Ruiz, Ghazi Triki, Adnen Manssouri',
[
'tool_enable' => 'boolean',
'host' => 'text',
@ -230,6 +230,15 @@ class BBBPlugin extends Plugin
]
);
Database::query(
"CREATE TABLE plugin_bbb_meeting_format (
id int unsigned not null PRIMARY KEY AUTO_INCREMENT,
meeting_id int unsigned not null,
format_type varchar(255) not null,
resource_url text not null
);"
);
// Copy icons into the main/img/icons folder
$iconName = 'bigbluebutton';
$iconsList = [
@ -313,6 +322,9 @@ class BBBPlugin extends Plugin
$sql = "DELETE FROM $t_tool WHERE name = 'bbb' AND c_id != 0";
Database::query($sql);
if ($sm->tablesExist('plugin_bbb_meeting_format')) {
Database::query('DROP TABLE IF EXISTS plugin_bbb_meeting_format');
}
if ($sm->tablesExist('plugin_bbb_room')) {
Database::query('DROP TABLE IF EXISTS plugin_bbb_room');
}

@ -84,12 +84,17 @@
{% endif %}
</td>
<td>
{% if meeting.record == 1 %}
{% if meeting.show_links.record %}
{# Record list #}
{{ meeting.show_links }}
{% else %}
{{ 'NoRecording'|get_plugin_lang('BBBPlugin') }}
{% for link in meeting.show_links %}
{% if link is not iterable %}
{{ link }}
{% endif %}
{% endfor %}
{% else %}
{{ 'NoRecording'|get_plugin_lang('BBBPlugin') }}
{% endif %}
</td>
{% if allow_to_edit %}
<td>

@ -1,3 +1,7 @@
v7.1 - 2021-10-26
====
Fix install issue with DB field type.
v7.0 - 2021-08-12
====
Added support for discount coupons.

@ -79,7 +79,7 @@ class BuyCoursesPlugin extends Plugin
public function __construct()
{
parent::__construct(
'5.0',
'7.1',
"
Jose Angel Ruiz - NoSoloRed (original author) <br/>
Francis Gonzales and Yannick Warnier - BeezNest (integration) <br/>

@ -95,7 +95,7 @@ class QuestionOptionsEvaluationPlugin extends Plugin
$extraFieldValue = new ExtraFieldValue('quiz');
$extraFieldValue->save(
[
'item_id' => $exercise->iId,
'item_id' => $exercise->iid,
'variable' => self::EXTRAFIELD_FORMULA,
'value' => $formula,
]

@ -58,6 +58,7 @@ class Forum extends Basic
'JustifyRight',
'JustifyBlock',
],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
];
}
@ -87,6 +88,7 @@ class Forum extends Basic
['BulletedList', 'NumberedList', 'HorizontalRule'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
['Format', 'Font', 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor', 'Source'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch'],
];
}

@ -62,6 +62,7 @@ class ForumStudent extends Basic
[api_get_setting('allow_spellcheck') == 'true' ? 'Scayt' : ''],
['Styles', 'Format', 'Font', 'FontSize'],
['PageBreak', 'ShowBlocks'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch'],
];
}
@ -83,6 +84,7 @@ class ForumStudent extends Basic
['Styles', 'Format', 'Font', 'FontSize'],
['Bold', 'Italic', 'Underline'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['ShowBlocks'],
];
}
@ -101,6 +103,7 @@ class ForumStudent extends Basic
['BulletedList', 'NumberedList', 'HorizontalRule'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
['Format', 'Font', 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch'],
];
}

@ -64,6 +64,7 @@ class LearningPathDocuments extends Basic
['Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript', '-', 'TextColor', 'BGColor'],
[api_get_setting('allow_spellcheck') === 'true' ? 'Scayt' : ''],
['Styles', 'Format', 'Font', 'FontSize'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['PageBreak', 'ShowBlocks', 'Source'],
['Toolbarswitch'],
];
@ -127,6 +128,7 @@ class LearningPathDocuments extends Basic
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
['Toolbarswitch'],
['Styles', 'Format', 'Font', 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['ShowBlocks', 'Source'],
];
}

@ -65,6 +65,7 @@ class Messages extends Basic
['Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript', '-', 'TextColor', 'BGColor'],
[api_get_setting('allow_spellcheck') == 'true' ? 'Scayt' : ''],
['Styles', 'Format', 'Font', 'FontSize'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['PageBreak', 'ShowBlocks', 'Source'],
['Toolbarswitch'],
];
@ -104,6 +105,7 @@ class Messages extends Basic
['BulletedList', 'NumberedList', 'HorizontalRule'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
['Format', 'Font', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch'],
];
}

@ -87,6 +87,7 @@ class TestAnswerFeedback extends Basic
['Bold', 'Italic', 'Underline', 'Strike', '-', 'Subscript', 'Superscript', '-', 'TextColor', 'BGColor'],
[api_get_setting('allow_spellcheck') == 'true' ? 'Scayt' : ''],
['Styles', 'Format', 'Font', 'FontSize'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['PageBreak', 'ShowBlocks', 'Source'],
['Toolbarswitch'],
];
@ -119,6 +120,7 @@ class TestAnswerFeedback extends Basic
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
['Asciimath', 'Asciisvg'],
['Format', 'Font', 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor', 'Source'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch'],
];
}

@ -73,6 +73,7 @@ class TestMatching extends Basic
[api_get_setting('allow_spellcheck') === 'true' ? 'Scayt' : ''],
['Styles', 'Format', 'Font', 'FontSize'],
['PageBreak', 'ShowBlocks'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch'],
];
}
@ -131,6 +132,7 @@ class TestMatching extends Basic
['BulletedList', 'NumberedList', 'HorizontalRule'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
['Format', 'Font', 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Source', 'Toolbarswitch'],
];
}

@ -62,6 +62,7 @@ class TestProposedAnswer extends Basic
api_get_setting('enabled_mathjax') === 'true' ? 'Mathjax' : '',
],
['Asciimath', 'Asciisvg'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Maximize', 'Source'],
];
}

@ -104,6 +104,7 @@ class TestQuestionDescription extends Basic
[api_get_setting('allow_spellcheck') == 'true' ? 'Scayt' : ''],
['Styles', 'Format', 'Font', 'FontSize'],
['PageBreak', 'ShowBlocks'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch', 'Source'],
];
}
@ -136,6 +137,7 @@ class TestQuestionDescription extends Basic
['Styles', 'Format', 'Font', 'FontSize'],
['Bold', 'Italic', 'Underline'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Source'],
];
}
@ -178,6 +180,7 @@ class TestQuestionDescription extends Basic
'BGColor',
api_get_configuration_value('translate_html') ? 'Language' : '',
],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch', 'Source'],
];
}

@ -64,6 +64,7 @@ class Work extends Basic
[api_get_setting('allow_spellcheck') == 'true' ? 'Scayt' : ''],
['Styles', 'Format', 'Font', 'FontSize'],
['PageBreak', 'ShowBlocks', 'Source'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch'],
];
}
@ -84,6 +85,7 @@ class Work extends Basic
['Font', 'FontSize'],
['Bold', 'Italic', 'Underline'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight', '-', 'NumberedList', 'BulletedList', '-', 'TextColor', 'BGColor'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Source'],
];
}
@ -101,6 +103,7 @@ class Work extends Basic
['Link', 'Image', 'Video', 'Flash', 'Youtube', 'VimeoEmbed', 'Audio', 'Table', 'Asciimath'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
['Format', 'Font', 'FontSize', 'Bold', 'Italic', 'TextColor', 'BGColor'],
api_get_setting('enabled_wiris') === 'true' ? ['ckeditor_wiris_formulaEditor', 'ckeditor_wiris_formulaEditorChemistry'] : [''],
['Toolbarswitch'],
];
}

@ -110,6 +110,11 @@ class Skill
*/
protected $updatedAt;
public function __construct()
{
$this->courses = new ArrayCollection();
}
/**
* @return string
*/
@ -415,6 +420,11 @@ class Skill
$this->items[] = $skillRelItem;
}
public function hasCourses()
{
return null !== $this->courses;
}
/**
* @return ArrayCollection
*/

@ -103,6 +103,7 @@ class CourseBuilder
to be added in the course obj (only works with LPs) */
public $specific_id_list = [];
public $documentsAddedInText = [];
public $itemListToAdd = [];
/**
* Create a new CourseBuilder.
@ -1479,6 +1480,7 @@ class CourseBuilder
$item['launch_data'] = $obj_item->launch_data;
$item['audio'] = $obj_item->audio;
$items[] = $item;
$this->itemListToAdd[$obj_item->item_type][] = $obj_item->path;
}
$sql = "SELECT id FROM $table_tool
@ -1565,6 +1567,156 @@ class CourseBuilder
}
}
/**
* It builds the resources used in a LP , also it adds the documents related.
*/
public function exportToCourseBuildFormat()
{
if (empty($this->itemListToAdd)) {
return false;
}
$itemList = $this->itemListToAdd;
$courseId = api_get_course_int_id();
$sessionId = api_get_session_id();
$courseInfo = api_get_course_info_by_id($courseId);
if (isset($itemList['document'])) {
// Get parents
foreach ($itemList['document'] as $documentId) {
$documentInfo = \DocumentManager::get_document_data_by_id($documentId, $courseInfo['code'], true);
if (!empty($documentInfo['parents'])) {
foreach ($documentInfo['parents'] as $parentInfo) {
if (in_array($parentInfo['iid'], $itemList['document'])) {
continue;
}
$itemList['document'][] = $parentInfo['iid'];
}
}
}
foreach ($itemList['document'] as $documentId) {
$documentInfo = \DocumentManager::get_document_data_by_id($documentId, $courseInfo['code']);
$items = \DocumentManager::get_resources_from_source_html(
$documentInfo['absolute_path'],
true,
TOOL_DOCUMENT
);
if (!empty($items)) {
foreach ($items as $item) {
// Get information about source url
$url = $item[0]; // url
$scope = $item[1]; // scope (local, remote)
$type = $item[2]; // type (rel, abs, url)
$origParseUrl = parse_url($url);
$realOrigPath = isset($origParseUrl['path']) ? $origParseUrl['path'] : null;
if ($scope == 'local') {
if ($type == 'abs' || $type == 'rel') {
$documentFile = strstr($realOrigPath, 'document');
$documentFile = (string) $documentFile;
$realOrigPath = (string) $realOrigPath;
if (!empty($documentFile) && false !== strpos($realOrigPath, $documentFile)) {
$documentFile = str_replace('document', '', $documentFile);
$itemDocumentId = \DocumentManager::get_document_id($courseInfo, $documentFile);
// Document found! Add it to the list
if ($itemDocumentId) {
$itemList['document'][] = $itemDocumentId;
}
}
}
}
}
}
}
$this->build_documents(
$sessionId,
$courseId,
true,
$itemList['document']
);
}
if (isset($itemList['quiz'])) {
$this->build_quizzes(
$sessionId,
$courseId,
true,
$itemList['quiz']
);
}
require_once api_get_path(SYS_CODE_PATH).'forum/forumfunction.inc.php';
if (!empty($itemList['thread'])) {
$threadList = [];
$em = Database::getManager();
$repo = $em->getRepository('ChamiloCourseBundle:CForumThread');
foreach ($itemList['thread'] as $threadId) {
/** @var \Chamilo\CourseBundle\Entity\CForumThread $thread */
$thread = $repo->find($threadId);
if ($thread) {
$itemList['forum'][] = $thread->getForumId();
$threadList[] = $thread->getIid();
}
}
if (!empty($threadList)) {
$this->build_forum_topics(
$sessionId,
$courseId,
null,
$threadList
);
}
}
$forumCategoryList = [];
if (isset($itemList['forum'])) {
foreach ($itemList['forum'] as $forumId) {
$forumInfo = get_forums($forumId);
$forumCategoryList[] = $forumInfo['forum_category'];
}
}
if (!empty($forumCategoryList)) {
$this->build_forum_category(
$sessionId,
$courseId,
true,
$forumCategoryList
);
}
if (!empty($itemList['forum'])) {
$this->build_forums(
$sessionId,
$courseId,
true,
$itemList['forum']
);
}
if (isset($itemList['link'])) {
$this->build_links(
$sessionId,
$courseId,
true,
$itemList['link']
);
}
if (!empty($itemList['student_publication'])) {
$this->build_works(
$sessionId,
$courseId,
true,
$itemList['student_publication']
);
}
}
/**
* Build the glossaries.
*
@ -1869,6 +2021,7 @@ class CourseBuilder
$result = Database::query($sql);
while ($row = Database::fetch_array($result, 'ASSOC')) {
$obj = new Work($row);
$this->findAndSetDocumentsInText($row['description']);
$this->course->add_resource($obj);
}
}

@ -2219,38 +2219,51 @@ class CourseRestorer
}
}
} else {
$new_options = [];
if (isset($question->question_options)) {
foreach ($question->question_options as $obj) {
$item = [];
$item['question_id'] = $new_id;
$item['c_id'] = $this->destination_course_id;
$item['name'] = $obj->obj->name;
$item['position'] = $obj->obj->position;
$question_option_id = Database::insert($table_options, $item);
if ($question_option_id) {
$new_options[$obj->obj->id] = $question_option_id;
$sql = "UPDATE $table_options SET id = iid WHERE iid = $question_option_id";
Database::query($sql);
}
if (count($question->question_options) < 3) {
$options = [1 => 'True', 2 => 'False', 3 => 'DoubtScore'];
for ($i = 1; $i <= 3; $i++) {
$lastId = Question::saveQuestionOption(
$new_id,
$options[$i],
$this->destination_course_id,
$i
);
$correct[$i] = $lastId;
}
} else {
$new_options = [];
if (isset($question->question_options)) {
foreach ($question->question_options as $obj) {
$item = [];
$item['question_id'] = $new_id;
$item['c_id'] = $this->destination_course_id;
$item['name'] = $obj->obj->name;
$item['position'] = $obj->obj->position;
$question_option_id = Database::insert($table_options, $item);
if ($question_option_id) {
$new_options[$obj->obj->id] = $question_option_id;
$sql = "UPDATE $table_options SET id = iid WHERE iid = $question_option_id";
Database::query($sql);
}
}
foreach ($correctAnswers as $answer_id => $correct_answer) {
$params = [];
$params['correct'] = isset($new_options[$correct_answer]) ? $new_options[$correct_answer] : '';
Database::update(
$table_ans,
$params,
[
'iid = ? AND c_id = ? AND question_id = ? ' => [
$answer_id,
$this->destination_course_id,
$new_id,
foreach ($correctAnswers as $answer_id => $correct_answer) {
$params = [];
$params['correct'] = isset($new_options[$correct_answer]) ? $new_options[$correct_answer] : '';
Database::update(
$table_ans,
$params,
[
'iid = ? AND c_id = ? AND question_id = ? ' => [
$answer_id,
$this->destination_course_id,
$new_id,
],
],
],
false
);
false
);
}
}
}
}

@ -110,12 +110,15 @@ class CourseSelectForm
function checkLearnPath(message){
d = document.course_select_form;
var backup = (typeof d.destination_course === 'undefined');
for (i = 0; i < d.elements.length; i++) {
if (d.elements[i].type == "checkbox") {
var name = d.elements[i].attributes.getNamedItem('name').nodeValue;
if( name.indexOf('learnpath') > 0 || name.indexOf('quiz') > 0){
if(d.elements[i].checked){
setCheckbox('document',true);
if (!backup) {
//setCheckbox('document', true);
}
alert(message);
break;
}
@ -744,12 +747,15 @@ class CourseSelectForm
}
function checkLearnPath(message){
d = document.course_select_form;
var backup = (typeof d.destination_course === 'undefined');
for (i = 0; i < d.elements.length; i++) {
if (d.elements[i].type == "checkbox") {
var name = d.elements[i].attributes.getNamedItem('name').nodeValue;
if( name.indexOf('learnpath') > 0 || name.indexOf('quiz') > 0){
if(d.elements[i].checked){
setCheckbox('document',true);
if (!backup) {
//setCheckbox('document', true);
}
alert(message);
break;
}

@ -10,8 +10,6 @@ use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
/**
* SkillRelCourse.
*
* @ORM\Table(name="skill_rel_course")
* ORM\Entity // uncomment if api_get_configuration_value('allow_skill_rel_items')
*/
@ -43,7 +41,7 @@ class SkillRelCourse
/**
* @var Session
* @ORM\ManyToOne(targetEntity="Chamilo\CoreBundle\Entity\Session", inversedBy="skills", cascade={"persist"})
* @ORM\JoinColumn(name="session_id", referencedColumnName="id", nullable=false)
* @ORM\JoinColumn(name="session_id", referencedColumnName="id")
*/
protected $session;

@ -6,10 +6,10 @@
User account synchronisation from LDAP
This script
creates new user accounts found in the LDAP directory
disables user accounts not found in the LDAP directory
updates existing user accounts found in the LDAP directory, re-enabling them if disabled
anonymizes user accounts disabled for more than 3 years
creates new user accounts found in the LDAP directory (if multiURL is enable, it creates the user on the URL for which the LDAP has been configured)
disables user accounts not found in the LDAP directory (it disbales the user for all URLs)
updates existing user accounts found in the LDAP directory, re-enabling them if disabled (it applies for all URLs)
anonymizes user accounts disabled for more than 3 years (applies for all URLs)
This script can be run unattended.
@ -17,7 +17,7 @@ It does not read any parameter from the command line, but uses the global config
$extldap_config
and
$extldap_user_correspondance
defined in app/config/auth.conf.php.
defined in app/config/auth.conf.php or overriden in app/config/configuration.php in MultiURL case.
username field is used to identify and match LDAP and Chamilo accounts together.
($extldap_user_correspondance['username'])
@ -41,8 +41,8 @@ if (php_sapi_name() !== 'cli') {
}
require $chamiloRoot.'/cli-config.php';
require_once $chamiloRoot.'/app/config/auth.conf.php';
require_once $chamiloRoot.'/main/inc/lib/api.lib.php';
require_once $chamiloRoot.'/app/config/auth.conf.php';
require_once $chamiloRoot.'/main/inc/lib/database.constants.inc.php';
ini_set('memory_limit', -1);
@ -53,187 +53,298 @@ ini_set('memory_limit', -1);
$tableFields = [];
$extraFields = [];
$ldapAttributes = [];
$tableFieldMap = $extldap_user_correspondance;
$generalTableFieldMap = $extldap_user_correspondance;
$extraFieldMap = [];
$multipleUrlLDAPConfig = false;
$allLdapUsers = [];
const EXTRA_ARRAY_KEY = 'extra';
if (array_key_exists(EXTRA_ARRAY_KEY, $tableFieldMap) and is_array($tableFieldMap[EXTRA_ARRAY_KEY])) {
$extraFieldMap = $tableFieldMap[EXTRA_ARRAY_KEY];
unset($tableFieldMap[EXTRA_ARRAY_KEY]);
}
$extraFieldRepository = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraField');
$extraFieldValueRepository = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraFieldValues');
foreach ([false => $tableFieldMap, true => $extraFieldMap] as $areExtra => $fields) {
foreach ($fields as $name => $value) {
$userField = (object)[
'name' => $name,
'constant' => '!' === $value[0] ? substr($value, 1) : null,
'function' => 'func' === $value,
'ldapAttribute' => ('!' !== $value[0] and 'func' !== $value) ? $value : null,
];
if (!$userField->constant and !$userField->function) {
$ldapAttributes[] = $value;
}
if ($areExtra) {
$userField->extraField = $extraFieldRepository->findOneBy(
[
'extraFieldType' => ExtraField::USER_FIELD_TYPE,
'variable' => $name,
]
) or die("Cannot find user extra field '$name'\n");
foreach ($extraFieldValueRepository->findBy(['field' => $userField->extraField]) as $extraFieldValue) {
$userField->extraFieldValues[$extraFieldValue->getItemId()] = $extraFieldValue;
}
$extraFields[] = $userField;
} else {
try {
$userField->getter = new ReflectionMethod(
'\Chamilo\UserBundle\Entity\User',
'get' . str_replace('_', '', ucfirst($name))
);
$userField->setter = new ReflectionMethod(
'\Chamilo\UserBundle\Entity\User',
'set' . str_replace('_', '', ucfirst($name))
);
} catch (ReflectionException $exception) {
die($exception->getMessage() . "\n");
}
$tableFields[] = $userField;
}
}
}
$allFields = array_merge($tableFields, $extraFields);
// Retrieve source information from LDAP
// read all users from the internal database
$ldap = false;
foreach ($extldap_config['host'] as $ldapHost) {
$ldap = array_key_exists('port', $extldap_config)
? ldap_connect($ldapHost, $extldap_config['port'])
: ldap_connect($ldapHost);
if (false !== $ldap) {
break;
$userRepository = Database::getManager()->getRepository('ChamiloUserBundle:User');
$dbUsers = [];
foreach ($userRepository->findAll() as $user) {
if ($user->getId() > 1) {
$username = strtolower($user->getUsername());
array_key_exists($username, $dbUsers) and die("duplicate username $username found in the database\n");
$dbUsers[$username] = $user;
}
}
if (false === $ldap) {
die("ldap_connect() failed\n");
}
if ($debug) {
echo "Connected to LDAP server $ldapHost.\n";
echo count($dbUsers) . " users with id > 1 found in internal database\n";
}
ldap_set_option(
$ldap,
LDAP_OPT_PROTOCOL_VERSION,
array_key_exists('protocol_version', $extldap_config) ? $extldap_config['protocol_version'] : 2
);
ldap_set_option(
$ldap,
LDAP_OPT_REFERRALS,
array_key_exists('referrals', $extldap_config) ? $extldap_config['referrals'] : false
);
if (api_is_multiple_url_enabled()) {
$accessUrls = api_get_access_urls(0,100000,'id');
$multipleUrlLDAPConfig = true;
if (array_key_exists('host', $extldap_config) && !empty($extldap_config['host'])) {
$multipleUrlLDAPConfig = false;
}
}
ldap_bind($ldap, $extldap_config['admin_dn'], $extldap_config['admin_password'])
or die('ldap_bind() failed: ' . ldap_error($ldap) . "\n");
if (!$multipleUrlLDAPConfig) {
$accessUrls['id'] = 0;
$generalTableFieldMap[0] = $generalTableFieldMap;
}
if ($debug) {
echo "Bound to LDAP server as ${extldap_config['admin_dn']}.\n";
echo "accessUrls = " . print_r($accessUrls,1);
}
foreach ($accessUrls as $accessUrl) {
$accessUrlId = $accessUrl['id'];
if (array_key_exists($accessUrlId, $generalTableFieldMap) && is_array($generalTableFieldMap[$accessUrlId])) {
$tableFieldMap = $generalTableFieldMap[$accessUrlId];
if (array_key_exists(EXTRA_ARRAY_KEY, $tableFieldMap) and is_array($tableFieldMap[EXTRA_ARRAY_KEY])) {
$extraFieldMap = $tableFieldMap[EXTRA_ARRAY_KEY];
unset($tableFieldMap[EXTRA_ARRAY_KEY]);
}
$extraFieldRepository = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraField');
$extraFieldValueRepository = Database::getManager()->getRepository('ChamiloCoreBundle:ExtraFieldValues');
foreach ([false => $tableFieldMap, true => $extraFieldMap] as $areExtra => $fields) {
foreach ($fields as $name => $value) {
$userField = (object)[
'name' => $name,
'constant' => '!' === $value[0] ? substr($value, 1) : null,
'function' => 'func' === $value,
'ldapAttribute' => ('!' !== $value[0] and 'func' !== $value) ? $value : null,
];
if (!$userField->constant and !$userField->function) {
$ldapAttributes[] = $value;
}
if ($areExtra) {
$userField->extraField = $extraFieldRepository->findOneBy(
[
'extraFieldType' => ExtraField::USER_FIELD_TYPE,
'variable' => $name,
]
) or die("Cannot find user extra field '$name'\n");
foreach ($extraFieldValueRepository->findBy(['field' => $userField->extraField]) as $extraFieldValue) {
$userField->extraFieldValues[$extraFieldValue->getItemId()] = $extraFieldValue;
}
$extraFields[] = $userField;
} else {
try {
$userField->getter = new ReflectionMethod(
'\Chamilo\UserBundle\Entity\User',
'get' . str_replace('_', '', ucfirst($name))
);
$userField->setter = new ReflectionMethod(
'\Chamilo\UserBundle\Entity\User',
'set' . str_replace('_', '', ucfirst($name))
);
} catch (ReflectionException $exception) {
die($exception->getMessage() . "\n");
}
$tableFields[] = $userField;
}
}
}
$baseDn = $extldap_config['base_dn']
or die("cannot read the LDAP directory base DN where to search for user entries\n");
$allFields = array_merge($tableFields, $extraFields);
}
// Retrieve source information from LDAP
$ldap = false;
if (array_key_exists($accessUrlId, $extldap_config) && is_array($extldap_config[$accessUrlId])) {
foreach ($extldap_config[$accessUrlId]['host'] as $ldapHost) {
$ldap = array_key_exists('port', $extldap_config)
? ldap_connect($ldapHost, $extldap_config['port'])
: ldap_connect($ldapHost);
if (false !== $ldap) {
break;
}
}
if (false === $ldap) {
die("ldap_connect() failed\n");
}
if ($debug) {
echo "Connected to LDAP server $ldapHost.\n";
}
$ldapUsernameAttribute = $extldap_user_correspondance['username']
or die("cannot read the name of the LDAP attribute where to find the username\n");
ldap_set_option(
$ldap,
LDAP_OPT_PROTOCOL_VERSION,
array_key_exists('protocol_version', $extldap_config[$accessUrlId]) ? $extldap_config[$accessUrlId]['protocol_version'] : 2
);
ldap_set_option(
$ldap,
LDAP_OPT_REFERRALS,
array_key_exists('referrals', $extldap_config[$accessUrlId]) ? $extldap_config[$accessUrlId]['referrals'] : false
);
ldap_bind($ldap, $extldap_config[$accessUrlId]['admin_dn'], $extldap_config[$accessUrlId]['admin_password'])
or die('ldap_bind() failed: ' . ldap_error($ldap) . "\n");
if ($debug) {
$adminDn = $extldap_config[$accessUrlId]['admin_dn'];
echo "Bound to LDAP server as $adminDn .\n";
}
$filter = "$ldapUsernameAttribute=*";
$baseDn = $extldap_config[$accessUrlId]['base_dn']
or die("cannot read the LDAP directory base DN where to search for user entries\n");
if (array_key_exists('filter', $extldap_config)) {
$filter = '(&('.$filter.')('.$extldap_config['filter'].'))';
}
$ldapUsernameAttribute = $extldap_user_correspondance[$accessUrlId]['username']
or die("cannot read the name of the LDAP attribute where to find the username\n");
$searchResult = ldap_search($ldap, $baseDn, $filter, $ldapAttributes)
or die("ldap_search(\$ldap, '$baseDn', '$filter', [".join(',', $ldapAttributes).']) failed: '.ldap_error($ldap)."\n");
$filter = "$ldapUsernameAttribute=*";
if ($debug) {
echo ldap_count_entries($ldap, $searchResult) . " LDAP entries found\n";
}
if (array_key_exists('filter', $extldap_config[$accessUrlId])) {
$filter = '(&('.$filter.')('.$extldap_config[$accessUrlId]['filter'].'))';
}
$searchResult = ldap_search($ldap, $baseDn, $filter, $ldapAttributes)
or die("ldap_search(\$ldap, '$baseDn', '$filter', [".join(',', $ldapAttributes).']) failed: '.ldap_error($ldap)."\n");
$ldapUsers = [];
$entry = ldap_first_entry($ldap, $searchResult);
while (false !== $entry) {
$attributes = ldap_get_attributes($ldap, $entry);
$ldapUser = [];
foreach ($allFields as $userField) {
if (!is_null($userField->constant)) {
$value = $userField->constant;
} elseif ($userField->function) {
switch ($userField->name) {
case 'status':
$value = STUDENT;
break;
case 'admin':
$value = false;
break;
default:
die("'func' not implemented for $userField->name\n");
if ($debug) {
echo ldap_count_entries($ldap, $searchResult) . " LDAP entries found\n";
}
$ldapUsers = [];
$entry = ldap_first_entry($ldap, $searchResult);
while (false !== $entry) {
$attributes = ldap_get_attributes($ldap, $entry);
$ldapUser = [];
foreach ($allFields as $userField) {
if (!is_null($userField->constant)) {
$value = $userField->constant;
} elseif ($userField->function) {
switch ($userField->name) {
case 'status':
$value = STUDENT;
break;
case 'admin':
$value = false;
break;
default:
die("'func' not implemented for $userField->name\n");
}
} else {
if (array_key_exists($userField->ldapAttribute, $attributes)) {
$values = ldap_get_values($ldap, $entry, $userField->ldapAttribute)
or die(
'could not read value of attribute ' . $userField->ldapAttribute
. ' of entry ' . ldap_get_dn($ldap, $entry)
. "\n"
);
(1 === $values['count'])
or die(
$values['count'] . ' values found (expected only one)'
. ' in attribute ' . $userField->ldapAttribute
. ' of entry ' . ldap_get_dn($ldap, $entry)
. "\n"
);
$value = $values[0];
} else {
$value = '';
}
}
$ldapUser[$userField->name] = $value;
}
} else {
if (array_key_exists($userField->ldapAttribute, $attributes)) {
$values = ldap_get_values($ldap, $entry, $userField->ldapAttribute)
or die(
'could not read value of attribute ' . $userField->ldapAttribute
. ' of entry ' . ldap_get_dn($ldap, $entry)
. "\n"
);
(1 === $values['count'])
or die(
$values['count'] . ' values found (expected only one)'
. ' in attribute ' . $userField->ldapAttribute
. ' of entry ' . ldap_get_dn($ldap, $entry)
. "\n"
);
$value = $values[0];
$username = strtolower($ldapUser['username']);
array_key_exists($username, $ldapUsers) and die("duplicate username '$username' found in LDAP\n");
$ldapUsers[$username] = $ldapUser;
$entry = ldap_next_entry($ldap, $entry);
}
ldap_close($ldap);
// create new user accounts found in the LDAP directory and update the existing ones, re-enabling them if disabled
foreach ($ldapUsers as $username => $ldapUser) {
if (array_key_exists($username, $dbUsers)) {
$user = $dbUsers[$username];
echo "User in DB = " . $username . " and user id = " . $user->getId() . "\n";
} else {
$value = '';
$user = new User();
$dbUsers[$username] = $user;
$user->setUsernameCanonical($username);
if ($debug) {
echo 'Created ' . $username . "\n";
echo "ldapUser = " . print_r($ldapUser,1) . "\n";
}
}
foreach ($tableFields as $userField) {
$value = $ldapUser[$userField->name];
if ($userField->getter->invoke($user) !== $value) {
$userField->setter->invoke($user, $value);
if ($debug) {
echo 'Updated ' . $username . ' field '.$userField->name."\n";
}
if ($userField->name == 'email') {
$user->setEmailCanonical($value);
}
}
}
if (!$user->isActive()) {
$user->setActive(true);
}
Database::getManager()->persist($user);
try {
Database::getManager()->flush();
} catch (OptimisticLockException $exception) {
die($exception->getMessage()."\n");
}
if($debug) {
echo 'Sent to DB ' . $username . " with user id = " . $user->getId() . "\n";
}
if ($multipleUrlLDAPConfig) {
UrlManager::add_user_to_url($user->getId(), $accessUrlId);
} elseif (!api_is_multiple_url_enabled()) {
//we are adding by default the access_url_user table with access_url_id = 1
UrlManager::add_user_to_url($user->getId(), 1);
}
}
$ldapUser[$userField->name] = $value;
}
$username = strtolower($ldapUser['username']);
array_key_exists($username, $ldapUsers) and die("duplicate username '$username' found in LDAP\n");
$ldapUsers[$username] = $ldapUser;
$entry = ldap_next_entry($ldap, $entry);
}
ldap_close($ldap);
// read all users from the internal database
// also update extra field values
foreach ($ldapUsers as $username => $ldapUser) {
$user = $dbUsers[$username];
foreach ($extraFields as $userField) {
$value = $ldapUser[$userField->name];
if (array_key_exists($user->getId(), $userField->extraFieldValues)) {
/**
* @var ExtraFieldValues $extraFieldValue
*/
$extraFieldValue = $userField->extraFieldValues[$user->getId()];
if ($extraFieldValue->getValue() !== $value) {
$extraFieldValue->setValue($value);
Database::getManager()->persist($extraFieldValue);
if ($debug) {
echo 'Updated ' . $username . ' extra field ' . $userField->name . "\n";
}
}
} else {
$extraFieldValue = new ExtraFieldValues();
$extraFieldValue->setValue($value);
$extraFieldValue->setField($userField->extraField);
$extraFieldValue->setItemId($user->getId());
Database::getManager()->persist($extraFieldValue);
$userField->extraFieldValues[$user->getId()] = $extraFieldValue;
if ($debug) {
echo 'Created ' . $username . ' extra field ' . $userField->name . "\n";
}
}
}
}
try {
Database::getManager()->flush();
} catch (OptimisticLockException $exception) {
die($exception->getMessage()."\n");
}
$userRepository = Database::getManager()->getRepository('ChamiloUserBundle:User');
$dbUsers = [];
foreach ($userRepository->findAll() as $user) {
if ($user->getId() > 1) {
$username = strtolower($user->getUsername());
array_key_exists($username, $dbUsers) and die("duplicate username $username found in the database\n");
$dbUsers[$username] = $user;
$allLdapUsers = array_merge($allLdapUsers, $ldapUsers);
}
}
if ($debug) {
echo count($dbUsers) . " users with id > 1 found in internal database\n";
}
// disable user accounts not found in the LDAP directory
// disable user accounts not found in the LDAP directories
$now = new DateTime();
foreach (array_diff(array_keys($dbUsers), array_keys($ldapUsers)) as $usernameToDisable) {
foreach (array_diff(array_keys($dbUsers), array_keys($allLdapUsers)) as $usernameToDisable) {
$user = $dbUsers[$usernameToDisable];
if ($user->isActive()) {
// In order to avoid slow individual SQL updates, we do not call
// UserManager::disable($user->getId());
$user->setActive(false);
UserManager::getManager()->save($user, false);
Database::getManager()->persist($user);
// In order to avoid slow individual SQL updates, we do not call
// Event::addEvent(LOG_USER_DISABLE, LOG_USER_ID, $user->getId());
$trackEDefault = new TrackEDefault();
@ -256,76 +367,6 @@ try {
}
// create new user accounts found in the LDAP directory and update the existing ones, re-enabling them if disabled
foreach ($ldapUsers as $username => $ldapUser) {
if (array_key_exists($username, $dbUsers)) {
$user = $dbUsers[$username];
} else {
$user = new User();
$dbUsers[$username] = $user;
if ($debug) {
echo 'Created ' . $username . "\n";
}
}
foreach ($tableFields as $userField) {
$value = $ldapUser[$userField->name];
if ($userField->getter->invoke($user) !== $value) {
$userField->setter->invoke($user, $value);
if ($debug) {
echo 'Updated ' . $username . ' field '.$userField->name."\n";
}
}
}
if (!$user->isActive()) {
$user->setActive(true);
}
UserManager::getManager()->save($user, false);
}
try {
Database::getManager()->flush();
} catch (OptimisticLockException $exception) {
die($exception->getMessage()."\n");
}
// also update extra field values
foreach ($ldapUsers as $username => $ldapUser) {
$user = $dbUsers[$username];
foreach ($extraFields as $userField) {
$value = $ldapUser[$userField->name];
if (array_key_exists($user->getId(), $userField->extraFieldValues)) {
/**
* @var ExtraFieldValues $extraFieldValue
*/
$extraFieldValue = $userField->extraFieldValues[$user->getId()];
if ($extraFieldValue->getValue() !== $value) {
$extraFieldValue->setValue($value);
Database::getManager()->persist($extraFieldValue);
if ($debug) {
echo 'Updated ' . $username . ' extra field ' . $userField->name . "\n";
}
}
} else {
$extraFieldValue = new ExtraFieldValues();
$extraFieldValue->setValue($value);
$extraFieldValue->setField($userField->extraField);
$extraFieldValue->setItemId($user->getId());
Database::getManager()->persist($extraFieldValue);
$userField->extraFieldValues[$user->getId()] = $extraFieldValue;
if ($debug) {
echo 'Created ' . $username . ' extra field ' . $userField->name . "\n";
}
}
}
}
try {
Database::getManager()->flush();
} catch (OptimisticLockException $exception) {
die($exception->getMessage()."\n");
}
// anonymize user accounts disabled for more than 3 years
$longDisabledUserIds = [];

Loading…
Cancel
Save