Explorar o código

Merge branch '1.11.x' of github.com:chamilo/chamilo-lms into 1.11.x

Yannick Warnier %!s(int64=6) %!d(string=hai) anos
pai
achega
85c16f76b6
Modificáronse 66 ficheiros con 1020 adicións e 281 borrados
  1. 1 0
      .php_cs.dist
  2. 4 0
      app/Resources/public/css/base.css
  3. 7 0
      index.php
  4. 13 2
      main/announcements/announcements.php
  5. 0 1
      main/auth/ldap/syncro_users.php
  6. 1 1
      main/blog/blog.php
  7. 48 17
      main/cron/import_csv.php
  8. 1 1
      main/document/showinframes.php
  9. 17 5
      main/exercise/MultipleAnswerTrueFalseDegreeCertainty.php
  10. 15 0
      main/exercise/admin.php
  11. 16 10
      main/exercise/exercise.class.php
  12. 7 3
      main/exercise/exercise_submit.php
  13. 1 0
      main/exercise/fill_blanks.class.php
  14. 3 4
      main/exercise/hotspot_admin.inc.php
  15. 15 12
      main/exercise/overview.php
  16. 16 1
      main/gradebook/gradebook_edit_result.php
  17. 15 8
      main/gradebook/gradebook_statistics.php
  18. 140 7
      main/gradebook/gradebook_view_result.php
  19. 6 0
      main/gradebook/lib/be/result.class.php
  20. 13 10
      main/gradebook/lib/fe/displaygradebook.php
  21. 5 0
      main/gradebook/lib/fe/evalform.class.php
  22. 68 3
      main/gradebook/lib/fe/resulttable.class.php
  23. 1 1
      main/gradebook/my_certificates.php
  24. BIN=BIN
      main/img/icons/128/external_link.png
  25. BIN=BIN
      main/img/icons/16/external_link.png
  26. BIN=BIN
      main/img/icons/22/external_link.png
  27. BIN=BIN
      main/img/icons/32/external_link.png
  28. BIN=BIN
      main/img/icons/48/external_link.png
  29. BIN=BIN
      main/img/icons/64/external_link.png
  30. 193 0
      main/img/icons/svg/external_link.svg
  31. 5 4
      main/inc/introductionSection.inc.php
  32. 19 0
      main/inc/lib/AnnouncementEmail.php
  33. 13 6
      main/inc/lib/AnnouncementManager.php
  34. 1 0
      main/inc/lib/add_course.lib.inc.php
  35. 1 1
      main/inc/lib/api.lib.php
  36. 11 6
      main/inc/lib/course.lib.php
  37. 1 1
      main/inc/lib/course_home.lib.php
  38. 1 0
      main/inc/lib/database.constants.inc.php
  39. 1 2
      main/inc/lib/notification.lib.php
  40. 6 1
      main/inc/lib/template.lib.php
  41. 3 1
      main/inc/lib/userportal.lib.php
  42. 2 2
      main/inc/local.inc.php
  43. 14 0
      main/install/configuration.dist.php
  44. 2 0
      main/lang/english/trad4all.inc.php
  45. 3 1
      main/lp/learnpath.class.php
  46. 2 0
      main/lp/learnpathItem.class.php
  47. 1 1
      main/lp/lp_view.php
  48. 1 0
      main/session/add_users_to_session.php
  49. 14 22
      main/social/personal_data.php
  50. 37 1
      main/template/default/agenda/month.tpl
  51. 6 0
      main/template/default/chat/chat.tpl
  52. 21 10
      main/work/work.lib.php
  53. 3 2
      plugin/customcertificate/src/CustomCertificatePlugin.php
  54. 9 0
      plugin/customcertificate/src/print_certificate.php
  55. 1 1
      plugin/learning_calendar/LearningCalendarPlugin.php
  56. 4 2
      plugin/learning_calendar/view/calendar.tpl
  57. 1 1
      plugin/search_course/plugin.php
  58. 1 1
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/TestMatching.php
  59. 1 1
      src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/TestQuestionDescription.php
  60. 20 0
      src/Chamilo/CoreBundle/Entity/ExtraFieldValues.php
  61. 4 0
      src/Chamilo/UserBundle/Entity/User.php
  62. 111 74
      src/Chamilo/UserBundle/Repository/UserRepository.php
  63. 0 24
      tests/behat/features/bootstrap/FeatureContext.php
  64. 50 30
      tests/behat/features/toolExercise.feature
  65. 42 0
      tests/scripts/fix_lp_calendar.php
  66. 2 0
      user_portal.php

+ 1 - 0
.php_cs.dist

@@ -17,6 +17,7 @@ $rules = [
     'yoda_style' => false,
     'phpdoc_to_comment' => false,
     'phpdoc_no_package' => false,
+    'phpdoc_annotation_without_dot' => false,
     'increment_style' => ['style' => 'post'],
     'no_useless_else' => false,
     //'no_php4_constructor' => true,

+ 4 - 0
app/Resources/public/css/base.css

@@ -138,6 +138,10 @@ select {
     margin-bottom: 10px;
 }
 
+#form-login .form-group small,  #form-login .form-group .form_required {
+    display: none;
+}
+
 #form-login label {
     display: none;
 }

+ 7 - 0
index.php

@@ -179,5 +179,12 @@ if (isset($_GET['firstpage'])) {
 } else {
     api_delete_firstpage_parameter();
 }
+
 $controller->setGradeBookDependencyBar(api_get_user_id());
 $controller->tpl->display_two_col_template();
+
+// Deleting the session_id.
+Session::erase('session_id');
+Session::erase('id_session');
+Session::erase('studentview');
+api_remove_in_gradebook();

+ 13 - 2
main/announcements/announcements.php

@@ -450,7 +450,7 @@ switch ($action) {
                 );
                 // when we want to remind the users who have never been active
                 // then we have a different subject and content for the announcement
-                if ($_GET['since'] == 'never') {
+                if ($_GET['since'] === 'never') {
                     $title_to_modify = sprintf(
                         get_lang('RemindInactiveLearnersMailSubject'),
                         api_get_setting('siteName')
@@ -475,6 +475,7 @@ switch ($action) {
         $announcementInfo = AnnouncementManager::get_by_id($courseId, $id);
         if (isset($announcementInfo) && !empty($announcementInfo)) {
             $to = AnnouncementManager::load_edit_users('announcement', $id);
+
             $defaults = [
                 'title' => $announcementInfo['title'],
                 'content' => $announcementInfo['content'],
@@ -545,6 +546,8 @@ switch ($action) {
             $form->addCheckBox('send_to_hrm_users', null, get_lang('SendAnnouncementCopyToDRH'));
         }
 
+        $form->addCheckBox('send_me_a_copy_by_email', null, get_lang('SendAnnouncementCopyToMyself'));
+        $defaults['send_me_a_copy_by_email'] = true;
         $form->addButtonSave(get_lang('ButtonPublishAnnouncement'));
         $form->setDefaults($defaults);
 
@@ -552,6 +555,7 @@ switch ($action) {
             $data = $form->getSubmitValues();
             $data['users'] = isset($data['users']) ? $data['users'] : [];
             $sendToUsersInSession = isset($data['send_to_users_in_session']) ? true : false;
+            $sendMeCopy = isset($data['send_me_a_copy_by_email']) ? true : false;
 
             if (isset($id) && $id) {
                 // there is an Id => the announcement already exists => update mode
@@ -576,10 +580,17 @@ switch ($action) {
                             api_get_session_id(),
                             $id,
                             $sendToUsersInSession,
-                            isset($data['send_to_hrm_users'])
+                            isset($data['send_to_hrm_users']),
+                            null,
+                            $sendMeCopy
                         );
                     }
 
+                    if ($sendMeCopy) {
+                        $email = new AnnouncementEmail(api_get_course_info(), api_get_session_id(), $id);
+                        $email->sendAnnouncementEmailToMySelf();
+                    }
+
                     Display::addFlash(
                         Display::return_message(
                             get_lang('AnnouncementModified'),

+ 0 - 1
main/auth/ldap/syncro_users.php

@@ -1,6 +1,5 @@
 <?php
 
-
 require_once '../../inc/global.inc.php';
 require_once 'authldap.php';
 

+ 1 - 1
main/blog/blog.php

@@ -436,7 +436,7 @@ $content = $tpl->fetch($blogLayout);
 if ($actionsLeft) {
     $tpl->assign(
         'actions',
-        Display::return_introduction_section(TOOL_BLOGS)
+        Display::return_introduction_section(TOOL_BLOGS."_$blog_id")
             .Display::toolbarAction('toolbar', [$actionsLeft])
     );
 }

+ 48 - 17
main/cron/import_csv.php

@@ -240,11 +240,11 @@ class ImportCsv
                 'teachers-static',
                 'courses-static',
                 'sessions-static',
-                'calendar-static',
                 'sessionsextid-static',
                 'unsubscribe-static',
                 'unsubsessionsextid-static',
                 'subsessionsextid-static',
+                'calendar-static',
             ];
 
             foreach ($sections as $section) {
@@ -667,6 +667,10 @@ class ImportCsv
                         false //$send_mail = false
                     );
 
+                    $row['extra_mail_notify_invitation'] = 1;
+                    $row['extra_mail_notify_message'] = 1;
+                    $row['extra_mail_notify_group_message'] = 1;
+
                     if ($userId) {
                         foreach ($row as $key => $value) {
                             if (substr($key, 0, 6) == 'extra_') {
@@ -832,6 +836,10 @@ class ImportCsv
                         false //$send_mail = false
                     );
 
+                    $row['extra_mail_notify_invitation'] = 1;
+                    $row['extra_mail_notify_message'] = 1;
+                    $row['extra_mail_notify_group_message'] = 1;
+
                     if ($result) {
                         foreach ($row as $key => $value) {
                             if (substr($key, 0, 6) === 'extra_') {
@@ -1026,13 +1034,20 @@ class ImportCsv
 
                 if (empty($courseInfo)) {
                     $this->logger->addInfo("Course '$courseCode' does not exists");
+                } else {
+                    if ($courseInfo['visibility'] == COURSE_VISIBILITY_HIDDEN) {
+                        $this->logger->addInfo("Course '".$courseInfo['code']."' has hidden visiblity. Skip");
+                        $errorFound = true;
+                    }
                 }
 
                 if (empty($sessionId)) {
-                    $this->logger->addInfo("external_sessionID: ".$externalSessionId." does not exists.");
+                    $this->logger->addInfo("external_sessionID: $externalSessionId does not exists.");
                 }
                 $teacherId = null;
+                $sessionInfo = [];
                 if (!empty($sessionId) && !empty($courseInfo)) {
+                    $sessionInfo = api_get_session_info($sessionId);
                     $courseIncluded = SessionManager::relation_session_course_exist(
                         $sessionId,
                         $courseInfo['real_id']
@@ -1054,7 +1069,6 @@ class ImportCsv
                             $teacher = current($teachers);
                             $teacherId = $teacher['user_id'];
                         } else {
-                            $sessionInfo = api_get_session_info($sessionId);
                             $teacherId = $sessionInfo['id_coach'];
                         }
                     }
@@ -1080,16 +1094,29 @@ class ImportCsv
                 $startDateMonth = substr($date, 4, 2);
                 $startDateDay = substr($date, 6, 8);
 
-                $startDate = $startDateYear.'-'.$startDateMonth.'-'.$startDateDay.' '.$startTime.":00";
-                $endDate = $startDateYear.'-'.$startDateMonth.'-'.$startDateDay.' '.$endTime.":00";
+                $startDate = $startDateYear.'-'.$startDateMonth.'-'.$startDateDay.' '.$startTime.':00';
+                $endDate = $startDateYear.'-'.$startDateMonth.'-'.$startDateDay.' '.$endTime.':00';
 
                 if (!api_is_valid_date($startDate) || !api_is_valid_date($endDate)) {
-                    $this->logger->addInfo(
-                        "Verify your dates:  '$startDate' : '$endDate' "
-                    );
+                    $this->logger->addInfo("Verify your dates:  '$startDate' : '$endDate' ");
                     $errorFound = true;
                 }
 
+                // Check session dates
+                if ($sessionInfo && !empty($sessionInfo['access_start_date'])) {
+                    $date = new \DateTime($sessionInfo['access_start_date']);
+                    $interval = new \DateInterval('P7D');
+                    $date->sub($interval);
+                    if ($date->getTimestamp() > time()) {
+                        $this->logger->addInfo(
+                            "Calendar event # ".$row['external_calendar_itemID']." 
+                            in session [$externalSessionId] was not added 
+                            because the startdate is more than 7 days in the future: ".$sessionInfo['access_start_date']
+                        );
+                        $errorFound = true;
+                    }
+                }
+
                 if ($errorFound == false) {
                     $eventsToCreate[] = [
                         'start' => $startDate,
@@ -1107,7 +1134,7 @@ class ImportCsv
             }
 
             if (empty($eventsToCreate)) {
-                $this->logger->addInfo("No events to add");
+                $this->logger->addInfo('No events to add');
 
                 return 0;
             }
@@ -1117,9 +1144,7 @@ class ImportCsv
             $externalEventId = null;
 
             $extraField = new ExtraField('calendar_event');
-            $extraFieldInfo = $extraField->get_handler_field_info_by_field_variable(
-                $extraFieldName
-            );
+            $extraFieldInfo = $extraField->get_handler_field_info_by_field_variable($extraFieldName);
 
             if (empty($extraFieldInfo)) {
                 $this->logger->addInfo(
@@ -1379,6 +1404,12 @@ class ImportCsv
                         $this->logger->addInfo(
                             "Mail to be sent because start date: ".$event['start']." and no announcement found."
                         );
+
+                        $senderId = $this->defaultAdminId;
+                        if (!empty($coaches) && isset($coaches[0]) && !empty($coaches[0])) {
+                            $senderId = $coaches[0];
+                        }
+
                         $announcementId = AnnouncementManager::add_announcement(
                             $courseInfo,
                             $event['session_id'],
@@ -1392,12 +1423,12 @@ class ImportCsv
                             null,
                             null,
                             false,
-                            $this->defaultAdminId
+                            $senderId
                         );
 
                         if ($announcementId) {
-                            $this->logger->addInfo("Announcement added: ".(int) ($announcementId)." in $info");
-                            $this->logger->addInfo("<<--SENDING MAIL-->>");
+                            $this->logger->addInfo("Announcement added: $announcementId in $info");
+                            $this->logger->addInfo("<<--SENDING MAIL Sender id: $senderId-->>");
                             $report['mail_sent']++;
                             AnnouncementManager::sendEmail(
                                 $courseInfo,
@@ -1406,11 +1437,11 @@ class ImportCsv
                                 false,
                                 false,
                                 $this->logger,
-                                $this->defaultAdminId
+                                $senderId
                             );
                         } else {
                             $this->logger->addError(
-                                "Error when trying to add announcement with title ".$subject." here: $info"
+                                "Error when trying to add announcement with title $subject here: $info and SenderId = $senderId"
                             );
                         }
                     } else {

+ 1 - 1
main/document/showinframes.php

@@ -27,7 +27,7 @@ require_once __DIR__.'/../inc/global.inc.php';
 api_protect_course_script();
 
 $header_file = isset($_GET['file']) ? Security::remove_XSS($_GET['file']) : null;
-$document_id = intval($_GET['id']);
+$document_id = (int) $_GET['id'];
 $originIsLearnpath = isset($_GET['origin']) && $_GET['origin'] === 'learnpathitem';
 $courseInfo = api_get_course_info();
 $course_code = api_get_course_id();

+ 17 - 5
main/exercise/MultipleAnswerTrueFalseDegreeCertainty.php

@@ -88,7 +88,7 @@ class MultipleAnswerTrueFalseDegreeCertainty extends Question
         if (!empty($this->id)) {
             $answer = new Answer($this->id);
             $answer->read();
-            if (count($answer->nbrAnswers) > 0 && !$form->isSubmitted()) {
+            if ($answer->nbrAnswers > 0 && !$form->isSubmitted()) {
                 $nbAnswers = $answer->nbrAnswers;
             }
         }
@@ -130,11 +130,23 @@ class MultipleAnswerTrueFalseDegreeCertainty extends Question
             $defaults['correct['.$i.']'] = '';
 
             if (is_object($answer)) {
-                $defaults['answer['.$i.']'] = $answer->answer[$i];
-                $defaults['comment['.$i.']'] = $answer->comment[$i];
-                $defaults['weighting['.$i.']'] = float_format($answer->weighting[$i], 1);
-                $correct = $answer->correct[$i];
+                $defaults['answer['.$i.']'] = isset($answer->answer[$i]) ? $answer->answer[$i] : '';
+                if (isset($_POST['answer']) && isset($_POST['answer'][$i])) {
+                    $defaults['answer['.$i.']'] = Security::remove_XSS($_POST['answer'][$i]);
+                }
+
+                $defaults['comment['.$i.']'] = isset($answer->comment[$i]) ? $answer->comment[$i] : '';
+                if (isset($_POST['comment']) && isset($_POST['comment'][$i])) {
+                    $defaults['comment['.$i.']'] = Security::remove_XSS($_POST['comment'][$i]);
+                }
+
+                $defaults['weighting['.$i.']'] = isset($answer->weighting[$i]) ? float_format($answer->weighting[$i], 1) : '';
+                $correct = isset($answer->correct[$i]) ? $answer->correct[$i] : '';
                 $defaults['correct['.$i.']'] = $correct;
+                if (isset($_POST['correct']) && isset($_POST['correct'][$i])) {
+                    $defaults['correct['.$i.']'] = Security::remove_XSS($_POST['correct'][$i]);
+                }
+
                 $j = 1;
                 if (!empty($optionData)) {
                     foreach ($optionData as $id => $data) {

+ 15 - 0
main/exercise/admin.php

@@ -64,6 +64,20 @@ if (empty($exerciseId)) {
     $exerciseId = isset($_GET['exerciseId']) ? intval($_GET['exerciseId']) : '0';
 }
 
+/*  stripslashes POST data  */
+if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+    foreach ($_POST as $key => $val) {
+        if (is_string($val)) {
+            $_POST[$key] = stripslashes($val);
+        } elseif (is_array($val)) {
+            foreach ($val as $key2 => $val2) {
+                $_POST[$key][$key2] = stripslashes($val2);
+            }
+        }
+        $GLOBALS[$key] = $_POST[$key];
+    }
+}
+
 $newQuestion = isset($_GET['newQuestion']) ? $_GET['newQuestion'] : 0;
 
 if (empty($modifyAnswers)) {
@@ -293,6 +307,7 @@ if ($modifyIn == 'thisExercise') {
         $modifyIn = 'allExercises';
     }
 }
+
 $htmlHeadXtra[] = api_get_js('jqueryui-touch-punch/jquery.ui.touch-punch.min.js');
 $htmlHeadXtra[] = api_get_js('jquery.jsPlumb.all.js');
 

+ 16 - 10
main/exercise/exercise.class.php

@@ -3229,7 +3229,11 @@ class Exercise
      */
     public function showTimeControlJS($time_left)
     {
-        $time_left = intval($time_left);
+        $time_left = (int) $time_left;
+        $script = "redirectExerciseToResult();";
+        if ($this->type == ALL_ON_ONE_PAGE) {
+            $script = "save_now_all('validate');";
+        }
 
         return "<script>
             function openClockWarning() {
@@ -3247,6 +3251,7 @@ class Exercise
                         send_form();
                     }
                 });
+                
                 $('#clock_warning').dialog('open');
                 $('#counter_to_redirect').epiclock({
                     mode: $.epiclock.modes.countdown,
@@ -3259,7 +3264,7 @@ class Exercise
 
             function send_form() {
                 if ($('#exercise_form').length) {
-                    save_now_all('validate');
+                    $script
                 } else {
                     // In exercise_reminder.php
                     final_submit();
@@ -4731,6 +4736,7 @@ class Exercise
                                 $questionScore
                             );
                         } elseif ($answerType == HOT_SPOT) {
+                            $correctAnswerId = 0;
                             /**
                              * @var int
                              * @var TrackEHotspot $hotspot
@@ -8200,17 +8206,17 @@ class Exercise
                 '<table width="730" height="136" border="0" cellpadding="3" cellspacing="3">';
             $msg .= $open_question_list;
             $msg .= '</table><br />';
-            $msg1 = str_replace("#exercise#", $this->exercise, $msg);
-            $msg = str_replace("#firstName#", $user_info['firstname'], $msg1);
-            $msg1 = str_replace("#lastName#", $user_info['lastname'], $msg);
-            $msg = str_replace("#mail#", $user_info['email'], $msg1);
-            $msg = str_replace("#course#", $courseInfo['name'], $msg1);
+
+            $msg = str_replace("#exercise#", $this->exercise, $msg);
+            $msg = str_replace("#firstName#", $user_info['firstname'], $msg);
+            $msg = str_replace("#lastName#", $user_info['lastname'], $msg);
+            $msg = str_replace("#mail#", $user_info['email'], $msg);
+            $msg = str_replace("#course#", $courseInfo['name'], $msg);
 
             if ($origin != 'learnpath') {
                 $msg .= '<br /><a href="#url#">'.get_lang('ClickToCommentAndGiveFeedback').'</a>';
             }
-            $msg1 = str_replace("#url#", $url_email, $msg);
-            $mail_content = $msg1;
+            $msg = str_replace("#url#", $url_email, $msg);
             $subject = get_lang('OpenQuestionsAttempted');
 
             if (!empty($teachers)) {
@@ -8218,7 +8224,7 @@ class Exercise
                     MessageManager::send_message_simple(
                         $user_id,
                         $subject,
-                        $mail_content
+                        $msg
                     );
                 }
             }

+ 7 - 3
main/exercise/exercise_submit.php

@@ -1201,8 +1201,13 @@ if (!empty($error)) {
             //$("#save_for_now_"+question_id).html(\''.Display::return_icon('save.png', get_lang('Saved'), [], ICON_SIZE_SMALL).'\');
             window.location = url;
         }
+        
+        function redirectExerciseToResult() 
+        {
+            window.location = "'.$script_php.'?'.$params.'";
+        }
 
-        function save_now(question_id, url_extra) {
+        function save_now(question_id, url_extra, validate) {
             //1. Normal choice inputs
             var my_choice = $(\'*[name*="choice[\'+question_id+\']"]\').serialize();
 
@@ -1245,7 +1250,7 @@ if (!empty($error)) {
                 success: function(return_value) {
                     if (return_value == "ok") {
                         $("#save_for_now_"+question_id).html(\''.
-                            Display::return_icon('save.png', get_lang('Saved'), [], ICON_SIZE_SMALL).'\');
+                        Display::return_icon('save.png', get_lang('Saved'), [], ICON_SIZE_SMALL).'\');                                                    
                     } else if (return_value == "error") {
                         $("#save_for_now_"+question_id).html(\''.
                             Display::return_icon('error.png', get_lang('Error'), [], ICON_SIZE_SMALL).'\');
@@ -1311,7 +1316,6 @@ if (!empty($error)) {
                 data: "'.$params.'&type=all&"+my_choice+"&"+hotspot+"&"+free_answers+"&"+remind_list,
                 success: function(return_value) {
                     if (return_value == "ok") {
-                        //$("#save_all_response").html(\''.Display::return_icon('accept.png').'\');
                         if (validate == "validate") {
                             window.location = "'.$script_php.'?'.$params.'";
                         } else {

+ 1 - 0
main/exercise/fill_blanks.class.php

@@ -664,6 +664,7 @@ class FillBlanks extends Question
                     $item = $listMenu[0];
                     if (!$fromDatabase) {
                         $item = sha1($item);
+                        $studentAnswer = sha1($studentAnswer);
                     }
                     if ($item === $studentAnswer) {
                         $result = true;

+ 3 - 4
main/exercise/hotspot_admin.inc.php

@@ -11,7 +11,7 @@ use ChamiloSession as Session;
  *
  * @author Toon Keppens
  */
-$modifyAnswers = intval($_GET['hotspotadmin']);
+$modifyAnswers = (int) $_GET['hotspotadmin'];
 
 if (!is_object($objQuestion)) {
     $objQuestion = Question::read($modifyAnswers);
@@ -30,7 +30,7 @@ if ($modifyIn) {
         echo '$modifyIn was set'."<br />\n";
     }
     // if the user has chosen to modify the question only in the current exercise
-    if ($modifyIn == 'thisExercise') {
+    if ($modifyIn === 'thisExercise') {
         // duplicates the question
         $questionId = $objQuestion->duplicate();
 
@@ -71,14 +71,13 @@ $hotspot_admin_url = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_c
 // the answer form has been submitted
 $submitAnswers = isset($_POST['submitAnswers']) ? true : false;
 $buttonBack = isset($_POST['buttonBack']) ? true : false;
-$nbrAnswers = isset($_POST['nbrAnswers']) ? intval($_POST['nbrAnswers']) : 0;
+$nbrAnswers = isset($_POST['nbrAnswers']) ? (int) $_POST['nbrAnswers'] : 0;
 
 if ($submitAnswers || $buttonBack) {
     if ($answerType == HOT_SPOT) {
         if ($debug > 0) {
             echo '$submitAnswers or $buttonBack was set'."<br />\n";
         }
-
         $questionWeighting = $nbrGoodAnswers = 0;
         for ($i = 1; $i <= $nbrAnswers; $i++) {
             if ($debug > 0) {

+ 15 - 12
main/exercise/overview.php

@@ -77,28 +77,31 @@ $html = '';
 $message = '';
 $html .= '<div class="exercise-overview">';
 $is_allowed_to_edit = api_is_allowed_to_edit(null, true);
-$edit_link = '';
-if ($is_allowed_to_edit && $objExercise->sessionId == $sessionId) {
-    $edit_link = Display::url(
-        Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL),
-        api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id
-    );
-} elseif (api_is_teacher()) {
-    $edit_link = Display::url(
+$editLink = '';
+if ($is_allowed_to_edit) {
+    if ($objExercise->sessionId == $sessionId) {
+        $editLink = Display::url(
+            Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL),
+            api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id
+        );
+    }
+    $editLink .= Display::url(
         Display::return_icon('test_results.png', get_lang('Results'), [], ICON_SIZE_SMALL),
-        'exercise_report.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id
+        api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.api_get_cidreq().'&exerciseId='.$objExercise->id,
+        ['title' => get_lang('Results')]
     );
 }
+
 $iconExercise = Display::return_icon('test-quiz.png', null, [], ICON_SIZE_MEDIUM);
-// Exercise name.
 
+// Exercise name.
 if (api_get_configuration_value('save_titles_as_html')) {
     $html .= Display::div(
-        $objExercise->get_formated_title().PHP_EOL.$edit_link
+        $objExercise->get_formated_title().PHP_EOL.$editLink
     );
 } else {
     $html .= Display::page_header(
-        $iconExercise.PHP_EOL.$objExercise->selectTitle().PHP_EOL.$edit_link
+        $iconExercise.PHP_EOL.$objExercise->selectTitle().PHP_EOL.$editLink
     );
 }
 

+ 16 - 1
main/gradebook/gradebook_edit_result.php

@@ -37,8 +37,23 @@ if ($edit_result_form->validate()) {
         if (empty($score)) {
             $score = 0;
         }
-        $result->set_score(api_number_format($score, api_get_setting('gradebook_number_decimals')));
+        $score = api_number_format($score, api_get_setting('gradebook_number_decimals'));
+        $result->set_score($score);
         $result->save();
+
+        $allowMultipleAttempts = api_get_configuration_value('gradebook_multiple_evaluation_attempts');
+        if ($allowMultipleAttempts) {
+            $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT);
+            $now = api_get_utc_datetime();
+            $params = [
+                'result_id' => $result->get_id(),
+                'score' => $score,
+                'comment' => '',
+                'created_at' => $now,
+                'updated_at' => $now,
+            ];
+            Database::insert($table, $params);
+        }
     }
     Display::addFlash(Display::return_message(get_lang('AllResultsEdited')));
     header('Location: gradebook_view_result.php?selecteval='.$select_eval.'&'.api_get_cidreq());

+ 15 - 8
main/gradebook/gradebook_statistics.php

@@ -10,13 +10,20 @@ require_once __DIR__.'/../inc/global.inc.php';
 
 api_block_anonymous_users();
 
-$eval = Evaluation :: load($_GET['selecteval']);
-if ($eval[0]->get_category_id() < 0) {
+$categoryId = (int) $_GET['selecteval'];
+$eval = Evaluation::load($categoryId);
+if (!isset($eval[0])) {
+    api_not_allowed(true);
+}
+/** @var Evaluation $eval */
+$eval = $eval[0];
+
+if ($eval->get_category_id() < 0) {
     // if category id is negative, then the evaluation's origin is a link
-    $link = LinkFactory::get_evaluation_link($eval[0]->get_id());
+    $link = LinkFactory::get_evaluation_link($eval->get_id());
     $currentcat = Category::load($link->get_category_id());
 } else {
-    $currentcat = Category::load($eval[0]->get_category_id());
+    $currentcat = Category::load($eval->get_category_id());
 }
 
 $interbreadcrumb[] = [
@@ -26,7 +33,7 @@ $interbreadcrumb[] = [
 
 if (api_is_allowed_to_edit()) {
     $interbreadcrumb[] = [
-        'url' => 'gradebook_view_result.php?selecteval='.intval($_GET['selecteval']).'&'.api_get_cidreq(),
+        'url' => 'gradebook_view_result.php?selecteval='.$categoryId.'&'.api_get_cidreq(),
         'name' => get_lang('ViewResult'),
     ];
 }
@@ -34,7 +41,7 @@ $displayScore = ScoreDisplay::instance();
 
 Display::display_header(get_lang('EvaluationStatistics'));
 DisplayGradebook::display_header_result(
-    $eval[0],
+    $eval,
     $currentcat[0]->get_id(),
     0,
     'statistics'
@@ -48,7 +55,7 @@ if (!$displayScore->is_custom() || empty($displays)) {
         echo Display::return_message(get_lang('PleaseEnableScoringSystem'), 'error', false);
     }
 } else {
-    $allresults = Result::load(null, null, $eval[0]->get_id());
+    $allresults = Result::load(null, null, $eval->get_id());
     $nr_items = [];
     foreach ($displays as $itemsdisplay) {
         $nr_items[$itemsdisplay['display']] = 0;
@@ -59,7 +66,7 @@ if (!$displayScore->is_custom() || empty($displays)) {
         $score = $result->get_score();
         if (isset($score)) {
             $display = $displayScore->display_score(
-                [$score, $eval[0]->get_max()],
+                [$score, $eval->get_max()],
                 SCORE_CUSTOM,
                 SCORE_ONLY_CUSTOM,
                 true

+ 140 - 7
main/gradebook/gradebook_view_result.php

@@ -54,9 +54,130 @@ if (isset($_GET['selecteval'])) {
     $iscourse = !empty(api_get_course_id());
 }
 
+$allowMultipleAttempts = api_get_configuration_value('gradebook_multiple_evaluation_attempts');
+
+if (isset($_GET['action'])) {
+    switch ($_GET['action']) {
+        case 'delete_attempt':
+            $result = Result::load($_GET['editres']);
+            if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()) {
+                /** @var Result $result */
+                $result = $result[0];
+                $url = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq().'&editres='.$result->get_id();
+                $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT);
+                if (isset($_GET['result_attempt_id'])) {
+                    $attemptId = (int) $_GET['result_attempt_id'];
+                    $sql = "DELETE FROM $table WHERE result_id = ".$result->get_id()." AND id = $attemptId";
+                    Database::query($sql);
+                    Display::addFlash(Display::return_message(get_lang('Deleted')));
+                }
+
+                header('Location: '.$url.'&action=add_attempt');
+                exit;
+            }
+            break;
+        case 'add_attempt':
+            $result = Result::load($_GET['editres']);
+            if ($allowMultipleAttempts && !empty($result) && isset($result[0]) && api_is_allowed_to_edit()) {
+                /** @var Result $result */
+                $result = $result[0];
+                $backUrl = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq();
+                $interbreadcrumb[] = [
+                    'url' => $backUrl,
+                    'name' => get_lang('Details'),
+                ];
+
+                /** @var Evaluation $evaluation */
+                $evaluation = $eval[0];
+
+                $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT);
+                $now = api_get_utc_datetime();
+
+                $url = api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq().'&editres='.$result->get_id();
+                $form = new FormValidator('attempt', 'post', $url.'&action=add_attempt');
+                $form->addHeader(get_lang('AddResult'));
+                $form->addLabel(get_lang('CurrentScore'), $result->get_score());
+
+                $form->addFloat(
+                    'score',
+                    [
+                        get_lang('Score'),
+                        null,
+                        '/ '.$evaluation->get_max(),
+                    ],
+                    true,
+                    [
+                        'size' => '4',
+                        'maxlength' => '5',
+                    ],
+                    false,
+                    0,
+                    $evaluation->get_max()
+                );
+
+                $form->addTextarea('comment', get_lang('Comment'));
+
+                /*$form->addRule(
+                    'score',
+                    get_lang('ValueTooBig'),
+                    'max_numeric_length',
+                    $evaluation->get_max()
+                );*/
+                $form->addButtonSave(get_lang('Save'));
+                $attemptList = ResultTable::getResultAttemptTable($result, $url);
+                $form->addLabel(get_lang('Attempts'), $attemptList);
+
+                if ($form->validate()) {
+                    $values = $form->getSubmitValues();
+                    $newScore = $values['score'];
+
+                    $newScore = api_number_format(
+                        $newScore,
+                        api_get_setting('gradebook_number_decimals')
+                    );
+
+                    $params = [
+                        'result_id' => $result->get_id(),
+                        'score' => $newScore,
+                        'comment' => $values['comment'],
+                        'created_at' => $now,
+                        'updated_at' => $now,
+                    ];
+
+                    Database::insert($table, $params);
+                    if ($newScore > $result->get_score()) {
+                        $result->set_score($newScore);
+                        $result->save();
+                    }
+                    Display::addFlash(Display::return_message(get_lang('Saved')));
+                    header('Location: '.api_get_self().'?selecteval='.$select_eval.'&'.api_get_cidreq());
+                    exit;
+                }
+
+                Display::display_header();
+
+                $items[] = [
+                    'url' => $backUrl,
+                    'content' => Display::return_icon(
+                        'back.png',
+                        get_lang('Back'),
+                        [],
+                        ICON_SIZE_MEDIUM
+                    ),
+                ];
+
+                echo Display::actions($items);
+                $form->display();
+                Display::display_footer();
+                exit;
+            }
+            break;
+    }
+}
+
 if (isset($_GET['editres'])) {
     $edit_res_xml = Security::remove_XSS($_GET['editres']);
-    $resultedit = Result :: load($edit_res_xml);
+    $resultedit = Result::load($edit_res_xml);
     $edit_res_form = new EvalForm(
         EvalForm::TYPE_RESULT_EDIT,
         $eval[0],
@@ -76,15 +197,27 @@ if (isset($_GET['editres'])) {
         $result->set_evaluation_id($select_eval);
         $row_value = isset($values['score']) ? $values['score'] : 0;
         if (!empty($row_value) || $row_value == 0) {
-            $result->set_score(
-                api_number_format(
-                    $row_value,
-                    api_get_setting('gradebook_number_decimals')
-                )
+            $row_value = api_number_format(
+                $row_value,
+                api_get_setting('gradebook_number_decimals')
             );
+            $result->set_score($row_value);
         }
         $result->save();
-        unset($result);
+
+        if ($allowMultipleAttempts && !empty($result->get_id())) {
+            $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT);
+            $now = api_get_utc_datetime();
+            $params = [
+                'result_id' => $result->get_id(),
+                'score' => $row_value,
+                'comment' => $values['comment'],
+                'created_at' => $now,
+                'updated_at' => $now,
+            ];
+
+            Database::insert($table, $params);
+        }
 
         Display::addFlash(Display::return_message(get_lang('ResultEdited'), 'normal', false));
         header('Location: gradebook_view_result.php?selecteval='.$select_eval.'&editresmessage=&'.api_get_cidreq());

+ 6 - 0
main/gradebook/lib/be/result.class.php

@@ -283,5 +283,11 @@ class Result
         $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT);
         $sql = 'DELETE FROM '.$table.' WHERE id = '.$this->id;
         Database::query($sql);
+        $allowMultipleAttempts = api_get_configuration_value('gradebook_multiple_evaluation_attempts');
+        if ($allowMultipleAttempts) {
+            $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT);
+            $sql = "DELETE FROM $table WHERE result_id = ".$this->id;
+            Database::query($sql);
+        }
     }
 }

+ 13 - 10
main/gradebook/lib/fe/displaygradebook.php

@@ -11,7 +11,7 @@ class DisplayGradebook
     /**
      * Displays the header for the result page containing the navigation tree and links.
      *
-     * @param $evalobj
+     * @param Evaluation $evalobj
      * @param $selectcat
      * @param $shownavbar 1=show navigation bar
      * @param $forpdf only output for pdf file
@@ -74,6 +74,17 @@ class DisplayGradebook
                     'h3',
                     get_lang('Score').': '.$scoredisplay->display_score($student_score, SCORE_DIV_PERCENT)
                 );
+
+                $allowMultipleAttempts = api_get_configuration_value('gradebook_multiple_evaluation_attempts');
+                if ($allowMultipleAttempts) {
+                    $results = Result::load(null, api_get_user_id(), $evalobj->get_id());
+                    if (!empty($results)) {
+                        /** @var Result $resultData */
+                        foreach ($results as $resultData) {
+                            $student_score .= ResultTable::getResultAttemptTable($resultData);
+                        }
+                    }
+                }
             }
         }
         $description = '';
@@ -100,15 +111,7 @@ class DisplayGradebook
         if (!$evalobj->has_results()) {
             $evalinfo .= '<br /><i>'.get_lang('NoResultsInEvaluation').'</i>';
         }
-        /* Code comment without reason
-        elseif ($scoredisplay->is_custom() && api_get_self() != '/main/gradebook/gradebook_statistics.php') {
-            if (api_is_allowed_to_edit(null, true)) {
-                if ($page != 'statistics') {
-                    //$evalinfo .= '<br /><br /><a href="gradebook_view_result.php?selecteval='.Security::remove_XSS($_GET['selecteval']).'"> '.Display::return_icon(('evaluation_rate.png'),get_lang('ViewResult'),'',ICON_SIZE_MEDIUM) . '</a>';
-                }
-            }
-        }
-        */
+
         if ($page != 'statistics') {
             if (api_is_allowed_to_edit(null, true)) {
                 $evalinfo .= '<br /><a href="gradebook_statistics.php?'.api_get_cidreq().'&selecteval='.Security::remove_XSS($_GET['selecteval']).'"> '.

+ 5 - 0
main/gradebook/lib/fe/evalform.class.php

@@ -395,6 +395,11 @@ class EvalForm extends FormValidator
             $this->evaluation_object->get_max()
         );
 
+        $allowMultipleAttempts = api_get_configuration_value('gradebook_multiple_evaluation_attempts');
+        if ($allowMultipleAttempts) {
+            $this->addTextarea('comment', get_lang('Comment'));
+        }
+
         $this->addButtonSave(get_lang('Edit'), 'submit');
         $this->addElement('hidden', 'hid_user_id', $this->result_object->get_user_id());
     }

+ 68 - 3
main/gradebook/lib/fe/resulttable.class.php

@@ -164,6 +164,56 @@ class ResultTable extends SortableTable
         return $sortable_data;
     }
 
+    /**
+     * @param Result $result
+     * @param string $url
+     *
+     * @return string
+     *
+     */
+    public static function getResultAttemptTable($result, $url = '')
+    {
+        if (empty($result)) {
+
+            return '';
+        }
+
+        $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT);
+
+        $sql = "SELECT * FROM $table WHERE result_id = ".$result->get_id().' ORDER BY created_at DESC';
+        $resultQuery = Database::query($sql);
+        $list = Database::store_result($resultQuery);
+
+        $htmlTable = new HTML_Table(['class' => 'data_table']);
+        $htmlTable->setHeaderContents(0, 0, get_lang('Score'));
+        $htmlTable->setHeaderContents(0, 1, get_lang('Comment'));
+        $htmlTable->setHeaderContents(0, 2, get_lang('CreatedAt'));
+
+        if (!empty($url)) {
+            $htmlTable->setHeaderContents(0, 3, get_lang('Actions'));
+        }
+
+        $row = 1;
+        foreach ($list as $data) {
+            $htmlTable->setCellContents($row, 0, $data['score']);
+            $htmlTable->setCellContents($row, 1, $data['comment']);
+            $htmlTable->setCellContents($row, 2, Display::dateToStringAgoAndLongDate($data['created_at']));
+            if (!empty($url)) {
+                $htmlTable->setCellContents(
+                    $row,
+                    3,
+                    Display::url(
+                        Display::return_icon('delete.png', get_lang('Delete')),
+                        $url.'&action=delete_attempt&result_attempt_id='.$data['id']
+                    )
+                );
+            }
+            $row++;
+        }
+
+        return $htmlTable->toHtml();
+    }
+
     /**
      * @param array $item
      *
@@ -172,10 +222,25 @@ class ResultTable extends SortableTable
     private function build_edit_column($item)
     {
         $locked_status = $this->evaluation->get_locked();
+        $allowMultipleAttempts = api_get_configuration_value('gradebook_multiple_evaluation_attempts');
+        $baseUrl = api_get_self().'?selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq();
         if (api_is_allowed_to_edit(null, true) && $locked_status == 0) {
-            //api_is_course_admin()
-            $edit_column = '<a href="'.api_get_self().'?editres='.$item['result_id'].'&selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq().'">'.
-                Display::return_icon('edit.png', get_lang('Modify'), '', '22').'</a>';
+            $edit_column = '';
+            if ($allowMultipleAttempts) {
+                if (!empty($item['percentage_score'])) {
+                    $edit_column .=
+                        Display::url(
+                            Display::return_icon('add.png', get_lang('AddAttempt'), '', '22'),
+                            $baseUrl.'&action=add_attempt&editres='.$item['result_id']
+                        );
+                } else {
+                    $edit_column .= '<a href="'.api_get_self().'?editres='.$item['result_id'].'&selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq().'">'.
+                        Display::return_icon('edit.png', get_lang('Modify'), '', '22').'</a>';
+                }
+            } else {
+                $edit_column .= '<a href="'.api_get_self().'?editres='.$item['result_id'].'&selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq().'">'.
+                    Display::return_icon('edit.png', get_lang('Modify'), '', '22').'</a>';
+            }
             $edit_column .= ' <a href="'.api_get_self().'?delete_mark='.$item['result_id'].'&selecteval='.$this->evaluation->get_id().'&'.api_get_cidreq().'">'.
                 Display::return_icon('delete.png', get_lang('Delete'), '', '22').'</a>';
         }

+ 1 - 1
main/gradebook/my_certificates.php

@@ -39,7 +39,7 @@ if (api_get_setting('allow_public_certificates') === 'true') {
         'actions',
         Display::toolbarButton(
             get_lang('SearchCertificates'),
-            api_get_path(WEB_CODE_PATH)."gradebook/search.php",
+            api_get_path(WEB_CODE_PATH).'gradebook/search.php',
             'search',
             'info'
         )

BIN=BIN
main/img/icons/128/external_link.png


BIN=BIN
main/img/icons/16/external_link.png


BIN=BIN
main/img/icons/22/external_link.png


BIN=BIN
main/img/icons/32/external_link.png


BIN=BIN
main/img/icons/48/external_link.png


BIN=BIN
main/img/icons/64/external_link.png


+ 193 - 0
main/img/icons/svg/external_link.svg

@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="128"
+   viewBox="0 0 128 128"
+   width="128"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="external_link.svg"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8">
+    <radialGradient
+       xlink:href="#linearGradient5060"
+       r="117.14286"
+       id="radialGradient6719"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(-2.774389,0,0,1.969706,112.7623,-872.8854)"
+       fy="486.64789"
+       fx="605.71429"
+       cy="486.64789"
+       cx="605.71429" />
+    <linearGradient
+       id="linearGradient5060">
+      <stop
+         style="stop-color:black;stop-opacity:1;"
+         offset="0"
+         id="stop5062" />
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="1"
+         id="stop5064" />
+    </linearGradient>
+    <radialGradient
+       xlink:href="#linearGradient5060"
+       r="117.14286"
+       id="radialGradient6717"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.774389,0,0,1.969706,-1891.633,-872.8854)"
+       fy="486.64789"
+       fx="605.71429"
+       cy="486.64789"
+       cx="605.71429" />
+    <linearGradient
+       id="linearGradient5048">
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="0"
+         id="stop5050" />
+      <stop
+         style="stop-color:black;stop-opacity:1;"
+         offset="0.5"
+         id="stop5056" />
+      <stop
+         style="stop-color:black;stop-opacity:0;"
+         offset="1"
+         id="stop5052" />
+    </linearGradient>
+    <linearGradient
+       y2="609.50507"
+       y1="366.64789"
+       xlink:href="#linearGradient5048"
+       x2="302.85715"
+       x1="302.85715"
+       id="linearGradient6715"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(2.774389,0,0,1.969706,-1892.179,-872.8854)" />
+    <linearGradient
+       id="linearGradient11520">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1.0000000;"
+         offset="0.0000000"
+         id="stop11522" />
+      <stop
+         style="stop-color:#dcdcdc;stop-opacity:1.0000000;"
+         offset="1.0000000"
+         id="stop11524" />
+    </linearGradient>
+    <radialGradient
+       xlink:href="#linearGradient5060"
+       r="6.5659914"
+       id="radialGradient1348"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1,0,0,0.338462,0,29.48178)"
+       fy="44.565483"
+       fx="30.203562"
+       cy="44.565483"
+       cx="30.203562" />
+    <radialGradient
+       xlink:href="#linearGradient11520"
+       r="20.530962"
+       id="radialGradient1366"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(1.0332421,0,0,1.0332421,-13.047087,-19.221421)"
+       fy="35.87817"
+       fx="24.44569"
+       cy="35.87817"
+       cx="24.44569" />
+  </defs>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1319"
+     inkscape:window-height="713"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="2"
+     inkscape:cx="102.21806"
+     inkscape:cy="29.007403"
+     inkscape:window-x="47"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4" />
+  <g
+     id="g1121"
+     transform="translate(0.58755422,-1.3133354)"
+     inkscape:export-filename="/var/www/chamilo/main/img/icons/16/external_link.png"
+     inkscape:export-xdpi="12"
+     inkscape:export-ydpi="12">
+    <rect
+       y="1.3133354"
+       x="-0.58755422"
+       height="128"
+       width="128"
+       id="rect1111"
+       style="opacity:1;fill:#cbe4ff;fill-opacity:0;stroke:none;stroke-width:25.76903534;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+    <g
+       transform="matrix(7.4377987,0,0,7.4377987,-27.054286,-19.297854)"
+       id="g1109">
+      <path
+         sodipodi:type="inkscape:offset"
+         inkscape:radius="0.43343499"
+         inkscape:original="M 13.705078 4.3925781 A 0.7727402 0.7727402 0 0 0 13.738281 5.9375 L 16.503906 5.9375 L 8.5605469 13.888672 A 0.7727402 0.7727402 0 1 0 9.6425781 14.986328 L 17.601562 7.0273438 L 17.601562 9.8007812 A 0.7727402 0.7727402 0 0 0 19.146484 9.8007812 L 19.146484 5.1640625 A 0.7727402 0.7727402 0 0 0 18.375 4.3925781 L 13.738281 4.3925781 A 0.7727402 0.7727402 0 0 0 13.705078 4.3925781 z "
+         style="fill:#3566a4;fill-opacity:1;stroke-width:0.77274019"
+         id="path1025"
+         d="m 13.689453,3.9589844 c -0.624612,0.023526 -1.17554,0.6075108 -1.162109,1.2324218 0.01342,0.6242928 0.586611,1.1819055 1.210937,1.1796876 h 1.71875 l -7.2031248,7.2109372 0.025391,-0.02344 c -0.2517986,0.214144 -0.4093209,0.535007 -0.4238281,0.865234 -0.014507,0.330227 0.1156067,0.66499 0.3476562,0.900391 0.2320495,0.2354 0.5641303,0.369146 0.8945312,0.359375 0.330401,-0.0098 0.6533617,-0.163407 0.8710938,-0.41211 l 7.199219,-7.1972652 v 1.7265624 a 0.43347834,0.43347834 0 0 0 0,0.011719 c 0.01668,0.620519 0.584334,1.173828 1.205078,1.173828 0.620743,0 1.190346,-0.553309 1.207031,-1.173828 a 0.43347834,0.43347834 0 0 0 0,-0.011719 V 5.1640625 C 19.579055,4.5338174 19.005245,3.9600081 18.375,3.9589844 h -4.626953 c -0.01727,-3.712e-4 -0.03546,-3.712e-4 -0.05274,0 a 0.43347834,0.43347834 0 0 0 -0.0059,0 z" />
+      <path
+         inkscape:connector-curvature="0"
+         d="M 17.601428,7.0271384 9.6422038,14.986362 A 0.7727402,0.7727402 0 1 1 8.5603676,13.889071 L 16.504137,5.9375747 h -2.76641 a 0.7727402,0.7727402 0 0 1 0,-1.5454804 h 4.636441 a 0.7727402,0.7727402 0 0 1 0.77274,0.7727402 v 4.6364412 a 0.7727402,0.7727402 0 0 1 -1.54548,0 z"
+         id="path819"
+         style="fill:#98b9dc;fill-opacity:1;stroke-width:0.77274019" />
+      <path
+         sodipodi:type="inkscape:offset"
+         inkscape:radius="0.49225906"
+         inkscape:original="M 6.7832031 5.9375 C 5.9331889 5.9375 5.2382812 6.6324077 5.2382812 7.4824219 L 5.2382812 16.755859 A 1.5454804 1.5454804 0 0 0 6.7832031 18.300781 L 16.056641 18.300781 A 1.5454804 1.5454804 0 0 0 17.601562 16.755859 L 17.601562 12.892578 A 0.7727405 0.7727405 0 0 0 16.056641 12.892578 L 16.056641 16.755859 L 6.7832031 16.755859 L 6.7832031 7.4824219 L 10.646484 7.4824219 A 0.7727402 0.7727402 0 0 0 10.646484 5.9375 L 6.7832031 5.9375 z "
+         style="fill:#3566a4;fill-opacity:1;stroke-width:0.77274019"
+         id="path1021"
+         d="m 6.7832031,5.4453125 c -1.1183035,0 -2.0371094,0.9188058 -2.0371093,2.0371094 v 9.2734371 c 1.926e-4,0.533234 0.2206032,1.0624 0.5976562,1.439453 0.377053,0.377054 0.9062196,0.597464 1.4394531,0.597657 h 9.2734379 c 0.533233,-1.93e-4 1.0624,-0.220603 1.439453,-0.597657 0.377053,-0.377053 0.597463,-0.906219 0.597656,-1.439453 v -3.863281 a 0.49230828,0.49230828 0 0 0 0,-0.01367 c -0.01752,-0.650764 -0.614624,-1.230469 -1.265625,-1.230468 -0.651001,0 -1.246148,0.579704 -1.263672,1.230468 a 0.49230828,0.49230828 0 0 0 0,0.01367 v 3.371094 H 7.2753906 V 7.9746094 h 3.3710934 a 0.49230828,0.49230828 0 0 0 0.01367,0 c 0.650781,-0.0175 1.230469,-0.6126563 1.230469,-1.2636719 0,-0.6510156 -0.579688,-1.2481246 -1.230469,-1.265625 a 0.49230828,0.49230828 0 0 0 -0.01367,0 z" />
+      <path
+         inkscape:connector-curvature="0"
+         d="m 16.055947,12.892236 a 0.7727405,0.7727405 0 0 1 1.545481,0 v 3.863702 a 1.5454804,1.5454804 0 0 1 -1.545481,1.54548 H 6.7830651 A 1.5454804,1.5454804 0 0 1 5.2375847,16.755938 V 7.4830551 c 0,-0.8500142 0.6954662,-1.5454804 1.5454804,-1.5454804 h 3.8637009 a 0.7727402,0.7727402 0 0 1 0,1.5454804 H 6.7830651 v 9.2728829 h 9.2728819 z"
+         id="path2"
+         style="fill:#5d91c9;fill-opacity:1;stroke-width:0.77274019" />
+      <path
+         style="fill:#98b9dc;fill-opacity:1;stroke-width:0.77274019"
+         id="path1045"
+         d="M 5.2375847,16.172145 V 7.4830551 c 0,-0.8500142 0.6954662,-1.5454804 1.5454804,-1.5454804 h 3.8637009 c 1.03032,0 1.03032,1.5454804 0,1.5454804 H 6.7830651 v 7.4866099 z"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cssssccc" />
+      <path
+         style="fill:#5d91c9;fill-opacity:1;stroke-width:0.77274019"
+         id="path1047"
+         d="M 17.601428,7.0271384 9.6422038,14.986362 c -0.3824604,0.43483 -0.9131974,0.259341 -1.1233276,0.05045 3.4505518,-3.49488 6.8466718,-7.0202284 10.3359958,-10.4765782 0.08898,0.064559 0.292036,0.2042691 0.292036,0.6046007 v 4.6364412 c -7.41e-4,1.0295783 -1.546221,1.0295783 -1.54548,0 z"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="ccccsccc" />
+    </g>
+  </g>
+</svg>

+ 5 - 4
main/inc/introductionSection.inc.php

@@ -32,6 +32,7 @@ use Chamilo\CourseBundle\Entity\CToolIntro;
 $em = Database::getManager();
 $intro_editAllowed = $is_allowed_to_edit = api_is_allowed_to_edit();
 $session_id = api_get_session_id();
+$blogParam = isset($_GET['blog_id']) ? ('&blog_id='.(int) $_GET['blog_id']) : '';
 
 $introduction_section = '';
 
@@ -46,7 +47,7 @@ if (!empty($courseId)) {
     $form = new FormValidator(
         'introduction_text',
         'post',
-        api_get_self().'?'.api_get_cidreq()
+        api_get_self().'?'.api_get_cidreq().$blogParam
     );
 } else {
     $form = new FormValidator('introduction_text');
@@ -304,7 +305,7 @@ if ($intro_dispCommand) {
         $toolbar .= '<div class="toolbar-edit">';
         $toolbar .= '<div class="btn-group pull-right" role="group">';
         if (!empty($courseId)) {
-            $textIntro = '<a class="btn btn-default" title="'.addslashes(get_lang('AddIntro')).'" href="'.api_get_self().'?'.api_get_cidreq().'&intro_cmdAdd=1">';
+            $textIntro = '<a class="btn btn-default" title="'.addslashes(get_lang('AddIntro')).'" href="'.api_get_self().'?'.api_get_cidreq().$blogParam.'&intro_cmdAdd=1">';
             $textIntro .= '<em class="fa fa-file-text"></em> ';
             $textIntro .= "</a>";
             $toolbar .= $textIntro.$editIconButton;
@@ -319,10 +320,10 @@ if ($intro_dispCommand) {
         $toolbar .= '<div class="btn-group pull-right" rol="group">';
         if (!empty($courseId)) {
             $toolbar .=
-                '<a class="btn btn-default" href="'.api_get_self().'?'.api_get_cidreq().'&intro_cmdEdit=1" title="'.get_lang('Modify').'">
+                '<a class="btn btn-default" href="'.api_get_self().'?'.api_get_cidreq().$blogParam.'&intro_cmdEdit=1" title="'.get_lang('Modify').'">
                 <em class="fa fa-pencil"></em></a>';
             $toolbar .= $editIconButton;
-            $toolbar .= "<a class=\"btn btn-default\" href=\"".api_get_self()."?".api_get_cidreq()."&intro_cmdDel=1\" onclick=\"javascript:
+            $toolbar .= "<a class=\"btn btn-default\" href=\"".api_get_self()."?".api_get_cidreq().$blogParam."&intro_cmdDel=1\" onclick=\"javascript:
                 if(!confirm('".addslashes(api_htmlentities(get_lang('ConfirmYourChoice'), ENT_QUOTES, $charset)).
                 "')) return false;\"><em class=\"fa fa-trash-o\"></em></a>";
         } else {

+ 19 - 0
main/inc/lib/AnnouncementEmail.php

@@ -274,6 +274,25 @@ class AnnouncementEmail
         return $result;
     }
 
+    /**
+     * Send announcement by email to myself
+     */
+    public function sendAnnouncementEmailToMySelf()
+    {
+        $userId = api_get_user_id();
+        $subject = $this->subject();
+        $message = $this->message($userId);
+
+        MessageManager::send_message_simple(
+            $userId,
+            $subject,
+            $message,
+            api_get_user_id(),
+            false,
+            true
+        );
+    }
+
     /**
      * Send emails to users.
      *

+ 13 - 6
main/inc/lib/AnnouncementManager.php

@@ -1,6 +1,7 @@
 <?php
 /* For licensing terms, see /license.txt */
 
+use Chamilo\CoreBundle\Entity\ExtraFieldValues;
 use Chamilo\CourseBundle\Entity\CAnnouncement;
 use Chamilo\CourseBundle\Entity\CItemProperty;
 
@@ -29,6 +30,7 @@ class AnnouncementManager
     {
         $tags = [
             '((user_name))',
+            '((user_email))',
             '((user_firstname))',
             '((user_lastname))',
             '((user_official_code))',
@@ -93,8 +95,10 @@ class AnnouncementManager
         $data['user_firstname'] = '';
         $data['user_lastname'] = '';
         $data['user_official_code'] = '';
+        $data['user_email'] = '';
         if (!empty($readerInfo)) {
             $data['user_name'] = $readerInfo['username'];
+            $data['user_email'] = $readerInfo['email'];
             $data['user_firstname'] = $readerInfo['firstname'];
             $data['user_lastname'] = $readerInfo['lastname'];
             $data['user_official_code'] = $readerInfo['official_code'];
@@ -110,7 +114,7 @@ class AnnouncementManager
             $extraFields = $extraField->get_all(['filter = ?' => 1]);
             if (!empty($extraFields)) {
                 foreach ($extraFields as $extra) {
-                    $data["extra_".$extra['variable']] = '';
+                    $data['extra_'.$extra['variable']] = '';
                 }
             }
 
@@ -119,7 +123,12 @@ class AnnouncementManager
                     if (isset($extra['value'])) {
                         /** @var \Chamilo\CoreBundle\Entity\ExtraFieldValues $value */
                         $value = $extra['value'];
-                        $data['extra_'.$value->getField()->getVariable()] = $value->getValue();
+                        if ($value instanceof ExtraFieldValues) {
+                            $field = $value->getField();
+                            if ($field) {
+                                $data['extra_'.$field->getVariable()] = $value->getValue();
+                            }
+                        }
                     }
                 }
             }
@@ -731,8 +740,7 @@ class AnnouncementManager
             }
 
             // Store in item_property (first the groups, then the users)
-            //if (!isset($to_users)) {
-            if (isset($to_users[0]) && $to_users[0] === 'everyone') {
+            if (empty($to_users) || (isset($to_users[0]) && $to_users[0] === 'everyone')) {
                 // when no user is selected we send it to everyone
                 $send_to = CourseManager::separateUsersGroups($to);
                 // storing the selected groups
@@ -1554,7 +1562,7 @@ class AnnouncementManager
         }
 
         if (!empty($userIdToSearch)) {
-            $userIdToSearch = intval($userIdToSearch);
+            $userIdToSearch = (int) $userIdToSearch;
             $searchCondition .= " AND (ip.insert_user_id = $userIdToSearch)";
         }
 
@@ -1569,7 +1577,6 @@ class AnnouncementManager
         ) {
             // A.1. you are a course admin with a USER filter
             // => see only the messages of this specific user + the messages of the group (s)he is member of.
-
             //if (!empty($user_id)) {
             if (0) {
                 if (is_array($group_memberships) &&

+ 1 - 0
main/inc/lib/add_course.lib.inc.php

@@ -661,6 +661,7 @@ class AddCourse
             ],
             'documents_default_visibility' => ['default' => 'visible', 'category' => 'document'],
             'show_course_in_user_language' => ['default' => 2, 'category' => null],
+            'email_to_teachers_on_new_work_feedback' => ['default' => 1, 'category' => null],
         ];
 
         $counter = 1;

+ 1 - 1
main/inc/lib/api.lib.php

@@ -1461,7 +1461,7 @@ function _api_format_user($user, $add_password = false, $loadAvatars = true)
     $result['complete_name'] = api_get_person_name($result['firstname'], $result['lastname']);
     $result['complete_name_with_username'] = $result['complete_name'];
 
-    if (!empty($user['username'])) {
+    if (!empty($user['username']) && !api_get_configuration_value('hide_username_with_complete_name')) {
         $result['complete_name_with_username'] = $result['complete_name'].' ('.$user['username'].')';
     }
 

+ 11 - 6
main/inc/lib/course.lib.php

@@ -4284,7 +4284,7 @@ class CourseManager
         $now = date('Y-m-d h:i:s');
         $user_id = api_get_user_id();
         $course_info = api_get_course_info_by_id($course['real_id']);
-        $course_visibility = $course_info['visibility'];
+        $course_visibility = (int) $course_info['visibility'];
 
         if ($course_visibility === COURSE_VISIBILITY_HIDDEN) {
             return '';
@@ -6051,7 +6051,7 @@ class CourseManager
     public static function addGroupMultiSelect($form, $groupInfo, $to = [])
     {
         $group_users = GroupManager::get_subscribed_users($groupInfo);
-        $array = self::buildSelectOptions(null, $group_users, $to);
+        $array = self::buildSelectOptions([$groupInfo], $group_users, $to);
 
         $result = [];
         foreach ($array as $content) {
@@ -6089,14 +6089,19 @@ class CourseManager
                         "GROUP:".$groupId,
                         $alreadySelected
                     )
-                    ) { // $alreadySelected is the array containing the groups (and users) that are already selected
-                        $user_label = ($thisGroup['userNb'] > 0) ? get_lang('Users') : get_lang('LowerCaseUser');
-                        $user_disabled = ($thisGroup['userNb'] > 0) ? "" : "disabled=disabled";
+                    ) {
+                        $userCount = isset($thisGroup['userNb']) ? $thisGroup['userNb'] : 0;
+                        if (empty($userCount)) {
+                            $userCount = isset($thisGroup['count_users']) ? $thisGroup['count_users'] : 0;
+                        }
+                        // $alreadySelected is the array containing the groups (and users) that are already selected
+                        $user_label = ($userCount > 0) ? get_lang('Users') : get_lang('LowerCaseUser');
+                        $user_disabled = ($userCount > 0) ? "" : "disabled=disabled";
                         $result[] = [
                             'disabled' => $user_disabled,
                             'value' => "GROUP:".$groupId,
                             // The space before "G" is needed in order to advmultiselect.php js puts groups first
-                            'content' => " G: ".$thisGroup['name']." - ".$thisGroup['userNb']." ".$user_label,
+                            'content' => " G: ".$thisGroup['name']." - ".$userCount." ".$user_label,
                         ];
                     }
                 }

+ 1 - 1
main/inc/lib/course_home.lib.php

@@ -991,7 +991,7 @@ class CourseHome
                 // If it's a link, we don't add the cidReq
 
                 if ($tool['image'] == 'file_html.png' || $tool['image'] == 'file_html_na.png') {
-                    $tool['link'] = $tool['link'].$qm_or_amp;
+                    $tool['link'] = $tool['link'];
                 } else {
                     $tool['link'] = $tool['link'].$qm_or_amp.api_get_cidreq();
                 }

+ 1 - 0
main/inc/lib/database.constants.inc.php

@@ -55,6 +55,7 @@ define('TABLE_MAIN_GRADEBOOK_LINK', 'gradebook_link');
 define('TABLE_MAIN_GRADEBOOK_SCORE_DISPLAY', 'gradebook_score_display');
 define('TABLE_MAIN_GRADEBOOK_CERTIFICATE', 'gradebook_certificate');
 define('TABLE_MAIN_GRADEBOOK_SCORE_LOG', 'gradebook_score_log');
+define('TABLE_MAIN_GRADEBOOK_RESULT_ATTEMPT', 'gradebook_result_attempt');
 
 // Profiling
 define('TABLE_EXTRA_FIELD', 'extra_field');

+ 1 - 2
main/inc/lib/notification.lib.php

@@ -303,8 +303,7 @@ class Notification extends Model
                     case self::NOTIFY_INVITATION_AT_ONCE:
                     case self::NOTIFY_GROUP_AT_ONCE:
                         $extraHeaders = [];
-                        $noReply = api_get_setting('noreply_email_address');
-                        if (empty($noReply) && isset($senderInfo['email'])) {
+                        if (isset($senderInfo['email'])) {
                             $extraHeaders = [
                                 'reply_to' => [
                                     'name' => $senderInfo['complete_name'],

+ 6 - 1
main/inc/lib/template.lib.php

@@ -612,10 +612,15 @@ class Template
             'bootstrap-daterangepicker/daterangepicker.css',
             'bootstrap-select/dist/css/bootstrap-select.min.css',
             'select2/dist/css/select2.min.css',
-            'flag-icon-css/css/flag-icon.min.css',
             'mediaelement/plugins/vrview/vrview.css',
         ];
 
+        $hide = api_get_configuration_value('hide_flag_language_switcher');
+
+        if ($hide === false) {
+            $bowerCSSFiles[] = 'flag-icon-css/css/flag-icon.min.css';
+        }
+
         $features = api_get_configuration_value('video_features');
         $defaultFeatures = ['playpause', 'current', 'progress', 'duration', 'tracks', 'volume', 'fullscreen', 'vrview'];
 

+ 3 - 1
main/inc/lib/userportal.lib.php

@@ -1677,7 +1677,9 @@ class IndexManager
                                                 $session_id,
                                                 'session_course_item'
                                             );
-                                            $html_courses_session[] = $c[1];
+                                            if (isset($c[1])) {
+                                                $html_courses_session[] = $c[1];
+                                            }
                                         }
                                         $count_courses_session++;
                                         $count++;

+ 2 - 2
main/inc/local.inc.php

@@ -944,12 +944,12 @@ if (!isset($_SESSION['login_as'])) {
     }
 }
 
-$sessionIdFromGet = isset($_GET['id_session']) ? (int) $_GET['id_session'] : 0;
+$sessionIdFromGet = isset($_GET['id_session']) ? (int) $_GET['id_session'] : false;
 // if a session id has been given in url, we store the session if course was set:
 $sessionIdFromSession = api_get_session_id();
 $checkFromDatabase = false;
 // User change from session id
-if (!empty($sessionIdFromGet) && $sessionIdFromGet != $sessionIdFromSession) {
+if (($sessionIdFromGet !== false && $sessionIdFromGet !== $sessionIdFromSession) || $cidReset) {
     $cidReset = true;
     $checkFromDatabase = true;
     Session::erase('session_name');

+ 14 - 0
main/install/configuration.dist.php

@@ -958,6 +958,20 @@ VALUES (2, 13, 'session_courses_read_only_mode', 'Lock Course In Session', 1, 1,
     ]
 ];*/
 
+// Hide the username when showing the complete name for a user.
+// Example: using api_get_user_info()['complete_name_with_username'] or $user->getCompleteNameWithUsername()
+//$_configuration['hide_username_with_complete_name'] = false;
+
+// Hide the username in course chat
+//$_configuration['hide_username_in_course_chat'] = false;
+
+// Allow multiple attempts in gradebook evaluations
+// CREATE TABLE gradebook_result_attempt (id INT AUTO_INCREMENT NOT NULL, result_id INT NOT NULL, score DOUBLE, comment LONGTEXT DEFAULT NULL, created_at DATETIME, updated_at DATETIME, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
+//$_configuration['gradebook_multiple_evaluation_attempts'] = false;
+
+// Hide country flags in the language switcher
+// $_configuration['hide_flag_language_switcher'] = false;
+
 // ------ Custom DB changes (keep this at the end)
 // Add user activation by confirmation email
 // This option prevents the new user to login in the platform if your account is not confirmed via email

+ 2 - 0
main/lang/english/trad4all.inc.php

@@ -8301,4 +8301,6 @@ $DifferenceOfDaysAndCalendar = "Difference between days and calendar";
 $SkillUserList = "Skills and users list";
 $CourseId = "Course ID";
 $CareDetailView = "Student care detail view";
+$MoreDataAvailableInTheDatabaseButTrunkedForEfficiencyReasons = "More data available in the database but trunked for efficiency reasons.";
+$SendAnnouncementCopyToMyself = "Send a copy by email to myself.";
 ?>

+ 3 - 1
main/lp/learnpath.class.php

@@ -10033,9 +10033,11 @@ class learnpath
             true
         );
 
+        $lpPathInfo = $this->generate_lp_folder(api_get_course_info());
+
         DocumentManager::build_directory_selector(
             $folders,
-            '',
+            $lpPathInfo['id'],
             [],
             true,
             $form,

+ 2 - 0
main/lp/learnpathItem.class.php

@@ -2651,6 +2651,8 @@ class learnpathItem
 
                                                 return $returnstatus;
                                             }
+                                        } else {
+                                            return $returnstatus;
                                         }
                                     }
                                 } else {

+ 1 - 1
main/lp/lp_view.php

@@ -379,7 +379,7 @@ if (!empty($_REQUEST['exeId']) &&
     if (intval($_GET['fb_type']) > 0) {
         $src = 'blank.php?msg=exerciseFinished';
     } else {
-        $src = api_get_path(WEB_CODE_PATH).'exercise/result.php?origin=learnpath&id='.$safe_exe_id.'&'.api_get_cidreq();
+        $src = api_get_path(WEB_CODE_PATH).'exercise/result.php?id='.$safe_exe_id.'&'.api_get_cidreq(true, true, 'learnpath');
         if ($debug) {
             error_log('Calling URL: '.$src);
         }

+ 1 - 0
main/session/add_users_to_session.php

@@ -336,6 +336,7 @@ if (isset($_POST['form_sent']) && $_POST['form_sent']) {
             null,
             !$notEmptyList
         );
+        Display::addFlash(Display::return_message(get_lang('Updated')));
         header('Location: resume_session.php?id_session='.$id_session);
         exit;
     }

+ 14 - 22
main/social/personal_data.php

@@ -41,7 +41,12 @@ if (api_get_setting('allow_terms_conditions') === 'true') {
     $formToString = $form->returnForm();
 
     $formDelete = new FormValidator('delete_account', 'post', api_get_self().'?action=delete_account&user_id='.$userId);
-    $formDelete->addTextarea('explanation', [get_lang('DeleteAccount'), get_lang('ExplanationDeleteAccount')], [], true);
+    $formDelete->addTextarea(
+        'explanation',
+        [get_lang('DeleteAccount'), get_lang('ExplanationDeleteAccount')],
+        [],
+        true
+    );
     $formDelete->addHidden('action', 'delete_account');
     $formDelete->addButtonDelete(get_lang('DeleteAccount'));
     $formToString .= $formDelete->returnForm();
@@ -105,19 +110,6 @@ switch ($action) {
             $explanation = $formDelete->getSubmitValue('explanation');
             UserManager::createDataPrivacyExtraFields();
 
-            // Remove delete agreement if it was sent:
-            /*UserManager::update_extra_field_value(
-                $userId,
-                'request_for_legal_agreement_consent_removal',
-                ''
-            );
-
-            UserManager::update_extra_field_value(
-                $userId,
-                'request_for_legal_agreement_consent_removal_justification',
-                ''
-            );*/
-
             UserManager::update_extra_field_value(
                 $userId,
                 'request_for_delete_account',
@@ -174,13 +166,6 @@ switch ($action) {
                 $explanation
             );
 
-            /*$extraFieldValue = new ExtraFieldValue('user');
-            $value = $extraFieldValue->get_values_by_handler_and_field_variable(
-                $userId,
-                'legal_accept'
-            );
-            $result = $extraFieldValue->delete($value['id']);*/
-
             Display::addFlash(Display::return_message(get_lang('Sent')));
 
             Event::addEvent(
@@ -255,7 +240,7 @@ if ($allowSocial) {
 $personalDataContent = '<ul>';
 $properties = json_decode($propertiesToJson);
 $webCoursePath = api_get_path(WEB_COURSE_PATH);
-
+$showWarningMessage = false;
 foreach ($properties as $key => $value) {
     if (is_array($value) || is_object($value)) {
         switch ($key) {
@@ -297,6 +282,9 @@ foreach ($properties as $key => $value) {
                     if (empty($subValue)) {
                         $personalDataContent .= '<li>'.get_lang('NoData').'</li>';
                     } else {
+                        if (count($subValue) === 1000) {
+                            $showWarningMessage = true;
+                        }
                         foreach ($subValue as $subSubValue) {
                             if ($category === 'DocumentsAdded') {
                                 $documentLink = Display::url(
@@ -447,6 +435,10 @@ if (api_get_setting('allow_terms_conditions') === 'true') {
     $termLink = Display::url(get_lang('ReadTermsAndConditions'), $url);
 }
 
+if ($showWarningMessage) {
+    Display::addFlash(Display::return_message(get_lang('MoreDataAvailableInTheDatabaseButTrunkedForEfficiencyReasons')));
+}
+
 // Block Social Avatar
 SocialManager::setSocialUserBlock($tpl, api_get_user_id(), 'messages');
 if (api_get_setting('allow_social_tool') === 'true') {

+ 37 - 1
main/template/default/agenda/month.tpl

@@ -204,8 +204,13 @@ $(document).ready(function() {
     });
 
     FC.views.CustomView = CustomView; // register our class with the view system
+    var height = '';
+    if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
+        height = 'auto';
+    }
 
 	var calendar = $('#calendar').fullCalendar({
+        height: height,
 		header: {
 			left: 'today,prev,next',
 			center: 'title',
@@ -412,7 +417,8 @@ $(document).ready(function() {
                     content: onHoverInfo,
                     position: {
                         at: 'top center',
-                        my: 'bottom center'
+                        my: 'bottom center',
+                        viewport: $(window)
                     }
                 });
 			}
@@ -423,6 +429,36 @@ $(document).ready(function() {
             var diffDays = moment(end).diff(start, 'days');
             var endDateMinusOne = '';
 
+            // If event is not editable then just return the qtip
+            if (!calEvent.editable) {
+                var onHoverInfo = '';
+                {% if calEvent.description %}
+                if (calEvent.description) {
+                    onHoverInfo = calEvent.description;
+                }
+                {% endif %}
+                {% if on_hover_info.comment %}
+                if (calEvent.comment) {
+                    onHoverInfo = onHoverInfo + calEvent.comment;
+                }
+                {% endif %}
+
+                if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
+                    $(this).qtip({
+                        overwrite: false,
+                        show: {ready: true},
+                        content: onHoverInfo,
+                        position: {
+                            at: 'top center',
+                            my: 'bottom center',
+                            viewport: $(window)
+                        }
+                    });
+
+                    return;
+                }
+            }
+
             if (end) {
                 var clone = end.clone();
                 endDateMinusOne = clone.subtract(1, 'days').format('{{ js_format_date }}');

+ 6 - 0
main/template/default/chat/chat.tpl

@@ -121,7 +121,13 @@ $(document).on('ready', function () {
                         '           </button>';
                 }
                 html += '           </li>' +
+
+                    {% if not 'hide_username_in_course_chat'|api_get_configuration_value %}
                     '           <li><small>' + user.username + '</small></li>' +
+                    {% else %}
+                    '           <li>&nbsp;</li>' +
+                    {% endif %}
+
                     '       </ul>' +
                     '   </div>' +
                     '</li>';

+ 21 - 10
main/work/work.lib.php

@@ -2829,18 +2829,25 @@ function getStudentSubscribedToWork(
 }
 
 /**
- * @param int $userId
- * @param int $workId
- * @param int $courseId
+ * @param int  $userId
+ * @param int  $workId
+ * @param int  $courseId
+ * @param bool $forceAccessForCourseAdmins
  *
  * @return bool
  */
-function allowOnlySubscribedUser($userId, $workId, $courseId)
+function allowOnlySubscribedUser($userId, $workId, $courseId, $forceAccessForCourseAdmins = false)
 {
     if (api_is_platform_admin() || api_is_allowed_to_edit()) {
         return true;
     }
 
+    if ($forceAccessForCourseAdmins) {
+        if (api_is_course_admin() || api_is_coach()) {
+            return true;
+        }
+    }
+
     return userIsSubscribedToWork($userId, $workId, $courseId);
 }
 
@@ -4981,7 +4988,7 @@ function getWorkUserListData(
  */
 function downloadFile($id, $course_info, $isCorrection)
 {
-    return getFile($id, $course_info, true, $isCorrection);
+    return getFile($id, $course_info, true, $isCorrection, true);
 }
 
 /**
@@ -4989,12 +4996,13 @@ function downloadFile($id, $course_info, $isCorrection)
  * @param array $course_info
  * @param bool  $download
  * @param bool  $isCorrection
+ * @param bool  $forceAccessForCourseAdmins
  *
  * @return bool
  */
-function getFile($id, $course_info, $download = true, $isCorrection = false)
+function getFile($id, $course_info, $download = true, $isCorrection = false, $forceAccessForCourseAdmins = false)
 {
-    $file = getFileContents($id, $course_info, 0, $isCorrection);
+    $file = getFileContents($id, $course_info, 0, $isCorrection, $forceAccessForCourseAdmins);
     if (!empty($file) && is_array($file)) {
         return DocumentManager::file_send_for_download(
             $file['path'],
@@ -5013,10 +5021,11 @@ function getFile($id, $course_info, $download = true, $isCorrection = false)
  * @param array $courseInfo
  * @param int   $sessionId
  * @param bool  $correction
+ * @param bool  $forceAccessForCourseAdmins
  *
  * @return array|bool
  */
-function getFileContents($id, $courseInfo, $sessionId = 0, $correction = false)
+function getFileContents($id, $courseInfo, $sessionId = 0, $correction = false, $forceAccessForCourseAdmins = false)
 {
     $id = (int) $id;
     if (empty($courseInfo) || empty($id)) {
@@ -5060,7 +5069,8 @@ function getFileContents($id, $courseInfo, $sessionId = 0, $correction = false)
             $isAllow = allowOnlySubscribedUser(
                 api_get_user_id(),
                 $row['parent_id'],
-                $courseInfo['real_id']
+                $courseInfo['real_id'],
+                $forceAccessForCourseAdmins
             );
 
             if (empty($isAllow)) {
@@ -5101,7 +5111,8 @@ function getFileContents($id, $courseInfo, $sessionId = 0, $correction = false)
             $is_editor = api_is_allowed_to_edit(true, true, true);
             $student_is_owner_of_work = user_is_author($row['id'], api_get_user_id());
 
-            if ($is_editor ||
+            if (($forceAccessForCourseAdmins && $isAllow) ||
+                $is_editor ||
                 $student_is_owner_of_work ||
                 ($doc_visible_for_all && $work_is_visible)
             ) {

+ 3 - 2
plugin/customcertificate/src/CustomCertificatePlugin.php

@@ -216,12 +216,13 @@ class CustomCertificatePlugin extends Plugin
             return [];
         }
 
+        $userId = api_get_user_id();
         $certificateTable = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CERTIFICATE);
         $categoryTable = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
         $sql = "SELECT cer.user_id AS user_id, cat.session_id AS session_id, cat.course_code AS course_code
                 FROM $certificateTable cer
                 INNER JOIN $categoryTable cat
-                ON (cer.cat_id = cat.id)
+                ON (cer.cat_id = cat.id AND cer.user_id = $userId)
                 WHERE cer.id = $id";
         $rs = Database::query($sql);
         if (Database::num_rows($rs) > 0) {
@@ -251,7 +252,7 @@ class CustomCertificatePlugin extends Plugin
     {
         $certId = (int) $certId;
         if (api_get_plugin_setting('customcertificate', 'enable_plugin_customcertificate') == 'true') {
-            $infoCertificate = CustomCertificatePlugin::getCertificateData($certId);
+            $infoCertificate = self::getCertificateData($certId);
             if (!empty($infoCertificate)) {
                 if ($certificate->user_id == api_get_user_id() && !empty($certificate->certificate_data)) {
                     $certificateId = $certificate->certificate_data['id'];

+ 9 - 0
plugin/customcertificate/src/print_certificate.php

@@ -32,6 +32,15 @@ if (intval($_GET['default']) == 1) {
     $enableCourse = api_get_course_setting('customcertificate_course_enable', $courseCode) == 1 ? true : false;
     $useDefault = api_get_course_setting('use_certificate_default', $courseCode) == 1 ? true : false;
 }
+
+if (empty($courseCode)) {
+    $courseCode = isset($_REQUEST['course_code']) ? Database::escape_string($_REQUEST['course_code']) : '';
+}
+
+if (empty($sessionId)) {
+    $sessionId = isset($_REQUEST['session_id']) ? (int) $_REQUEST['session_id'] : '';
+}
+
 $accessUrlId = api_get_current_access_url_id();
 
 $userList = [];

+ 1 - 1
plugin/learning_calendar/LearningCalendarPlugin.php

@@ -709,7 +709,7 @@ class LearningCalendarPlugin extends Plugin
                 ON (r.evaluation_id = e.id)
                 WHERE 
                   e.type = 'evaluation' AND
-                  r.score > 0 AND
+                  r.score >= 2 AND
                   r.user_id = $userId   
                   $courseSessionConditionToString                  
         ";

+ 4 - 2
plugin/learning_calendar/view/calendar.tpl

@@ -6,11 +6,13 @@
             url: "{{ ajax_url }}&a=get_events",
             success: function(response) {
                 for (var i = 0; i < response.length; i++) {
+                    var startDate = moment(response[i].start_date + " 13:00:00").toDate();
+                    var endDate = moment(response[i].end_date + " 13:00:00").toDate();
                     myData.push({
                         id: response[i].id,
                         title: response[i].title,
-                        startDate: new Date(response[i].start_date),
-                        endDate: new Date(response[i].end_date),
+                        startDate: startDate,
+                        endDate: endDate,
                         color: response[i].color
                     });
                 }

+ 1 - 1
plugin/search_course/plugin.php

@@ -1,4 +1,4 @@
-<?php 
+<?php
 /**
  * @copyright (c) 2011 University of Geneva
  * @license GNU General Public License - http://www.gnu.org/copyleft/gpl.html

+ 1 - 1
src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/TestMatching.php

@@ -105,7 +105,7 @@ class TestMatching extends Basic
             ['BulletedList', 'NumberedList', 'HorizontalRule'],
             ['JustifyLeft', 'JustifyCenter', 'JustifyBlock'],
             ['Format', 'Font', 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor'],
-            ['Toolbarswitch'],
+            ['Source', 'Toolbarswitch'],
         ];
     }
 }

+ 1 - 1
src/Chamilo/CoreBundle/Component/Editor/CkEditor/Toolbar/TestQuestionDescription.php

@@ -131,7 +131,7 @@ class TestQuestionDescription extends Basic
             ['Link', 'Unlink', 'Image', 'Video', 'Flash', 'Audio', 'Table', 'Asciimath', 'Asciisvg'],
             ['BulletedList', 'NumberedList', 'HorizontalRule'],
             ['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
-            ['Format', 'Font', 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor'],
+            ['Styles', 'Format', 'Font', 'FontSize', 'Bold', 'Italic', 'Underline', 'TextColor', 'BGColor'],
             ['Source', 'Toolbarswitch'],
         ];
     }

+ 20 - 0
src/Chamilo/CoreBundle/Entity/ExtraFieldValues.php

@@ -190,4 +190,24 @@ class ExtraFieldValues extends BaseAttributeValue
     {
         return $this->id;
     }
+
+    /**
+     * @return string
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * @param string $value
+     *
+     * @return ExtraFieldValues
+     */
+    public function setValue($value)
+    {
+        $this->value = $value;
+
+        return $this;
+    }
 }

+ 4 - 0
src/Chamilo/UserBundle/Entity/User.php

@@ -793,6 +793,10 @@ class User implements UserInterface //implements ParticipantInterface, ThemeUser
      */
     public function getCompleteNameWithUsername()
     {
+        if (api_get_configuration_value('hide_username_with_complete_name')) {
+            return $this->getCompleteName();
+        }
+
         return api_get_person_name($this->firstname, $this->lastname).' ('.$this->username.')';
     }
 

+ 111 - 74
src/Chamilo/UserBundle/Repository/UserRepository.php

@@ -578,10 +578,11 @@ class UserRepository extends EntityRepository
         $gradebookCertificate = [];
         /** @var GradebookCertificate $item */
         foreach ($result as $item) {
+            $createdAt = $item->getCreatedAt() ? $item->getCreatedAt()->format($dateFormat) : '';
             $list = [
                 'Score: '.$item->getScoreCertificate(),
                 'Path: '.$item->getPathCertificate(),
-                'Created at: '.$item->getCreatedAt()->format($dateFormat),
+                'Created at: '.$createdAt,
             ];
             $gradebookCertificate[] = implode(', ', $list);
         }
@@ -594,9 +595,10 @@ class UserRepository extends EntityRepository
         $trackEExercises = [];
         /** @var TrackEExercises $item */
         foreach ($result as $item) {
+            $date = $item->getExeDate() ? $item->getExeDate()->format($dateFormat) : '';
             $list = [
                 'IP: '.$item->getUserIp(),
-                'Start: '.$item->getExeDate()->format($dateFormat),
+                'Start: '.$date,
                 'Status: '.$item->getStatus(),
                // 'Result: '.$item->getExeResult(),
                // 'Weighting: '.$item->getExeWeighting(),
@@ -612,6 +614,7 @@ class UserRepository extends EntityRepository
         $trackEAttempt = [];
         /** @var TrackEAttempt $item */
         foreach ($result as $item) {
+            $date = $item->getTms() ? $item->getTms()->format($dateFormat) : '';
             $list = [
                 'Attempt #'.$item->getExeId(),
                 'Course # '.$item->getCId(),
@@ -619,7 +622,7 @@ class UserRepository extends EntityRepository
                 'Session #'.$item->getSessionId(),
                 //'Marks: '.$item->getMarks(),
                 'Position: '.$item->getPosition(),
-                'Date: '.$item->getTms()->format($dateFormat),
+                'Date: '.$date,
             ];
             $trackEAttempt[] = implode(', ', $list);
         }
@@ -632,57 +635,91 @@ class UserRepository extends EntityRepository
         $trackECourseAccessList = [];
         /** @var TrackECourseAccess $item */
         foreach ($result as $item) {
+            $startDate = $item->getLoginCourseDate() ? $item->getLoginCourseDate()->format($dateFormat) : '';
+            $endDate = $item->getLogoutCourseDate() ? $item->getLogoutCourseDate()->format($dateFormat) : '';
             $list = [
                 'IP: '.$item->getUserIp(),
-                'Start: '.$item->getLoginCourseDate()->format($dateFormat),
-                'End: '.$item->getLogoutCourseDate()->format($dateFormat),
+                'Start: '.$startDate,
+                'End: '.$endDate,
             ];
             $trackECourseAccessList[] = implode(', ', $list);
         }
 
-        // TrackELogin
-        $criteria = [
-            'loginUserId' => $userId,
+        $checkEntities = [
+            'ChamiloCoreBundle:TrackELogin' => 'loginUserId',
+            'ChamiloCoreBundle:TrackEAccess' => 'accessUserId',
+            'ChamiloCoreBundle:TrackEOnline' => 'loginUserId',
+            'ChamiloCoreBundle:TrackEDefault' => 'defaultUserId',
+            'ChamiloCoreBundle:TrackELastaccess' => 'accessUserId',
+            'ChamiloCoreBundle:TrackEUploads' => 'uploadUserId',
+            'ChamiloCoreBundle:GradebookResult' => 'userId',
+            'ChamiloCoreBundle:TrackEDownloads' => 'downUserId',
         ];
-        $result = $em->getRepository('ChamiloCoreBundle:TrackELogin')->findBy($criteria);
+
+        $maxResults = 1000;
+        $trackResults = [];
+        foreach ($checkEntities as $entity => $field) {
+            $qb = $em->createQueryBuilder();
+            $qb->select($qb->expr()->count('l'))
+                ->from($entity, 'l')
+                ->where("l.$field = :login")
+                ->setParameter('login', $userId);
+            $query = $qb->getQuery();
+            $count = $query->getSingleScalarResult();
+
+            if ($count > $maxResults) {
+                $qb = $em->getRepository($entity)->createQueryBuilder('l');
+                $qb
+                    ->select('l')
+                    ->where("l.$field = :login")
+                    ->setParameter('login', $userId);
+                $qb
+                    ->setFirstResult(0)
+                    ->setMaxResults($maxResults)
+                ;
+                $result = $qb->getQuery()->getResult();
+            } else {
+                $criteria = [
+                    $field => $userId,
+                ];
+                $result = $em->getRepository($entity)->findBy($criteria);
+            }
+            $trackResults[$entity] = $result;
+        }
+
         $trackELoginList = [];
         /** @var TrackELogin $item */
-        foreach ($result as $item) {
+        foreach ($trackResults['ChamiloCoreBundle:TrackELogin'] as $item) {
+            $startDate = $item->getLoginDate() ? $item->getLoginDate()->format($dateFormat) : '';
+            $endDate = $item->getLogoutDate() ? $item->getLogoutDate()->format($dateFormat) : '';
             $list = [
                 'IP: '.$item->getUserIp(),
-                'Start: '.$item->getLoginDate()->format($dateFormat),
-                'End: '.$item->getLogoutDate()->format($dateFormat),
+                'Start: '.$startDate,
+                'End: '.$endDate,
             ];
             $trackELoginList[] = implode(', ', $list);
         }
 
         // TrackEAccess
-        $criteria = [
-            'accessUserId' => $userId,
-        ];
-        $result = $em->getRepository('ChamiloCoreBundle:TrackEAccess')->findBy($criteria);
         $trackEAccessList = [];
         /** @var TrackEAccess $item */
-        foreach ($result as $item) {
+        foreach ($trackResults['ChamiloCoreBundle:TrackEAccess'] as $item) {
+            $date = $item->getAccessDate() ? $item->getAccessDate()->format($dateFormat) : '';
             $list = [
                 'IP: '.$item->getUserIp(),
                 'Tool: '.$item->getAccessTool(),
-                'End: '.$item->getAccessDate()->format($dateFormat),
+                'End: '.$date,
             ];
             $trackEAccessList[] = implode(', ', $list);
         }
 
         // TrackEOnline
-        $criteria = [
-            'loginUserId' => $userId,
-        ];
-        $result = $em->getRepository('ChamiloCoreBundle:TrackEOnline')->findBy($criteria);
-        $trackEOnlineList = [];
         /** @var TrackEOnline $item */
-        foreach ($result as $item) {
+        foreach ($trackResults['ChamiloCoreBundle:TrackEOnline'] as $item) {
+            $date = $item->getLoginDate() ? $item->getLoginDate()->format($dateFormat) : '';
             $list = [
                 'IP: '.$item->getUserIp(),
-                'Login date: '.$item->getLoginDate()->format($dateFormat),
+                'Login date: '.$date,
                 'Course # '.$item->getCId(),
                 'Session # '.$item->getSessionId(),
             ];
@@ -690,18 +727,15 @@ class UserRepository extends EntityRepository
         }
 
         // TrackEDefault
-        $criteria = [
-            'defaultUserId' => $userId,
-        ];
-        $result = $em->getRepository('ChamiloCoreBundle:TrackEDefault')->findBy($criteria);
         $trackEDefault = [];
         /** @var TrackEDefault $item */
-        foreach ($result as $item) {
+        foreach ($trackResults['ChamiloCoreBundle:TrackEDefault'] as $item) {
+            $date = $item->getDefaultDate() ? $item->getDefaultDate()->format($dateFormat) : '';
             $list = [
                 'Type: '.$item->getDefaultEventType(),
                 'Value: '.$item->getDefaultValue(),
                 'Value type: '.$item->getDefaultValueType(),
-                'Date: '.$item->getDefaultDate()->format($dateFormat),
+                'Date: '.$date,
                 'Course #'.$item->getCId(),
                 'Session # '.$item->getSessionId(),
             ];
@@ -709,63 +743,51 @@ class UserRepository extends EntityRepository
         }
 
         // TrackELastaccess
-        $criteria = [
-            'accessUserId' => $userId,
-        ];
-        $result = $em->getRepository('ChamiloCoreBundle:TrackELastaccess')->findBy($criteria);
         $trackELastaccess = [];
         /** @var TrackELastaccess $item */
-        foreach ($result as $item) {
+        foreach ($trackResults['ChamiloCoreBundle:TrackELastaccess'] as $item) {
+            $date = $item->getAccessDate() ? $item->getAccessDate()->format($dateFormat) : '';
             $list = [
                 'Course #'.$item->getCId(),
                 'Session # '.$item->getAccessSessionId(),
                 'Tool: '.$item->getAccessTool(),
-                'Access date: '.$item->getAccessDate()->format($dateFormat),
+                'Access date: '.$date,
             ];
             $trackELastaccess[] = implode(', ', $list);
         }
 
         // TrackEUploads
-        $criteria = [
-            'uploadUserId' => $userId,
-        ];
-        $result = $em->getRepository('ChamiloCoreBundle:TrackEUploads')->findBy($criteria);
         $trackEUploads = [];
         /** @var TrackEUploads $item */
-        foreach ($result as $item) {
+        foreach ($trackResults['ChamiloCoreBundle:TrackEUploads'] as $item) {
+            $date = $item->getUploadDate() ? $item->getUploadDate()->format($dateFormat) : '';
             $list = [
                 'Course #'.$item->getCId(),
-                'Uploaded at: '.$item->getUploadDate()->format($dateFormat),
+                'Uploaded at: '.$date,
                 'Upload id # '.$item->getUploadId(),
             ];
             $trackEUploads[] = implode(', ', $list);
         }
 
-        $criteria = [
-            'userId' => $userId,
-        ];
-        $result = $em->getRepository('ChamiloCoreBundle:GradebookResult')->findBy($criteria);
         $gradebookResult = [];
         /** @var GradebookResult $item */
-        foreach ($result as $item) {
+        foreach ($trackResults['ChamiloCoreBundle:GradebookResult'] as $item) {
+            $date = $item->getCreatedAt() ? $item->getCreatedAt()->format($dateFormat) : '';
             $list = [
                 'Evaluation id# '.$item->getEvaluationId(),
                 //'Score: '.$item->getScore(),
-                'Creation date: '.$item->getCreatedAt()->format($dateFormat),
+                'Creation date: '.$date,
             ];
             $gradebookResult[] = implode(', ', $list);
         }
 
-        $criteria = [
-            'downUserId' => $userId,
-        ];
-        $result = $em->getRepository('ChamiloCoreBundle:TrackEDownloads')->findBy($criteria);
         $trackEDownloads = [];
         /** @var TrackEDownloads $item */
-        foreach ($result as $item) {
+        foreach ($trackResults['ChamiloCoreBundle:TrackEDownloads'] as $item) {
+            $date = $item->getDownDate() ? $item->getDownDate()->format($dateFormat) : '';
             $list = [
                 'File: '.$item->getDownDocPath(),
-                'Download at: '.$item->getDownDate()->format($dateFormat),
+                'Download at: '.$date,
             ];
             $trackEDownloads[] = implode(', ', $list);
         }
@@ -792,9 +814,10 @@ class UserRepository extends EntityRepository
         $cForumPostList = [];
         /** @var CForumPost $item */
         foreach ($result as $item) {
+            $date = $item->getPostDate() ? $item->getPostDate()->format($dateFormat) : '';
             $list = [
                 'Title: '.$item->getPostTitle(),
-                'Creation date: '.$item->getPostDate()->format($dateFormat),
+                'Creation date: '.$date,
             ];
             $cForumPostList[] = implode(', ', $list);
         }
@@ -807,9 +830,10 @@ class UserRepository extends EntityRepository
         $cForumThreadList = [];
         /** @var CForumThread $item */
         foreach ($result as $item) {
+            $date = $item->getThreadDate() ? $item->getThreadDate()->format($dateFormat) : '';
             $list = [
                 'Title: '.$item->getThreadTitle(),
-                'Creation date: '.$item->getThreadDate()->format($dateFormat),
+                'Creation date: '.$date,
             ];
             $cForumThreadList[] = implode(', ', $list);
         }
@@ -868,9 +892,10 @@ class UserRepository extends EntityRepository
         $cBlog = [];
         /** @var CBlogPost $item */
         foreach ($result as $item) {
+            $date = $item->getDateCreation() ? $item->getDateCreation()->format($dateFormat) : '';
             $list = [
                 'Title: '.$item->getTitle(),
-                'Date: '.$item->getDateCreation()->format($dateFormat),
+                'Date: '.$date,
             ];
             $cBlog[] = implode(', ', $list);
         }
@@ -898,9 +923,10 @@ class UserRepository extends EntityRepository
         $messageList = [];
         /** @var Message $item */
         foreach ($result as $item) {
+            $date = $item->getSendDate() ? $item->getSendDate()->format($dateFormat) : '';
             $list = [
                 'Title: '.$item->getTitle(),
-                'Sent date: '.$item->getSendDate()->format($dateFormat),
+                'Sent date: '.$date,
                 'To user # '.$item->getUserReceiverId(),
                 'Status'.$item->getMsgStatus(),
             ];
@@ -930,9 +956,10 @@ class UserRepository extends EntityRepository
         $cDropboxFile = [];
         /** @var CDropboxFile $item */
         foreach ($result as $item) {
+            $date = $item->getUploadDate() ? $item->getUploadDate()->format($dateFormat) : '';
             $list = [
                 'Title: '.$item->getTitle(),
-                'Uploaded date: '.$item->getUploadDate()->format($dateFormat),
+                'Uploaded date: '.$date,
                 'File: '.$item->getFilename(),
             ];
             $cDropboxFile[] = implode(', ', $list);
@@ -961,10 +988,11 @@ class UserRepository extends EntityRepository
         $cDropboxFeedback = [];
         /** @var CDropboxFeedback $item */
         foreach ($result as $item) {
+            $date = $item->getFeedbackDate() ? $item->getFeedbackDate()->format($dateFormat) : '';
             $list = [
                 'File #'.$item->getFileId(),
                 'Feedback: '.$item->getFeedback(),
-                'Date: '.$item->getFeedbackDate()->format($dateFormat),
+                'Date: '.$date,
             ];
             $cDropboxFeedback[] = implode(', ', $list);
         }
@@ -977,9 +1005,10 @@ class UserRepository extends EntityRepository
         $cNotebook = [];
         /** @var CNotebook $item */
         foreach ($result as $item) {
+            $date = $item->getUpdateDate() ? $item->getUpdateDate()->format($dateFormat) : '';
             $list = [
                 'Title: '.$item->getTitle(),
-                'Date: '.$item->getUpdateDate()->format($dateFormat),
+                'Date: '.$date,
             ];
             $cNotebook[] = implode(', ', $list);
         }
@@ -1025,11 +1054,12 @@ class UserRepository extends EntityRepository
         $cStudentPublicationComment = [];
         /** @var CStudentPublicationComment $item */
         foreach ($result as $item) {
+            $date = $item->getSentAt() ? $item->getSentAt()->format($dateFormat) : '';
             $list = [
                 'Commment: '.$item->getComment(),
                 'File '.$item->getFile(),
                 'Course # '.$item->getCId(),
-                'Date: '.$item->getSentAt()->format($dateFormat),
+                'Date: '.$date,
             ];
             $cStudentPublicationComment[] = implode(', ', $list);
         }
@@ -1073,11 +1103,12 @@ class UserRepository extends EntityRepository
         $ticketMessage = [];
         /** @var \Chamilo\TicketBundle\Entity\Message $item */
         foreach ($result as $item) {
+            $date = $item->getInsertDateTime() ? $item->getInsertDateTime()->format($dateFormat) : '';
             $list = [
                 'Subject: '.$item->getSubject(),
                 'IP: '.$item->getIpAddress(),
                 'Status: '.$item->getStatus(),
-                'Creation date: '.$item->getInsertDateTime()->format($dateFormat),
+                'Creation date: '.$date,
             ];
             $ticketMessage[] = implode(', ', $list);
         }
@@ -1090,10 +1121,11 @@ class UserRepository extends EntityRepository
         $skillRelUserComment = [];
         /** @var SkillRelUserComment $item */
         foreach ($result as $item) {
+            $date = $item->getFeedbackDateTime() ? $item->getFeedbackDateTime()->format($dateFormat) : '';
             $list = [
                 'Feedback: '.$item->getFeedbackText(),
                 'Value: '.$item->getFeedbackValue(),
-                'Created at: '.$item->getFeedbackDateTime()->format($dateFormat),
+                'Created at: '.$date,
             ];
             $skillRelUserComment[] = implode(', ', $list);
         }
@@ -1122,13 +1154,17 @@ class UserRepository extends EntityRepository
         $userApiKey = [];
         /** @var UserApiKey $item */
         foreach ($result as $item) {
+            $validityStart = $item->getValidityStartDate() ? $item->getValidityStartDate()->format($dateFormat) : '';
+            $validityEnd = $item->getValidityEndDate() ? $item->getValidityEndDate()->format($dateFormat) : '';
+            $created = $item->getCreatedDate() ? $item->getCreatedDate()->format($dateFormat) : '';
+
             $list = [
                 'ApiKey #'.$item->getApiKey(),
                 'Service: '.$item->getApiService(),
                 'EndPoint: '.$item->getApiEndPoint(),
-                'Validity start date: '.$item->getValidityEndDate()->format($dateFormat),
-                'Validity enddate: '.$item->getValidityStartDate()->format($dateFormat),
-                'Created at: '.$item->getCreatedDate()->format($dateFormat),
+                'Validity start date: '.$validityStart,
+                'Validity enddate: '.$validityEnd,
+                'Created at: '.$created,
             ];
             $userApiKey[] = implode(', ', $list);
         }
@@ -1188,12 +1224,13 @@ class UserRepository extends EntityRepository
         $user->setCurriculumItems([]);
 
         $portals = $user->getPortals();
-
-        $list = [];
-        /** @var AccessUrlRelUser $portal */
-        foreach ($portals as $portal) {
-            $portalInfo = \UrlManager::get_url_data_from_id($portal->getAccessUrlId());
-            $list[] = $portalInfo['url'];
+        if (!empty($portals)) {
+            $list = [];
+            /** @var AccessUrlRelUser $portal */
+            foreach ($portals as $portal) {
+                $portalInfo = \UrlManager::get_url_data_from_id($portal->getAccessUrlId());
+                $list[] = $portalInfo['url'];
+            }
         }
         $user->setPortals($list);
 

+ 0 - 24
tests/behat/features/bootstrap/FeatureContext.php

@@ -473,30 +473,6 @@ class FeatureContext extends MinkContext
         return false;
     }
 
-    /**
-     * @Given /^I am a student subscribed to session "([^"]*)"$/
-     *
-     * @param string$sessionName
-     */
-    public function iAmStudentSubscribedToXSession($sessionName)
-    {
-        $this->iAmAPlatformAdministrator();
-        $this->visit('/main/session/session_add.php');
-        $this->fillField('name', $sessionName);
-        $this->pressButton('Next step');
-        $this->selectOption('NoSessionCoursesList[]', 'TEMP (TEMP)');
-        $this->pressButton('add_course');
-        $this->pressButton('Next step');
-        $this->assertPageContainsText('Update successful');
-        $this->fillField('user_to_add', 'acostea');
-        $this->waitForThePageToBeLoaded();
-        $this->clickLink('Costea Andrea (acostea)');
-        $this->pressButton('Finish session creation');
-        $this->assertPageContainsText('Session overview');
-        //$this->assertPageContainsText('Costea Andrea (acostea)');
-        $this->iAmAStudent();
-    }
-
     /**
      * Example: Then I should see the table "#category_results":
      *               | Categories    | Absolute score | Relative score |

+ 50 - 30
tests/behat/features/toolExercise.feature

@@ -232,6 +232,29 @@ Feature: Exercise tool
     And I press "submitQuestion"
     Then I should see "Item added"
 
+  Scenario: Duplicate exercise
+    Given I am on "/main/exercise/exercise.php?cidReq=TEMP"
+    And I follow "Copy this exercise as a new one"
+    And I confirm the popup
+    Then I should see "Exercise copied"
+    And I should see "Exercise 1 - Copy"
+
+  Scenario: Import exercise to test questions categories
+    Given I am on "/main/exercise/upload_exercise.php?cidReq=TEMP"
+    And I should see "Import quiz from Excel"
+    And I attach the file "/tests/behat/uploadable_files/exercise.xls" to "user_upload_quiz"
+    When I press "Upload"
+    And wait for the page to be loaded
+    Then I should see "Exercise for Behat test"
+
+    Scenario: Import exercise from excel
+    Given I am on "/main/exercise/upload_exercise.php?cidReq=TEMP"
+    Then I should see "Import quiz from Excel"
+    Then I attach the file "/main/exercise/quiz_template.xls" to "user_upload_quiz"
+    And I press "Upload"
+    And wait for the page to be loaded
+    Then I should see "Definition of oligarchy"
+
   Scenario: Try exercise "Exercise 1"
     Given I am on "/main/exercise/exercise.php?cidReq=TEMP"
     And I follow "Exercise 1"
@@ -303,28 +326,28 @@ Feature: Exercise tool
     And I follow "Grade activity"
     Then I should see "Score for the test: 83 / 117"
 
-  Scenario: Duplicate exercise
-    Given I am on "/main/exercise/exercise.php?cidReq=TEMP"
-    And I follow "Copy this exercise as a new one"
-    And I confirm the popup
-    Then I should see "Exercise copied"
-    And I should see "Exercise 1 - Copy"
-
-  Scenario: Import exercise to test questions categories
-    Given I am on "/main/exercise/upload_exercise.php?cidReq=TEMP"
-    And I should see "Import quiz from Excel"
-    And I attach the file "/tests/behat/uploadable_files/exercise.xls" to "user_upload_quiz"
-    When I press "Upload"
-    And wait for the page to be loaded
-    Then I should see "Exercise for Behat test"
+  Scenario: Create a session "Session Exercise" and add user "acostea"
+    Given I am on "/main/session/session_add.php"
+    When I fill in the following:
+      | name | Session Exercise |
+    And I fill in select2 input "#coach_username" with id "1" and value "admin"
+    And I press "submit"
+    Then wait for the page to be loaded
+    Then I should see "Add courses to this session (Session Exercise)"
+    Then I select "TEMP (TEMP)" from "NoSessionCoursesList[]"
+    And I press "add_course"
+    And I press "next"
+    Then I should see "Update successful"
+    Then I follow "Multiple registration"
+    Then I select "Costea Andrea (acostea)" from "nosessionUsersList[]"
+    And I press "add_user"
+    And I press "next"
+    Then I should see "Update successful"
 
   Scenario: Try exercise with categorized questions as student
-    Given I am a student subscribed to session "Session Exercise"
-    And I am on "/user_portal.php"
-    And I follow "Session Exercise"
-    And wait for the page to be loaded
-    And I follow "tabs2"
-    And I follow "TEMP"
+    Given I am a student
+    And I am on course "TEMP" homepage in session "Session Exercise"
+    Then I should see "TEMP (Session Exercise)"
     And I am on "/main/exercise/exercise.php?cidReq=TEMP"
     And I follow "Exercise for Behat test"
     And I follow "Start test"
@@ -363,10 +386,8 @@ Feature: Exercise tool
 
   Scenario: Teacher see exercise results by categories
     Given I am on "/user_portal.php"
-    And I follow "Session Exercise"
-    And wait for the page to be loaded
-    And I follow "tabs2"
-    And I follow "TEMP"
+    And I am on course "TEMP" homepage in session "Session Exercise"
+    Then I should see "TEMP (Session Exercise)"
     And I am on "/main/exercise/exercise.php?cidReq=TEMP"
     And I follow "Exercise for Behat test"
     And I follow "Results and feedback"
@@ -397,10 +418,9 @@ Feature: Exercise tool
     And I follow "Delete"
     Then I should see "Category deleted"
 
-  Scenario: Import exercise from excel
-    Given I am on "/main/exercise/upload_exercise.php?cidReq=TEMP"
-    Then I should see "Import quiz from Excel"
-    Then I attach the file "/main/exercise/quiz_template.xls" to "user_upload_quiz"
-    And I press "Upload"
+  Scenario: Delete session
+    Given I am on "/main/session/session_list.php?keyword=Session+Exercise"
     And wait for the page to be loaded
-    Then I should see "Definition of oligarchy"
+    And I follow "Delete"
+    And I confirm the popup
+    Then I should see "Deleted"

+ 42 - 0
tests/scripts/fix_lp_calendar.php

@@ -0,0 +1,42 @@
+<?php
+
+
+require __DIR__ . '/../../main/inc/global.inc.php';
+
+
+
+$extraField = new ExtraField('lp_item');
+$extraFieldInfo = $extraField->get_handler_field_info_by_field_variable('calendar');
+if (empty($extraFieldInfo)) {
+    echo 'No calendar extra field';
+    exit;
+}
+
+$extraFieldId = $extraFieldInfo['id'];
+
+$sql = 'select iid from c_lp_item where title like "%(+)%"';
+$result = Database::query($sql);
+while ($row = Database::fetch_array($result)) {
+    $lpItemId = $row['iid'];
+    
+    $extraField = new ExtraFieldValue('lp_item');
+    $values = $extraField->get_values_by_handler_and_field_variable($lpItemId, 'calendar');
+
+    if (empty($values)) {
+        $value = 1;
+        $params = [
+            'field_id' => $extraFieldId,
+            'value' => $value,
+            'item_id' => $lpItemId,
+        ];
+        //var_dump($params);
+ 	echo 'LP item id added: '.$lpItemId.PHP_EOL;
+	$extraField->save($params);
+    }
+}
+
+exit;
+
+
+
+

+ 2 - 0
user_portal.php

@@ -300,5 +300,7 @@ $controller->tpl->display_two_col_template();
 
 // Deleting the session_id.
 Session::erase('session_id');
+Session::erase('id_session');
 Session::erase('studentview');
 api_remove_in_gradebook();
+