Browse Source

Update from 1.11.x

Julio Montoya 5 years ago
parent
commit
8690b88f0a
50 changed files with 2824 additions and 972 deletions
  1. 5 5
      main/admin/questions.php
  2. 12 0
      main/admin/statistics/index.php
  3. 25 13
      main/announcements/announcements.php
  4. 4 3
      main/cron/import_csv.php
  5. 42 47
      main/exercise/hotspot_admin.inc.php
  6. 1 1
      main/gradebook/lib/be/category.class.php
  7. 2 2
      main/gradebook/lib/be/exerciselink.class.php
  8. 5 0
      main/gradebook/lib/scoredisplay.class.php
  9. 2 2
      main/group/group.php
  10. 5 4
      main/group/group_category.php
  11. 2 2
      main/group/group_creation.php
  12. 2 0
      main/group/member_settings.php
  13. 38 7
      main/group/settings.php
  14. 7 1
      main/inc/ajax/announcement.ajax.php
  15. 49 23
      main/inc/lib/AnnouncementManager.php
  16. 15 8
      main/inc/lib/api.lib.php
  17. 60 0
      main/inc/lib/certificate.lib.php
  18. 33 15
      main/inc/lib/course.lib.php
  19. 2 2
      main/inc/lib/course_home.lib.php
  20. 21 3
      main/inc/lib/formvalidator/Element/DateRangePicker.php
  21. 13 7
      main/inc/lib/groupmanager.lib.php
  22. 123 106
      main/inc/lib/javascript/chat/css/chat.css
  23. 12 26
      main/inc/lib/javascript/chat/js/chat.js
  24. 7 4
      main/inc/lib/javascript/ckeditor/plugins/qmarkersrolls/dialogs/qmarkersrolls.js
  25. 110 306
      main/inc/lib/javascript/jquery.frameready.js
  26. 1315 0
      main/inc/lib/javascript/map/markerclusterer.js
  27. 28 0
      main/inc/lib/javascript/map/oms.min.js
  28. 19 4
      main/inc/lib/pdf.lib.php
  29. 28 79
      main/inc/lib/pear/HTML/QuickForm.php
  30. 5 19
      main/inc/lib/pear/HTML/QuickForm/input.php
  31. 16 4
      main/inc/lib/social.lib.php
  32. 131 0
      main/inc/lib/statistics.lib.php
  33. 4 4
      main/inc/lib/tracking.lib.php
  34. 17 5
      main/link/link.php
  35. 49 48
      main/lp/learnpath.class.php
  36. 4 0
      main/lp/lp_add_audio.php
  37. 1 1
      main/lp/lp_add_category.php
  38. 7 3
      main/lp/lp_controller.php
  39. 1 1
      main/lp/lp_stats.php
  40. 6 6
      main/messages/new_message.php
  41. 219 24
      main/mySpace/lp_tracking.php
  42. 204 39
      main/mySpace/session.php
  43. 5 3
      main/mySpace/works_in_session_report.php
  44. 29 25
      main/session/add_courses_to_session.php
  45. 14 14
      main/session/add_users_to_session.php
  46. 79 79
      main/session/session_category_list.php
  47. 6 26
      main/session/session_list.php
  48. 30 0
      main/social/download_my_files.php
  49. 4 1
      main/social/home.php
  50. 6 0
      main/social/my_skills_report.php

+ 5 - 5
main/admin/questions.php

@@ -39,8 +39,8 @@ $questionCount = 0;
 
 if ($formSent) {
     $id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : '';
-    $description = isset($_REQUEST['description']) ? Security::remove_XSS($_REQUEST['description']) : '';
-    $title = isset($_REQUEST['title']) ? Security::remove_XSS($_REQUEST['title']) : '';
+    $description = isset($_REQUEST['description']) ? $_REQUEST['description'] : '';
+    $title = isset($_REQUEST['title']) ? $_REQUEST['title'] : '';
     $page = isset($_GET['page']) && !empty($_GET['page']) ? (int) $_GET['page'] : 1;
 
     $em = Database::getManager();
@@ -51,7 +51,7 @@ if ($formSent) {
     }
 
     if (!empty($description)) {
-        $criteria->orWhere($criteria->expr()->contains('description', "%$description%"));
+        $criteria->orWhere($criteria->expr()->contains('description', $description."\r"));
     }
 
     if (!empty($title)) {
@@ -65,8 +65,8 @@ if ($formSent) {
     }
     $params = [
         'id' => $id,
-        'title' => $title,
-        'description' => $description,
+        'title' => Security::remove_XSS($title),
+        'description' => Security::remove_XSS($description),
         'form_sent' => 1,
     ];
     $url = api_get_self().'?'.http_build_query($params);

+ 12 - 0
main/admin/statistics/index.php

@@ -130,6 +130,10 @@ if ($report == 'user_session') {
     $htmlHeadXtra[] = api_get_jqgrid_js();
 }
 
+if (isset($_GET['export'])) {
+    ob_start();
+}
+
 $tool_name = get_lang('Statistics');
 Display::display_header($tool_name);
 echo Display::page_header($tool_name);
@@ -153,6 +157,7 @@ $tools[$strUsers]['report=logins&type=month'] = get_lang('Logins').' ('.get_
 $tools[$strUsers]['report=logins&type=day'] = get_lang('Logins').' ('.get_lang('PeriodDay').')';
 $tools[$strUsers]['report=logins&type=hour'] = get_lang('Logins').' ('.get_lang('PeriodHour').')';
 $tools[$strUsers]['report=pictures'] = get_lang('CountUsers').' ('.get_lang('UserPicture').')';
+$tools[$strUsers]['report=logins_by_date'] = get_lang('LoginsByDate');
 $tools[$strUsers]['report=no_login_users'] = get_lang('StatsUsersDidNotLoginInLastPeriods');
 $tools[$strUsers]['report=zombies'] = get_lang('Zombies');
 
@@ -370,6 +375,13 @@ switch ($report) {
         $friends = Statistics::getFriends();
         Statistics::printStats(get_lang('CountFriends'), $friends);
         break;
+    case 'logins_by_date':
+        Statistics::printLoginsByDate();
+        break;
 }
 
 Display::display_footer();
+
+if (isset($_GET['export'])) {
+    ob_end_clean();
+}

+ 25 - 13
main/announcements/announcements.php

@@ -37,6 +37,7 @@ $allowToEdit = (
     api_is_allowed_to_edit(false, true) ||
     (api_get_course_setting('allow_user_edit_announcement') && !api_is_anonymous())
 );
+$allowStudentInGroupToSend = false;
 
 $sessionId = api_get_session_id();
 $drhHasAccessToSessionContent = api_drh_can_access_all_session_content();
@@ -60,15 +61,16 @@ $tbl_courses = Database::get_main_table(TABLE_MAIN_COURSE);
 $tbl_sessions = Database::get_main_table(TABLE_MAIN_SESSION);
 $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
 $tbl_item_property = Database::get_course_table(TABLE_ITEM_PROPERTY);
+
 $isTutor = false;
 if (!empty($group_id)) {
-    $groupProperties = GroupManager:: get_group_properties($group_id);
+    $groupProperties = GroupManager::get_group_properties($group_id);
     $interbreadcrumb[] = [
-        'url' => api_get_path(WEB_CODE_PATH)."group/group.php?".api_get_cidreq(),
+        'url' => api_get_path(WEB_CODE_PATH).'group/group.php?'.api_get_cidreq(),
         'name' => get_lang('Groups'),
     ];
     $interbreadcrumb[] = [
-        'url' => api_get_path(WEB_CODE_PATH)."group/group_space.php?".api_get_cidreq(),
+        'url' => api_get_path(WEB_CODE_PATH).'group/group_space.php?'.api_get_cidreq(),
         'name' => get_lang('GroupSpace').' '.$groupProperties['name'],
     ];
 
@@ -78,10 +80,14 @@ if (!empty($group_id)) {
         if ($isTutor) {
             $allowToEdit = true;
         }
+
+        // Last chance ... students can send announcements
+        if ($groupProperties['announcements_state'] == GroupManager::TOOL_PRIVATE_BETWEEN_USERS) {
+            $allowStudentInGroupToSend = true;
+        }
     }
 }
 
-/* Tracking */
 Event::event_access_tool(TOOL_ANNOUNCEMENT);
 
 $announcement_id = isset($_GET['id']) ? (int) $_GET['id'] : null;
@@ -166,9 +172,11 @@ switch ($action) {
             'url' => api_get_path(WEB_CODE_PATH).'announcements/announcements.php?'.api_get_cidreq(),
             'name' => $nameTools,
         ];
-
         $nameTools = get_lang('View');
         $content = AnnouncementManager::displayAnnouncement($announcement_id);
+        if (empty($content)) {
+            api_not_allowed(true);
+        }
         break;
     case 'list':
         $htmlHeadXtra[] = api_get_jqgrid_js();
@@ -291,20 +299,22 @@ switch ($action) {
 
         if (empty($count)) {
             $html = '';
-            if ($allowToEdit && (empty($_GET['origin']) || $_GET['origin'] !== 'learnpath')) {
-                $html .= '<div id="no-data-view" class="tool-view">';
+            if (($allowToEdit || $allowStudentInGroupToSend) &&
+                (empty($_GET['origin']) || $_GET['origin'] !== 'learnpath')
+            ) {
+                $html .= '<div id="no-data-view">';
                 $html .= '<h3>'.get_lang('Announcements').'</h3>';
                 $html .= Display::return_icon('valves.png', '', [], 64);
                 $html .= '<div class="controls">';
                 $html .= Display::url(
-                    get_lang('Add Announcement'),
+                    get_lang('AddAnnouncement'),
                     api_get_self()."?".api_get_cidreq()."&action=add",
                     ['class' => 'btn btn-primary']
                 );
                 $html .= '</div>';
                 $html .= '</div>';
             } else {
-                $html = Display::return_message(get_lang('No Announcements'), 'warning');
+                $html = Display::return_message(get_lang('NoAnnouncements'), 'warning');
             }
             $content = $html;
         } else {
@@ -389,8 +399,10 @@ switch ($action) {
             api_not_allowed(true);
         }
 
-        if (!$allowToEdit) {
-            api_not_allowed(true);
+        if ($allowStudentInGroupToSend === false) {
+            if (!$allowToEdit) {
+                api_not_allowed(true);
+            }
         }
 
         // DISPLAY ADD ANNOUNCEMENT COMMAND
@@ -768,7 +780,7 @@ if (empty($_GET['origin']) || $_GET['origin'] !== 'learnpath') {
 // Actions
 $show_actions = false;
 $actionsLeft = '';
-if ($allowToEdit && (empty($_GET['origin']) || $_GET['origin'] !== 'learnpath')) {
+if (($allowToEdit || $allowStudentInGroupToSend) && (empty($_GET['origin']) || $_GET['origin'] !== 'learnpath')) {
     if (in_array($action, ['add', 'modify', 'view'])) {
         $actionsLeft .= "<a href='".api_get_self()."?".api_get_cidreq()."'>".
             Display::return_icon('back.png', get_lang('Back'), '', ICON_SIZE_MEDIUM).
@@ -788,7 +800,7 @@ if ($allowToEdit && (empty($_GET['origin']) || $_GET['origin'] !== 'learnpath'))
 
 if ($allowToEdit && api_get_group_id() == 0) {
     $allow = api_get_configuration_value('disable_delete_all_announcements');
-    if ($allow === false) {
+    if ($allow === false && api_is_allowed_to_edit()) {
         if (!isset($_GET['action']) ||
             isset($_GET['action']) && $_GET['action'] == 'list'
         ) {

+ 4 - 3
main/cron/import_csv.php

@@ -826,7 +826,7 @@ class ImportCsv
                     $userInfoByOfficialCode = api_get_user_info_from_official_code($row['official_code']);
                 }
 
-                $userInfoFromUsername = api_get_user_info_from_official_code($row['username']);
+                $userInfoFromUsername = api_get_user_info_from_username($row['username']);
                 if (!empty($userInfoFromUsername)) {
                     $extraFieldValue = new ExtraFieldValue('user');
                     $extraFieldValues = $extraFieldValue->get_values_by_handler_and_field_variable(
@@ -842,11 +842,12 @@ class ImportCsv
                         if (!empty($user_id) && $value != $user_id) {
                             $emails = api_get_configuration_value('cron_notification_help_desk');
                             if (!empty($emails)) {
+                                $this->logger->addInfo('Preparing email to users in configuration: "cron_notification_help_desk"');
                                 $subject = 'User not added due to same username';
                                 $body = 'Cannot add username: "'.$row['username'].'" 
                                     with external_user_id: '.$row['extra_'.$this->extraFieldIdNameList['user']].'
-                                    because '.$userInfoFromUsername['username'].' with external_user_id '.$value.' exists on the portal    
-    ';
+                                    because '.$userInfoFromUsername['username'].' with external_user_id '.$value.' exists on the portal';
+                                $this->logger->addInfo($body);
                                 foreach ($emails as $email) {
                                     api_mail_html('', $email, $subject, $body);
                                 }

+ 42 - 47
main/exercise/hotspot_admin.inc.php

@@ -9,7 +9,7 @@ use ChamiloSession as Session;
  *
  * @package chamilo.exercise
  *
- * @author Toon Keppens
+ * @author  Toon Keppens
  */
 $modifyAnswers = (int) $_GET['hotspotadmin'];
 
@@ -576,13 +576,14 @@ if (isset($modifyAnswers)) {
         echo Display::return_message($msgErr, 'normal'); //main API
     }
 
-    $hotspot_admin_url = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&hotspotadmin='.$modifyAnswers.'&exerciseId='.$exerciseId; ?>
+    $hotspot_admin_url = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&'
+        .http_build_query(['hotspotadmin' => $modifyAnswers, 'exerciseId' => $exerciseId]); ?>
     <form method="post" action="<?php echo $hotspot_admin_url; ?>" class="form-horizontal" id="frm_exercise"
           name="frm_exercise">
         <div class="form-group">
             <div class="col-sm-12">
                 <?php if ($answerType == HOT_SPOT_DELINEATION) {
-        ?>
+            ?>
                     <button type="submit" class="btn btn-danger" name="lessAnswers" value="lessAnswers">
                         <em class="fa fa-trash"></em> <?php echo get_lang('LessOAR'); ?>
                     </button>
@@ -590,8 +591,8 @@ if (isset($modifyAnswers)) {
                         <em class="fa fa-plus"></em> <?php echo get_lang('MoreOAR'); ?>
                     </button>
                 <?php
-    } else {
-        ?>
+        } else {
+            ?>
                     <button type="submit" class="btn btn-danger" name="lessAnswers" value="lessAnswers">
                         <em class="fa fa-trash"></em> <?php echo get_lang('LessHotspots'); ?>
                     </button>
@@ -599,7 +600,7 @@ if (isset($modifyAnswers)) {
                         <em class="fa fa-plus"></em> <?php echo get_lang('MoreHotspots'); ?>
                     </button>
                 <?php
-    } ?>
+        } ?>
                 <button type="submit" class="btn btn-primary" name="submitAnswers" value="submitAnswers">
                     <em class="fa fa-save"></em> <?php echo get_lang('AddQuestionToExercise'); ?>
                 </button>
@@ -662,7 +663,8 @@ if (isset($modifyAnswers)) {
             foreach ($question_list as $key => $questionid) {
                 $selected = '';
                 $question = Question::read($questionid);
-                $val = 'Q'.$key.' :'.substrwords($question->selectTitle(), ICON_SIZE_SMALL);
+                $questionTitle = strip_tags($question->selectTitle());
+                $val = "Q$key: $questionTitle";
 
                 if (isset($select_question[$i]) && $questionid == $select_question[$i]) {
                     $selected = 'selected="selected"';
@@ -740,8 +742,8 @@ if (isset($modifyAnswers)) {
                                     <div class="checkbox">
                                         <p>
                                             <label>
-                                                <input type="checkbox" class="checkbox"
-                                                       name="<?php echo 'try['.$i; ?>]" <?php if ($try[$i] == 1) {
+                                                <input type="checkbox" class="checkbox" name="<?php echo 'try['.$i; ?>]"
+                                                <?php if ($try[$i] == 1) {
                                         echo 'checked';
                                     } ?> />
                                                 <?php echo get_lang('TryAgain'); ?>
@@ -766,7 +768,7 @@ if (isset($modifyAnswers)) {
                                         </select>
                                     </p>
                                 </td>
-                            <?php
+                                <?php
                                 } else {
                                     ?>
                                 <td> &nbsp;</td>
@@ -833,11 +835,11 @@ if (isset($modifyAnswers)) {
                                             </tr>
                                         </table>
                                     </td>
-                                <?php
+                                    <?php
                 } else {
                     ?>
                                     <td>&nbsp;</td>
-                                <?php
+                                    <?php
                 } ?>
                             </tr>
                             <?php
@@ -854,15 +856,15 @@ if (isset($modifyAnswers)) {
                                         <th><?php if ($answerType == HOT_SPOT_DELINEATION) {
                             echo get_lang('Scenario');
                         } ?></th>
-                                    <?php
+                                        <?php
                     } else {
                         ?>
                                         <th colspan="3"><?php echo get_lang('Comment'); ?></th>
-                                    <?php
+                                        <?php
                     } ?>
                                     <th>&nbsp;</th>
                                 </tr>
-                            <?php
+                                <?php
                 } ?>
                             <tr>
                             <td>
@@ -871,15 +873,12 @@ if (isset($modifyAnswers)) {
                             </td>
                             <td>
                                 <input class="form-control" type="text" name="reponse[<?php echo $i; ?>]"
-                                       value="<?php echo isset($reponse[$i])
-                                           ? Security::remove_XSS($reponse[$i])
-                                           : ''; ?>"/>
+                                       value="<?php echo isset($reponse[$i]) ? Security::remove_XSS($reponse[$i]) : ''; ?>"/>
                             </td>
                             <td colspan="2" align="left">
                                 <textarea class="form-control" wrap="virtual" rows="3" cols="25"
                                           name="comment[<?php echo $i; ?>]"
-                                          style="width: 100%"><?php echo isset($comment[$i])
-                                        ? Security::remove_XSS($comment[$i]) : ''; ?></textarea>
+                                          style="width: 100%"><?php echo isset($comment[$i]) ? Security::remove_XSS($comment[$i]) : ''; ?></textarea>
                                 <input type="hidden" name="hotspot_type[<?php echo $i; ?>]" value="oar"/>
                                 <input type="hidden" name="hotspot_coordinates[<?php echo $i; ?>]" value="<?php
                                 echo empty($hotspot_coordinates[$i]) ? '0;0|0|0' : $hotspot_coordinates[$i]; ?>"/>
@@ -891,9 +890,7 @@ if (isset($modifyAnswers)) {
                                         <p>
                                             <label>
                                                 <input type="checkbox" class="checkbox"
-                                                       name="<?php echo 'try['.$i; ?>]" <?php if (isset($try[$i])
-                                                    && $try[$i] == 1
-                                                ) {
+                                                       name="<?php echo 'try['.$i; ?>]" <?php if (isset($try[$i]) && $try[$i] == 1) {
                                         echo 'checked';
                                     } ?> />
                                                 <?php echo get_lang('TryAgain'); ?>
@@ -918,7 +915,7 @@ if (isset($modifyAnswers)) {
                                         </select>
                                     </p>
                                 </td>
-                            <?php
+                                <?php
                                 } else {
                                     ?>
                                 <td>&nbsp;</td>
@@ -948,15 +945,13 @@ if (isset($modifyAnswers)) {
             $renderer = $form->defaultRenderer();
             $form_template = '{content}';
             $renderer->setFormTemplate($form_template);
-            $element_template = '
-                                        {label}
-                                        {element}';
+            $element_template = '{label} {element}';
             $renderer->setElementTemplate($element_template);
 
             $form->setDefaults(['comment['.$i.']' => $commentValue]);
             $return = $form->returnForm(); ?>
                         <td colspan="2" align="left"><?php echo $return; ?></td>
-                    <?php
+                        <?php
         } ?>
                     <td>
                         <?php
@@ -964,7 +959,7 @@ if (isset($modifyAnswers)) {
                             if ($_SESSION['tmp_answers']['hotspot_type'][$i] == 'oar') {
                                 ?>
                                 <input type="hidden" name="weighting[<?php echo $i; ?>]" class="form-cotrol" value="0"/>
-                            <?php
+                                <?php
                             } else {
                                 ?>
                                 <input class="form-control" type="text" name="weighting[<?php echo $i; ?>]"
@@ -982,7 +977,7 @@ if (isset($modifyAnswers)) {
                                        : $hotspot_coordinates[$i]; ?>"/>
                             <input type="hidden" name="hotspot_type[<?php echo $i; ?>]"
                                    value="<?php echo empty($hotspot_type[$i]) ? 'square' : $hotspot_type[$i]; ?>"/>
-                        <?php
+                            <?php
         } ?>
                     </td>
                     </tr>
@@ -1017,7 +1012,8 @@ if (isset($modifyAnswers)) {
     foreach ($question_list as $key => $questionid) {
         $selected = '';
         $question = Question::read($questionid);
-        $val = 'Q'.$key.' :'.substrwords($question->selectTitle(), ICON_SIZE_SMALL);
+        $questionTitle = $question->selectTitle();
+        $val = "Q$key: $questionTitle";
         if ($questionid == $selectQuestionNoError) {
             $selected = 'selected="selected"';
         }
@@ -1051,7 +1047,7 @@ if (isset($modifyAnswers)) {
                         </td>
                         <td colspan="2" align="left">
                             <textarea class="form-control" wrap="virtual" rows="3" cols="25"
-                                name="comment_noerror"><?php echo Security::remove_XSS($comment_noerror); ?></textarea>
+                                      name="comment_noerror"><?php echo Security::remove_XSS($comment_noerror); ?></textarea>
                         </td>
                         <?php if ($objExercise->selectFeedbackType() == EXERCISE_FEEDBACK_TYPE_DIRECT) {
             ?>
@@ -1103,30 +1099,29 @@ if (isset($modifyAnswers)) {
                 $swf_loaded = $answerType == HOT_SPOT_DELINEATION ? 'hotspot_delineation_admin' : 'hotspot_admin';
     $height = 450;
     $relPath = api_get_path(WEB_CODE_PATH); ?>
-                <div id="hotspot-container" class="center-block">
-                </div>
+                <div id="hotspot-container" class="center-block"></div>
             </div>
         </div>
     </form>
     <script>
-        $(function() {
+        $(function () {
             <?php if ($answerType == HOT_SPOT_DELINEATION) {
         ?>
-            new DelineationQuestion({
-                questionId: <?php echo $modifyAnswers; ?>,
-                selector: '#hotspot-container',
-                for: 'admin',
-                relPath: '<?php echo $relPath; ?>'
-            });
+                new DelineationQuestion({
+                    questionId: <?php echo $modifyAnswers; ?>,
+                    selector: '#hotspot-container',
+                    for: 'admin',
+                    relPath: '<?php echo $relPath; ?>'
+                });
             <?php
     } else {
         ?>
-            new HotspotQuestion({
-                questionId: <?php echo $modifyAnswers; ?>,
-                selector: '#hotspot-container',
-                for: 'admin',
-                relPath: '<?php echo $relPath; ?>'
-            });
+                new HotspotQuestion({
+                    questionId: <?php echo $modifyAnswers; ?>,
+                    selector: '#hotspot-container',
+                    for: 'admin',
+                    relPath: '<?php echo $relPath; ?>'
+                });
             <?php
     } ?>
         });

+ 1 - 1
main/gradebook/lib/be/category.class.php

@@ -393,7 +393,7 @@ class Category implements GradebookItem
         $courseId = api_get_course_int_id();
         $courseInfo = api_get_course_info_by_id($courseId);
         $courseCode = $courseInfo['code'];
-        $session_id = intval($session_id);
+        $session_id = (int) $session_id;
 
         if (!empty($session_id)) {
             $table = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);

+ 2 - 2
main/gradebook/lib/be/exerciselink.class.php

@@ -57,10 +57,10 @@ class ExerciseLink extends AbstractLink
         // @todo
         $uploadPath = null;
 
-        $sql = 'SELECT id,title FROM '.$exerciseTable.'
+        $sql = 'SELECT iid, title FROM '.$exerciseTable.'
 				WHERE c_id = '.$this->course_id.' AND active=1  '.$session_condition;
 
-        $sqlLp = "SELECT e.id, e.title 
+        $sqlLp = "SELECT e.iid, e.title 
                   FROM $exerciseTable e 
                   INNER JOIN $lpItemTable i
                   ON (e.c_id = i.c_id AND e.id = i.path)

+ 5 - 0
main/gradebook/lib/scoredisplay.class.php

@@ -307,6 +307,11 @@ class ScoreDisplay
 
             return Display::bar_progress($percentage);
         }
+        if ($type == SCORE_NUMERIC) {
+            $percentage = $my_score[0] / $my_score[1] * 100;
+
+            return round($percentage);
+        }
 
         if ($type == SCORE_SIMPLE) {
             $simpleScore = $this->format_score($my_score[0], $ignoreDecimals);

+ 2 - 2
main/group/group.php

@@ -44,7 +44,7 @@ $nameTools = get_lang('GroupManagement');
 /*
  * Self-registration and un-registration
  */
-$my_group_id = isset($_GET['group_id']) ? intval($_GET['group_id']) : null;
+$my_group_id = isset($_GET['group_id']) ? (int) $_GET['group_id'] : null;
 $my_group = isset($_REQUEST['group']) ? Security::remove_XSS($_REQUEST['group']) : null;
 $my_get_id1 = isset($_GET['id1']) ? Security::remove_XSS($_GET['id1']) : null;
 $my_get_id2 = isset($_GET['id2']) ? Security::remove_XSS($_GET['id2']) : null;
@@ -246,7 +246,7 @@ if (api_get_setting('allow_group_categories') === 'true') {
         if (api_is_allowed_to_edit(false, true) && !empty($categoryId) && empty($sessionId)) {
             // Edit
             $actions .= '<a href="group_category.php?'.api_get_cidreq().'&id='.$categoryId.'" title="'.get_lang('Edit').'">'.
-                Display::return_icon('edit.png', get_lang('EditGroup'), '', ICON_SIZE_SMALL).'</a>';
+                Display::return_icon('edit.png', get_lang('EditCategory'), '', ICON_SIZE_SMALL).'</a>';
 
             // Delete
             $actions .= Display::url(

+ 5 - 4
main/group/group_category.php

@@ -53,10 +53,10 @@ function check_max_number_of_members($value)
  */
 function check_groups_per_user($value)
 {
-    $groups_per_user = $value['groups_per_user'];
+    $groups_per_user = (int) $value['groups_per_user'];
     if (isset($_POST['id']) &&
-        intval($groups_per_user) != GroupManager::GROUP_PER_MEMBER_NO_LIMIT &&
-        GroupManager::get_current_max_groups_per_user($_POST['id']) > intval($groups_per_user)) {
+        $groups_per_user != GroupManager::GROUP_PER_MEMBER_NO_LIMIT &&
+        GroupManager::get_current_max_groups_per_user($_POST['id']) > $groups_per_user) {
         return false;
     }
 
@@ -127,8 +127,9 @@ for ($i = 1; $i <= 10; $i++) {
     $possible_values[$i] = $i;
 }
 $possible_values[GroupManager::GROUP_PER_MEMBER_NO_LIMIT] = get_lang('All');
+
 $group = [
-    $form->createElement('select', 'groups_per_user', null, $possible_values),
+    $form->createElement('select', 'groups_per_user', null, $possible_values, ['id' => 'groups_per_user']),
     $form->createElement('static', null, null, get_lang('QtyOfUserCanSubscribe_PartAfterNumber')),
 ];
 $form->addGroup($group, 'limit_group', get_lang('QtyOfUserCanSubscribe_PartBeforeNumber'), null, false);

+ 2 - 2
main/group/group_creation.php

@@ -134,7 +134,7 @@ if (isset($_POST['number_of_groups'])) {
     <?php
         }
         $group_categories = GroupManager::get_categories();
-        $group_id = GroupManager:: get_number_of_groups() + 1;
+        $group_id = GroupManager::get_number_of_groups() + 1;
         $cat_options = [];
         foreach ($group_categories as $index => $category) {
             $cat_options[$category['id']] = $category['title'];
@@ -233,7 +233,7 @@ EOT;
             $form->addGroup($group_el, 'group_'.$group_number, null, '</td><td>', false);
         }
         $defaults['action'] = 'create_groups';
-        $defaults['number_of_groups'] = intval($_POST['number_of_groups']);
+        $defaults['number_of_groups'] = (int) $_POST['number_of_groups'];
         $form->setDefaults($defaults);
         $form->addButtonCreate(get_lang('CreateGroup'), 'submit');
         $form->display();

+ 2 - 0
main/group/member_settings.php

@@ -209,6 +209,8 @@ if ($form->validate()) {
 
     // Returning to the group area (note: this is inconsistent with the rest of chamilo)
     $cat = GroupManager::get_category_from_group($current_group['iid']);
+    $max_member = $current_group['max_student'];
+
     if (isset($_POST['group_members']) &&
         count($_POST['group_members']) > $max_member &&
         $max_member != GroupManager::MEMBER_PER_GROUP_NO_LIMIT

+ 38 - 7
main/group/settings.php

@@ -21,12 +21,16 @@ api_protect_course_script(true);
 $group_id = api_get_group_id();
 $current_group = GroupManager::get_group_properties($group_id);
 
+if (empty($current_group)) {
+    api_not_allowed(true);
+}
+
 $nameTools = get_lang('EditGroup');
 $interbreadcrumb[] = ['url' => 'group.php?'.api_get_cidreq(), 'name' => get_lang('Groups')];
 $interbreadcrumb[] = ['url' => 'group_space.php?'.api_get_cidreq(), 'name' => $current_group['name']];
-$is_group_member = GroupManager::is_tutor_of_group(api_get_user_id(), $current_group);
+$groupMember = GroupManager::is_tutor_of_group(api_get_user_id(), $current_group);
 
-if (!api_is_allowed_to_edit(false, true) && !$is_group_member) {
+if (!$groupMember && !api_is_allowed_to_edit(false, true)) {
     api_not_allowed(true);
 }
 
@@ -67,8 +71,21 @@ $form->addElement('html', '<div class="col-md-6">');
 
 // Members per group
 $group = [
-    $form->createElement('radio', 'max_member_no_limit', get_lang('GroupLimit'), get_lang('NoLimit'), GroupManager::MEMBER_PER_GROUP_NO_LIMIT),
-    $form->createElement('radio', 'max_member_no_limit', null, get_lang('MaximumOfParticipants'), 1, ['id' => 'max_member_selected']),
+    $form->createElement(
+        'radio',
+        'max_member_no_limit',
+        get_lang('GroupLimit'),
+        get_lang('NoLimit'),
+        GroupManager::MEMBER_PER_GROUP_NO_LIMIT
+    ),
+    $form->createElement(
+        'radio',
+        'max_member_no_limit',
+        null,
+        get_lang('MaximumOfParticipants'),
+        1,
+        ['id' => 'max_member_selected']
+    ),
     $form->createElement('text', 'max_member', null, ['class' => 'span1', 'id' => 'max_member']),
     $form->createElement('static', null, null, ' '.get_lang('GroupPlacesThis')),
 ];
@@ -81,13 +98,26 @@ $form->addElement('html', '<div class="col-md-6">');
 
 // Self registration
 $group = [
-    $form->createElement('checkbox', 'self_registration_allowed', get_lang('GroupSelfRegistration'), get_lang('GroupAllowStudentRegistration'), 1),
-    $form->createElement('checkbox', 'self_unregistration_allowed', null, get_lang('GroupAllowStudentUnregistration'), 1),
+    $form->createElement(
+        'checkbox',
+        'self_registration_allowed',
+        get_lang('GroupSelfRegistration'),
+        get_lang('GroupAllowStudentRegistration'),
+        1
+    ),
+    $form->createElement(
+        'checkbox',
+        'self_unregistration_allowed',
+        null,
+        get_lang('GroupAllowStudentUnregistration'),
+        1
+    ),
 ];
 $form->addGroup(
     $group,
     '',
-    Display::return_icon('user.png', get_lang('GroupSelfRegistration')).'<span>'.get_lang('GroupSelfRegistration').'</span>',
+    Display::return_icon('user.png', get_lang('GroupSelfRegistration')).
+    '<span>'.get_lang('GroupSelfRegistration').'</span>',
     null,
     false
 );
@@ -217,6 +247,7 @@ $group = [
     $form->createElement('radio', 'announcements_state', get_lang('GroupAnnouncements'), get_lang('NotAvailable'), GroupManager::TOOL_NOT_AVAILABLE),
     $form->createElement('radio', 'announcements_state', null, get_lang('Public'), GroupManager::TOOL_PUBLIC),
     $form->createElement('radio', 'announcements_state', null, get_lang('Private'), GroupManager::TOOL_PRIVATE),
+    $form->createElement('radio', 'announcements_state', null, get_lang('PrivateBetweenUsers'), GroupManager::TOOL_PRIVATE_BETWEEN_USERS),
 ];
 
 $form->addGroup(

+ 7 - 1
main/inc/ajax/announcement.ajax.php

@@ -10,6 +10,7 @@ $action = isset($_REQUEST['a']) ? $_REQUEST['a'] : null;
 
 $isAllowedToEdit = api_is_allowed_to_edit();
 $courseInfo = api_get_course_info();
+$courseId = api_get_course_int_id();
 $groupId = api_get_group_id();
 $sessionId = api_get_session_id();
 
@@ -35,12 +36,17 @@ switch ($action) {
         }
 
         if ($allowToEdit === false && !empty($groupId)) {
-            $groupProperties = GroupManager:: get_group_properties($groupId);
+            $groupProperties = GroupManager::get_group_properties($groupId);
             // Check if user is tutor group
             $isTutor = GroupManager::is_tutor_of_group(api_get_user_id(), $groupProperties, $courseId);
             if ($isTutor) {
                 $allowToEdit = true;
             }
+
+            // Last chance ... students can send announcements.
+            if ($groupProperties['announcements_state'] == GroupManager::TOOL_PRIVATE_BETWEEN_USERS) {
+                $allowToEdit = true;
+            }
         }
 
         if ($allowToEdit === false) {

+ 49 - 23
main/inc/lib/AnnouncementManager.php

@@ -127,7 +127,7 @@ class AnnouncementManager
                         $value = $extra['value'];
                         if ($value instanceof ExtraFieldValues) {
                             $field = $value->getField();
-                            if ($field) {
+                            if ($field instanceof ExtraFieldEntity) {
                                 $data['extra_'.$field->getVariable()] = $value->getValue();
                             }
                         }
@@ -162,7 +162,7 @@ class AnnouncementManager
      */
     public static function get_all_annoucement_by_course($course_info, $session_id = 0)
     {
-        $session_id = intval($session_id);
+        $session_id = (int) $session_id;
         $courseId = $course_info['real_id'];
 
         $tbl_announcement = Database::get_course_table(TABLE_ANNOUNCEMENT);
@@ -318,13 +318,15 @@ class AnnouncementManager
      * @param int $announcementId
      * @param int $courseId
      * @param int $userId
+     * @param int $groupId
      *
      * @return array
      */
     public static function getAnnouncementInfoById(
         $announcementId,
         $courseId,
-        $userId
+        $userId,
+        $groupId = 0
     ) {
         if (api_is_allowed_to_edit(false, true) ||
             (api_get_course_setting('allow_user_edit_announcement') && !api_is_anonymous())
@@ -339,13 +341,22 @@ class AnnouncementManager
                         a.cId = :course
                     ORDER BY a.displayOrder DESC";
         } else {
-            $group_list = GroupManager::get_group_ids($courseId, api_get_user_id());
-
-            if (empty($group_list)) {
-                $group_list[] = 0;
-            }
+            $groupId = (int) $groupId;
+            $groupList[] = $groupId;
 
             if (api_get_user_id() != 0) {
+                $extraGroupCondition = '';
+                if (!empty($groupId)) {
+                    $groupProperties = GroupManager::get_group_properties($groupId);
+                    if ($groupProperties['announcements_state'] == GroupManager::TOOL_PRIVATE_BETWEEN_USERS) {
+                        $extraGroupCondition = " AND (
+                            ip.toUser = $userId AND ip.group = $groupId OR
+                            (ip.group IN ('0') OR ip.group IS NULL) OR
+                            (ip.group = $groupId AND (ip.toUser IS NULL OR ip.toUser = 0))
+                        )";
+                    }
+                }
+
                 $dql = "SELECT a, ip
                         FROM ChamiloCourseBundle:CAnnouncement a 
                         JOIN ChamiloCourseBundle:CItemProperty ip
@@ -355,11 +366,12 @@ class AnnouncementManager
                             ip.tool='announcement' AND
                             (
                                 ip.toUser = $userId OR
-                                ip.group IN ('0', '".implode("', '", $group_list)."') OR
+                                ip.group IN ('0', '".$groupId."') OR
                                 ip.group IS NULL
                             ) AND
                             ip.visibility = '1' AND                       
                             ip.course = :course
+                            $extraGroupCondition
                         ORDER BY a.displayOrder DESC";
             } else {
                 $dql = "SELECT a, ip
@@ -383,14 +395,14 @@ class AnnouncementManager
             ]
         );
 
-        if (empty($result)) {
-            return [];
+        if (!empty($result)) {
+            return [
+                'announcement' => $result[0],
+                'item_property' => $result[1],
+            ];
         }
 
-        return [
-            'announcement' => $result[0],
-            'item_property' => $result[1],
-        ];
+        return [];
     }
 
     /**
@@ -402,8 +414,10 @@ class AnnouncementManager
      */
     public static function displayAnnouncement($id)
     {
-        if ($id != strval(intval($id))) {
-            return null;
+        $id = (int) $id;
+
+        if (empty($id)) {
+            return '';
         }
 
         global $charset;
@@ -412,8 +426,14 @@ class AnnouncementManager
         $result = self::getAnnouncementInfoById(
             $id,
             api_get_course_int_id(),
-            api_get_user_id()
+            api_get_user_id(),
+            api_get_group_id()
         );
+
+        if (empty($result)) {
+            return '';
+        }
+
         /** @var CAnnouncement $announcement */
         $announcement = $result['announcement'];
         /** @var CItemProperty $itemProperty */
@@ -470,7 +490,7 @@ class AnnouncementManager
         $html .= "</td></tr>";
 
         $allow = !api_get_configuration_value('hide_announcement_sent_to_users_info');
-        if (api_is_allowed_to_edit(false, true) && $allow) {
+        if ($allow && api_is_allowed_to_edit(false, true)) {
             $sent_to = self::sent_to('announcement', $id);
             $sentToForm = self::sent_to_form($sent_to);
             $html .= Display::tag(
@@ -505,7 +525,7 @@ class AnnouncementManager
             }
             $html .= '</td></tr>';
         }
-        $html .= "</table>";
+        $html .= '</table>';
 
         return $html;
     }
@@ -1783,7 +1803,7 @@ class AnnouncementManager
                 if ($user_id) {
                     if ($allowUserEditSetting && !api_is_anonymous()) {
                         $cond_user_id = " AND (
-                            ip.lastedit_user_id = '".api_get_user_id()."' OR
+                                ip.lastedit_user_id = '".api_get_user_id()."' OR
                                 ((ip.to_user_id='$user_id' OR ip.to_user_id IS NULL) AND 
                                 (ip.to_group_id='0' OR ip.to_group_id IS NULL)
                             )
@@ -2021,8 +2041,14 @@ class AnnouncementManager
                     'id' => $row['id'],
                     'title' => $title,
                     'username' => $username_span,
-                    'insert_date' => api_convert_and_format_date($row['insert_date'], DATE_TIME_FORMAT_LONG),
-                    'lastedit_date' => api_convert_and_format_date($row['lastedit_date'], DATE_TIME_FORMAT_LONG),
+                    'insert_date' => api_convert_and_format_date(
+                        $row['insert_date'],
+                        DATE_TIME_FORMAT_LONG
+                    ),
+                    'lastedit_date' => api_convert_and_format_date(
+                        $row['lastedit_date'],
+                        DATE_TIME_FORMAT_LONG
+                    ),
                     'actions' => $modify_icons,
                 ];
 

+ 15 - 8
main/inc/lib/api.lib.php

@@ -407,6 +407,7 @@ define('SCORE_CUSTOM', 10); // Good!
 define('SCORE_DIV_SIMPLE_WITH_CUSTOM', 11); // X - Good!
 define('SCORE_DIV_SIMPLE_WITH_CUSTOM_LETTERS', 12); // X - Good!
 define('SCORE_ONLY_SCORE', 13); // X - Good!
+define('SCORE_NUMERIC', 14);
 
 define('SCORE_BOTH', 1);
 define('SCORE_ONLY_DEFAULT', 2);
@@ -948,6 +949,7 @@ function api_get_path($path = '', $configuration = [])
             //$paths[$root_web][REL_PATH] = $virtualChamilo[REL_PATH];
             //$paths[$root_web][REL_COURSE_PATH] = $virtualChamilo[REL_COURSE_PATH];
         }
+
         $isInitialized[$root_web] = true;
     }
 
@@ -959,6 +961,7 @@ function api_get_path($path = '', $configuration = [])
     }
 
     // Second purification.
+
     // Replacing Windows back slashes.
     $path = str_replace('\\', '/', $path);
     // Query strings sometimes mighth wrongly appear in non-URLs.
@@ -968,6 +971,7 @@ function api_get_path($path = '', $configuration = [])
     }
 
     // Detection of the input path type. Conversion to semi-absolute type ( /chamilo/main/inc/.... ).
+
     if (preg_match(VALID_WEB_PATH, $path)) {
         // A special case: When a URL points to the document download script directly, without
         // mod-rewrite translation, we have to translate it into an "ordinary" web path.
@@ -1148,7 +1152,6 @@ function api_valid_email($address)
 function api_protect_course_script($print_headers = false, $allow_session_admins = false, $allow_drh = false)
 {
     $course_info = api_get_course_info();
-
     if (empty($course_info)) {
         api_not_allowed($print_headers);
 
@@ -1664,7 +1667,7 @@ function _api_format_user($user, $add_password = false, $loadAvatars = true)
  * @param bool $loadAvatars              turn off to improve performance and if avatars are not needed
  * @param bool $updateCache              update apc cache if exists
  *
- * @return array $user_info user_id, lastname, firstname, username, email, etc
+ * @return mixed $user_info user_id, lastname, firstname, username, email, etc or false on error
  *
  * @author Patrick Cool <patrick.cool@UGent.be>
  * @author Julio Montoya
@@ -4014,13 +4017,13 @@ function convert_sql_date($last_post_datetime)
  * the only one that is actually from the session, in case there are results from
  * session 0 *AND* session n).
  *
- * @param array     Course properties array (result of api_get_course_info())
- * @param string    Tool (learnpath, document, etc)
- * @param int       The item ID in the given tool
- * @param int       The session ID (optional)
- * @param string $tool
+ * @param array  $_course  Course properties array (result of api_get_course_info())
+ * @param string $tool     Tool (learnpath, document, etc)
+ * @param int    $id       The item ID in the given tool
+ * @param int    $session  The session ID (optional)
  * @param int    $user_id
  * @param string $type
+ * @param string $group_id
  *
  * @return int -1 on error, 0 if invisible, 1 if visible
  */
@@ -4897,8 +4900,12 @@ function api_get_language_id($language)
  */
 function api_get_language_info($languageId)
 {
+    if (empty($languageId)) {
+        return [];
+    }
+
     $language = Database::getManager()
-        ->find('ChamiloCoreBundle:Language', intval($languageId));
+        ->find('ChamiloCoreBundle:Language', $languageId);
 
     if (!$language) {
         return [];

+ 60 - 0
main/inc/lib/certificate.lib.php

@@ -871,4 +871,64 @@ class Certificate extends Model
 
         return Database::store_result($rs, 'ASSOC');
     }
+
+    /**
+     * @param int $userId
+     */
+    public static function generateUserSkills($userId)
+    {
+        $controller = new IndexManager(get_lang('MyCourses'));
+        $courseAndSessions = $controller->returnCoursesAndSessions($userId, true, null, true, false);
+
+        if (isset($courseAndSessions['courses']) && !empty($courseAndSessions['courses'])) {
+            foreach ($courseAndSessions['courses'] as $course) {
+                $cats = Category::load(
+                    null,
+                    null,
+                    $course['code'],
+                    null,
+                    null,
+                    null,
+                    false
+                );
+
+                if (isset($cats[0]) && !empty($cats[0])) {
+                    Category::generateUserCertificate(
+                        $cats[0]->get_id(),
+                        $userId
+                    );
+                }
+            }
+        }
+
+        if (isset($courseAndSessions['sessions']) && !empty($courseAndSessions['sessions'])) {
+            foreach ($courseAndSessions['sessions'] as $sessionCategory) {
+                if (isset($sessionCategory['sessions'])) {
+                    foreach ($sessionCategory['sessions'] as $sessionData) {
+                        if (!empty($sessionData['courses'])) {
+                            $sessionId = $sessionData['session_id'];
+                            foreach ($sessionData['courses'] as $courseData) {
+                                $cats = Category:: load(
+                                    null,
+                                    null,
+                                    $courseData['course_code'],
+                                    null,
+                                    null,
+                                    $sessionId,
+                                    false
+                                );
+
+                                if (isset($cats[0]) && !empty($cats[0])) {
+                                    Category::generateUserCertificate(
+                                        $cats[0]->get_id(),
+                                        $userId
+                                    );
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
 }

+ 33 - 15
main/inc/lib/course.lib.php

@@ -3209,29 +3209,47 @@ class CourseManager
      */
     public static function get_details_course_description_html(
         $descriptions,
-        $fieldValues
+        $charset,
+        $action_show = true
     ) {
         $data = null;
-        $info = null;
-        $data['extrafield'] = $fieldValues;
         if (isset($descriptions) && count($descriptions) > 0) {
             foreach ($descriptions as $description) {
-                if ($description->description_type === '1') {
-                    $data['description'] = [
-                        'title' => Security::remove_XSS($description->title),
-                        'content' => Security::remove_XSS($description->content),
-                    ];
+                $data .= '<div class="sectiontitle">';
+                if (api_is_allowed_to_edit() && $action_show) {
+                    //delete
+                    $data .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&action=delete&description_id='.$description->id.'" onclick="javascript:if(!confirm(\''.addslashes(api_htmlentities(
+                        get_lang('ConfirmYourChoice'),
+                                ENT_QUOTES,
+                        $charset
+                    )).'\')) return false;">';
+                    $data .= Display::return_icon(
+                        'delete.gif',
+                        get_lang('Delete'),
+                        ['style' => 'vertical-align:middle;float:right;']
+                    );
+                    $data .= '</a> ';
+                    //edit
+                    $data .= '<a href="'.api_get_self().'?'.api_get_cidreq().'&description_id='.$description->id.'">';
+                    $data .= Display::return_icon(
+                        'edit.png',
+                        get_lang('Edit'),
+                        ['style' => 'vertical-align:middle;float:right; padding-right:4px;'],
+                        ICON_SIZE_SMALL
+                    );
+                    $data .= '</a> ';
                 }
+                $data .= $description->title;
+                $data .= '</div>';
+                $data .= '<div class="sectioncomment">';
+                $data .= Security::remove_XSS($description->content);
+                $data .= '</div>';
             }
+        } else {
+            $data .= '<em>'.get_lang('ThisCourseDescriptionIsEmpty').'</em>';
         }
-        $template = new Template(null);
-        $essence = new Essence\Essence();
-        $template->assign('essence', $essence);
-        $template->assign('data', $data);
-        $layout = $template->get_template('auth/modal_description.html.twig');
-        $info = $template->fetch($layout);
 
-        return $info;
+        return $data;
     }
 
     /**

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

@@ -460,7 +460,7 @@ class CourseHome
                     }
                 }
                 if (is_array($lnk)) {
-                    $html .= '<div class="float-right">';
+                    $html .= '<div class="pull-right">';
                     $html .= '<div class="btn-options">';
                     $html .= '<div class="btn-group btn-group-sm" role="group">';
                     foreach ($lnk as &$this_link) {
@@ -846,7 +846,7 @@ class CourseHome
                 $studentview = false;
                 $tool['original_link'] = $tool['link'];
                 if ($tool['image'] == 'scormbuilder.gif') {
-                    // check if the published learnpath is visible for student
+                    // Check if the published learnpath is visible for student
                     $lpId = self::getPublishedLpIdFromLink($tool['link']);
                     if (api_is_allowed_to_edit(null, true)) {
                         $studentview = true;

+ 21 - 3
main/inc/lib/formvalidator/Element/DateRangePicker.php

@@ -63,9 +63,27 @@ class DateRangePicker extends HTML_QuickForm_text
         $start = isset($dates[0]) ? $dates[0] : '';
         $end = isset($dates[1]) ? $dates[1] : '';
 
+        $pattern = 'yyyy-MM-dd HH:mm';
+
+        if ('false' === $this->getAttribute('timePicker') &&
+            false === strpos($this->getAttribute('format'), 'HH:mm')) {
+            $pattern = 'yyyy-MM-dd';
+        }
+
+        $formatter = new IntlDateFormatter(
+            'en',
+            IntlDateFormatter::NONE,
+            IntlDateFormatter::NONE,
+            'UTC',
+            IntlDateFormatter::GREGORIAN,
+            $pattern
+        );
+        $resultStart = $formatter->format($formatter->parse($start));
+        $resultEnd = $formatter->format($formatter->parse($end));
+
         return [
-            'start' => $start,
-            'end' => $end,
+            'start' => $resultStart,
+            'end' => $resultEnd,
         ];
     }
 
@@ -161,7 +179,7 @@ class DateRangePicker extends HTML_QuickForm_text
         $timePicker = 'true';
         $timePickerValue = Security::remove_XSS($this->getAttribute('timePicker'));
         if (!empty($timePickerValue)) {
-            $timePicker = $timePickerValue;
+            $timePicker = 'false';
         }
 
         // timeFormat: 'hh:mm'

+ 13 - 7
main/inc/lib/groupmanager.lib.php

@@ -1819,11 +1819,11 @@ class GroupManager
         $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
         $group_id = $groupInfo['id'];
 
-        $table = Database::get_course_table(TABLE_GROUP_USER);
         if (!empty($user_ids)) {
+            $table = Database::get_course_table(TABLE_GROUP_USER);
             foreach ($user_ids as $user_id) {
                 if (self::canUserSubscribe($user_id, $groupInfo)) {
-                    $user_id = intval($user_id);
+                    $user_id = (int) $user_id;
                     $sql = "INSERT INTO ".$table." (c_id, user_id, group_id)
                             VALUES ('$course_id', '".$user_id."', '".$group_id."')";
                     Database::query($sql);
@@ -2142,6 +2142,13 @@ class GroupManager
                     return true;
                 }
                 break;
+            case self::TOOL_PRIVATE_BETWEEN_USERS:
+                // Only works for announcements for now
+                $userIsInGroup = self::is_user_in_group($user_id, $groupInfo);
+                if ($userIsInGroup && $tool == self::GROUP_TOOL_ANNOUNCEMENT) {
+                    return true;
+                }
+                break;
         }
 
         return false;
@@ -2860,17 +2867,16 @@ class GroupManager
         echo '
             <ul class="toolbar-groups nav nav-tabs">
                 <li class="'.$activeSettings.'">
-                    <a href="'.sprintf($url, 'settings.php').'">
+                    <a id="group_settings_tab" href="'.sprintf($url, 'settings.php').'">
                     '.Display::return_icon('settings.png').' '.get_lang('Settings').'
                     </a>
                 </li>
                 <li class="'.$activeMember.'">
-                    <a href="'.sprintf($url, 'member_settings.php').'">
-                    '.Display::return_icon('user.png').' '.get_lang('GroupMembers').'
-                    </a>
+                    <a id="group_members_tab" href="'.sprintf($url, 'member_settings.php').'">
+                    '.Display::return_icon('user.png').' '.get_lang('GroupMembers').'</a>
                 </li>
                 <li class="'.$activeTutor.'">
-                    <a href="'.sprintf($url, 'tutor_settings.php').'">
+                    <a id="group_tutors_tab" href="'.sprintf($url, 'tutor_settings.php').'">
                     '.Display::return_icon('teacher.png').' '.get_lang('GroupTutors').'
                     </a>
                 </li>

+ 123 - 106
main/inc/lib/javascript/chat/css/chat.css

@@ -1,181 +1,198 @@
 .chatboxmain {
-	position: fixed;
-	width: auto;
-	z-index: 9000;
-	bottom: 0px;
-	right: 20px;
-	display: block;
+    position: fixed;
+    width: auto;
+    z-index: 9000;
+    bottom: 0px;
+    right: 20px;
+    display: block;
 }
+
 .chatboxheadmain {
-	color: #ffffff;
-	background-color: #000;
-	min-height: 34px;
-	padding: 0 10px;
+    color: #ffffff;
+    background-color: #000;
+    min-height: 34px;
+    padding: 0 10px;
 }
 
 #chatboxtitlemain {
-  font-weight: normal;
-	float: left;
-	font-size: 12px;
-  padding-top: 2px;
-	cursor:pointer;
+    font-weight: normal;
+    float: left;
+    font-size: 12px;
+    padding-top: 2px;
+    cursor: pointer;
 }
+
 .user_status_main {
-	width:18px;
-	display:inline;
-	float:left;
+    width: 18px;
+    display: inline;
+    float: left;
 }
+
 .chatbox {
-	position: fixed;
-	position:expression("absolute");
-	width: 320px;
-	display:none;
-	z-index: 9000;
+    position: fixed;
+    position: expression("absolute");
+    width: 320px;
+    display: none;
+    z-index: 9000;
+}
+
+.chatbox-common {
+    word-wrap: break-word;
 }
 
 .chatboxmessage_me {
     background-color: #4080ff;
     color: #fff;
     width: 80%;
-    float:right !important;
+    float: right !important;
     border-radius: 12px;
 }
 
 .chatboxmessage {
     background-color: #ebedf2;
     color: #2a2f35;
-	margin-left:1em;
+    margin-left: 1em;
     width: 80%;
-    float:left;
+    float: left;
     border-radius: 12px;
 }
 
 .chatboxmessagecontent {
 }
 
+.chatboxmessage_me .chatboxmessagecontent a {
+    color: #ffffff;
+}
+
 .chatboxcontent {
-	font-family: arial,sans-serif;
-	font-size: 13px;
-	color: #333333;
-	height:200px;
-	overflow-y:auto;
-	overflow-x:auto;
-	padding:7px;
-	border-left:1px solid #cccccc;
-	border-right:1px solid #cccccc;
-	border-bottom:1px solid #eeeeee;
-	background-color: #ffffff;
-	line-height: 1.3em;
+    font-family: arial, sans-serif;
+    font-size: 13px;
+    color: #333333;
+    max-height: 200px;
+    overflow-y: auto;
+    overflow-x: auto;
+    padding: 7px;
+    border-left: 1px solid #cccccc;
+    border-right: 1px solid #cccccc;
+    border-bottom: 1px solid #eeeeee;
+    background-color: #ffffff;
+    line-height: 1.3em;
+}
+
+.chatboxcontent .alert {
+    margin-bottom: 0;
 }
 
 .user_status {
-	width:8px;
-	display:inline-block;
-  margin-right: 5px;
+    width: 8px;
+    display: inline-block;
+    margin-right: 5px;
 }
+
 .chatimage {
-  display: inline-block;
-  margin-right: 5px;
+    display: inline-block;
+    margin-right: 5px;
 }
 
 .chatboxtitle {
-	font-weight: normal;
-	display: inline-block;
-	font-size: 10px;
-	width: auto;
-	cursor:pointer;
-	overflow: hidden;
-	padding-left: 2px;
-	text-overflow: ellipsis;
-	white-space: nowrap;
-	line-height: 10px;
+    font-weight: normal;
+    display: inline-block;
+    font-size: 10px;
+    width: auto;
+    cursor: pointer;
+    overflow: hidden;
+    padding-left: 2px;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    line-height: 10px;
 }
 
 .chatboxhead {
-	/* background-color: #222; */
-	padding: 5px;
-	color: #ffffff;
-	border-right:1px solid #222;
-	border-left:1px solid #222;
-	background-color: #222;
-	background-repeat: repeat-x;
-	background-image: -khtml-gradient(linear, left top, left bottom, from(#333333), to(#222222));
-	background-image: -moz-linear-gradient(top, #333333, #222222);
-	background-image: -ms-linear-gradient(top, #333333, #222222);
-	background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #333333), color-stop(100%, #222222));
-	background-image: -webkit-linear-gradient(top, #333333, #222222);
-	background-image: -o-linear-gradient(top, #333333, #222222);
-	background-image: linear-gradient(top, #333333, #222222);
-	filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);
-	-webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
-	-moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
-	box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+    /* background-color: #222; */
+    padding: 5px;
+    color: #ffffff;
+    border-right: 1px solid #222;
+    border-left: 1px solid #222;
+    background-color: #222;
+    background-repeat: repeat-x;
+    background-image: -khtml-gradient(linear, left top, left bottom, from(#333333), to(#222222));
+    background-image: -moz-linear-gradient(top, #333333, #222222);
+    background-image: -ms-linear-gradient(top, #333333, #222222);
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #333333), color-stop(100%, #222222));
+    background-image: -webkit-linear-gradient(top, #333333, #222222);
+    background-image: -o-linear-gradient(top, #333333, #222222);
+    background-image: linear-gradient(top, #333333, #222222);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);
+    -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+    -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1);
 }
 
 .chatboxblink {
-	background-color: #FF921F;
-	border-right:1px solid #EF7A00;
-	border-left:1px solid #EF7A00;
+    background-color: #FF921F;
+    border-right: 1px solid #EF7A00;
+    border-left: 1px solid #EF7A00;
     background-repeat: repeat-x;
-	background-image: -khtml-gradient(linear, left top, left bottom, from(#FF921F), to(#FF921F));
-	background-image: -moz-linear-gradient(top, #FF921F, #FFAC55);
-	background-image: -ms-linear-gradient(top, #FF921F, #FF921F);
-	background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #FF921F), color-stop(100%, #FF921F));
-	background-image: -webkit-linear-gradient(top, #FF921F, #FF921F);
-	background-image: -o-linear-gradient(top, #FF921F, #FF921F);
-	background-image: linear-gradient(top, #FF921F, #FF921F);
+    background-image: -khtml-gradient(linear, left top, left bottom, from(#FF921F), to(#FF921F));
+    background-image: -moz-linear-gradient(top, #FF921F, #FFAC55);
+    background-image: -ms-linear-gradient(top, #FF921F, #FF921F);
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #FF921F), color-stop(100%, #FF921F));
+    background-image: -webkit-linear-gradient(top, #FF921F, #FF921F);
+    background-image: -o-linear-gradient(top, #FF921F, #FF921F);
+    background-image: linear-gradient(top, #FF921F, #FF921F);
 }
 
 .chatboxinput {
-	padding: 5px;
-	background-color: #ffffff;
-	border-left:1px solid #cccccc;
-	border-right:1px solid #cccccc;
-	border-bottom:1px solid #cccccc;
+    padding: 5px;
+    background-color: #ffffff;
+    border-left: 1px solid #cccccc;
+    border-right: 1px solid #cccccc;
+    border-bottom: 1px solid #cccccc;
 }
 
 .chatboxtextarea {
-	width: 300px;
-	height:44px;
-	padding:3px 0pt 3px 3px;
-	border: 1px solid #eeeeee;
-	margin: 1px;
-	overflow:hidden;
+    width: 300px;
+    height: 44px;
+    padding: 3px 0pt 3px 3px;
+    border: 1px solid #eeeeee;
+    margin: 1px;
+    overflow: hidden;
 }
 
 .chatboxtextareaselected {
-	border: 2px solid #f99d39;
-	margin:0;
+    border: 2px solid #f99d39;
+    margin: 0;
 }
+
 .chatboxinfo {
-	margin-left:-1em;
-	color:#666666;
+    margin-left: -1em;
+    color: #666666;
 }
 
 .chatboxmessagefrom {
-	margin-left:-1em;
-	font-weight: bold;
+    margin-left: -1em;
+    font-weight: bold;
 }
 
 .chatboxoptions {
-	float: right;
+    float: right;
 }
 
 .chatboxoptions a {
-	text-decoration: none;
-	color: white;
-	font-weight:bold;
-	font-family:Verdana,Arial,"Bitstream Vera Sans",sans-serif;
+    text-decoration: none;
+    color: white;
+    font-weight: bold;
+    font-family: Verdana, Arial, "Bitstream Vera Sans", sans-serif;
 }
 
 .chatboxoptions a:hover {
-	background-color: #aaa;
+    background-color: #aaa;
 }
 
 .chatbox_checks {
-	float:right;
+    float: right;
 }
 
 .chatbox_checked {
-	color: #13A7F0;
+    color: #13A7F0;
 }

+ 12 - 26
main/inc/lib/javascript/chat/js/chat.js

@@ -165,7 +165,6 @@ function startChatSession()
 										window_user_info.avatar_small
 									);
 								}
-
 								createChatBubble(my_user_id, item);
 							}
 						});
@@ -279,7 +278,6 @@ function chatHeartbeat()
 						newMessagesWin[my_user_id]= {'status':true, 'username':item.username};
 
 						var chatBubble = createChatBubble(my_user_id, item);
-						//$("#chatbox_"+my_user_id+" .chatboxcontent").append(chatBubble);
 
 						$("#chatbox_"+my_user_id+" .chatboxcontent").scrollTop(
 							$("#chatbox_"+my_user_id+" .chatboxcontent")[0].scrollHeight
@@ -338,18 +336,22 @@ function createChatBubble(my_user_id, item, appendType = 'append')
 
 	var messageObject = $("#chatbox_"+my_user_id+" .chatboxcontent").find('#message_id_' + item.id);
 	var exists = messageObject.length !== 0;
-	var messageHeader = '<div id="message_id_'+item.id+'" class="chatbox-common boot-tooltip well '+myDiv+'" title="'+sentDate+'" >';
+	var messageHeader = '<div id="message_id_'+item.id+'" class="chatbox-common well '+myDiv+'" title="'+sentDate+'" >';
 	var messageEnd = '</div>';
 
 	var message = '';
 	if (my_user_id == item.from_user_info.id) {
 		message += '<span class="chatboxmessagefrom">'+item.from_user_info.complete_name+':&nbsp;&nbsp;</span>';
 	}
+
 	message += '<div class="chatboxmessagecontent">'+item.message+'</div>';
 	message += '<div class="chatbox_checks' + unCheckClass + '">'+check+'</div>';
 
 	if (exists) {
 		messageObject.html(message);
+		$(messageObject).linkify({
+			target: "_blank"
+		});
 	} else {
 		message = messageHeader + message + messageEnd;
 		if (appendType == 'append') {
@@ -358,6 +360,9 @@ function createChatBubble(my_user_id, item, appendType = 'append')
 			$("#chatbox_"+my_user_id+" .chatboxcontent").prepend(message);
 		}
 
+		$("#chatbox_"+my_user_id+" .chatboxcontent").linkify({
+			target: "_blank"
+		});
 	}
 
 	return message;
@@ -369,9 +374,7 @@ function createChatBubble(my_user_id, item, appendType = 'append')
 function closeChat()
 {
 	$.post(
-		ajax_url + "?action=close",
-		{
-		},
+		ajax_url + "?action=close", {},
 		function (data) {
 			// Disconnects from chat
 			set_user_status(0);
@@ -730,7 +733,7 @@ function createChatBox(user_id, chatboxtitle, minimizeChatBox, online, userImage
 
 	$("#chatbox_"+user_id).click(function() {
 		if ($('#chatbox_'+user_id+' .chatboxcontent').css('display') != 'none') {
-			$("#chatbox_"+user_id+" .chatboxtextarea").focus();
+			//$("#chatbox_"+user_id+" .chatboxtextarea").focus();
 		}
 	});
 
@@ -756,36 +759,21 @@ function getMoreItems(userId, scrollType)
 		cache: false,
 		dataType: "json",
 		success: function(items) {
-			console.log(items);
 			items = items.reverse();
 			$.each(items, function(i, item) {
-				console.log(i);
 				if (item) {
 					if ($("#chatbox_"+userId).css('display') == 'none') {
 						$("#chatbox_"+userId).css('display','block');
 						restructureChatBoxes();
 					}
 					var chatBubble = createChatBubble(userId, item, 'prepend');
-					//$("#chatbox_"+userId+" .chatboxcontent").prepend(chatBubble);
-
 					if ($('#chatbox_'+userId+' .chatboxcontent').css('display') == 'none') {
 						$('#chatbox_'+userId+' .chatboxhead').toggleClass('chatboxblink');
 					}
-
-					// When using scroll set the scroll window to the first
-					var scrollValue = 10;
-					if (scrollType === 'last') {
-						// When loading for the first time show the last msg
-						//scrollValue = $("#chatbox_"+userId+" .chatboxcontent").height();
-					}
-
-					/*$("#chatbox_"+userId+" .chatboxcontent").scrollTop(
-						scrollValue
-					);*/
 				}
 			});
 		}
-	}); //ajax
+	});
 }
 
 /**
@@ -847,7 +835,6 @@ function toggleChatBoxGrowth(user_id)
 		Cookies.set('chatbox_minimized', newCookie);
 		$('#chatbox_'+user_id+' .chatboxcontent').css('display','block');
 		$('#chatbox_'+user_id+' .chatboxinput').css('display','block');
-		//$("#chatbox_"+user_id+" .chatboxcontent").scrollTop($("#chatbox_"+user_id+" .chatboxcontent")[0].scrollHeight);
 		$('.togglelink').html('<em class="fa fa-toggle-down"></em>');
 	} else {
 		// hide box
@@ -926,8 +913,7 @@ function checkChatBoxInputKey(event, chatboxtextarea, user_id)
 						message: message,
 						id: messageId
 					};
-					var bubble = createChatBubble(user_id, item);
-					//$("#chatbox_" + user_id + " .chatboxcontent").append(bubble);
+					createChatBubble(user_id, item);
 					$("#chatbox_" + user_id + " .chatboxcontent").scrollTop(
 						$("#chatbox_" + user_id + " .chatboxcontent")[0].scrollHeight
 					);

+ 7 - 4
main/inc/lib/javascript/ckeditor/plugins/qmarkersrolls/dialogs/qmarkersrolls.js

@@ -133,7 +133,9 @@ CKEDITOR.dialog.add('qMarkersrollsDialog', function (editor) {
             var label = document.createElement('label');
             label.textContent = quiz.title;
             label.htmlFor = 'ck-qmr-quiz-' + quiz.id;
-            label.style.verticalAlign = 'super';
+            label.style.verticalAlign = 'top';
+            label.style.whiteSpace = 'normal';
+            label.style.display = 'inline-block';
 
             var radio = document.createElement('input');
             radio.type = 'radio';
@@ -141,7 +143,7 @@ CKEDITOR.dialog.add('qMarkersrollsDialog', function (editor) {
             radio.id = 'ck-qmr-quiz-' + quiz.id;
             radio.value = quiz.id;
 
-            var row = document.createElement('div');
+            var row = document.createElement('li');
             row.appendChild(radio);
             row.appendChild(label);
 
@@ -187,6 +189,7 @@ CKEDITOR.dialog.add('qMarkersrollsDialog', function (editor) {
             }, false);
 
             var divMarker = document.createElement('span');
+            divMarker.style.whiteSpace = 'normal';
             divMarker.innerHTML = ' <strong>' + encodeTime(markerRoll[0]) + '</strong> &mdash; '
                 + makerForQuiz.title;
 
@@ -239,8 +242,8 @@ CKEDITOR.dialog.add('qMarkersrollsDialog', function (editor) {
                                     {
                                         type: 'html',
                                         html: lang.embeddableQuizzes + ' '
-                                            + '<div id="ck-qmr-quizzes-container" '
-                                            + 'style="max-height: 110px; overflow: hidden auto;"></div>'
+                                            + '<ul id="ck-qmr-quizzes-container" '
+                                            + 'style="max-height: 110px; overflow: hidden auto; list-style: none;"></ul>'
                                     },
                                     {
                                         type: 'button',

+ 110 - 306
main/inc/lib/javascript/jquery.frameready.js

@@ -1,306 +1,110 @@
-/****
- *
- * frameReady: Remote function calling for jQuery
- *
- * Version 1.2.1
- *
- * Copyright (c) 2007 Daemach (John Wilson) <daemach@gmail.com>, http://ideamill.synaptrixgroup.com
- * Licensed under the MIT License:
- * http://www.opensource.org/licenses/mit-license.php
- *
- *	Credit John Resig and his excellent book for the ready function concepts.
- *
- * Credit to Mike Alsup for the logging code.
- * ============================================================================================
- * Usage: $.frameReady(function (function),target (string)[,options (map)][,callback (function)]);
- *
- * Function: (function/required) An anonymous function to be run within the target frame.
- *
- * Target: (string/required) The target frame.  This must be a window object name (in quotes),
- *		so work from the top down.  If you have 3 frames named topFrame, navFrame, mainFrame, and
- *		an iframe inside of mainframe named iFrame, use "top.topFrame", "top.navFrame",
- *		"top.mainFrame", "top.mainFrame.iFrame" respectively.
- *
- *	Options: (object/optional) Map of options in object literal form.  Options include:
- *
- * 		remote: (boolean/ default true)  Run the function in the context of the target frame.
- *				If true, jQuery will be loaded in the target frame automatically and you can run
- *				jQuery selectors in the target frame as if they were local. ie: $("p") instead of
- *				$("p",top.mainFrame.document).  If false, jQuery will not be loaded automatically
- *				and you must use a context in jquery selectors.
- *
- *		data: (object)  An object to be passed as-is to the target frame.  This is where you pass
- *			variable data, rather than in a closure.
- *
- *		load: (object or array of objects)  jquery is loaded by default. You can pass a single object to
- *			frameReady, or an array of objects that will be loaded and tested in order.  2 types
- *			of files can be loaded.  Scripts	and stylesheets:
- *
- *			scripts: {type:"script", src:"/js/myscript.js", id:"_ms", test:"afunction"}
- *			stylesheets: {type:"stylesheet", src:"/css/mycss.css", id:"_ss"}
- *
- *				type: (string/required) "script" for script files, "stylesheet" for stylesheets.
- *				src: (string/required)  The source of the file, ie: /js/myscript.js.
- *				id: (string/optional)  An id for the id attribute.  If one isn't provided it
- *					will be generated.
- *				test: (sting/optional) The name of a function that should exist once the script
- *					is loaded properly.  Until this function becomes available, the script will
- *					be considered not ready and no other files will be loaded.  If a test is not
- *					provided, the next file will be loaded immediately.  Tests are not useful
- *					with stylesheets.
- *
- *	One gotcha: You must have something other than space characters within the body tags of
- * 	target frame documents for frameReady to work properly.  A single character is enough.
- *		The reason for this is a workaround for an iFrame bug in Firefox, of all things.
- * ==============================================================================================
- *
- * Example:
- *
- *		$.frameReady(function(){
- *		  $("<div>I am a div element</div>").prependTo("body");
- *		}, "top.mainFrame",
- *		{ load: [
- *			{type:"script",id:"_fr",src:"/js/jquery.frameReady.js",test: "$.frameReady"},
- *			{type:"stylesheet",id:"_ss",src:"frameReady.css"}
- *			] }
- *		);
- *
- *
- * Release Notes:
- *
- *	1.2.0 - Added provision for a local callback function;
- *	        Added functionality to reset frame information if frame unloads for any reason;
- *
- *	1.1.0 - Added the ability to load scripts and stylesheets inside the target frame before
- *					processing function stack;
- *
- ****/
-
-if (typeof $daemach == "undefined") {
-    $daemach = {};
-    $daemach.debug = false;  // set this to true to enable logging
-    $daemach.log = function() {
-        if (!top.window.console || !top.window.console.log || !$daemach.debug) {
-            return;
-        } else {
-            top.window.console.log([].join.call(arguments,''));
-        };
-    };
-    $daemach.time = function() {
-        if (!top.window.console || !top.window.console.time || !$daemach.debug) {
-            return;
-        } else {
-            top.window.console.time([].join.call(arguments,''));
-        };
-    };
-    $daemach.timeEnd = function() {
-        if (!top.window.console || !top.window.console.timeEnd || !$daemach.debug) {
-            return;
-        } else {
-            top.window.console.timeEnd([].join.call(arguments,''));
-        };
-    };
-};
-
-if (typeof $daemach["frameReady"] == "undefined") {
-    $daemach["frameReady"] = {};
-};
-
-jQuery.frameReady = function(f,t,r,j) {
-
-    /************************************************************
-        You must specify the path to your jquery.js file below!
-    *************************************************************/
-
-    //var jQueryPath = "/main/inc/lib/javascript/jquery.js";
-    var jQueryPath = jQueryFrameReadyConfigPath; // Define this configuration parameter before loading this script.
-
-    var u = "undefined";
-    var $fr = $daemach["frameReady"];
-    var fn = t.split(".").join("_");
-    // create a branch
-    if (typeof $fr[fn] == u) {
-        $fr[fn] = {};
-        $fr[fn]["settings"] = {
-            remote: true,
-            jquery: true,
-            load: [ {type:"script",id:"_jq", src:jQueryPath, test:"jQuery"} ],
-            bLoaded: false,
-            loadInit: [],
-            data: {},
-            callback: false
-        };
-        $fr[fn]["target"] = t;
-    };
-
-    var fr = $fr[fn];
-    var frs = fr["settings"];
-
-    if (fr.done) {
-        $daemach.log(fr.target + " is ready.  Running functions now.");
-        return (frs.remote) ? eval(fr.target).eval("(" + f.toString() + ")()") : f();
-    };
-
-    // process arguments
-
-    for (var a=2;a<arguments.length;a++){
-        var arg = arguments[a];
-
-        if ($.isFunction(arg)){
-            frs.callback = arg;
-        } else if (typeof arg == "object") {
-            if (typeof arg.remote !== u) {
-                frs.remote = arg.remote;
-            };
-            if (typeof arg.jquery !== u) {
-                frs.jquery = arg.jquery;
-            };
-            if (typeof arg.data !== u) {
-                frs.data = arg.data;
-            };
-
-            // if we're not running functions in the remote frame itself, no need for jQuery
-            if (!frs.remote || !frs.jquery) {
-                frs.load.pop();
-            };
-
-            if (typeof arg.load !== u) {
-                var bl = true;
-                if (arg.load.constructor == Array && arg.load.length){
-                    for (var i=0;i<arg.load.length;i++){
-                        bl = true;
-                        for (var h=0;h<frs.load.length;h++){
-                            if (frs.load[h].src == arg.load[i].src) { bl=false; };
-                        };
-                        if (bl) { frs.load.push(arg.load[i]); };
-                    };
-                } else if (typeof arg.load == "object") {
-                    for (var h=0;h<frs.load.length;h++){
-                        if (frs.load[h].src == arg.load.src) { bl=false; };
-                    };
-                    if (bl) { frs.load.push(arg.load); };
-                };
-            };
-        };
-    };
-
-    if (fr.timer) {
-        fr.ready.push(f);
-    } else {
-        fr.ready=[f];
-        if (typeof addEvent !== "undefined"){ addEvent(window,"load",function(){ jQuery.isFrameReady(fn); }); };
-        fr.timer = setInterval(function(){ jQuery.isFrameReady(fn); },13);
-    };
-};
-
-jQuery.isFrameReady = function(fn){
-    var u = "undefined";
-    var $d = $daemach;
-    var fr = $d["frameReady"][fn];
-    var frs = fr["settings"];
-
-    if (fr.done) { return false; };
-    
-    var fx = eval(fr.target);
-    
-    $d.log(fn, ": New Pass. Checking target");
-    // make sure we have a target
-    if (typeof fx !== "undefined") {
-        
-        $d.log(fn, ": Found target.  Checking DOM");
-        
-        try {
-            var fd = fx.document;
-        } catch (ex) { }
-        
-        // make sure we have a DOM
-        if (fd && fd.getElementsByTagName && fd.getElementById && fd.body && fd.body.innerHTML.length) {
-
-            $d.log(fn, ": Found DOM");
-
-            if (frs.load.length && !frs.bLoaded){
-                for (var i=0;i<frs.load.length;i++){
-                    var s = frs.load[i];
-                    var _test;
-                    try { _test = eval('typeof fx.'+s.test+ ' !== "undefined"'); }
-                    catch(ex){ _test = false;}
-                    finally { $d.log(fn, ": Running test for script ",i,". ", (_test || !s.test) ? "Passed.":"Failed."); };
-
-                    if ((_test || !s.test) && frs.loadInit[i]) {
-                        frs.bLoaded = (typeof s.test == u) ? true : _test;
-                        continue;
-                    } else {
-                        frs.bLoaded = false;
-                        if (typeof frs.loadInit[i] == u){
-                            var id = s.id || "frs_"+i;
-                            switch (s.type) {
-                                case "script" :
-                                    $d.log(fn, ": Loading script "+ i + " (" + s.src + ")");
-                                    var ele=fd.createElement('script');
-                                    ele.setAttribute('id', id);
-                                    ele.setAttribute('src', s.src);
-                                    
-                                    if (fd.getElementsByTagName("body") && fd.getElementsByTagName("body")[0]) {
-                                        fd.getElementsByTagName("body")[0].appendChild(ele);
-                                    }
-                                    
-                                    frs.loadInit[i] = true;
-                                    break;
-                                case "stylesheet" :
-                                    $d.log(fn, ": Loading stylesheet "+ i + " (" + s.src + ")");
-                                    var ele=fd.createElement('link');
-                                    ele.setAttribute('href', s.src);
-                                    ele.setAttribute('rel', "stylesheet");
-                                    ele.setAttribute('type', "text/css");
-                                    fd.getElementsByTagName("body")[0].appendChild(ele);
-                                    frs.loadInit[i] = true;
-                                break;
-                                default :
-                                    $d.log(fn, ": Script "+i+" has a bad or missing type attribute..." );
-                            };
-                        };
-                        break;
-                    };
-                };
-            } else {
-                clearInterval(fr.timer);
-                fr.timer = null;
-                for (i in frs.data){
-                    if (!fx.frData){
-                        fx.frData={};
-                    }
-                    fx.frData[i] = frs.data[i];
-                };
-
-                fr.ready.push(function(){ window.frameReadyUnload = function(root, fn){ $(window).bind("unload",function(){ root.jQuery.frameReady.unload(fn); }); } });
-
-                $d.log(fn, ": Processing function stack:");
-                for (var i=0; i<fr.ready.length;i++){
-                    (frs.remote) ? fx.eval("(" + fr.ready[i].toString() + ")()") : fr.ready[i]();
-                };
-
-                fx.frameReadyUnload(window,fn);
-
-                $d.log(fn, ": Function stack processing complete.");
-
-                // we're done here.  let's have a beer.
-                fr.ready = null;
-                fr.done=true;
-
-                if (frs.callback){
-                    $d.log(fn, ": Found a callback.  Executing...");
-                    frs.callback();
-                };
-            };
-        };
-    };
-
-    $d.log(fn, ":");
-};
-
-jQuery.frameReady.unload = function(fn){
-    $daemach.log("Frame " + fn + " is unloading.  Resetting state.");
-    $daemach["frameReady"][fn].done = false;
-    $daemach["frameReady"][fn]["settings"].bLoaded = false;
-    $daemach["frameReady"][fn]["settings"].loadInit = [];
-};
+/**
+ * @param {function} callback
+ * @param {string} target
+ * @param {Array} resources
+ * @constructor
+ */
+$.frameReady = function (callback, targetSelector, resources) {
+    /**
+     * @type {window}
+     */
+    var targetWindow = document.querySelector(targetSelector);
+    /**
+     * @type {Document}
+     */
+    var targetDocument = null;
+
+    var scripts = resources.filter(function (resource) {
+        return resource.type === 'script';
+    });
+    var stylesheets = resources.filter(function (resource) {
+        return resource.type === 'stylesheet';
+    });
+
+    var scriptsCount = (function () {
+        var count = 0;
+
+        /**
+         * @param {Object} parentScript
+         */
+        function countScripts(parentScript) {
+            count++;
+
+            if (!parentScript.hasOwnProperty('deps')) {
+                return;
+            }
+
+            parentScript.deps.forEach(countScripts);
+        }
+
+        scripts.forEach(countScripts);
+
+        return count;
+    })();
+    var scripsLoadedCount = 0;
+
+    targetWindow.onload = function () {
+        scripsLoadedCount = 0;
+
+        targetDocument = targetWindow.contentDocument;
+
+        scripts.forEach(function (script) {
+            createScript(script);
+        });
+
+        stylesheets.forEach(function (stylesheet) {
+            createStylesheet(stylesheet);
+        });
+    };
+
+    /**
+     * @param {Object} script
+     */
+    function createScript(script) {
+        /**
+         * @type {HTMLScriptElement}
+         */
+        var elParent = targetWindow.contentDocument.createElement('script');
+        elParent.async = false;
+        elParent.onload = function () {
+            scripsLoadedCount++;
+
+            if (!script.hasOwnProperty('deps')) {
+                tryExecuteCallback();
+
+                return;
+            }
+
+            script.deps.forEach(function (scriptB) {
+                createScript(scriptB);
+            });
+        };
+        elParent.setAttribute('src', script.src);
+
+        targetDocument.body.appendChild(elParent);
+    }
+
+    /**
+     * @param {Object} stylesheet
+     */
+    function createStylesheet(stylesheet) {
+        /**
+         * @type {HTMLLinkElement}
+         */
+        var el = targetWindow.contentDocument.createElement('link');
+        el.setAttribute('href', stylesheet.src);
+        el.setAttribute('rel', "stylesheet");
+        el.setAttribute('type', "text/css");
+        if (targetDocument.head) {
+            targetDocument.head.appendChild(el);
+        }
+    }
+
+    function tryExecuteCallback() {
+        if (scripsLoadedCount < scriptsCount) {
+            return;
+        }
+
+        targetWindow.contentWindow.eval('(' + callback.toString() + ')();');
+    }
+};

+ 1315 - 0
main/inc/lib/javascript/map/markerclusterer.js

@@ -0,0 +1,1315 @@
+// ==ClosureCompiler==
+// @compilation_level ADVANCED_OPTIMIZATIONS
+// @externs_url https://raw.githubusercontent.com/google/closure-compiler/master/contrib/externs/maps/google_maps_api_v3.js
+// ==/ClosureCompiler==
+
+/**
+ * @name MarkerClusterer for Google Maps v3
+ * @version version 1.0
+ * @author Luke Mahe
+ * @fileoverview
+ * The library creates and manages per-zoom-level clusters for large amounts of
+ * markers.
+ * <br/>
+ * This is a v3 implementation of the
+ * <a href="http://gmaps-utility-library-dev.googlecode.com/svn/tags/markerclusterer/"
+ * >v2 MarkerClusterer</a>.
+ */
+
+/**
+ * @license
+ * Copyright 2010 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * A Marker Clusterer that clusters markers.
+ *
+ * @param {google.maps.Map} map The Google map to attach to.
+ * @param {Array.<google.maps.Marker>=} opt_markers Optional markers to add to
+ *   the cluster.
+ * @param {Object=} opt_options support the following options:
+ *     'gridSize': (number) The grid size of a cluster in pixels.
+ *     'maxZoom': (number) The maximum zoom level that a marker can be part of a
+ *                cluster.
+ *     'zoomOnClick': (boolean) Whether the default behaviour of clicking on a
+ *                    cluster is to zoom into it.
+ *     'averageCenter': (boolean) Whether the center of each cluster should be
+ *                      the average of all markers in the cluster.
+ *     'minimumClusterSize': (number) The minimum number of markers to be in a
+ *                           cluster before the markers are hidden and a count
+ *                           is shown.
+ *     'styles': (object) An object that has style properties:
+ *       'url': (string) The image url.
+ *       'height': (number) The image height.
+ *       'width': (number) The image width.
+ *       'anchor': (Array) The anchor position of the label text.
+ *       'textColor': (string) The text color.
+ *       'textSize': (number) The text size.
+ *       'backgroundPosition': (string) The position of the backgound x, y.
+ *       'iconAnchor': (Array) The anchor position of the icon x, y.
+ * @constructor
+ * @extends google.maps.OverlayView
+ */
+function MarkerClusterer(map, opt_markers, opt_options) {
+    // MarkerClusterer implements google.maps.OverlayView interface. We use the
+    // extend function to extend MarkerClusterer with google.maps.OverlayView
+    // because it might not always be available when the code is defined so we
+    // look for it at the last possible moment. If it doesn't exist now then
+    // there is no point going ahead :)
+    this.extend(MarkerClusterer, google.maps.OverlayView);
+    this.map_ = map;
+
+    /**
+     * @type {Array.<google.maps.Marker>}
+     * @private
+     */
+    this.markers_ = [];
+
+    /**
+     *  @type {Array.<Cluster>}
+     */
+    this.clusters_ = [];
+
+    this.sizes = [53, 56, 66, 78, 90];
+
+    /**
+     * @private
+     */
+    this.styles_ = [];
+
+    /**
+     * @type {boolean}
+     * @private
+     */
+    this.ready_ = false;
+
+    var options = opt_options || {};
+
+    /**
+     * @type {number}
+     * @private
+     */
+    this.gridSize_ = options['gridSize'] || 60;
+
+    /**
+     * @private
+     */
+    this.minClusterSize_ = options['minimumClusterSize'] || 2;
+
+
+    /**
+     * @type {?number}
+     * @private
+     */
+    this.maxZoom_ = options['maxZoom'] || null;
+
+    this.styles_ = options['styles'] || [];
+
+    /**
+     * @type {string}
+     * @private
+     */
+    this.imagePath_ = options['imagePath'] ||
+        this.MARKER_CLUSTER_IMAGE_PATH_;
+
+    /**
+     * @type {string}
+     * @private
+     */
+    this.imageExtension_ = options['imageExtension'] ||
+        this.MARKER_CLUSTER_IMAGE_EXTENSION_;
+
+    /**
+     * @type {boolean}
+     * @private
+     */
+    this.zoomOnClick_ = true;
+
+    if (options['zoomOnClick'] != undefined) {
+        this.zoomOnClick_ = options['zoomOnClick'];
+    }
+
+    /**
+     * @type {boolean}
+     * @private
+     */
+    this.averageCenter_ = false;
+
+    if (options['averageCenter'] != undefined) {
+        this.averageCenter_ = options['averageCenter'];
+    }
+
+    this.setupStyles_();
+
+    this.setMap(map);
+
+    /**
+     * @type {number}
+     * @private
+     */
+    this.prevZoom_ = this.map_.getZoom();
+
+    // Add the map event listeners
+    var that = this;
+    google.maps.event.addListener(this.map_, 'zoom_changed', function() {
+        var zoom = that.map_.getZoom();
+
+        if (that.prevZoom_ != zoom) {
+            that.prevZoom_ = zoom;
+            that.resetViewport();
+        }
+    });
+
+    google.maps.event.addListener(this.map_, 'idle', function() {
+        that.redraw();
+    });
+
+    // Finally, add the markers
+    if (opt_markers && opt_markers.length) {
+        this.addMarkers(opt_markers, false);
+    }
+}
+
+
+/**
+ * The marker cluster image path.
+ *
+ * @type {string}
+ * @private
+ */
+MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m';
+
+
+/**
+ * The marker cluster image path.
+ *
+ * @type {string}
+ * @private
+ */
+MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png';
+
+
+/**
+ * Extends a objects prototype by anothers.
+ *
+ * @param {Object} obj1 The object to be extended.
+ * @param {Object} obj2 The object to extend with.
+ * @return {Object} The new extended object.
+ * @ignore
+ */
+MarkerClusterer.prototype.extend = function(obj1, obj2) {
+    return (function(object) {
+        for (var property in object.prototype) {
+            this.prototype[property] = object.prototype[property];
+        }
+        return this;
+    }).apply(obj1, [obj2]);
+};
+
+
+/**
+ * Implementaion of the interface method.
+ * @ignore
+ */
+MarkerClusterer.prototype.onAdd = function() {
+    this.setReady_(true);
+};
+
+/**
+ * Implementaion of the interface method.
+ * @ignore
+ */
+MarkerClusterer.prototype.draw = function() {};
+
+/**
+ * Sets up the styles object.
+ *
+ * @private
+ */
+MarkerClusterer.prototype.setupStyles_ = function() {
+    if (this.styles_.length) {
+        return;
+    }
+
+    for (var i = 0, size; size = this.sizes[i]; i++) {
+        this.styles_.push({
+            url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_,
+            height: size,
+            width: size
+        });
+    }
+};
+
+/**
+ *  Fit the map to the bounds of the markers in the clusterer.
+ */
+MarkerClusterer.prototype.fitMapToMarkers = function() {
+    var markers = this.getMarkers();
+    var bounds = new google.maps.LatLngBounds();
+    for (var i = 0, marker; marker = markers[i]; i++) {
+        bounds.extend(marker.getPosition());
+    }
+
+    this.map_.fitBounds(bounds);
+};
+
+
+/**
+ *  Sets the styles.
+ *
+ *  @param {Object} styles The style to set.
+ */
+MarkerClusterer.prototype.setStyles = function(styles) {
+    this.styles_ = styles;
+};
+
+
+/**
+ *  Gets the styles.
+ *
+ *  @return {Object} The styles object.
+ */
+MarkerClusterer.prototype.getStyles = function() {
+    return this.styles_;
+};
+
+
+/**
+ * Whether zoom on click is set.
+ *
+ * @return {boolean} True if zoomOnClick_ is set.
+ */
+MarkerClusterer.prototype.isZoomOnClick = function() {
+    return this.zoomOnClick_;
+};
+
+/**
+ * Whether average center is set.
+ *
+ * @return {boolean} True if averageCenter_ is set.
+ */
+MarkerClusterer.prototype.isAverageCenter = function() {
+    return this.averageCenter_;
+};
+
+
+/**
+ *  Returns the array of markers in the clusterer.
+ *
+ *  @return {Array.<google.maps.Marker>} The markers.
+ */
+MarkerClusterer.prototype.getMarkers = function() {
+    return this.markers_;
+};
+
+
+/**
+ *  Returns the number of markers in the clusterer
+ *
+ *  @return {Number} The number of markers.
+ */
+MarkerClusterer.prototype.getTotalMarkers = function() {
+    return this.markers_.length;
+};
+
+
+/**
+ *  Sets the max zoom for the clusterer.
+ *
+ *  @param {number} maxZoom The max zoom level.
+ */
+MarkerClusterer.prototype.setMaxZoom = function(maxZoom) {
+    this.maxZoom_ = maxZoom;
+};
+
+
+/**
+ *  Gets the max zoom for the clusterer.
+ *
+ *  @return {number} The max zoom level.
+ */
+MarkerClusterer.prototype.getMaxZoom = function() {
+    return this.maxZoom_;
+};
+
+
+/**
+ *  The function for calculating the cluster icon image.
+ *
+ *  @param {Array.<google.maps.Marker>} markers The markers in the clusterer.
+ *  @param {number} numStyles The number of styles available.
+ *  @return {Object} A object properties: 'text' (string) and 'index' (number).
+ *  @private
+ */
+MarkerClusterer.prototype.calculator_ = function(markers, numStyles) {
+    var index = 0;
+    var count = markers.length;
+    var dv = count;
+    while (dv !== 0) {
+        dv = parseInt(dv / 10, 10);
+        index++;
+    }
+
+    index = Math.min(index, numStyles);
+    return {
+        text: count,
+        index: index
+    };
+};
+
+
+/**
+ * Set the calculator function.
+ *
+ * @param {function(Array, number)} calculator The function to set as the
+ *     calculator. The function should return a object properties:
+ *     'text' (string) and 'index' (number).
+ *
+ */
+MarkerClusterer.prototype.setCalculator = function(calculator) {
+    this.calculator_ = calculator;
+};
+
+
+/**
+ * Get the calculator function.
+ *
+ * @return {function(Array, number)} the calculator function.
+ */
+MarkerClusterer.prototype.getCalculator = function() {
+    return this.calculator_;
+};
+
+
+/**
+ * Add an array of markers to the clusterer.
+ *
+ * @param {Array.<google.maps.Marker>} markers The markers to add.
+ * @param {boolean=} opt_nodraw Whether to redraw the clusters.
+ */
+MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) {
+    for (var i = 0, marker; marker = markers[i]; i++) {
+        this.pushMarkerTo_(marker);
+    }
+    if (!opt_nodraw) {
+        this.redraw();
+    }
+};
+
+
+/**
+ * Pushes a marker to the clusterer.
+ *
+ * @param {google.maps.Marker} marker The marker to add.
+ * @private
+ */
+MarkerClusterer.prototype.pushMarkerTo_ = function(marker) {
+    marker.isAdded = false;
+    if (marker['draggable']) {
+        // If the marker is draggable add a listener so we update the clusters on
+        // the drag end.
+        var that = this;
+        google.maps.event.addListener(marker, 'dragend', function() {
+            marker.isAdded = false;
+            that.repaint();
+        });
+    }
+    this.markers_.push(marker);
+};
+
+
+/**
+ * Adds a marker to the clusterer and redraws if needed.
+ *
+ * @param {google.maps.Marker} marker The marker to add.
+ * @param {boolean=} opt_nodraw Whether to redraw the clusters.
+ */
+MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) {
+    this.pushMarkerTo_(marker);
+    if (!opt_nodraw) {
+        this.redraw();
+    }
+};
+
+
+/**
+ * Removes a marker and returns true if removed, false if not
+ *
+ * @param {google.maps.Marker} marker The marker to remove
+ * @return {boolean} Whether the marker was removed or not
+ * @private
+ */
+MarkerClusterer.prototype.removeMarker_ = function(marker) {
+    var index = -1;
+    if (this.markers_.indexOf) {
+        index = this.markers_.indexOf(marker);
+    } else {
+        for (var i = 0, m; m = this.markers_[i]; i++) {
+            if (m == marker) {
+                index = i;
+                break;
+            }
+        }
+    }
+
+    if (index == -1) {
+        // Marker is not in our list of markers.
+        return false;
+    }
+
+    marker.setMap(null);
+
+    this.markers_.splice(index, 1);
+
+    return true;
+};
+
+
+/**
+ * Remove a marker from the cluster.
+ *
+ * @param {google.maps.Marker} marker The marker to remove.
+ * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
+ * @return {boolean} True if the marker was removed.
+ */
+MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) {
+    var removed = this.removeMarker_(marker);
+
+    if (!opt_nodraw && removed) {
+        this.resetViewport();
+        this.redraw();
+        return true;
+    } else {
+        return false;
+    }
+};
+
+
+/**
+ * Removes an array of markers from the cluster.
+ *
+ * @param {Array.<google.maps.Marker>} markers The markers to remove.
+ * @param {boolean=} opt_nodraw Optional boolean to force no redraw.
+ */
+MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) {
+    var removed = false;
+
+    for (var i = 0, marker; marker = markers[i]; i++) {
+        var r = this.removeMarker_(marker);
+        removed = removed || r;
+    }
+
+    if (!opt_nodraw && removed) {
+        this.resetViewport();
+        this.redraw();
+        return true;
+    }
+};
+
+
+/**
+ * Sets the clusterer's ready state.
+ *
+ * @param {boolean} ready The state.
+ * @private
+ */
+MarkerClusterer.prototype.setReady_ = function(ready) {
+    if (!this.ready_) {
+        this.ready_ = ready;
+        this.createClusters_();
+    }
+};
+
+
+/**
+ * Returns the number of clusters in the clusterer.
+ *
+ * @return {number} The number of clusters.
+ */
+MarkerClusterer.prototype.getTotalClusters = function() {
+    return this.clusters_.length;
+};
+
+
+/**
+ * Returns the google map that the clusterer is associated with.
+ *
+ * @return {google.maps.Map} The map.
+ */
+MarkerClusterer.prototype.getMap = function() {
+    return this.map_;
+};
+
+
+/**
+ * Sets the google map that the clusterer is associated with.
+ *
+ * @param {google.maps.Map} map The map.
+ */
+MarkerClusterer.prototype.setMap = function(map) {
+    this.map_ = map;
+};
+
+
+/**
+ * Returns the size of the grid.
+ *
+ * @return {number} The grid size.
+ */
+MarkerClusterer.prototype.getGridSize = function() {
+    return this.gridSize_;
+};
+
+
+/**
+ * Sets the size of the grid.
+ *
+ * @param {number} size The grid size.
+ */
+MarkerClusterer.prototype.setGridSize = function(size) {
+    this.gridSize_ = size;
+};
+
+
+/**
+ * Returns the min cluster size.
+ *
+ * @return {number} The grid size.
+ */
+MarkerClusterer.prototype.getMinClusterSize = function() {
+    return this.minClusterSize_;
+};
+
+/**
+ * Sets the min cluster size.
+ *
+ * @param {number} size The grid size.
+ */
+MarkerClusterer.prototype.setMinClusterSize = function(size) {
+    this.minClusterSize_ = size;
+};
+
+
+/**
+ * Extends a bounds object by the grid size.
+ *
+ * @param {google.maps.LatLngBounds} bounds The bounds to extend.
+ * @return {google.maps.LatLngBounds} The extended bounds.
+ */
+MarkerClusterer.prototype.getExtendedBounds = function(bounds) {
+    var projection = this.getProjection();
+
+    // Turn the bounds into latlng.
+    var tr = new google.maps.LatLng(bounds.getNorthEast().lat(),
+        bounds.getNorthEast().lng());
+    var bl = new google.maps.LatLng(bounds.getSouthWest().lat(),
+        bounds.getSouthWest().lng());
+
+    // Convert the points to pixels and the extend out by the grid size.
+    var trPix = projection.fromLatLngToDivPixel(tr);
+    trPix.x += this.gridSize_;
+    trPix.y -= this.gridSize_;
+
+    var blPix = projection.fromLatLngToDivPixel(bl);
+    blPix.x -= this.gridSize_;
+    blPix.y += this.gridSize_;
+
+    // Convert the pixel points back to LatLng
+    var ne = projection.fromDivPixelToLatLng(trPix);
+    var sw = projection.fromDivPixelToLatLng(blPix);
+
+    // Extend the bounds to contain the new bounds.
+    bounds.extend(ne);
+    bounds.extend(sw);
+
+    return bounds;
+};
+
+
+/**
+ * Determins if a marker is contained in a bounds.
+ *
+ * @param {google.maps.Marker} marker The marker to check.
+ * @param {google.maps.LatLngBounds} bounds The bounds to check against.
+ * @return {boolean} True if the marker is in the bounds.
+ * @private
+ */
+MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) {
+    return bounds.contains(marker.getPosition());
+};
+
+
+/**
+ * Clears all clusters and markers from the clusterer.
+ */
+MarkerClusterer.prototype.clearMarkers = function() {
+    this.resetViewport(true);
+
+    // Set the markers a empty array.
+    this.markers_ = [];
+};
+
+
+/**
+ * Clears all existing clusters and recreates them.
+ * @param {boolean} opt_hide To also hide the marker.
+ */
+MarkerClusterer.prototype.resetViewport = function(opt_hide) {
+    // Remove all the clusters
+    for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
+        cluster.remove();
+    }
+
+    // Reset the markers to not be added and to be invisible.
+    for (var i = 0, marker; marker = this.markers_[i]; i++) {
+        marker.isAdded = false;
+        if (opt_hide) {
+            marker.setMap(null);
+        }
+    }
+
+    this.clusters_ = [];
+};
+
+/**
+ *
+ */
+MarkerClusterer.prototype.repaint = function() {
+    var oldClusters = this.clusters_.slice();
+    this.clusters_.length = 0;
+    this.resetViewport();
+    this.redraw();
+
+    // Remove the old clusters.
+    // Do it in a timeout so the other clusters have been drawn first.
+    window.setTimeout(function() {
+        for (var i = 0, cluster; cluster = oldClusters[i]; i++) {
+            cluster.remove();
+        }
+    }, 0);
+};
+
+
+/**
+ * Redraws the clusters.
+ */
+MarkerClusterer.prototype.redraw = function() {
+    this.createClusters_();
+};
+
+
+/**
+ * Calculates the distance between two latlng locations in km.
+ * @see http://www.movable-type.co.uk/scripts/latlong.html
+ *
+ * @param {google.maps.LatLng} p1 The first lat lng point.
+ * @param {google.maps.LatLng} p2 The second lat lng point.
+ * @return {number} The distance between the two points in km.
+ * @private
+ */
+MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) {
+    if (!p1 || !p2) {
+        return 0;
+    }
+
+    var R = 6371; // Radius of the Earth in km
+    var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
+    var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
+    var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+        Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
+        Math.sin(dLon / 2) * Math.sin(dLon / 2);
+    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+    var d = R * c;
+    return d;
+};
+
+
+/**
+ * Add a marker to a cluster, or creates a new cluster.
+ *
+ * @param {google.maps.Marker} marker The marker to add.
+ * @private
+ */
+MarkerClusterer.prototype.addToClosestCluster_ = function(marker) {
+    var distance = 40000; // Some large number
+    var clusterToAddTo = null;
+    var pos = marker.getPosition();
+    for (var i = 0, cluster; cluster = this.clusters_[i]; i++) {
+        var center = cluster.getCenter();
+        if (center) {
+            var d = this.distanceBetweenPoints_(center, marker.getPosition());
+            if (d < distance) {
+                distance = d;
+                clusterToAddTo = cluster;
+            }
+        }
+    }
+
+    if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
+        clusterToAddTo.addMarker(marker);
+    } else {
+        var cluster = new Cluster(this);
+        cluster.addMarker(marker);
+        this.clusters_.push(cluster);
+    }
+};
+
+
+/**
+ * Creates the clusters.
+ *
+ * @private
+ */
+MarkerClusterer.prototype.createClusters_ = function() {
+    if (!this.ready_) {
+        return;
+    }
+
+    // Get our current map view bounds.
+    // Create a new bounds object so we don't affect the map.
+    var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(),
+        this.map_.getBounds().getNorthEast());
+    var bounds = this.getExtendedBounds(mapBounds);
+
+    for (var i = 0, marker; marker = this.markers_[i]; i++) {
+        if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) {
+            this.addToClosestCluster_(marker);
+        }
+    }
+};
+
+
+/**
+ * A cluster that contains markers.
+ *
+ * @param {MarkerClusterer} markerClusterer The markerclusterer that this
+ *     cluster is associated with.
+ * @constructor
+ * @ignore
+ */
+function Cluster(markerClusterer) {
+    this.markerClusterer_ = markerClusterer;
+    this.map_ = markerClusterer.getMap();
+    this.gridSize_ = markerClusterer.getGridSize();
+    this.minClusterSize_ = markerClusterer.getMinClusterSize();
+    this.averageCenter_ = markerClusterer.isAverageCenter();
+    this.center_ = null;
+    this.markers_ = [];
+    this.bounds_ = null;
+    this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(),
+        markerClusterer.getGridSize());
+}
+
+/**
+ * Determins if a marker is already added to the cluster.
+ *
+ * @param {google.maps.Marker} marker The marker to check.
+ * @return {boolean} True if the marker is already added.
+ */
+Cluster.prototype.isMarkerAlreadyAdded = function(marker) {
+    if (this.markers_.indexOf) {
+        return this.markers_.indexOf(marker) != -1;
+    } else {
+        for (var i = 0, m; m = this.markers_[i]; i++) {
+            if (m == marker) {
+                return true;
+            }
+        }
+    }
+    return false;
+};
+
+
+/**
+ * Add a marker the cluster.
+ *
+ * @param {google.maps.Marker} marker The marker to add.
+ * @return {boolean} True if the marker was added.
+ */
+Cluster.prototype.addMarker = function(marker) {
+    if (this.isMarkerAlreadyAdded(marker)) {
+        return false;
+    }
+
+    if (!this.center_) {
+        this.center_ = marker.getPosition();
+        this.calculateBounds_();
+    } else {
+        if (this.averageCenter_) {
+            var l = this.markers_.length + 1;
+            var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l;
+            var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l;
+            this.center_ = new google.maps.LatLng(lat, lng);
+            this.calculateBounds_();
+        }
+    }
+
+    marker.isAdded = true;
+    this.markers_.push(marker);
+
+    var len = this.markers_.length;
+    if (len < this.minClusterSize_ && marker.getMap() != this.map_) {
+        // Min cluster size not reached so show the marker.
+        marker.setMap(this.map_);
+    }
+
+    if (len == this.minClusterSize_) {
+        // Hide the markers that were showing.
+        for (var i = 0; i < len; i++) {
+            this.markers_[i].setMap(null);
+        }
+    }
+
+    if (len >= this.minClusterSize_) {
+        marker.setMap(null);
+    }
+
+    this.updateIcon();
+    return true;
+};
+
+
+/**
+ * Returns the marker clusterer that the cluster is associated with.
+ *
+ * @return {MarkerClusterer} The associated marker clusterer.
+ */
+Cluster.prototype.getMarkerClusterer = function() {
+    return this.markerClusterer_;
+};
+
+
+/**
+ * Returns the bounds of the cluster.
+ *
+ * @return {google.maps.LatLngBounds} the cluster bounds.
+ */
+Cluster.prototype.getBounds = function() {
+    var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
+    var markers = this.getMarkers();
+    for (var i = 0, marker; marker = markers[i]; i++) {
+        bounds.extend(marker.getPosition());
+    }
+    return bounds;
+};
+
+
+/**
+ * Removes the cluster
+ */
+Cluster.prototype.remove = function() {
+    this.clusterIcon_.remove();
+    this.markers_.length = 0;
+    delete this.markers_;
+};
+
+
+/**
+ * Returns the center of the cluster.
+ *
+ * @return {number} The cluster center.
+ */
+Cluster.prototype.getSize = function() {
+    return this.markers_.length;
+};
+
+
+/**
+ * Returns the center of the cluster.
+ *
+ * @return {Array.<google.maps.Marker>} The cluster center.
+ */
+Cluster.prototype.getMarkers = function() {
+    return this.markers_;
+};
+
+
+/**
+ * Returns the center of the cluster.
+ *
+ * @return {google.maps.LatLng} The cluster center.
+ */
+Cluster.prototype.getCenter = function() {
+    return this.center_;
+};
+
+
+/**
+ * Calculated the extended bounds of the cluster with the grid.
+ *
+ * @private
+ */
+Cluster.prototype.calculateBounds_ = function() {
+    var bounds = new google.maps.LatLngBounds(this.center_, this.center_);
+    this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds);
+};
+
+
+/**
+ * Determines if a marker lies in the clusters bounds.
+ *
+ * @param {google.maps.Marker} marker The marker to check.
+ * @return {boolean} True if the marker lies in the bounds.
+ */
+Cluster.prototype.isMarkerInClusterBounds = function(marker) {
+    return this.bounds_.contains(marker.getPosition());
+};
+
+
+/**
+ * Returns the map that the cluster is associated with.
+ *
+ * @return {google.maps.Map} The map.
+ */
+Cluster.prototype.getMap = function() {
+    return this.map_;
+};
+
+
+/**
+ * Updates the cluster icon
+ */
+Cluster.prototype.updateIcon = function() {
+    var zoom = this.map_.getZoom();
+    var mz = this.markerClusterer_.getMaxZoom();
+
+    if (mz && zoom > mz) {
+        // The zoom is greater than our max zoom so show all the markers in cluster.
+        for (var i = 0, marker; marker = this.markers_[i]; i++) {
+            marker.setMap(this.map_);
+        }
+        return;
+    }
+
+    if (this.markers_.length < this.minClusterSize_) {
+        // Min cluster size not yet reached.
+        this.clusterIcon_.hide();
+        return;
+    }
+
+    var numStyles = this.markerClusterer_.getStyles().length;
+    var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles);
+    this.clusterIcon_.setCenter(this.center_);
+    this.clusterIcon_.setSums(sums);
+    this.clusterIcon_.show();
+};
+
+
+/**
+ * A cluster icon
+ *
+ * @param {Cluster} cluster The cluster to be associated with.
+ * @param {Object} styles An object that has style properties:
+ *     'url': (string) The image url.
+ *     'height': (number) The image height.
+ *     'width': (number) The image width.
+ *     'anchor': (Array) The anchor position of the label text.
+ *     'textColor': (string) The text color.
+ *     'textSize': (number) The text size.
+ *     'backgroundPosition: (string) The background postition x, y.
+ * @param {number=} opt_padding Optional padding to apply to the cluster icon.
+ * @constructor
+ * @extends google.maps.OverlayView
+ * @ignore
+ */
+function ClusterIcon(cluster, styles, opt_padding) {
+    cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView);
+
+    this.styles_ = styles;
+    this.padding_ = opt_padding || 0;
+    this.cluster_ = cluster;
+    this.center_ = null;
+    this.map_ = cluster.getMap();
+    this.div_ = null;
+    this.sums_ = null;
+    this.visible_ = false;
+
+    this.setMap(this.map_);
+}
+
+
+/**
+ * Triggers the clusterclick event and zoom's if the option is set.
+ *
+ * @param {google.maps.MouseEvent} event The event to propagate
+ */
+ClusterIcon.prototype.triggerClusterClick = function(event) {
+    var markerClusterer = this.cluster_.getMarkerClusterer();
+
+    // Trigger the clusterclick event.
+    google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_, event);
+
+    if (markerClusterer.isZoomOnClick()) {
+        // Zoom into the cluster.
+        this.map_.fitBounds(this.cluster_.getBounds());
+    }
+};
+
+
+/**
+ * Adding the cluster icon to the dom.
+ * @ignore
+ */
+ClusterIcon.prototype.onAdd = function() {
+    this.div_ = document.createElement('DIV');
+    if (this.visible_) {
+        var pos = this.getPosFromLatLng_(this.center_);
+        this.div_.style.cssText = this.createCss(pos);
+        this.div_.innerHTML = this.sums_.text;
+    }
+
+    var panes = this.getPanes();
+    panes.overlayMouseTarget.appendChild(this.div_);
+
+    var that = this;
+    var isDragging = false;
+    google.maps.event.addDomListener(this.div_, 'click', function(event) {
+        // Only perform click when not preceded by a drag
+        if (!isDragging) {
+            that.triggerClusterClick(event);
+        }
+    });
+    google.maps.event.addDomListener(this.div_, 'mousedown', function() {
+        isDragging = false;
+    });
+    google.maps.event.addDomListener(this.div_, 'mousemove', function() {
+        isDragging = true;
+    });
+};
+
+
+/**
+ * Returns the position to place the div dending on the latlng.
+ *
+ * @param {google.maps.LatLng} latlng The position in latlng.
+ * @return {google.maps.Point} The position in pixels.
+ * @private
+ */
+ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) {
+    var pos = this.getProjection().fromLatLngToDivPixel(latlng);
+
+    if (typeof this.iconAnchor_ === 'object' && this.iconAnchor_.length === 2) {
+        pos.x -= this.iconAnchor_[0];
+        pos.y -= this.iconAnchor_[1];
+    } else {
+        pos.x -= parseInt(this.width_ / 2, 10);
+        pos.y -= parseInt(this.height_ / 2, 10);
+    }
+    return pos;
+};
+
+
+/**
+ * Draw the icon.
+ * @ignore
+ */
+ClusterIcon.prototype.draw = function() {
+    if (this.visible_) {
+        var pos = this.getPosFromLatLng_(this.center_);
+        this.div_.style.top = pos.y + 'px';
+        this.div_.style.left = pos.x + 'px';
+    }
+};
+
+
+/**
+ * Hide the icon.
+ */
+ClusterIcon.prototype.hide = function() {
+    if (this.div_) {
+        this.div_.style.display = 'none';
+    }
+    this.visible_ = false;
+};
+
+
+/**
+ * Position and show the icon.
+ */
+ClusterIcon.prototype.show = function() {
+    if (this.div_) {
+        var pos = this.getPosFromLatLng_(this.center_);
+        this.div_.style.cssText = this.createCss(pos);
+        this.div_.style.display = '';
+    }
+    this.visible_ = true;
+};
+
+
+/**
+ * Remove the icon from the map
+ */
+ClusterIcon.prototype.remove = function() {
+    this.setMap(null);
+};
+
+
+/**
+ * Implementation of the onRemove interface.
+ * @ignore
+ */
+ClusterIcon.prototype.onRemove = function() {
+    if (this.div_ && this.div_.parentNode) {
+        this.hide();
+        this.div_.parentNode.removeChild(this.div_);
+        this.div_ = null;
+    }
+};
+
+
+/**
+ * Set the sums of the icon.
+ *
+ * @param {Object} sums The sums containing:
+ *   'text': (string) The text to display in the icon.
+ *   'index': (number) The style index of the icon.
+ */
+ClusterIcon.prototype.setSums = function(sums) {
+    this.sums_ = sums;
+    this.text_ = sums.text;
+    this.index_ = sums.index;
+    if (this.div_) {
+        this.div_.innerHTML = sums.text;
+    }
+
+    this.useStyle();
+};
+
+
+/**
+ * Sets the icon to the the styles.
+ */
+ClusterIcon.prototype.useStyle = function() {
+    var index = Math.max(0, this.sums_.index - 1);
+    index = Math.min(this.styles_.length - 1, index);
+    var style = this.styles_[index];
+    this.url_ = style['url'];
+    this.height_ = style['height'];
+    this.width_ = style['width'];
+    this.textColor_ = style['textColor'];
+    this.anchor_ = style['anchor'];
+    this.textSize_ = style['textSize'];
+    this.backgroundPosition_ = style['backgroundPosition'];
+    this.iconAnchor_ = style['iconAnchor'];
+};
+
+
+/**
+ * Sets the center of the icon.
+ *
+ * @param {google.maps.LatLng} center The latlng to set as the center.
+ */
+ClusterIcon.prototype.setCenter = function(center) {
+    this.center_ = center;
+};
+
+
+/**
+ * Create the css text based on the position of the icon.
+ *
+ * @param {google.maps.Point} pos The position.
+ * @return {string} The css style text.
+ */
+ClusterIcon.prototype.createCss = function(pos) {
+    var style = [];
+    style.push('background-image:url(' + this.url_ + ');');
+    var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0';
+    style.push('background-position:' + backgroundPosition + ';');
+
+    if (typeof this.anchor_ === 'object') {
+        if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 &&
+            this.anchor_[0] < this.height_) {
+            style.push('height:' + (this.height_ - this.anchor_[0]) +
+                'px; padding-top:' + this.anchor_[0] + 'px;');
+        } else if (typeof this.anchor_[0] === 'number' && this.anchor_[0] < 0 &&
+            -this.anchor_[0] < this.height_) {
+            style.push('height:' + this.height_ + 'px; line-height:' + (this.height_ + this.anchor_[0]) +
+                'px;');
+        } else {
+            style.push('height:' + this.height_ + 'px; line-height:' + this.height_ +
+                'px;');
+        }
+        if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 &&
+            this.anchor_[1] < this.width_) {
+            style.push('width:' + (this.width_ - this.anchor_[1]) +
+                'px; padding-left:' + this.anchor_[1] + 'px;');
+        } else {
+            style.push('width:' + this.width_ + 'px; text-align:center;');
+        }
+    } else {
+        style.push('height:' + this.height_ + 'px; line-height:' +
+            this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;');
+    }
+
+    var txtColor = this.textColor_ ? this.textColor_ : 'black';
+    var txtSize = this.textSize_ ? this.textSize_ : 11;
+
+    style.push('cursor:pointer; top:' + pos.y + 'px; left:' +
+        pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' +
+        txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold');
+    return style.join('');
+};
+
+
+// Export Symbols for Closure
+// If you are not going to compile with closure then you can remove the
+// code below.
+window['MarkerClusterer'] = MarkerClusterer;
+MarkerClusterer.prototype['addMarker'] = MarkerClusterer.prototype.addMarker;
+MarkerClusterer.prototype['addMarkers'] = MarkerClusterer.prototype.addMarkers;
+MarkerClusterer.prototype['clearMarkers'] =
+    MarkerClusterer.prototype.clearMarkers;
+MarkerClusterer.prototype['fitMapToMarkers'] =
+    MarkerClusterer.prototype.fitMapToMarkers;
+MarkerClusterer.prototype['getCalculator'] =
+    MarkerClusterer.prototype.getCalculator;
+MarkerClusterer.prototype['getGridSize'] =
+    MarkerClusterer.prototype.getGridSize;
+MarkerClusterer.prototype['getExtendedBounds'] =
+    MarkerClusterer.prototype.getExtendedBounds;
+MarkerClusterer.prototype['getMap'] = MarkerClusterer.prototype.getMap;
+MarkerClusterer.prototype['getMarkers'] = MarkerClusterer.prototype.getMarkers;
+MarkerClusterer.prototype['getMaxZoom'] = MarkerClusterer.prototype.getMaxZoom;
+MarkerClusterer.prototype['getStyles'] = MarkerClusterer.prototype.getStyles;
+MarkerClusterer.prototype['getTotalClusters'] =
+    MarkerClusterer.prototype.getTotalClusters;
+MarkerClusterer.prototype['getTotalMarkers'] =
+    MarkerClusterer.prototype.getTotalMarkers;
+MarkerClusterer.prototype['redraw'] = MarkerClusterer.prototype.redraw;
+MarkerClusterer.prototype['removeMarker'] =
+    MarkerClusterer.prototype.removeMarker;
+MarkerClusterer.prototype['removeMarkers'] =
+    MarkerClusterer.prototype.removeMarkers;
+MarkerClusterer.prototype['resetViewport'] =
+    MarkerClusterer.prototype.resetViewport;
+MarkerClusterer.prototype['repaint'] =
+    MarkerClusterer.prototype.repaint;
+MarkerClusterer.prototype['setCalculator'] =
+    MarkerClusterer.prototype.setCalculator;
+MarkerClusterer.prototype['setGridSize'] =
+    MarkerClusterer.prototype.setGridSize;
+MarkerClusterer.prototype['setMaxZoom'] =
+    MarkerClusterer.prototype.setMaxZoom;
+MarkerClusterer.prototype['onAdd'] = MarkerClusterer.prototype.onAdd;
+MarkerClusterer.prototype['draw'] = MarkerClusterer.prototype.draw;
+
+Cluster.prototype['getCenter'] = Cluster.prototype.getCenter;
+Cluster.prototype['getSize'] = Cluster.prototype.getSize;
+Cluster.prototype['getMarkers'] = Cluster.prototype.getMarkers;
+
+ClusterIcon.prototype['onAdd'] = ClusterIcon.prototype.onAdd;
+ClusterIcon.prototype['draw'] = ClusterIcon.prototype.draw;
+ClusterIcon.prototype['onRemove'] = ClusterIcon.prototype.onRemove;

+ 28 - 0
main/inc/lib/javascript/map/oms.min.js

@@ -0,0 +1,28 @@
+/*
+ OverlappingMarkerSpiderfier
+https://github.com/jawj/OverlappingMarkerSpiderfier
+Copyright (c) 2011 - 2017 George MacKerron
+Released under the MIT licence: http://opensource.org/licenses/mit-license
+Note: The Google Maps API v3 must be included *before* this code
+*/
+(function(){var m,t,w,y,u,z={}.hasOwnProperty,A=[].slice;this.OverlappingMarkerSpiderfier=function(){function r(a,d){var b,f,e;this.map=a;null==d&&(d={});null==this.constructor.N&&(this.constructor.N=!0,h=google.maps,l=h.event,p=h.MapTypeId,c.keepSpiderfied=!1,c.ignoreMapClick=!1,c.markersWontHide=!1,c.markersWontMove=!1,c.basicFormatEvents=!1,c.nearbyDistance=20,c.circleSpiralSwitchover=9,c.circleFootSeparation=23,c.circleStartAngle=x/12,c.spiralFootSeparation=26,c.spiralLengthStart=11,c.spiralLengthFactor=
+    4,c.spiderfiedZIndex=h.Marker.MAX_ZINDEX+2E4,c.highlightedLegZIndex=h.Marker.MAX_ZINDEX+1E4,c.usualLegZIndex=h.Marker.MAX_ZINDEX+1,c.legWeight=1.5,c.legColors={usual:{},highlighted:{}},e=c.legColors.usual,f=c.legColors.highlighted,e[p.HYBRID]=e[p.SATELLITE]="#fff",f[p.HYBRID]=f[p.SATELLITE]="#f00",e[p.TERRAIN]=e[p.ROADMAP]="#444",f[p.TERRAIN]=f[p.ROADMAP]="#f00",this.constructor.j=function(a){return this.setMap(a)},this.constructor.j.prototype=new h.OverlayView,this.constructor.j.prototype.draw=function(){});
+    for(b in d)z.call(d,b)&&(f=d[b],this[b]=f);this.g=new this.constructor.j(this.map);this.C();this.c={};this.B=this.l=null;this.addListener("click",function(a,b){return l.trigger(a,"spider_click",b)});this.addListener("format",function(a,b){return l.trigger(a,"spider_format",b)});this.ignoreMapClick||l.addListener(this.map,"click",function(a){return function(){return a.unspiderfy()}}(this));l.addListener(this.map,"maptypeid_changed",function(a){return function(){return a.unspiderfy()}}(this));l.addListener(this.map,
+        "zoom_changed",function(a){return function(){a.unspiderfy();if(!a.basicFormatEvents)return a.h()}}(this))}var l,h,m,v,p,c,t,x,u;c=r.prototype;t=[r,c];m=0;for(v=t.length;m<v;m++)u=t[m],u.VERSION="1.0.3";x=2*Math.PI;h=l=p=null;r.markerStatus={SPIDERFIED:"SPIDERFIED",SPIDERFIABLE:"SPIDERFIABLE",UNSPIDERFIABLE:"UNSPIDERFIABLE",UNSPIDERFIED:"UNSPIDERFIED"};c.C=function(){this.a=[];this.s=[]};c.addMarker=function(a,d){a.setMap(this.map);return this.trackMarker(a,d)};c.trackMarker=function(a,d){var b;if(null!=
+    a._oms)return this;a._oms=!0;b=[l.addListener(a,"click",function(b){return function(d){return b.V(a,d)}}(this))];this.markersWontHide||b.push(l.addListener(a,"visible_changed",function(b){return function(){return b.D(a,!1)}}(this)));this.markersWontMove||b.push(l.addListener(a,"position_changed",function(b){return function(){return b.D(a,!0)}}(this)));null!=d&&b.push(l.addListener(a,"spider_click",d));this.s.push(b);this.a.push(a);this.basicFormatEvents?this.trigger("format",a,this.constructor.markerStatus.UNSPIDERFIED):
+    (this.trigger("format",a,this.constructor.markerStatus.UNSPIDERFIABLE),this.h());return this};c.D=function(a,d){if(!this.J&&!this.K)return null==a._omsData||!d&&a.getVisible()||this.unspiderfy(d?a:null),this.h()};c.getMarkers=function(){return this.a.slice(0)};c.removeMarker=function(a){this.forgetMarker(a);return a.setMap(null)};c.forgetMarker=function(a){var d,b,f,e,g;null!=a._omsData&&this.unspiderfy();d=this.A(this.a,a);if(0>d)return this;g=this.s.splice(d,1)[0];b=0;for(f=g.length;b<f;b++)e=g[b],
+    l.removeListener(e);delete a._oms;this.a.splice(d,1);this.h();return this};c.removeAllMarkers=c.clearMarkers=function(){var a,d,b,f;f=this.getMarkers();this.forgetAllMarkers();a=0;for(d=f.length;a<d;a++)b=f[a],b.setMap(null);return this};c.forgetAllMarkers=function(){var a,d,b,f,e,g,c,q;this.unspiderfy();q=this.a;a=d=0;for(b=q.length;d<b;a=++d){g=q[a];e=this.s[a];c=0;for(a=e.length;c<a;c++)f=e[c],l.removeListener(f);delete g._oms}this.C();return this};c.addListener=function(a,d){var b;(null!=(b=this.c)[a]?
+    b[a]:b[a]=[]).push(d);return this};c.removeListener=function(a,d){var b;b=this.A(this.c[a],d);0>b||this.c[a].splice(b,1);return this};c.clearListeners=function(a){this.c[a]=[];return this};c.trigger=function(){var a,d,b,f,e,g;d=arguments[0];a=2<=arguments.length?A.call(arguments,1):[];d=null!=(b=this.c[d])?b:[];g=[];f=0;for(e=d.length;f<e;f++)b=d[f],g.push(b.apply(null,a));return g};c.L=function(a,d){var b,f,e,g,c;g=this.circleFootSeparation*(2+a)/x;f=x/a;c=[];for(b=e=0;0<=a?e<a:e>a;b=0<=a?++e:--e)b=
+    this.circleStartAngle+b*f,c.push(new h.Point(d.x+g*Math.cos(b),d.y+g*Math.sin(b)));return c};c.M=function(a,d){var b,f,e,c,k;c=this.spiralLengthStart;b=0;k=[];for(f=e=0;0<=a?e<a:e>a;f=0<=a?++e:--e)b+=this.spiralFootSeparation/c+5E-4*f,f=new h.Point(d.x+c*Math.cos(b),d.y+c*Math.sin(b)),c+=x*this.spiralLengthFactor/b,k.push(f);return k};c.V=function(a,d){var b,f,e,c,k,q,n,l,h;(q=null!=a._omsData)&&this.keepSpiderfied||this.unspiderfy();if(q||this.map.getStreetView().getVisible()||"GoogleEarthAPI"===
+    this.map.getMapTypeId())return this.trigger("click",a,d);q=[];n=[];b=this.nearbyDistance;l=b*b;k=this.f(a.position);h=this.a;b=0;for(f=h.length;b<f;b++)e=h[b],null!=e.map&&e.getVisible()&&(c=this.f(e.position),this.i(c,k)<l?q.push({R:e,G:c}):n.push(e));return 1===q.length?this.trigger("click",a,d):this.W(q,n)};c.markersNearMarker=function(a,d){var b,f,e,c,k,q,n,l,h,m;null==d&&(d=!1);if(null==this.g.getProjection())throw"Must wait for 'idle' event on map before calling markersNearMarker";b=this.nearbyDistance;
+    n=b*b;k=this.f(a.position);q=[];l=this.a;b=0;for(f=l.length;b<f&&!(e=l[b],e!==a&&null!=e.map&&e.getVisible()&&(c=this.f(null!=(h=null!=(m=e._omsData)?m.v:void 0)?h:e.position),this.i(c,k)<n&&(q.push(e),d)));b++);return q};c.F=function(){var a,d,b,f,e,c,k,l,n,h,m;if(null==this.g.getProjection())throw"Must wait for 'idle' event on map before calling markersNearAnyOtherMarker";n=this.nearbyDistance;n*=n;var p;e=this.a;p=[];h=0;for(d=e.length;h<d;h++)f=e[h],p.push({H:this.f(null!=(a=null!=(b=f._omsData)?
+        b.v:void 0)?a:f.position),b:!1});h=this.a;a=b=0;for(f=h.length;b<f;a=++b)if(d=h[a],null!=d.getMap()&&d.getVisible()&&(c=p[a],!c.b))for(m=this.a,d=l=0,e=m.length;l<e;d=++l)if(k=m[d],d!==a&&null!=k.getMap()&&k.getVisible()&&(k=p[d],(!(d<a)||k.b)&&this.i(c.H,k.H)<n)){c.b=k.b=!0;break}return p};c.markersNearAnyOtherMarker=function(){var a,d,b,c,e,g,k;e=this.F();g=this.a;k=[];a=d=0;for(b=g.length;d<b;a=++d)c=g[a],e[a].b&&k.push(c);return k};c.setImmediate=function(a){return window.setTimeout(a,0)};c.h=
+    function(){if(!this.basicFormatEvents&&null==this.l)return this.l=this.setImmediate(function(a){return function(){a.l=null;return null!=a.g.getProjection()?a.w():null!=a.B?void 0:a.B=l.addListenerOnce(a.map,"idle",function(){return a.w()})}}(this))};c.w=function(){var a,d,b,c,e,g,k;if(this.basicFormatEvents){e=[];d=0;for(b=markers.length;d<b;d++)c=markers[d],a=null!=c._omsData?"SPIDERFIED":"UNSPIDERFIED",e.push(this.trigger("format",c,this.constructor.markerStatus[a]));return e}e=this.F();g=this.a;
+    k=[];a=b=0;for(d=g.length;b<d;a=++b)c=g[a],a=null!=c._omsData?"SPIDERFIED":e[a].b?"SPIDERFIABLE":"UNSPIDERFIABLE",k.push(this.trigger("format",c,this.constructor.markerStatus[a]));return k};c.P=function(a){return{m:function(d){return function(){return a._omsData.o.setOptions({strokeColor:d.legColors.highlighted[d.map.mapTypeId],zIndex:d.highlightedLegZIndex})}}(this),u:function(d){return function(){return a._omsData.o.setOptions({strokeColor:d.legColors.usual[d.map.mapTypeId],zIndex:d.usualLegZIndex})}}(this)}};
+    c.W=function(a,d){var b,c,e,g,k,q,n,m,p,r;this.J=!0;r=a.length;b=this.T(function(){var b,d,c;c=[];b=0;for(d=a.length;b<d;b++)m=a[b],c.push(m.G);return c}());g=r>=this.circleSpiralSwitchover?this.M(r,b).reverse():this.L(r,b);b=function(){var b,d,f;f=[];b=0;for(d=g.length;b<d;b++)e=g[b],c=this.U(e),p=this.S(a,function(a){return function(b){return a.i(b.G,e)}}(this)),n=p.R,q=new h.Polyline({map:this.map,path:[n.position,c],strokeColor:this.legColors.usual[this.map.mapTypeId],strokeWeight:this.legWeight,
+        zIndex:this.usualLegZIndex}),n._omsData={v:n.getPosition(),X:n.getZIndex(),o:q},this.legColors.highlighted[this.map.mapTypeId]!==this.legColors.usual[this.map.mapTypeId]&&(k=this.P(n),n._omsData.O={m:l.addListener(n,"mouseover",k.m),u:l.addListener(n,"mouseout",k.u)}),this.trigger("format",n,this.constructor.markerStatus.SPIDERFIED),n.setPosition(c),n.setZIndex(Math.round(this.spiderfiedZIndex+e.y)),f.push(n);return f}.call(this);delete this.J;this.I=!0;return this.trigger("spiderfy",b,d)};c.unspiderfy=
+        function(a){var d,b,c,e,g,k,h;null==a&&(a=null);if(null==this.I)return this;this.K=!0;h=[];g=[];k=this.a;d=0;for(b=k.length;d<b;d++)e=k[d],null!=e._omsData?(e._omsData.o.setMap(null),e!==a&&e.setPosition(e._omsData.v),e.setZIndex(e._omsData.X),c=e._omsData.O,null!=c&&(l.removeListener(c.m),l.removeListener(c.u)),delete e._omsData,e!==a&&(c=this.basicFormatEvents?"UNSPIDERFIED":"SPIDERFIABLE",this.trigger("format",e,this.constructor.markerStatus[c])),h.push(e)):g.push(e);delete this.K;delete this.I;
+            this.trigger("unspiderfy",h,g);return this};c.i=function(a,d){var b,c;b=a.x-d.x;c=a.y-d.y;return b*b+c*c};c.T=function(a){var c,b,f,e,g;c=e=g=0;for(b=a.length;c<b;c++)f=a[c],e+=f.x,g+=f.y;a=a.length;return new h.Point(e/a,g/a)};c.f=function(a){return this.g.getProjection().fromLatLngToDivPixel(a)};c.U=function(a){return this.g.getProjection().fromDivPixelToLatLng(a)};c.S=function(a,c){var b,d,e,g,k,h;e=k=0;for(h=a.length;k<h;e=++k)if(g=a[e],g=c(g),"undefined"===typeof b||null===b||g<d)d=g,b=e;return a.splice(b,
+        1)[0]};c.A=function(a,c){var b,d,e,g;if(null!=a.indexOf)return a.indexOf(c);b=d=0;for(e=a.length;d<e;b=++d)if(g=a[b],g===c)return b;return-1};return r}();t=/(\?.*(&|&amp;)|\?)spiderfier_callback=(\w+)/;m=document.currentScript;null==m&&(m=function(){var m,l,h,w,v;h=document.getElementsByTagName("script");v=[];m=0;for(l=h.length;m<l;m++)u=h[m],null!=(w=u.getAttribute("src"))&&w.match(t)&&v.push(u);return v}()[0]);if(null!=m&&(m=null!=(w=m.getAttribute("src"))?null!=(y=w.match(t))?y[3]:void 0:void 0)&&
+    "function"===typeof window[m])window[m]();"function"===typeof window.spiderfier_callback&&window.spiderfier_callback()}).call(this);
+/* Thu 11 May 2017 08:40:57 BST */

+ 19 - 4
main/inc/lib/pdf.lib.php

@@ -46,6 +46,7 @@ class PDF
         $params['right'] = isset($params['right']) ? $params['right'] : 15;
         $params['top'] = isset($params['top']) ? $params['top'] : 30;
         $params['bottom'] = isset($params['bottom']) ? $params['bottom'] : 30;
+        $params['margin_footer'] = isset($params['margin_footer']) ? $params['margin_footer'] : 8;
 
         $this->params['filename'] = isset($params['filename']) ? $params['filename'] : api_get_local_time();
         $this->params['pdf_title'] = isset($params['pdf_title']) ? $params['pdf_title'] : '';
@@ -204,6 +205,7 @@ class PDF
      * @param bool   $print_title    add title
      * @param bool   $complete_style show header and footer if true
      * @param bool   $addStyle
+     * @param string $mainTitle
      *
      * @return false|null
      */
@@ -213,7 +215,8 @@ class PDF
         $courseCode = null,
         $printTitle = false,
         $complete_style = true,
-        $addStyle = true
+        $addStyle = true,
+        $mainTitle = ''
     ) {
         if (empty($htmlFileArray)) {
             return false;
@@ -248,7 +251,6 @@ class PDF
             if ($counter == count($htmlFileArray)) {
                 $pageBreak = '';
             }
-            $counter++;
 
             //if the array provided contained subarrays with 'title' entry,
             // then print the title in the PDF
@@ -260,14 +262,27 @@ class PDF
                 $htmlTitle = basename($file);
             }
 
+            $counter++;
+
             if (empty($file) && !empty($htmlTitle)) {
-                //this is a chapter, print title & skip the rest
+                // this is a chapter, print title & skip the rest
+                if ($counter === 2 && !empty($mainTitle)) {
+                    $this->pdf->WriteHTML(
+                        '<html><body><h2 style="text-align: center">'.$mainTitle.'</h2></body></html>'
+                    );
+                }
                 if ($printTitle) {
                     $this->pdf->WriteHTML(
-                        '<html><body><h3>'.$htmlTitle.'</h3></body></html>'.$pageBreak
+                        '<html><body><h3>'.$html_title.'</h3></body></html>'.$page_break
                     );
                 }
                 continue;
+            } else {
+                if ($counter === 2 && !empty($mainTitle)) {
+                    $this->pdf->WriteHTML(
+                        '<html><body><h2 style="text-align: center">'.$mainTitle.'</h2></body></html>'
+                    );
+                }
             }
 
             if (!file_exists($file)) {

+ 28 - 79
main/inc/lib/pear/HTML/QuickForm.php

@@ -29,7 +29,7 @@
  * @global array $GLOBALS['_HTML_QuickForm_registered_rules']
  */
 
-/**#@+
+/**
  * Error codes for HTML_QuickForm
  *
  * Codes are mapped to textual messages by errorMessage() method, if you add a
@@ -62,7 +62,6 @@ define('QUICKFORM_INVALID_DATASOURCE', -9);
 class HTML_QuickForm extends HTML_Common
 {
     const MAX_ELEMENT_ARGUMENT = 10;
-
     private $dateTimePickerLibraryAdded;
 
     /**
@@ -267,15 +266,6 @@ class HTML_QuickForm extends HTML_Common
                     $this->_maxFileSize = $matches['1'];
             }
         }
-
-//        $course_id = api_get_course_int_id();
-//        //If I'm in a course replace the default max filesize with the course limits
-//        if (!empty($course_id)) {
-//            $free_course_quota = DocumentManager::get_course_quota() - DocumentManager::documents_total_space();
-//            if (empty($this->_maxFileSize) || $free_course_quota <= $this->_maxFileSize) {
-//                $this->_maxFileSize = intval($free_course_quota);
-//            }
-//        }
     }
 
     /**
@@ -300,7 +290,7 @@ class HTML_QuickForm extends HTML_Common
      * @access    public
      * @return    void
      */
-    function registerElementType($typeName, $include, $className)
+    public function registerElementType($typeName, $include, $className)
     {
         $GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES'][strtolower($typeName)] = array($include, $className);
     }
@@ -454,11 +444,12 @@ class HTML_QuickForm extends HTML_Common
     public function &createElement($elementType)
     {
         $args = func_get_args();
-        $element = HTML_QuickForm::_loadElement(
+        $element = $this->_loadElement(
             'createElement',
             $elementType,
             array_slice($args, 1)
         );
+
         return $element;
     }
 
@@ -667,7 +658,7 @@ class HTML_QuickForm extends HTML_Common
      * @param    string     $name           (optional)group name
      * @param    string     $groupLabel     (optional)group label
      * @param    string     $separator      (optional)string to separate elements
-     * @param    string     $appendName     (optional)specify whether the group name should be
+     * @param    bool       $appendName     (optional)specify whether the group name should be
      *                                      used in the form element name ex: group[element]
      * @return   HTML_QuickForm_group       reference to a newly added group
      * @since    2.8
@@ -749,7 +740,7 @@ class HTML_QuickForm extends HTML_Common
      * @return    mixed     element value
      * @throws    HTML_QuickForm_Error
      */
-    function &getElementValue($element)
+    public function &getElementValue($element)
     {
         if (!isset($this->_elementIndex[$element])) {
             throw new \Exception("Element '$element' does not exist in HTML_QuickForm::getElementValue()");
@@ -860,7 +851,7 @@ class HTML_QuickForm extends HTML_Common
     * @param  string  The key from the $_FILES array that should be appended
     * @return array
     */
-    function _reindexFiles($value, $key)
+    public function _reindexFiles($value, $key)
     {
         if (!is_array($value)) {
             return array($key => $value);
@@ -881,15 +872,12 @@ class HTML_QuickForm extends HTML_Common
      * @access    public
      * @return    string    error message corresponding to checked element
      */
-    function getElementError($element)
+    public function getElementError($element)
     {
         if (isset($this->_errors[$element])) {
             return $this->_errors[$element];
         }
-    } // end func getElementError
-
-    // }}}
-    // {{{ setElementError()
+    }
 
     /**
      * Set error message for a form element
@@ -900,17 +888,14 @@ class HTML_QuickForm extends HTML_Common
      * @access    public
      * @return    void
      */
-    function setElementError($element, $message = null)
+    public function setElementError($element, $message = null)
     {
         if (!empty($message)) {
             $this->_errors[$element] = $message;
         } else {
             unset($this->_errors[$element]);
         }
-    } // end func setElementError
-
-     // }}}
-     // {{{ getElementType()
+    }
 
      /**
       * Returns the type of the given element
@@ -920,16 +905,13 @@ class HTML_QuickForm extends HTML_Common
       * @access     public
       * @return     string    Type of the element, false if the element is not found
       */
-     function getElementType($element)
+    public function getElementType($element)
      {
          if (isset($this->_elementIndex[$element])) {
              return $this->_elements[$this->_elementIndex[$element]]->getType();
          }
          return false;
-     } // end func getElementType
-
-     // }}}
-     // {{{ updateElementAttr()
+     }
 
     /**
      * Updates Attributes for one or more elements
@@ -940,7 +922,7 @@ class HTML_QuickForm extends HTML_Common
      * @access     public
      * @return     void
      */
-    function updateElementAttr($elements, $attrs)
+    public function updateElementAttr($elements, $attrs)
     {
         if (is_string($elements)) {
             $elements = split('[ ]?,[ ]?', $elements);
@@ -973,7 +955,7 @@ class HTML_QuickForm extends HTML_Common
      * @return HTML_QuickForm_element    a reference to the removed element
      * @throws HTML_QuickForm_Error
      */
-    function &removeElement($elementName, $removeRules = true)
+    public function &removeElement($elementName, $removeRules = true)
     {
         if (!isset($this->_elementIndex[$elementName])) {
             throw new \Exception("Element '$elementName' does not exist in HTML_QuickForm::removeElement()");
@@ -1089,7 +1071,7 @@ class HTML_QuickForm extends HTML_Common
      * @access   public
      * @throws   HTML_QuickForm_Error
      */
-    function addGroupRule($group, $arg1, $type='', $format=null, $howmany=0, $validation = 'server', $reset = false)
+    public function addGroupRule($group, $arg1, $type='', $format=null, $howmany=0, $validation = 'server', $reset = false)
     {
         if (!$this->elementExists($group)) {
             throw new \Exception("Group '$group' does not exist in HTML_QuickForm::addGroupRule()");
@@ -1166,10 +1148,7 @@ class HTML_QuickForm extends HTML_Common
                 $this->updateAttributes(array('onsubmit' => 'try { var myValidator = validate_' . $this->_attributes['id'] . '; } catch(e) { return true; } return myValidator(this);'));
             }
         }
-    } // end func addGroupRule
-
-    // }}}
-    // {{{ addFormRule()
+    }
 
    /**
     * Adds a global validation rule
@@ -1191,9 +1170,6 @@ class HTML_QuickForm extends HTML_Common
         $this->_formRules[] = $rule;
     }
 
-    // }}}
-    // {{{ applyFilter()
-
     /**
      * Applies a data filter for the given field(s)
      *
@@ -1203,7 +1179,7 @@ class HTML_QuickForm extends HTML_Common
      * @access   public
      * @throws   HTML_QuickForm_Error
      */
-    function applyFilter($element, $filter)
+    public function applyFilter($element, $filter)
     {
         if (!is_callable($filter)) {
             throw new \Exception("Callback function does not exist in QuickForm::applyFilter()");
@@ -1229,10 +1205,7 @@ class HTML_QuickForm extends HTML_Common
                 }
             }
         }
-    } // end func applyFilter
-
-    // }}}
-    // {{{ _recursiveFilter()
+    }
 
     /**
      * Recursively apply a filter function
@@ -1243,7 +1216,7 @@ class HTML_QuickForm extends HTML_Common
      * @access    private
      * @return    cleaned values
      */
-    function _recursiveFilter($filter, $value)
+    public function _recursiveFilter($filter, $value)
     {
         if (is_array($value)) {
             $cleanValues = array();
@@ -1254,10 +1227,7 @@ class HTML_QuickForm extends HTML_Common
         } else {
             return call_user_func($filter, $value);
         }
-    } // end func _recursiveFilter
-
-    // }}}
-    // {{{ arrayMerge()
+    }
 
    /**
     * Merges two arrays
@@ -1288,10 +1258,7 @@ class HTML_QuickForm extends HTML_Common
             }
         }
         return $a;
-    } // end func arrayMerge
-
-    // }}}
-    // {{{ isTypeRegistered()
+    }
 
     /**
      * Returns whether or not the form element type is supported
@@ -1316,10 +1283,7 @@ class HTML_QuickForm extends HTML_Common
     function getRegisteredTypes()
     {
         return array_keys($GLOBALS['HTML_QUICKFORM_ELEMENT_TYPES']);
-    } // end func getRegisteredTypes
-
-    // }}}
-    // {{{ isRuleRegistered()
+    }
 
     /**
      * Returns whether or not the given rule is supported
@@ -1345,10 +1309,7 @@ class HTML_QuickForm extends HTML_Common
     function getRegisteredRules()
     {
         return array_keys($GLOBALS['_HTML_QuickForm_registered_rules']);
-    } // end func getRegisteredRules
-
-    // }}}
-    // {{{ isElementRequired()
+    }
 
     /**
      * Returns whether or not the form element is required
@@ -1361,10 +1322,7 @@ class HTML_QuickForm extends HTML_Common
     function isElementRequired($element)
     {
         return in_array($element, $this->_required, true);
-    } // end func isElementRequired
-
-    // }}}
-    // {{{ isElementFrozen()
+    }
 
     /**
      * Returns whether or not the form element is frozen
@@ -1380,10 +1338,7 @@ class HTML_QuickForm extends HTML_Common
              return $this->_elements[$this->_elementIndex[$element]]->isFrozen();
          }
          return false;
-    } // end func isElementFrozen
-
-    // }}}
-    // {{{ setJsWarnings()
+    }
 
     /**
      * Sets JavaScript warning messages
@@ -1398,10 +1353,7 @@ class HTML_QuickForm extends HTML_Common
     {
         $this->_jsPrefix = $pref;
         $this->_jsPostfix = $post;
-    } // end func setJsWarnings
-
-    // }}}
-    // {{{ setRequiredNote()
+    }
 
     /**
      * Sets required-note
@@ -1414,10 +1366,7 @@ class HTML_QuickForm extends HTML_Common
     function setRequiredNote($note)
     {
         $this->_requiredNote = $note;
-    } // end func setRequiredNote
-
-    // }}}
-    // {{{ getRequiredNote()
+    }
 
     /**
      * Returns the required note

+ 5 - 19
main/inc/lib/pear/HTML/QuickForm/input.php

@@ -33,20 +33,6 @@
  */
 class HTML_QuickForm_input extends HTML_QuickForm_element
 {
-    /**
-     * Class constructor
-     *
-     * @param    string     Input field name attribute
-     * @param    mixed      Label(s) for the input field
-     * @param    mixed      Either a typical HTML attribute string or an associative array
-     * @since     1.0
-     * @access    public
-     * @return    void
-     */
-    public function __construct($elementName=null, $elementLabel=null, $attributes=null)
-    {
-        parent::__construct($elementName, $elementLabel, $attributes);
-    }
 
     /**
      * Sets the element type
@@ -56,7 +42,7 @@ class HTML_QuickForm_input extends HTML_QuickForm_element
      * @access    public
      * @return    void
      */
-    function setType($type)
+    public function setType($type)
     {
         $this->_type = $type;
         $this->updateAttributes(array('type'=>$type));
@@ -70,7 +56,7 @@ class HTML_QuickForm_input extends HTML_QuickForm_element
      * @access    public
      * @return    void
      */
-    function setName($name)
+    public function setName($name)
     {
         $this->updateAttributes(array('name'=>$name));
     }
@@ -82,7 +68,7 @@ class HTML_QuickForm_input extends HTML_QuickForm_element
      * @access    public
      * @return    string
      */
-    function getName()
+    public function getName()
     {
         return $this->getAttribute('name');
     }
@@ -139,7 +125,7 @@ class HTML_QuickForm_input extends HTML_QuickForm_element
      * @return    void
      * @throws
      */
-    function onQuickFormEvent($event, $arg, &$caller)
+    public function onQuickFormEvent($event, $arg, &$caller)
     {
         // do not use submit values for button-type elements
         $type = $this->getType();
@@ -161,7 +147,7 @@ class HTML_QuickForm_input extends HTML_QuickForm_element
    /**
     * We don't need values from button-type elements (except submit) and files
     */
-    function exportValue(&$submitValues, $assoc = false)
+    public function exportValue(&$submitValues, $assoc = false)
     {
         $type = $this->getType();
         if ('reset' == $type || 'image' == $type || 'button' == $type || 'file' == $type) {

+ 16 - 4
main/inc/lib/social.lib.php

@@ -1063,6 +1063,7 @@ class SocialManager extends UserManager
                         </a>
                     </li>';
                 $links .= $personalData;
+
                 $links .= '</ul>';
             }
 
@@ -1087,7 +1088,7 @@ class SocialManager extends UserManager
         if ($show === 'shared_profile') {
             $links = '<ul class="nav nav-pills nav-stacked">';
             // My own profile
-            if ($show_full_profile && $user_id == intval(api_get_user_id())) {
+            if ($show_full_profile && $user_id == api_get_user_id()) {
                 $links .= '
                     <li class="home-icon '.$active.'">
                         <a href="'.api_get_path(WEB_CODE_PATH).'social/home.php">
@@ -1155,6 +1156,18 @@ class SocialManager extends UserManager
                         </li>
                     ';
                 }
+
+                if (!api_get_configuration_value('disable_gdpr')) {
+                    $active = $show == 'personal-data' ? 'active' : null;
+                    $personalData = '
+                    <li class="personal-data-icon '.$active.'">
+                        <a href="'.api_get_path(WEB_CODE_PATH).'social/personal_data.php">
+                            '.$personalDataIcon.' '.get_lang('PersonalDataReport').'
+                        </a>
+                    </li>';
+                    $links .= $personalData;
+                    $links .= '</ul>';
+                }
             }
 
             // My friend profile.
@@ -1220,7 +1233,7 @@ class SocialManager extends UserManager
                 'sn-sidebar-collapse'
             );
 
-            if ($show_full_profile && $user_id == intval(api_get_user_id())) {
+            if ($show_full_profile && $user_id == api_get_user_id()) {
                 $personal_course_list = UserManager::get_personal_session_course_list($user_id);
                 $course_list_code = [];
                 $i = 1;
@@ -1238,14 +1251,13 @@ class SocialManager extends UserManager
                 }
 
                 // Announcements
-                $my_announcement_by_user_id = intval($user_id);
                 $announcements = [];
                 foreach ($course_list_code as $course) {
                     $course_info = api_get_course_info($course['code']);
                     if (!empty($course_info)) {
                         $content = AnnouncementManager::get_all_annoucement_by_user_course(
                             $course_info['code'],
-                            $my_announcement_by_user_id
+                            $user_id
                         );
 
                         if (!empty($content)) {

+ 131 - 0
main/inc/lib/statistics.lib.php

@@ -1149,4 +1149,135 @@ class Statistics
 
         return $chartCode;
     }
+
+    /**
+     * Display the Logins By Date report and allow export its result to XLS.
+     */
+    public static function printLoginsByDate()
+    {
+        if (isset($_GET['export']) && 'xls' === $_GET['export']) {
+            $result = self::getLoginsByDate($_GET['start'], $_GET['end']);
+            $data = [[get_lang('Username'), get_lang('FirstName'), get_lang('LastName'), get_lang('TotalTime')]];
+
+            foreach ($result as $i => $item) {
+                $data[] = [
+                    $item['username'],
+                    $item['firstname'],
+                    $item['lastname'],
+                    api_time_to_hms($item['time_count']),
+                ];
+            }
+
+            Export::arrayToXls($data);
+
+            exit;
+        }
+
+        echo Display::page_header(get_lang('LoginsByDate'));
+
+        $actions = '';
+        $content = '';
+
+        $form = new FormValidator('frm_logins_by_date', 'get');
+        $form->addDateRangePicker(
+            'daterange',
+            get_lang('DateRange'),
+            true,
+            ['format' => 'YYYY-MM-DD', 'timePicker' => 'false', 'validate_format' => 'Y-m-d']
+        );
+        $form->addHidden('report', 'logins_by_date');
+        $form->addButtonFilter(get_lang('Search'));
+
+        if ($form->validate()) {
+            $values = $form->exportValues();
+
+            $result = self::getLoginsByDate($values['daterange_start'], $values['daterange_end']);
+
+            if (!empty($result)) {
+                $actions = Display::url(
+                    Display::return_icon('excel.png', get_lang('ExportToXls'), [], ICON_SIZE_MEDIUM),
+                    api_get_self().'?'.http_build_query(
+                        [
+                            'report' => 'logins_by_date',
+                            'export' => 'xls',
+                            'start' => Security::remove_XSS($values['daterange_start']),
+                            'end' => Security::remove_XSS($values['daterange_end']),
+                        ]
+                    )
+                );
+            }
+
+            $table = new HTML_Table(['class' => 'data_table']);
+            $table->setHeaderContents(0, 0, get_lang('Username'));
+            $table->setHeaderContents(0, 1, get_lang('FirstName'));
+            $table->setHeaderContents(0, 2, get_lang('LastName'));
+            $table->setHeaderContents(0, 3, get_lang('TotalTime'));
+
+            foreach ($result as $i => $item) {
+                $table->setCellContents($i + 1, 0, $item['username']);
+                $table->setCellContents($i + 1, 1, $item['firstname']);
+                $table->setCellContents($i + 1, 2, $item['lastname']);
+                $table->setCellContents($i + 1, 3, api_time_to_hms($item['time_count']));
+            }
+
+            $table->setColAttributes(0, ['class' => 'text-center']);
+            $table->setColAttributes(3, ['class' => 'text-center']);
+            $content = $table->toHtml();
+        }
+
+        $form->display();
+
+        if (!empty($actions)) {
+            echo  Display::toolbarAction('logins_by_date_toolbar', [$actions]);
+        }
+
+        echo $content;
+    }
+
+    /**
+     * @param string $startDate
+     * @param string $endDate
+     *
+     * @return array
+     */
+    private static function getLoginsByDate($startDate, $endDate)
+    {
+        /** @var DateTime $startDate */
+        $startDate = api_get_utc_datetime("$startDate 00:00:00");
+        /** @var DateTime $endDate */
+        $endDate = api_get_utc_datetime("$endDate 23:59:59");
+
+        if (empty($startDate) || empty($endDate)) {
+            return [];
+        }
+
+        $tblUser = Database::get_main_table(TABLE_MAIN_USER);
+        $tblLogin = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
+        $urlJoin = '';
+        $urlWhere = '';
+
+        if (api_is_multiple_url_enabled()) {
+            $tblUrlUser = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
+
+            $urlJoin = "INNER JOIN $tblUrlUser au ON u.id = au.user_id";
+            $urlWhere = 'AND au.access_url_id = '.api_get_current_access_url_id();
+        }
+
+        $sql = "SELECT u.id,
+                    u.firstname,
+                    u.lastname,
+                    u.username,
+                    SUM(TIMESTAMPDIFF(SECOND, l.login_date, l.logout_date)) AS time_count
+                FROM $tblUser u
+                INNER JOIN $tblLogin l ON u.id = l.login_user_id
+                $urlJoin
+                WHERE l.login_date BETWEEN '$startDate' AND '$endDate'
+                $urlWhere
+                GROUP BY u.id";
+
+        $stmt = Database::query($sql);
+        $result = Database::store_result($stmt, 'ASSOC');
+
+        return $result;
+    }
 }

+ 4 - 4
main/inc/lib/tracking.lib.php

@@ -563,7 +563,7 @@ class Tracking
                         }
 
                         if ($extend_this_attempt || $extend_all) {
-                            $list1 = learnpath::get_iv_interactions_array($row['iv_id']);
+                            $list1 = learnpath::get_iv_interactions_array($row['iv_id'], $course_id);
                             foreach ($list1 as $id => $interaction) {
                                 $oddclass = 'row_even';
                                 if (($counter % 2) == 0) {
@@ -591,7 +591,7 @@ class Tracking
                                     </tr>';
                                 $counter++;
                             }
-                            $list2 = learnpath::get_iv_objectives_array($row['iv_id']);
+                            $list2 = learnpath::get_iv_objectives_array($row['iv_id'], $course_id);
                             foreach ($list2 as $id => $interaction) {
                                 if (($counter % 2) == 0) {
                                     $oddclass = 'row_odd';
@@ -951,7 +951,7 @@ class Tracking
                     }
 
                     if ($extend_this_attempt || $extend_all) {
-                        $list1 = learnpath::get_iv_interactions_array($row['iv_id']);
+                        $list1 = learnpath::get_iv_interactions_array($row['iv_id'], $course_id);
                         foreach ($list1 as $id => $interaction) {
                             if (($counter % 2) == 0) {
                                 $oddclass = 'row_odd';
@@ -979,7 +979,7 @@ class Tracking
                                </tr>';
                             $counter++;
                         }
-                        $list2 = learnpath::get_iv_objectives_array($row['iv_id']);
+                        $list2 = learnpath::get_iv_objectives_array($row['iv_id'], $course_id);
 
                         foreach ($list2 as $id => $interaction) {
                             if (($counter % 2) == 0) {

+ 17 - 5
main/link/link.php

@@ -20,7 +20,6 @@
  */
 require_once __DIR__.'/../inc/global.inc.php';
 $current_course_tool = TOOL_LINK;
-
 $this_section = SECTION_COURSES;
 api_protect_course_script(true);
 
@@ -93,7 +92,7 @@ Event::event_access_tool(TOOL_LINK);
 $id = isset($_REQUEST['id']) ? $_REQUEST['id'] : null;
 $scope = isset($_REQUEST['scope']) ? $_REQUEST['scope'] : null;
 $show = isset($_REQUEST['show']) && in_array(trim($_REQUEST['show']), ['all', 'none']) ? $_REQUEST['show'] : '';
-$categoryId = isset($_REQUEST['category_id']) ? intval($_REQUEST['category_id']) : '';
+$categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : '';
 $linkListUrl = api_get_self().'?'.api_get_cidreq().'&category_id='.$categoryId.'&show='.$show;
 $content = '';
 $token = Security::get_existing_token();
@@ -212,11 +211,24 @@ switch ($action) {
         header('Location: '.$linkListUrl);
         exit;
         break;
+    case 'export':
+        $content = Link::listLinksAndCategories($course_id, $session_id, $categoryId, $show, null, false);
+        $pdf = new PDF();
+        $pdf->content_to_pdf(
+            $content,
+            null,
+            $courseInfo['code'].get_lang('Link'),
+            $courseInfo['code'],
+            'D',
+            false,
+            null,
+            false,
+            true
+        );
+        break;
     case 'list':
     default:
-        ob_start();
-        Link::listLinksAndCategories($course_id, $session_id, $categoryId, $show);
-        $content = ob_get_clean();
+        $content = Link::listLinksAndCategories($course_id, $session_id, $categoryId, $show);
         break;
 }
 

+ 49 - 48
main/lp/learnpath.class.php

@@ -3009,13 +3009,14 @@ class learnpath
      *
      * @todo    Transcode labels instead of switching to HTML (which requires to know the encoding of the LP)
      */
-    public static function get_iv_interactions_array($lp_iv_id)
+    public static function get_iv_interactions_array($lp_iv_id, $course_id = 0)
     {
-        $course_id = api_get_course_int_id();
+        $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
         $list = [];
         $table = Database::get_course_table(TABLE_LP_IV_INTERACTION);
+        $lp_iv_id = (int) $lp_iv_id;
 
-        if (empty($lp_iv_id)) {
+        if (empty($lp_iv_id) || empty($course_id)) {
             return [];
         }
 
@@ -3034,18 +3035,29 @@ class learnpath
                 'student_response' => api_htmlentities(get_lang('StudentResponse'), ENT_QUOTES),
                 'result' => api_htmlentities(get_lang('Result'), ENT_QUOTES),
                 'latency' => api_htmlentities(get_lang('LatencyTimeSpent'), ENT_QUOTES),
+                'student_response_formatted' => '',
             ];
             while ($row = Database::fetch_array($res)) {
+                $studentResponseFormatted = urldecode($row['student_response']);
+                $content_student_response = explode('__|', $studentResponseFormatted);
+                if (count($content_student_response) > 0) {
+                    if (count($content_student_response) >= 3) {
+                        // Pop the element off the end of array.
+                        array_pop($content_student_response);
+                    }
+                    $studentResponseFormatted = implode(',', $content_student_response);
+                }
+
                 $list[] = [
-                    'order_id' => ($row['order_id'] + 1),
+                    'order_id' => $row['order_id'] + 1,
                     'id' => urldecode($row['interaction_id']), //urldecode because they often have %2F or stuff like that
                     'type' => $row['interaction_type'],
                     'time' => $row['completion_time'],
-                    //'correct_responses' => $row['correct_responses'],
                     'correct_responses' => '', // Hide correct responses from students.
                     'student_response' => $row['student_response'],
                     'result' => $row['result'],
                     'latency' => $row['latency'],
+                    'student_response_formatted' => $studentResponseFormatted,
                 ];
             }
         }
@@ -3085,16 +3097,21 @@ class learnpath
      * This method can be used as static.
      *
      * @param int $lpItemViewId Learnpath Item View ID
+     * @param int $course_id
      *
      * @return array
      *
      * @todo    Translate labels
      */
-    public static function get_iv_objectives_array($lpItemViewId = 0)
+    public static function get_iv_objectives_array($lpItemViewId = 0, $course_id = 0)
     {
-        $course_id = api_get_course_int_id();
+        $course_id = empty($course_id) ? api_get_course_int_id() : (int) $course_id;
         $lpItemViewId = (int) $lpItemViewId;
 
+        if (empty($course_id) || empty($lpItemViewId)) {
+            return [];
+        }
+
         $table = Database::get_course_table(TABLE_LP_IV_OBJECTIVE);
         $sql = "SELECT * FROM $table
                 WHERE c_id = $course_id AND lp_iv_id = $lpItemViewId
@@ -3113,7 +3130,7 @@ class learnpath
             ];
             while ($row = Database::fetch_array($res)) {
                 $list[] = [
-                    'order_id' => ($row['order_id'] + 1),
+                    'order_id' => $row['order_id'] + 1,
                     'objective_id' => urldecode($row['objective_id']), // urldecode() because they often have %2F
                     'score_raw' => $row['score_raw'],
                     'score_max' => $row['score_max'],
@@ -8101,8 +8118,8 @@ class learnpath
             $item_title = stripslashes($extra_info['title']);
         } elseif (is_numeric($extra_info)) {
             $sql = "SELECT forum_title as title, forum_comment as comment
-                    FROM ".$tbl_forum."
-                    WHERE c_id = ".$course_id." AND forum_id = ".$extra_info;
+                    FROM $tbl_forum
+                    WHERE c_id = $course_id AND forum_id = ".$extra_info;
 
             $result = Database::query($sql);
             $row = Database::fetch_array($result);
@@ -10038,30 +10055,6 @@ class learnpath
         $_course = api_get_course_info();
         $course_code = api_get_course_id();
         $return = '<div class="actions">';
-        switch ($item_type) {
-            case 'dir':
-                // Commented the message cause should not show it.
-                //$lang = get_lang('TitleManipulateChapter');
-                break;
-            case TOOL_LP_FINAL_ITEM:
-            case TOOL_DOCUMENT:
-                // Commented the message cause should not show it.
-                //$lang = get_lang('TitleManipulateDocument');
-                break;
-            case TOOL_LINK:
-            case 'link':
-                // Commented the message cause should not show it.
-                //$lang = get_lang('TitleManipulateLink');
-                break;
-            case TOOL_QUIZ:
-                // Commented the message cause should not show it.
-                //$lang = get_lang('TitleManipulateQuiz');
-                break;
-            case TOOL_STUDENTPUBLICATION:
-                // Commented the message cause should not show it.
-                //$lang = get_lang('TitleManipulateStudentPublication');
-                break;
-        }
 
         $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
         $item_id = (int) $item_id;
@@ -10251,6 +10244,7 @@ class learnpath
     {
         $return = '';
         if (is_numeric($item_id)) {
+            $item_id = (int) $item_id;
             $tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
 
             $sql = "SELECT * FROM $tbl_lp_item
@@ -10603,7 +10597,7 @@ class learnpath
             $form->createElement(
                 'radio',
                 'if_exists',
-                get_lang("UplWhatIfFileExists"),
+                get_lang('UplWhatIfFileExists'),
                 get_lang('UplDoNothing'),
                 'nothing'
             ),
@@ -10791,7 +10785,7 @@ class learnpath
             $session_id,
             true,
             true,
-            "link.session_id"
+            'link.session_id'
         );
 
         $sql = "SELECT 
@@ -10803,7 +10797,7 @@ class learnpath
                 FROM $tbl_link as link
                 LEFT JOIN $linkCategoryTable as link_category
                 ON (link.category_id = link_category.id AND link.c_id = link_category.c_id)
-                WHERE link.c_id = ".$course_id." $condition_session
+                WHERE link.c_id = $course_id $condition_session
                 ORDER BY link_category.category_title ASC, link.title ASC";
         $result = Database::query($sql);
         $categorizedLinks = [];
@@ -12048,7 +12042,10 @@ EOD;
     {
         $lp_id = (int) $lp_id;
         $files_to_export = [];
+
+        $sessionId = api_get_session_id();
         $course_data = api_get_course_info($this->cc);
+
         if (!empty($course_data)) {
             $scorm_path = api_get_path(SYS_COURSE_PATH).$course_data['path'].'/scorm/'.$this->path;
             $list = self::get_flat_ordered_items_list($lp_id);
@@ -12057,7 +12054,7 @@ EOD;
                     $item = $this->items[$item_id];
                     switch ($item->type) {
                         case 'document':
-                            //Getting documents from a LP with chamilo documents
+                            // Getting documents from a LP with chamilo documents
                             $file_data = DocumentManager::get_document_data_by_id($item->path, $this->cc);
                             // Try loading document from the base course.
                             if (empty($file_data) && !empty($sessionId)) {
@@ -12095,12 +12092,16 @@ EOD;
                     }
                 }
             }
+
             $pdf = new PDF();
             $result = $pdf->html_to_pdf(
                 $files_to_export,
                 $this->name,
                 $this->cc,
-                true
+                true,
+                true,
+                true,
+                $this->get_name()
             );
 
             return $result;
@@ -12214,8 +12215,8 @@ EOD;
     public function set_autolaunch($lp_id, $status)
     {
         $course_id = api_get_course_int_id();
-        $lp_id = intval($lp_id);
-        $status = intval($status);
+        $lp_id = (int) $lp_id;
+        $status = (int) $status;
         $lp_table = Database::get_course_table(TABLE_LP_MAIN);
 
         // Setting everything to autolaunch = 0
@@ -12527,11 +12528,6 @@ EOD;
     public static function getCategories($courseId)
     {
         $em = Database::getManager();
-        //Default behaviour
-        /*$items = $em->getRepository('ChamiloCourseBundle:CLpCategory')->findBy(
-            array('cId' => $course_id),
-            array('name' => 'ASC')
-        );*/
 
         // Using doctrine extensions
         /** @var SortableRepository $repo */
@@ -13121,7 +13117,7 @@ EOD;
                 'lp_id' => $this->lp_id,
                 'forum_title' => $this->name,
                 'forum_comment' => null,
-                'forum_category' => intval($forumCategoryId),
+                'forum_category' => (int) $forumCategoryId,
                 'students_can_edit_group' => ['students_can_edit' => 0],
                 'allow_new_threads_group' => ['allow_new_threads' => 0],
                 'default_view_type_group' => ['default_view_type' => 'flat'],
@@ -13303,7 +13299,7 @@ EOD;
         if ($this->debug > 0) {
             error_log('In learnpath::setAccumulateScormTime()', 0);
         }
-        $this->accumulateScormTime = intval($value);
+        $this->accumulateScormTime = (int) $value;
         $lp_table = Database::get_course_table(TABLE_LP_MAIN);
         $lp_id = $this->get_id();
         $sql = "UPDATE $lp_table 
@@ -14009,6 +14005,11 @@ EOD;
         return $icon;
     }
 
+    /**
+     * @param int $lpId
+     *
+     * @return string
+     */
     public static function getSelectedIconHtml($lpId)
     {
         $icon = self::getSelectedIcon($lpId);

+ 4 - 0
main/lp/lp_add_audio.php

@@ -87,6 +87,10 @@ $suredel = trim(get_lang('AreYouSureToDeleteJS'));
 
 $lpPathInfo = $lp->generate_lp_folder(api_get_course_info());
 
+DocumentManager::createDefaultAudioFolder($courseInfo);
+
+$audioFolderId = DocumentManager::get_document_id($courseInfo, '/audio');
+
 $file = null;
 if (isset($lp_item->audio) && !empty($lp_item->audio)) {
     $file = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/audio/'.$lp_item->audio;

+ 1 - 1
main/lp/lp_add_category.php

@@ -37,7 +37,7 @@ if (api_get_configuration_value('save_titles_as_html')) {
         get_lang('Name'),
         true,
         false,
-        ['ToolbarSet' => 'Minimal']
+        ['ToolbarSet' => 'Minimal', 'Height' => '100']
     );
 } else {
     $form->addText('name', get_lang('Name'), true);

+ 7 - 3
main/lp/lp_controller.php

@@ -888,14 +888,18 @@ switch ($action) {
         }
         break;
     case 'export_to_pdf':
-        if (!learnpath::is_lp_visible_for_student($_SESSION['oLP']->lp_id, api_get_user_id())) {
-            api_not_allowed();
-        }
         $hideScormPdfLink = api_get_setting('hide_scorm_pdf_link');
         if ($hideScormPdfLink === 'true') {
             api_not_allowed(true);
         }
 
+        // Teachers can export to PDF
+        if (!$is_allowed_to_edit) {
+            if (!learnpath::is_lp_visible_for_student($_SESSION['oLP']->lp_id, api_get_user_id())) {
+                api_not_allowed();
+            }
+        }
+
         if (!$lp_found) {
             require 'lp_list.php';
         } else {

+ 1 - 1
main/lp/lp_stats.php

@@ -25,7 +25,7 @@ $extendId = isset($_GET['extend_id']) ? $_GET['extend_id'] : null;
 $extendAttemptId = isset($_GET['extend_attempt_id']) ? $_GET['extend_attempt_id'] : null;
 $extendedAttempt = isset($_GET['extend_attempt']) ? $_GET['extend_attempt'] : null;
 $extendedAll = isset($_GET['extend_all']) ? $_GET['extend_all'] : null;
-$export = isset($_GET['export']) && $_GET['export'] === 'csv' ? true : false;
+$export = isset($_GET['export']) && $_GET['export'] === 'csv';
 $allowExtend = isset($_GET['allow_extend']) ? $_GET['allow_extend'] : 1;
 
 $lpReportType = api_get_setting('lp_show_reduced_report');

+ 6 - 6
main/messages/new_message.php

@@ -60,7 +60,7 @@ function add_image_form() {
 </script>';
 $nameTools = get_lang('ComposeMessage');
 
-$tpl = new Template($nameTools);
+$tpl = new Template(get_lang('ComposeMessage'));
 
 /**
  * Shows the compose area + a list of users to select from.
@@ -179,7 +179,7 @@ function manageForm($default, $select_from_user_list = null, $sent_to = '', $tpl
         get_lang('Message'),
         false,
         false,
-        ['ToolbarSet' => 'Messages', 'Width' => '100%', 'Height' => '250']
+        ['ToolbarSet' => 'Messages', 'Width' => '100%', 'Height' => '250', 'style' => true]
     );
 
     if (isset($_GET['re_id'])) {
@@ -191,10 +191,10 @@ function manageForm($default, $select_from_user_list = null, $sent_to = '', $tpl
         // Adding reply mail
         $user_reply_info = api_get_user_info($message_reply_info['user_sender_id']);
         $default['content'] = '<p><br/></p>'.sprintf(
-                get_lang('XWroteY'),
-                $user_reply_info['complete_name'],
-                Security::filter_terms($message_reply_info['content'])
-            );
+            get_lang('XWroteY'),
+            $user_reply_info['complete_name'],
+            Security::filter_terms($message_reply_info['content'])
+        );
     }
 
     if (isset($_GET['forward_id'])) {

+ 219 - 24
main/mySpace/lp_tracking.php

@@ -1,5 +1,9 @@
 <?php
 /* For licensing terms, see /license.txt */
+
+use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
+use Chamilo\CourseBundle\Entity\CLpItemView;
+
 /**
  * Learning paths reporting.
  *
@@ -7,7 +11,6 @@
  */
 require_once __DIR__.'/../inc/global.inc.php';
 
-// resetting the course id
 $cidReset = true;
 $from_myspace = false;
 $from_link = '';
@@ -18,18 +21,16 @@ if (isset($_GET['from']) && $_GET['from'] == 'myspace') {
     $this_section = SECTION_COURSES;
 }
 
-$session_id = isset($_REQUEST['id_session']) && !empty($_REQUEST['id_session'])
-    ? intval($_REQUEST['id_session'])
-    : api_get_session_id();
-$export_csv = isset($_GET['export']) && $_GET['export'] == 'csv' ? true : false;
-$user_id = isset($_GET['student_id']) ? intval($_GET['student_id']) : api_get_user_id();
+$session_id = isset($_REQUEST['id_session']) ? (int) $_REQUEST['id_session'] : api_get_session_id();
+$export_csv = isset($_GET['export']) && $_GET['export'] == 'csv';
+$user_id = isset($_GET['student_id']) ? (int) $_GET['student_id'] : api_get_user_id();
 $courseCode = isset($_GET['course']) ? Security::remove_XSS($_GET['course']) : api_get_course_id();
 $origin = api_get_origin();
-$lp_id = intval($_GET['lp_id']);
+$lp_id = (int) $_GET['lp_id'];
 $csv_content = [];
-$course_info = api_get_course_info($courseCode);
+$courseInfo = api_get_course_info($courseCode);
 
-if (empty($course_info) || empty($lp_id)) {
+if (empty($courseInfo) || empty($lp_id)) {
     api_not_allowed(api_get_origin() !== 'learnpath');
 }
 $userInfo = api_get_user_info($user_id);
@@ -43,44 +44,238 @@ if (!api_is_platform_admin(true) &&
     !api_is_drh() &&
     !api_is_course_tutor()
 ) {
-    api_not_allowed(
-        api_get_origin() !== 'learnpath'
-    );
+    api_not_allowed(api_get_origin() !== 'learnpath');
 }
 
-if ($origin == 'user_course') {
+if ($origin === 'user_course') {
     $interbreadcrumb[] = [
-        'url' => api_get_path(WEB_COURSE_PATH).$course_info['directory'],
-        'name' => $course_info['name'],
+        'url' => api_get_path(WEB_COURSE_PATH).$courseInfo['directory'],
+        'name' => $courseInfo['name'],
     ];
     $interbreadcrumb[] = [
         'url' => "../user/user.php?cidReq=$courseCode",
-        'name' => get_lang("Users"),
+        'name' => get_lang('Users'),
     ];
-} elseif ($origin == 'tracking_course') {
+} elseif ($origin === 'tracking_course') {
     $interbreadcrumb[] = [
         'url' => "../tracking/courseLog.php?cidReq=$courseCode&id_session=$session_id",
-        'name' => get_lang("Tracking"),
+        'name' => get_lang('Tracking'),
     ];
 } else {
     $interbreadcrumb[] = ['url' => 'index.php', 'name' => get_lang('MySpace')];
-    $interbreadcrumb[] = ['url' => 'student.php', 'name' => get_lang("MyStudents")];
-    $interbreadcrumb[] = ['url' => "myStudents.php?student=$user_id", 'name' => get_lang("StudentDetails")];
-    $nameTools = get_lang("DetailsStudentInCourse");
+    $interbreadcrumb[] = ['url' => 'student.php', 'name' => get_lang('MyStudents')];
+    $interbreadcrumb[] = ['url' => "myStudents.php?student=$user_id", 'name' => get_lang('StudentDetails')];
+    $nameTools = get_lang('DetailsStudentInCourse');
 }
 
 $interbreadcrumb[] = [
     'url' => "myStudents.php?student=$user_id&course=$courseCode&details=true&origin=$origin",
-    'name' => get_lang("DetailsStudentInCourse"),
+    'name' => get_lang('DetailsStudentInCourse'),
 ];
 $nameTools = get_lang('LearningPathDetails');
 $sql = 'SELECT name	FROM '.Database::get_course_table(TABLE_LP_MAIN).' 
-        WHERE c_id = '.$course_info['real_id'].' AND id='.$lp_id;
+        WHERE c_id = '.$courseInfo['real_id'].' AND id='.$lp_id;
 $rs = Database::query($sql);
 $lp_title = Database::result($rs, 0, 0);
 
 $origin = 'tracking';
 
+$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
+switch ($action) {
+    case 'export_stats':
+        if (!api_is_allowed_to_edit()) {
+            api_not_allowed();
+        }
+        $tpl = new Template(null, false, false);
+        $itemId = isset($_REQUEST['extend_id']) ? $_REQUEST['extend_id'] : 0;
+        $itemViewId = isset($_REQUEST['extend_attempt_id']) ? $_REQUEST['extend_attempt_id'] : 0;
+        $em = Database::getManager();
+
+        $repo = $em->getRepository('ChamiloCourseBundle:CLpItemView');
+        /** @var CLpItemView $itemView */
+        $itemView = $repo->find($itemViewId);
+
+        if (!$itemView) {
+            api_not_allowed();
+        }
+
+        $view = $em->getRepository('ChamiloCourseBundle:CLpView')->find($itemView->getLpViewId());
+        $lp = $em->getRepository('ChamiloCourseBundle:CLp')->find($view->getLpId());
+
+        $duration = learnpathItem::getScormTimeFromParameter('js', $itemView->getTotalTime());
+        $endTime = $itemView->getStartTime() + $itemView->getTotalTime();
+
+        $list1 = learnpath::get_iv_interactions_array($itemViewId, $courseInfo['real_id']);
+        $counter = 0;
+        $table = new HTML_Table();
+
+        $total = 0;
+        $numberChoices = 0;
+        $questionCounter = 0;
+
+        $studentName = '';
+        $questions = [];
+        $categories = [];
+        foreach ($list1 as $id => $interaction) {
+            $counter++;
+            if ($counter === 1) {
+                continue;
+            } elseif ($counter === 2) {
+                $studentName = $interaction['student_response_formatted'];
+            } else {
+                $data = $interaction['student_response_formatted'];
+
+                switch ($interaction['type']) {
+                    case 'fill-in':
+                        $questionCounter++;
+                        $questions[$questionCounter]['question'] = $data;
+                        break;
+                    case 'choice':
+                        $questions[$questionCounter]['options'][] = $interaction;
+                        $numberChoices++;
+                        break;
+                    case 'matching':
+                        $list = explode(',', $data);
+                        if (!empty($list)) {
+                            foreach ($list as &$item) {
+                                $item = cut($item, 30);
+                            }
+                            $interaction['student_response_formatted'] = implode('<br />', $list);
+                        }
+                        $questions[$questionCounter]['options'][] = $interaction;
+                        $numberChoices++;
+                        break;
+                }
+            }
+        }
+
+        $counter = 1;
+        $table = new HTML_Table(['class' => 'table data_table']);
+        $row = 0;
+        $scoreDisplay = new ScoreDisplay();
+        $globalTotal = 0;
+        $globalTotalCount = 0;
+        foreach ($questions as $data) {
+            // Question title
+            $table->setCellContents($row, 0, $data['question']);
+            $table->setCellAttributes($row, 0, ['colspan' => '3', 'style' => 'text-align:center; font-weight:bold']);
+            $choiceCounter = 1;
+            $row++;
+            $total = 0;
+            // Question options
+            foreach ($data['options'] as $option) {
+                if ($option['result'] === 'correct') {
+                    $total++;
+                    $globalTotal++;
+                }
+                $table->setCellContents($row, 0, 'Q'.$choiceCounter);
+                $table->setCellContents($row, 1, $option['student_response_formatted']);
+                $result = Display::return_icon('icon_check.png', null, [], ICON_SIZE_SMALL);
+                if ($option['result'] === 'wrong') {
+                    $result = Display::return_icon('icon_error.png', null, [], ICON_SIZE_SMALL);
+                }
+
+                $table->setCellContents($row, 2, $result);
+                $choiceCounter++;
+                $row++;
+            }
+
+            // Question total
+            $table->setCellContents($row, 0, get_lang('Total'));
+            $table->setCellContents($row, 1, $data['question']);
+
+            $totalOptions = count($data['options']);
+            $arrayScore = [0 => $total, 1 => $totalOptions];
+            $scoreToString = $scoreDisplay->display_score($arrayScore);
+            $table->setCellContents($row, 2, $scoreToString);
+
+            $table->setCellAttributes($row, 0, ['style' => 'font-weight:bold']);
+            $table->setCellAttributes($row, 1, ['style' => 'font-weight:bold']);
+            $table->setCellAttributes($row, 2, ['style' => 'font-weight:bold']);
+
+            $categories[] = [
+                'name' => $data['question'],
+                'score' => $scoreDisplay->display_score($arrayScore, SCORE_DIV),
+                'score_numeric' => $scoreDisplay->display_score($arrayScore, SCORE_NUMERIC),
+                'score_percentage' => $scoreDisplay->display_score($arrayScore, SCORE_PERCENT),
+            ];
+            $tpl->assign('categories', $categories);
+
+            $globalTotalCount += $totalOptions;
+            $row++;
+        }
+
+        $globalScoreTotal = [0 => $globalTotal, 1 => $globalTotalCount];
+        $score = $scoreDisplay->display_score($globalScoreTotal);
+        $generalScore[] = [
+            'score' => $scoreDisplay->display_score($globalScoreTotal, SCORE_DIV),
+            'score_numeric' => $scoreDisplay->display_score($globalScoreTotal, SCORE_NUMERIC),
+            'score_percentage' => $scoreDisplay->display_score($globalScoreTotal, SCORE_PERCENT),
+        ];
+        $tpl->assign('general_score', $generalScore);
+        $tpl->assign('global_total', $score);
+
+        $tableToString = $table->toHtml();
+
+        $duration = learnpathItem::getScormTimeFromParameter('js', $itemView->getTotalTime());
+
+        $dataLpInfo = [
+            'name' => $lp->getName(),
+            'attempt' => $itemView->getViewCount(),
+            'score' => $score,
+            'duration' => $duration,
+            'start_time' => api_get_local_time($itemView->getStartTime()),
+            'start_date' => api_get_local_time($itemView->getStartTime(), null, null, null, false),
+            'end_time' => api_get_local_time($endTime),
+            'candidate' => $studentName,
+        ];
+
+        $tpl->assign('data', $dataLpInfo);
+        $contentText = $tpl->fetch($tpl->get_template('my_space/pdf_tracking_lp.tpl'));
+
+        $content = $contentText.'<pagebreak>'.$tableToString;
+
+        $pdf = new PDF('A4', 'P', ['margin_footer' => 4, 'top' => 40, 'bottom' => 25]);
+
+        $table = new HTML_Table(['class' => 'table', 'style' => 'display: block; margin-bottom: 50px;']);
+        $logo = ChamiloApi::getPlatformLogo(
+            $theme,
+            [
+                'title' => '',
+                'style' => 'max-width:180px, margin-bottom: 100px;',
+                'id' => 'header-logo',
+            ]
+        );
+        $table->setCellContents(0, 0, $logo);
+
+        $secondLogo = api_get_path(SYS_PATH).'custompages/url-images/'.api_get_current_access_url_id().'_url_image_2.png';
+        $logo2 = Display::img($secondLogo, null, ['style' => 'height:70px;']);
+        $table->setCellContents(0, 1, $logo2);
+        $table->setCellAttributes(0, 1, ['style' => 'display:block;float:right;text-align:right']);
+
+        $pdf->set_custom_header($table->toHtml());
+
+        $background = api_get_path(SYS_PATH).'custompages/url-images/'.api_get_current_access_url_id().'_pdf_background.png';
+
+        $content =
+            '<html>
+                <body style="background-image-resize: 5; background-position: top left; background-image: url('.$background.');">'
+            .$content.'</body></html>';
+
+        $pdf->content_to_pdf(
+            $content,
+            null,
+            $courseInfo['code'].'_'.$lp->getName().'_'.api_get_local_time(),
+            $courseInfo['code'],
+            'D',
+            false,
+            null,
+            false,
+            true
+        );
+        break;
+}
+
 $output = require_once api_get_path(SYS_CODE_PATH).'lp/lp_stats.php';
 
 $actions = [];
@@ -106,7 +301,7 @@ echo Display::toolbarAction(
 $table_title = $session_id
     ? Display::return_icon('session.png', get_lang('Session')).PHP_EOL.api_get_session_name($session_id).PHP_EOL
     : PHP_EOL;
-$table_title .= Display::return_icon('course.png', get_lang('Course')).PHP_EOL.$course_info['name'].PHP_EOL
+$table_title .= Display::return_icon('course.png', get_lang('Course')).PHP_EOL.$courseInfo['name'].PHP_EOL
     .Display::return_icon('user.png', get_lang('User')).' '.$name;
 
 echo Display::page_header($table_title);

+ 204 - 39
main/mySpace/session.php

@@ -14,13 +14,13 @@ api_block_anonymous_users();
 
 $this_section = SECTION_TRACKING;
 $export_csv = false;
-if (isset($_GET['export']) && $_GET['export'] == 'csv') {
+if (isset($_REQUEST['export']) && $_REQUEST['export'] == 'csv') {
     $export_csv = true;
 }
 
 $id_coach = api_get_user_id();
-if (isset($_GET['id_coach']) && $_GET['id_coach'] != '') {
-    $id_coach = (int) $_GET['id_coach'];
+if (isset($_REQUEST['id_coach']) && $_REQUEST['id_coach'] != '') {
+    $id_coach = (int) $_REQUEST['id_coach'];
 }
 
 $allowToTrack = api_is_platform_admin(true, true) || api_is_teacher();
@@ -121,59 +121,224 @@ if (api_is_platform_admin(true, true)) {
 
 $form = new FormValidator(
     'search_course',
-    'get',
+    'post',
     api_get_path(WEB_CODE_PATH).'mySpace/session.php'
 );
 $form->addElement('text', 'keyword', get_lang('Keyword'));
+
+$extraFieldSession = new ExtraField('session');
+$extraFieldSession->addElements(
+    $form,
+    null,
+    [], //exclude
+    true
+);
+
 $form->addButtonSearch(get_lang('Search'));
 $keyword = '';
+$result = SessionManager::getGridColumns('my_space');
+
+$columns = $result['columns'];
+$columnModel = $result['column_model'];
+
+$filterToString = '';
 if ($form->validate()) {
-    $keyword = $form->getSubmitValue('keyword');
+    $values = $form->getSubmitValues();
+    $keyword = Security::remove_XSS($form->getSubmitValue('keyword'));
+    $extraField = new ExtraField('session');
+    $extraFields = $extraField->get_all(null, 'option_order');
+    $extraFields = array_column($extraFields, 'variable');
+    $filter = new stdClass();
+    $filter->groupOp = 'AND';
+
+    foreach ($columnModel as $col) {
+        if (isset($values[$col['index']]) && !empty($values[$col['index']]) &&
+            in_array(str_replace('extra_', '', $col['index']), $extraFields)
+        ) {
+            $rule = new stdClass();
+            $rule->field = $col['index'];
+            $rule->op = 'in';
+            $rule->data = Security::remove_XSS($values[$col['index']]);
+            $filter->rules[] = $rule;
+            $filter->groupOp = 'AND';
+        }
+    }
+    $filterToString = json_encode($filter);
 }
 $form->setDefaults(['keyword' => $keyword]);
 
-$url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_sessions_tracking&keyword='.Security::remove_XSS($keyword);
-
-$columns = [
-    get_lang('Title'),
-    get_lang('Date'),
-    get_lang('NbCoursesPerSession'),
-    get_lang('NbStudentPerSession'),
-    get_lang('Details'),
-];
+$url = api_get_path(WEB_AJAX_PATH).
+    'model.ajax.php?a=get_sessions_tracking&_search=true&_force_search=true&filters='.$filterToString.'&keyword='.$keyword;
 
 // Column config
-$columnModel = [
-    ['name' => 'name', 'index' => 'name', 'width' => '255', 'align' => 'left'],
-    ['name' => 'date', 'index' => 'access_start_date', 'width' => '150', 'align' => 'left'],
-    ['name' => 'course_per_session', 'index' => 'course_per_session', 'width' => '150', 'sortable' => 'false'],
-    ['name' => 'student_per_session', 'index' => 'student_per_session', 'width' => '100', 'sortable' => 'false'],
-    ['name' => 'details', 'index' => 'details', 'width' => '100', 'sortable' => 'false'],
-];
-
 $extraParams = [
     'autowidth' => 'true',
     'height' => 'auto',
 ];
 
-$js = '<script>
-    $(function() {
-        '.Display::grid_js(
-        'session_tracking',
-        $url,
-        $columns,
-        $columnModel,
-        $extraParams,
-        [],
-        null,
-        true
-    ).'
-    });
-</script>';
-
-echo $js;
+/*$extraParams['postData'] = [
+    'filters' => [
+        'groupOp' => 'AND',
+        'rules' => $result['rules'],
+    ],
+];*/
+
+$urlAjaxExtraField = api_get_path(WEB_AJAX_PATH).'extra_field.ajax.php?1=1';
+$allowOrder = api_get_configuration_value('session_list_order');
+$orderUrl = api_get_path(WEB_AJAX_PATH).'session.ajax.php?a=order';
+
+?>
+    <script>
+        function setSearchSelect(columnName) {
+            $("#sessions").jqGrid('setColProp', columnName, {
+            });
+        }
+        var added_cols = [];
+        var original_cols = [];
+        function clean_cols(grid, added_cols) {
+            // Cleaning
+            for (key in added_cols) {
+                grid.hideCol(key);
+            };
+            grid.showCol('name');
+            grid.showCol('display_start_date');
+            grid.showCol('display_end_date');
+            grid.showCol('course_title');
+        }
+
+        function show_cols(grid, added_cols) {
+            grid.showCol('name').trigger('reloadGrid');
+            for (key in added_cols) {
+                grid.showCol(key);
+            };
+        }
+
+        var second_filters = [];
+
+        $(function() {
+            date_pick_today = function(elem) {
+                $(elem).datetimepicker({dateFormat: "yy-mm-dd"});
+                $(elem).datetimepicker('setDate', (new Date()));
+            }
+            date_pick_one_month = function(elem) {
+                $(elem).datetimepicker({dateFormat: "yy-mm-dd"});
+                next_month = Date.today().next().month();
+                $(elem).datetimepicker('setDate', next_month);
+            }
+
+            // Great hack
+            register_second_select = function(elem) {
+                second_filters[$(elem).val()] = $(elem);
+            }
+
+            fill_second_select = function(elem) {
+                $(elem).on("change", function() {
+                    composed_id = $(this).val();
+                    field_id = composed_id.split("#")[0];
+                    id = composed_id.split("#")[1];
+                    $.ajax({
+                        url: "<?php echo $urlAjaxExtraField; ?>&a=get_second_select_options",
+                        dataType: "json",
+                        data: "type=session&field_id="+field_id+"&option_value_id="+id,
+                        success: function(data) {
+                            my_select = second_filters[field_id];
+                            my_select.empty();
+                            $.each(data, function(index, value) {
+                                my_select.append($("<option/>", {
+                                    value: index,
+                                    text: value
+                                }));
+                            });
+                        }
+                    });
+                });
+            }
+
+            <?php
+            echo Display::grid_js(
+                'sessions',
+                $url,
+                $columns,
+                $columnModel,
+                $extraParams,
+                [],
+                null,
+                true
+            );
+            ?>
+
+            setSearchSelect("status");
+
+            var grid = $("#sessions"),
+                prmSearch = {
+                    multipleSearch : true,
+                    overlay : false,
+                    width: 'auto',
+                    caption: '<?php echo addslashes(get_lang('Search')); ?>',
+                    formclass:'data_table',
+                    onSearch : function() {
+                        var postdata = grid.jqGrid('getGridParam', 'postData');
+                        if (postdata && postdata.filters) {
+                            filters = jQuery.parseJSON(postdata.filters);
+                            clean_cols(grid, added_cols);
+                            added_cols = [];
+                            $.each(filters, function(key, value) {
+                                if (key == 'rules') {
+                                    $.each(value, function(subkey, subvalue) {
+                                        added_cols[subvalue.field] = subvalue.field;
+                                    });
+                                }
+                            });
+                            show_cols(grid, added_cols);
+                        }
+                    },
+                    onReset: function() {
+                        clean_cols(grid, added_cols);
+                    }
+                };
+
+            original_cols = grid.jqGrid('getGridParam', 'colModel');
+
+            grid.jqGrid('navGrid','#sessions_pager',
+                {edit:false,add:false,del:false},
+                {height:280,reloadAfterSubmit:false}, // edit options
+                {height:280,reloadAfterSubmit:false}, // add options
+                {reloadAfterSubmit:false},// del options
+                prmSearch
+            );
+
+            <?php
+            // Create the searching dialog.
+            //echo 'grid.searchGrid(prmSearch);';
+            ?>
+            // Fixes search table.
+            var searchDialogAll = $("#fbox_"+grid[0].id);
+            searchDialogAll.addClass("table");
+            var searchDialog = $("#searchmodfbox_"+grid[0].id);
+            searchDialog.addClass("ui-jqgrid ui-widget ui-widget-content ui-corner-all");
+            searchDialog.css({position:"adsolute", "z-index":"100", "float":"left", "top":"55%", "left" : "25%", "padding" : "5px", "border": "1px solid #CCC"})
+            var gbox = $("#gbox_"+grid[0].id);
+            gbox.before(searchDialog);
+            gbox.css({clear:"left"});
+
+            // Select first elements by default
+            $('.input-elm').each(function() {
+                $(this).find('option:first').attr('selected', 'selected');
+            });
+
+            $('.delete-rule').each(function(){
+                $(this).click(function(){
+                    $('.input-elm').each(function(){
+                        $(this).find('option:first').attr('selected', 'selected');
+                    });
+                });
+            });
+        });
+    </script>
+<?php
+
 $form->display();
 
-echo Display::grid_html('session_tracking');
+echo Display::grid_html('sessions');
 
 Display::display_footer();

+ 5 - 3
main/mySpace/works_in_session_report.php

@@ -38,10 +38,12 @@ foreach ($sessionList as $sessionInfo) {
     $selectSession->addOption($sessionInfo['name'], $sessionInfo['id']);
 }
 
-if (isset($_GET['session']) && intval($_GET['session'])) {
-    $form->setDefaults(['session' => intval($_GET['session'])]);
+$sessionId = isset($_GET['session']) ? (int) $_GET['session'] : 0;
+$session = null;
+if (!empty($sessionId)) {
+    $form->setDefaults(['session' => $sessionId]);
     /** @var Session $session */
-    $session = $em->find('ChamiloCoreBundle:Session', intval($_GET['session']));
+    $session = $em->find('ChamiloCoreBundle:Session', $sessionId);
 }
 
 $coursesInfo = [];

+ 29 - 25
main/session/add_courses_to_session.php

@@ -6,8 +6,6 @@
  *
  * @todo use formvalidator
  */
-
-// resetting the course id.
 $cidReset = true;
 
 require_once __DIR__.'/../inc/global.inc.php';
@@ -29,7 +27,7 @@ $interbreadcrumb[] = [
     'name' => get_lang('SessionList'),
 ];
 $interbreadcrumb[] = [
-    'url' => "resume_session.php?id_session=".$sessionId,
+    'url' => "resume_session.php?id_session=$sessionId",
     'name' => get_lang('SessionOverview'),
 ];
 
@@ -85,29 +83,32 @@ $courses = $sessions = [];
 if (isset($_POST['formSent']) && $_POST['formSent']) {
     $courseList = $_POST['SessionCoursesList'];
     $copyEvaluation = isset($_POST['copy_evaluation']);
+    $copyCourseTeachersAsCoach = isset($_POST['import_teachers_as_course_coach']);
 
     SessionManager::add_courses_to_session(
         $sessionId,
         $courseList,
         true,
-        $copyEvaluation
+        $copyEvaluation,
+        $copyCourseTeachersAsCoach
     );
 
     Display::addFlash(Display::return_message(get_lang('Updated')));
 
+    $url = api_get_path(WEB_CODE_PATH).'session/';
     if (isset($add)) {
-        header('Location: add_users_to_session.php?id_session='.$sessionId.'&add=true');
+        header('Location: '.$url.'add_users_to_session.php?id_session='.$sessionId.'&add=true');
     } else {
-        header('Location: resume_session.php?id_session='.$sessionId);
+        header('Location: '.$url.'resume_session.php?id_session='.$sessionId);
     }
 
     exit;
 }
 
-// display the header
+// Display the header
 Display::display_header($tool_name);
 
-if ($add_type == 'multiple') {
+if ($add_type === 'multiple') {
     $link_add_type_unique = '<a href="'.api_get_self().'?id_session='.$sessionId.'&add='.$add.'&add_type=unique">'.
         Display::return_icon('single.gif').get_lang('SessionAddTypeUnique').'</a>';
     $link_add_type_multiple = Display::return_icon('multiple.gif').get_lang('SessionAddTypeMultiple').' ';
@@ -123,7 +124,7 @@ echo '<div class="actions">';
 echo $link_add_type_unique.$link_add_type_multiple;
 echo '</div>';
 
-$ajax_search = $add_type == 'unique' ? true : false;
+$ajax_search = $add_type === 'unique' ? true : false;
 $nosessionCourses = $sessionCourses = [];
 if ($ajax_search) {
     $sql = "SELECT course.id, code, title, visual_code, session_id
@@ -131,8 +132,8 @@ if ($ajax_search) {
 			INNER JOIN $tbl_session_rel_course session_rel_course
             ON
                 course.id = session_rel_course.c_id AND
-                session_rel_course.session_id = ".$sessionId."
-			ORDER BY ".(sizeof($courses) ? "(code IN(".implode(',', $courses).")) DESC," : "")." title";
+                session_rel_course.session_id = $sessionId
+			ORDER BY ".(count($courses) ? "(code IN (".implode(',', $courses).")) DESC," : '')." title";
 
     if (api_is_multiple_url_enabled()) {
         $tbl_course_rel_access_url = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
@@ -141,12 +142,11 @@ if ($ajax_search) {
             $sql = "SELECT course.id, code, title, visual_code, session_id
                     FROM $tbl_course course
                     INNER JOIN $tbl_session_rel_course session_rel_course
-                        ON course.id = session_rel_course.c_id
-                        AND session_rel_course.session_id = ".$sessionId."
-                        INNER JOIN $tbl_course_rel_access_url url_course
-                        ON (url_course.c_id = course.id)
+                    ON course.id = session_rel_course.c_id AND session_rel_course.session_id = $sessionId
+                    INNER JOIN $tbl_course_rel_access_url url_course
+                    ON (url_course.c_id = course.id)
                     WHERE access_url_id = $access_url_id
-                    ORDER BY ".(sizeof($courses) ? "(code IN(".implode(',', $courses).")) DESC," : "")." title";
+                    ORDER BY ".(count($courses) ? " (code IN(".implode(',', $courses).")) DESC," : '')." title";
         }
     }
 
@@ -161,8 +161,8 @@ if ($ajax_search) {
 			LEFT JOIN $tbl_session_rel_course session_rel_course
             ON
                 course.id = session_rel_course.c_id AND
-                session_rel_course.session_id = ".$sessionId."
-			ORDER BY ".(sizeof($courses) ? "(code IN(".implode(',', $courses).")) DESC," : "")." title";
+                session_rel_course.session_id = $sessionId
+			ORDER BY ".(count($courses) ? "(code IN(".implode(',', $courses).")) DESC," : '')." title";
 
     if (api_is_multiple_url_enabled()) {
         $tbl_course_rel_access_url = Database::get_main_table(TABLE_MAIN_ACCESS_URL_REL_COURSE);
@@ -173,11 +173,11 @@ if ($ajax_search) {
                     LEFT JOIN $tbl_session_rel_course session_rel_course
                     ON
                         course.id = session_rel_course.c_id AND
-                        session_rel_course.session_id = ".$sessionId."
+                        session_rel_course.session_id = $sessionId
                     INNER JOIN $tbl_course_rel_access_url url_course
                     ON (url_course.c_id = course.id)
                     WHERE access_url_id = $access_url_id
-                    ORDER BY ".(sizeof($courses) ? "(code IN(".implode(',', $courses).")) DESC," : "")." title";
+                    ORDER BY ".(count($courses) ? "(code IN(".implode(',', $courses).")) DESC," : '')." title";
         }
     }
     $result = Database::query($sql);
@@ -208,7 +208,8 @@ if (!api_is_platform_admin() && api_is_teacher()) {
 
 unset($Courses);
 ?>
-<form name="formulaire" method="post" action="<?php echo api_get_self(); ?>?page=<?php echo $page; ?>&id_session=<?php echo $sessionId; ?><?php if (!empty($_GET['add'])) {
+<form name="formulaire"
+      method="post" action="<?php echo api_get_self(); ?>?page=<?php echo $page; ?>&id_session=<?php echo $sessionId; if (!empty($_GET['add'])) {
     echo '&add=true';
 } ?>" style="margin:0px;" <?php if ($ajax_search) {
     echo ' onsubmit="valide();"';
@@ -251,7 +252,7 @@ unset($Courses);
                 <div class="code-course">
                     <?php echo get_lang('FirstLetterCourse'); ?> :
 
-                    <select name="firstLetterCourse" onchange = "xajax_search_courses(this.value,'multiple', <?php echo $sessionId; ?>)" class="selectpicker show-tick form-control">
+                    <select name="firstLetterCourse" onchange = "xajax_search_courses(this.value,'multiple', <?php echo $sessionId; ?>)" class="selectpicker form-control">
                         <option value="%">--</option>
                         <?php
                         echo Display :: get_alphabet_options();
@@ -289,6 +290,10 @@ unset($Courses);
                         <input type="checkbox" name="copy_evaluation">
                         <?php echo get_lang('ImportGradebookInCourse'); ?>
                     </label>
+                    <label>
+                        <input type="checkbox" name="import_teachers_as_course_coach">
+                        <?php echo get_lang('ImportCourseTeachersAsCourseCoach'); ?>
+                    </label>
                 </div>
             <?php
             echo '<div class="separate-action">';
@@ -304,7 +309,6 @@ unset($Courses);
         <div class="col-md-4">
             <label><?php echo get_lang('CourseListInSession'); ?> :</label>
             <select id='destination' name="SessionCoursesList[]" multiple="multiple" size="20" class="form-control">
-
                 <?php
                 foreach ($sessionCourses as $enreg) {
                     ?>
@@ -347,10 +351,10 @@ unset($Courses);
     }
 
     function mysort(a, b) {
-        if(a.text.toLowerCase() > b.text.toLowerCase()){
+        if (a.text.toLowerCase() > b.text.toLowerCase()){
             return 1;
         }
-        if(a.text.toLowerCase() < b.text.toLowerCase()){
+        if (a.text.toLowerCase() < b.text.toLowerCase()){
             return -1;
         }
         return 0;

+ 14 - 14
main/session/add_users_to_session.php

@@ -7,7 +7,6 @@
 // resetting the course id
 $cidReset = true;
 
-// including some necessary files
 require_once __DIR__.'/../inc/global.inc.php';
 $xajax = new xajax();
 $xajax->registerFunction('search_users');
@@ -43,7 +42,6 @@ if (isset($_REQUEST['add_type']) && $_REQUEST['add_type'] != '') {
 $page = isset($_GET['page']) ? Security::remove_XSS($_GET['page']) : null;
 
 // Checking for extra field with filter on
-
 $extra_field_list = UserManager::get_extra_fields();
 
 $new_field_list = [];
@@ -108,7 +106,7 @@ function search_users($needle, $type)
         // Only for single & multiple
         if (in_array($type, ['single', 'multiple'])) {
             if (!empty($id_session)) {
-                $id_session = intval($id_session);
+                $id_session = (int) $id_session;
                 // check id_user from session_rel_user table
                 $sql = "
                     SELECT user_id FROM $tbl_session_rel_user
@@ -180,14 +178,14 @@ function search_users($needle, $type)
                             INNER JOIN $tbl_user_rel_access_url url_user
                                 ON (url_user.user_id = user.id)
                             WHERE
-                                access_url_id = '$access_url_id'
-                                AND (
-                                    username LIKE '$needle%'
-                                    OR lastname LIKE '$needle%'
-                                    OR firstname LIKE '$needle%'
-                                )
-                                AND user.status <> 6
-                                AND user.status <> ".DRH."
+                                access_url_id = '$access_url_id' AND
+                                (
+                                    username LIKE '$needle%' OR 
+                                    lastname LIKE '$needle%' OR 
+                                    firstname LIKE '$needle%'
+                                ) AND 
+                                user.status <> 6 AND 
+                                user.status <> ".DRH."
                             $order_clause LIMIT 11
                         ";
                         break;
@@ -238,8 +236,11 @@ function search_users($needle, $type)
                             $officialCode.$user['lastname'].' '.$user['firstname'].' ('.$user['username'].')';
                     }
 
-                    $return .= '<a href="javascript: void(0);" onclick="javascript: add_user_to_session(\''.$user['id']
-                        .'\',\''.$person_name.' '.'\')">'.$person_name.' </a><br />';
+                    $return .= Display::url(
+                        $person_name,
+                        'javascript: void(0);',
+                        ['onclick' => "add_user_to_session('".$user['id']."', '".addslashes($person_name)."');"]
+                    ).'<br>';
                 } else {
                     $return .= '...<br />';
                 }
@@ -247,7 +248,6 @@ function search_users($needle, $type)
 
             $xajax_response->addAssign('ajax_list_users_single', 'innerHTML', api_utf8_encode($return));
         } else {
-            global $nosessionUsersList;
             $return .= '<select id="origin_users" name="nosessionUsersList[]" multiple="multiple" size="15" style="width:360px;">';
             while ($user = Database:: fetch_array($rs)) {
                 $person_name =

+ 79 - 79
main/session/session_category_list.php

@@ -104,7 +104,7 @@ if (isset($_GET['search']) && $_GET['search'] === 'advanced') {
     <div class="actions">
         <div class="row">
             <div class="col-md-6">
-            <?php
+                <?php
                 echo Display::url(
                     Display::return_icon('new_folder.png', get_lang('AddSessionCategory'), [], ICON_SIZE_MEDIUM),
                     api_get_path(WEB_CODE_PATH).'session/session_category_add.php'
@@ -116,16 +116,16 @@ if (isset($_GET['search']) && $_GET['search'] === 'advanced') {
             </div>
             <div class="col-md-6">
                 <div class="pull-right">
-                <form method="POST" action="session_category_list.php" class="form-inline">
-                    <div class="form-group">
-                    <input class="form-control" type="text" name="keyword" value="<?php echo $keyword; ?>"
-                    aria-label="<?php echo get_lang('Search'); ?>"/>
+                    <form method="POST" action="session_category_list.php" class="form-inline">
+                        <div class="form-group">
+                            <input class="form-control" type="text" name="keyword" value="<?php echo $keyword; ?>"
+                                   aria-label="<?php echo get_lang('Search'); ?>"/>
                             <button class="btn btn-default" type="submit" name="name"
                                     value="<?php echo get_lang('Search'); ?>"><em
                                         class="fa fa-search"></em> <?php echo get_lang('Search'); ?></button>
-                    <!-- <a href="session_list.php?search=advanced"><?php echo get_lang('AdvancedSearch'); ?></a> -->
-                    </div>
-                </form>
+                            <!-- <a href="session_list.php?search=advanced"><?php echo get_lang('AdvancedSearch'); ?></a> -->
+                        </div>
+                    </form>
                 </div>
             </div>
         </div>
@@ -140,39 +140,39 @@ if (isset($_GET['search']) && $_GET['search'] === 'advanced') {
                 ?>
                 <div>
                     <?php
-                if ($page) {
-                    ?>
+                    if ($page) {
+                        ?>
                         <a href="<?php echo api_get_self(); ?>?page=<?php echo $page
                             - 1; ?>&sort=<?php echo $sort; ?>&order=<?php echo Security::remove_XSS(
                             $order
                         ); ?>&keyword=<?php echo $keyword; ?><?php echo @$cond_url; ?>"><?php echo get_lang(
                                 'Previous'
                             ); ?></a>
+                        <?php
+                    } else {
+                        echo get_lang('Previous');
+                    } ?>
+                    |
                     <?php
-                } else {
-                    echo get_lang('Previous');
-                } ?>
-                |
-                <?php
-                if ($nbr_results > $limit) {
-                    ?>
+                    if ($nbr_results > $limit) {
+                        ?>
                         <a href="<?php echo api_get_self(); ?>?page=<?php echo $page
                             + 1; ?>&sort=<?php echo $sort; ?>&order=<?php echo Security::remove_XSS(
                             $order
                         ); ?>&keyword=<?php echo $keyword; ?><?php echo @$cond_url; ?>"><?php echo get_lang(
                                 'Next'
                             ); ?></a>
-                    <?php
-                } else {
-                    echo get_lang('Next');
-                } ?>
-        </div>
+                        <?php
+                    } else {
+                        echo get_lang('Next');
+                    } ?>
+                </div>
                 <?php
             } ?>
 
             <table class="data_table" width="100%">
-            <tr>
-                <th>&nbsp;</th>
+                <tr>
+                    <th>&nbsp;</th>
                     <th><a href="<?php echo api_get_self(); ?>?sort=name&order=<?php echo ($sort == 'name') ? $order
                             : 'ASC'; ?>"><?php echo get_lang('SessionCategoryName'); ?></a></th>
                     <th><a href="<?php echo api_get_self(); ?>?sort=nbr_session&order=<?php echo ($sort
@@ -181,11 +181,11 @@ if (isset($_GET['search']) && $_GET['search'] === 'advanced') {
                             ? $order : 'ASC'; ?>"><?php echo get_lang('StartDate'); ?></a></th>
                     <th><a href="<?php echo api_get_self(); ?>?sort=date_end&order=<?php echo ($sort == 'date_end')
                             ? $order : 'ASC'; ?>"><?php echo get_lang('EndDate'); ?></a></th>
-                <th><?php echo get_lang('Actions'); ?></th>
-            </tr>
+                    <th><?php echo get_lang('Actions'); ?></th>
+                </tr>
 
-            <?php
-            $i = 0;
+                <?php
+                $i = 0;
             $x = 0;
             foreach ($Sessions as $key => $enreg) {
                 if ($key == $limit) {
@@ -201,103 +201,103 @@ if (isset($_GET['search']) && $_GET['search'] === 'advanced') {
 
                 $rs = Database::query($sql);
                 list($nb_courses) = Database::fetch_array($rs); ?>
-                <tr class="<?php echo $i ? 'row_odd' : 'row_even'; ?>">
+                    <tr class="<?php echo $i ? 'row_odd' : 'row_even'; ?>">
                         <td><input type="checkbox" id="idChecked_<?php echo $x; ?>" name="idChecked[]"
                                    value="<?php echo $enreg['id']; ?>"></td>
-                    <td><?php echo api_htmlentities($enreg['name'], ENT_QUOTES, $charset); ?></td>
+                        <td><?php echo api_htmlentities($enreg['name'], ENT_QUOTES, $charset); ?></td>
                         <td><?php echo "<a href=\"session_list.php?id_category=".$enreg['id']."\">".$nb_courses
                                 ." Session(s) </a>"; ?></td>
-                    <td><?php echo api_format_date($enreg['date_start'], DATE_FORMAT_SHORT); ?></td>
-                    <td>
-                        <?php
-                        if (!empty($enreg['date_end']) && $enreg['date_end'] != '0000-00-00') {
-                            echo api_format_date($enreg['date_end'], DATE_FORMAT_SHORT);
-                        } else {
-                            echo '-';
-                        } ?>
-                    </td>
-                    <td>
-                        <a href="session_category_edit.php?&id=<?php echo $enreg['id']; ?>">
-                            <?php Display::display_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL); ?>
-                        </a>
+                        <td><?php echo api_format_date($enreg['date_start'], DATE_FORMAT_SHORT); ?></td>
+                        <td>
+                            <?php
+                            if (!empty($enreg['date_end']) && $enreg['date_end'] != '0000-00-00') {
+                                echo api_format_date($enreg['date_end'], DATE_FORMAT_SHORT);
+                            } else {
+                                echo '-';
+                            } ?>
+                        </td>
+                        <td>
+                            <a href="session_category_edit.php?&id=<?php echo $enreg['id']; ?>">
+                                <?php Display::display_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL); ?>
+                            </a>
                             <a href="<?php echo api_get_self(
                             ); ?>?sort=<?php echo $sort; ?>&action=delete_off_session&idChecked=<?php echo $enreg['id']; ?>"
                                onclick="if(!confirm('<?php echo get_lang(
                                    'ConfirmYourChoice'
                                ); ?>')) return false;">
-                            <?php Display::display_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL); ?>
-                        </a>
-                    </td>
-                </tr>
-                <?php
-                $i = $i ? 0 : 1;
+                                <?php Display::display_icon('delete.png', get_lang('Delete'), [], ICON_SIZE_SMALL); ?>
+                            </a>
+                        </td>
+                    </tr>
+                    <?php
+                    $i = $i ? 0 : 1;
                 $x++;
             }
             unset($Sessions); ?>
-        </table>
-        <br />
+            </table>
+            <br/>
             <div>
-        <?php
-        if ($num > $limit) {
-            if ($page) {
-                ?>
+                <?php
+                if ($num > $limit) {
+                    if ($page) {
+                        ?>
                         <a href="<?php echo api_get_self(); ?>?page=<?php echo $page
                             - 1; ?>&sort=<?php echo $sort; ?>&order=<?php echo Security::remove_XSS(
                             $_REQUEST['order']
                         ); ?>&keyword=<?php echo $_REQUEST['keyword']; ?><?php echo @$cond_url; ?>">
-                        <?php echo get_lang('Previous'); ?></a>
-                <?php
-            } else {
-                echo get_lang('Previous');
-            } ?>
-                |
-            <?php
-            if ($nbr_results > $limit) {
-                ?>
+                            <?php echo get_lang('Previous'); ?></a>
+                        <?php
+                    } else {
+                        echo get_lang('Previous');
+                    } ?>
+                    |
+                    <?php
+                    if ($nbr_results > $limit) {
+                        ?>
 
                         <a href="<?php echo api_get_self(); ?>?page=<?php echo $page
                             + 1; ?>&sort=<?php echo $sort; ?>&order=<?php echo Security::remove_XSS(
                             $_REQUEST['order']
                         ); ?>&keyword=<?php echo $_REQUEST['keyword']; ?><?php echo @$cond_url; ?>">
-                    <?php echo get_lang('Next'); ?></a>
+                            <?php echo get_lang('Next'); ?></a>
 
-                <?php
-            } else {
-                echo get_lang('Next');
-            }
-        } ?>
-        </div>
+                        <?php
+                    } else {
+                        echo get_lang('Next');
+                    }
+                } ?>
+            </div>
             <div class="row">
                 <div class="col-sm-4">
-        <div class="btn-group">
+                    <div class="btn-group">
                         <button type="button" class="btn btn-default" onclick="selectAll('idChecked',<?php echo $x; ?>,'true');">
                             <?php echo get_lang('SelectAll'); ?>
                         </button>
                         <button type="button" class="btn btn-default" onclick="selectAll('idChecked',<?php echo $x; ?>,'false');">
                             <?php echo get_lang('UnSelectAll'); ?>
                         </button>
-        </div>
+                    </div>
                 </div>
                 <div class="col-sm-6">
-            <select class="selectpicker show-tick form-control" name="action">
+                    <select class="selectpicker form-control" name="action">
                         <option value="delete_off_session" selected="selected">
                             <?php echo get_lang('DeleteSelectedSessionCategory'); ?>
                         </option>
                         <option value="delete_on_session">
                             <?php echo get_lang('DeleteSelectedFullSessionCategory'); ?>
                         </option>
-            </select>
-        </div>
+                    </select>
+                </div>
                 <div class="col-sm-2">
                     <button class="btn btn-success" type="submit" name="name" value="<?php echo get_lang('Ok'); ?>">
                         <?php echo get_lang('Ok'); ?>
                     </button>
                 </div>
             </div>
-    <?php
+            <?php
         } ?>
-    </table>
+        </table>
     </form>
-<?php
+    <?php
 }
 Display::display_footer();

+ 6 - 26
main/session/session_list.php

@@ -136,13 +136,8 @@ $extra_params['height'] = 'auto';
 if (!isset($_GET['keyword'])) {
     $extra_params['postData'] = [
         'filters' => [
-            "groupOp" => "AND",
-            "rules" => $result['rules'],
-            /*array(
-                array( "field" => "display_start_date", "op" => "gt", "data" => ""),
-                array( "field" => "display_end_date", "op" => "gt", "data" => "")
-            ),*/
-            //'groups' => $groups
+            'groupOp' => 'AND',
+            'rules' => $result['rules'],
         ],
     ];
 }
@@ -167,23 +162,14 @@ $orderUrl = api_get_path(WEB_AJAX_PATH).'session.ajax.php?a=order';
     <script>
         function setSearchSelect(columnName) {
             $("#sessions").jqGrid('setColProp', columnName, {
-                /*searchoptions:{
-                 dataInit:function(el){
-                 $("option[value='1']",el).attr("selected", "selected");
-                 setTimeout(function(){
-                 $(el).trigger('change');
-                 }, 1000);
-                 }
-                 }*/
             });
         }
         var added_cols = [];
         var original_cols = [];
 
         function clean_cols(grid, added_cols) {
-            //Cleaning
+            // Cleaning
             for (key in added_cols) {
-                //console.log('hide: ' + key);
                 grid.hideCol(key);
             };
             grid.showCol('name');
@@ -195,7 +181,6 @@ $orderUrl = api_get_path(WEB_AJAX_PATH).'session.ajax.php?a=order';
         function show_cols(grid, added_cols) {
             grid.showCol('name').trigger('reloadGrid');
             for (key in added_cols) {
-                //console.log('show: ' + key);
                 grid.showCol(key);
             };
         }
@@ -271,17 +256,12 @@ $orderUrl = api_get_path(WEB_AJAX_PATH).'session.ajax.php?a=order';
                             filters = jQuery.parseJSON(postdata.filters);
                             clean_cols(grid, added_cols);
                             added_cols = [];
-                            $.each(filters, function(key, value){
-                                //console.log('key: ' + key );
+                            $.each(filters, function(key, value) {
                                 if (key == 'rules') {
                                     $.each(value, function(subkey, subvalue) {
                                         if (subvalue.data == undefined) {
                                         }
-
-                                        //if (added_cols[value.field] == undefined) {
                                         added_cols[subvalue.field] = subvalue.field;
-                                        //}
-                                        //grid.showCol(value.field);
                                     });
                                 }
                             });
@@ -311,10 +291,10 @@ $orderUrl = api_get_path(WEB_AJAX_PATH).'session.ajax.php?a=order';
                     });
                     orderList = JSON.stringify(orderList);
                     $.get("<?php echo $orderUrl; ?>", "order="+orderList, function (result) {
-                        //console.log(result);
                     });
                 }
             };
+
             // Sortable rows
             grid.jqGrid('sortableRows', options);
             <?php
@@ -345,7 +325,7 @@ $orderUrl = api_get_path(WEB_AJAX_PATH).'session.ajax.php?a=order';
             gbox.before(searchDialog);
             gbox.css({clear:"left"});
 
-            //Select first elements by default
+            // Select first elements by default
             $('.input-elm').each(function(){
                 $(this).find('option:first').attr('selected', 'selected');
             });

+ 30 - 0
main/social/download_my_files.php

@@ -0,0 +1,30 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+require_once __DIR__.'/../inc/global.inc.php';
+
+$userId = isset($_GET['user_id']) ? (int) $_GET['user_id'] : 0;
+$file = isset($_GET['file']) ? $_GET['file'] : '';
+
+if (empty($userId) || empty($file)) {
+    exit;
+}
+
+$dir = UserManager::getUserPathById($userId, 'system');
+if (empty($dir)) {
+    exit;
+}
+$file = $dir.'/my_files/'.$file;
+
+$config = api_get_configuration_value('block_my_files_access');
+
+if ($config) {
+    api_block_anonymous_users();
+}
+
+if (Security::check_abs_path($file, $dir.'my_files/')) {
+    $result = DocumentManager::file_send_for_download($file);
+    if ($result === false) {
+        exit;
+    }
+}

+ 4 - 1
main/social/home.php

@@ -88,6 +88,9 @@ $formSearch->addText(
     ]
 );
 
+// Added a Jquery Function to return the Preview of OpenGraph URL Content
+$htmlHeadXtra[] = SocialManager::getScriptToGetOpenGraph();
+
 $tpl = new Template(get_lang('SocialNetwork'));
 SocialManager::setSocialUserBlock($tpl, $user_id, 'home');
 $tpl->assign('social_wall_block', $wallSocialAddPost);
@@ -97,7 +100,7 @@ $tpl->assign('social_auto_extend_link', $socialAutoExtendLink);
 $tpl->assign('search_friends_form', $formSearch->returnForm());
 $tpl->assign('social_friend_block', $friend_html);
 $tpl->assign('social_search_block', $social_search_block);
-$tpl->assign('social_skill_block', SocialManager::getSkillBlock($user_id));
+$tpl->assign('social_skill_block', SocialManager::getSkillBlock($user_id, 'vertical'));
 $tpl->assign('social_group_block', $social_group_block);
 $tpl->assign('session_list', $social_session_block);
 $social_layout = $tpl->get_template('social/home.tpl');

+ 6 - 0
main/social/my_skills_report.php

@@ -28,6 +28,12 @@ switch ($action) {
         $certificate = new Certificate(0, api_get_user_id(), false, false);
         $certificate->generatePdfFromCustomCertificate();
         break;
+    case 'generate':
+        $certificate = Certificate::generateUserSkills(api_get_user_id());
+        Display::addFlash(Display::return_message(get_lang('Updated')));
+        header('Location: '.api_get_self());
+        exit;
+        break;
 }
 
 $skillTable = Database::get_main_table(TABLE_MAIN_SKILL);