Merge branch 'm36_MDL-51080_MAriaDB_Wrong_DB_Type' of https://github.com/scara/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Jun 2018 21:25:03 +0000 (23:25 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Jun 2018 21:25:03 +0000 (23:25 +0200)
132 files changed:
admin/renderer.php
admin/roles/admins.php
admin/searchareas.php
admin/tests/behat/filter_users.feature
admin/tool/customlang/renderer.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/templates/data_requests.mustache
admin/tool/dataprivacy/templates/my_data_requests.mustache
admin/tool/dataprivacy/templates/request_details.mustache
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/xmldb/actions/edit_table/edit_table.class.php
admin/tool/xmldb/actions/edit_xml_file/edit_xml_file.class.php
admin/tool/xmldb/actions/view_table_php/view_table_php.class.php
auth/db/auth.php
auth/db/lang/en/auth_db.php
backup/moodle2/tests/restore_stepslib_date_test.php
backup/util/ui/restore_ui_stage.class.php
course/completion.js
course/renderer.php
enrol/paypal/db/upgrade.php
enrol/paypal/version.php
filter/emoticon/filter.php
filter/emoticon/tests/filter_test.php
grade/edit/tree/index.php
lang/en/admin.php
lang/en/backup.php
lang/en/filters.php
lang/en/mimetypes.php
lang/en/search.php
lib/adminlib.php
lib/amd/build/tag.min.js
lib/amd/src/tag.js
lib/classes/filetypes.php
lib/editor/atto/plugins/charmap/styles.css
lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-debug.js
lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-min.js
lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button.js
lib/editor/atto/plugins/charmap/yui/src/button/js/button.js
lib/editor/atto/plugins/equation/styles.css
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js
lib/editor/atto/plugins/equation/yui/src/button/js/button.js
lib/editor/atto/plugins/image/styles.css
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
lib/editor/atto/plugins/image/yui/src/button/js/button.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js
lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js
lib/editor/atto/plugins/link/yui/src/button/js/button.js
lib/editor/atto/plugins/media/styles.css
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js
lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js
lib/editor/atto/plugins/media/yui/src/button/js/button.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js
lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/audiomodule.js
lib/editor/atto/plugins/recordrtc/yui/src/recording/js/videomodule.js
lib/editor/atto/plugins/table/styles.css
lib/editor/atto/plugins/table/tests/behat/table.feature
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js
lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js
lib/editor/atto/plugins/table/yui/src/button/js/button.js
lib/editor/atto/styles.css
lib/evalmath/evalmath.class.php
lib/evalmath/readme_moodle.txt
lib/excellib.class.php
lib/filelib.php
lib/mathslib.php
lib/moodlelib.php
lib/tests/mathslib_test.php
message/classes/task/migrate_message_data.php
message/output/popup/amd/build/message_popover_controller.min.js
message/output/popup/amd/build/notification_popover_controller.min.js
message/output/popup/amd/src/message_popover_controller.js
message/output/popup/amd/src/notification_popover_controller.js
message/output/popup/lib.php
message/output/popup/templates/message_popover.mustache
message/output/popup/templates/notification_popover.mustache
message/tests/migrate_message_data_task_test.php
mod/assign/gradingtable.php
mod/assign/tests/locallib_test.php
mod/forum/post.php
mod/page/lang/en/page.php
mod/page/lib.php
mod/page/mod_form.php
mod/page/settings.php
mod/page/tests/behat/page_appearance.feature [new file with mode: 0644]
mod/page/tests/generator/lib.php
mod/page/version.php
mod/page/view.php
mod/quiz/report/overview/db/install.xml
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/overview/version.php
mod/scorm/classes/privacy/provider.php
mod/scorm/lang/en/scorm.php
mod/scorm/tests/privacy_test.php
mod/wiki/view.php
question/engine/renderer.php
question/engine/tests/helpers.php
question/engine/tests/questionengine_test.php
question/type/shortanswer/renderer.php
search/index.php
tag/classes/privacy/provider.php
tag/tests/privacy_test.php
theme/boost/amd/build/drawer.min.js
theme/boost/amd/src/drawer.js
theme/boost/classes/privacy/provider.php
theme/boost/lang/en/theme_boost.php
theme/boost/scss/moodle/bs4alphacompat.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/course.scss
theme/boost/scss/moodle/undo.scss
theme/boost/templates/core/filemanager_fileselect.mustache
theme/boost/templates/core/filemanager_loginform.mustache
theme/boost/templates/core/filemanager_selectlayout.mustache
theme/boost/templates/core/filemanager_uploadform.mustache
theme/boost/templates/core_form/element-date_time_selector-inline.mustache
theme/boost/templates/core_form/element-date_time_selector.mustache
theme/boost/tests/privacy_test.php [new file with mode: 0644]
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/style/moodle.css
user/filters/anycourses.php [new file with mode: 0644]
user/filters/lib.php
version.php

index f22c6a2..da59ef6 100644 (file)
@@ -1990,7 +1990,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 if (empty($CFG->docroot) or $environment_result->plugin) {
                     $report = get_string($stringtouse, 'admin', $rec);
                 } else {
-                    $report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec));
+                    $report = $this->doc_link(join($linkparts, '/'), get_string($stringtouse, 'admin', $rec), true);
                 }
                 // Enclose report text in div so feedback text will be displayed underneath it.
                 $report = html_writer::div($report);
index 8245949..4b341b3 100644 (file)
@@ -80,9 +80,16 @@ if (optional_param('add', false, PARAM_BOOL) and confirm_sesskey()) {
         }
 
         if (isset($admins[$newmain])) {
+            $logstringold = implode(', ', $admins);
+
             unset($admins[$newmain]);
             array_unshift($admins, $newmain);
+
+            $logstringnew = implode(', ', $admins);
+
             set_config('siteadmins', implode(',', $admins));
+            add_to_config_log('siteadmins', $logstringold, $logstringnew, null);
+
             redirect($PAGE->url);
         }
     }
@@ -95,8 +102,16 @@ if (optional_param('add', false, PARAM_BOOL) and confirm_sesskey()) {
             $admins[$admin] = $admin;
         }
     }
+
+    $logstringold = implode(', ', $admins);
+
     $admins[$confirmadd] = $confirmadd;
+
+    $logstringnew = implode(', ', $admins);
+
     set_config('siteadmins', implode(',', $admins));
+    add_to_config_log('siteadmins', $logstringold, $logstringnew, 'core');
+
     redirect($PAGE->url);
 
 } else if ($confirmdel and confirm_sesskey() and $confirmdel != $USER->id) {
@@ -107,8 +122,16 @@ if (optional_param('add', false, PARAM_BOOL) and confirm_sesskey()) {
             $admins[$admin] = $admin;
         }
     }
+
+    $logstringold = implode(', ', $admins);
+
     unset($admins[$confirmdel]);
+
+    $logstringnew = implode(', ', $admins);
+
     set_config('siteadmins', implode(',', $admins));
+    add_to_config_log('siteadmins', $logstringold, $logstringnew, 'core');
+
     redirect($PAGE->url);
 }
 
index bf178b5..911a86b 100644 (file)
@@ -28,113 +28,139 @@ admin_externalpage_setup('searchareas');
 
 $areaid = optional_param('areaid', null, PARAM_ALPHAEXT);
 $action = optional_param('action', null, PARAM_ALPHA);
+$indexingenabled = \core_search\manager::is_indexing_enabled(); // This restricts many of the actions on this page.
 
+// Get a search manager instance, which we'll need for display and to handle some actions.
 try {
     $searchmanager = \core_search\manager::instance();
 } catch (core_search\engine_exception $searchmanagererror) {
-    // Continue, we return an error later depending on the requested action.
+    // In action cases, we'll throw this exception below. In non-action cases, we produce a lang string error.
 }
 
+// Handle all the actions.
 if ($action) {
-
+    // If dealing with an areaid, we need to check that the area exists.
     if ($areaid) {
-        // We need to check that the area exists.
         $area = \core_search\manager::get_search_area($areaid);
         if ($area === false) {
             throw new moodle_exception('invalidrequest');
         }
     }
 
-    if ($action !== 'enable' && $action !== 'disable') {
-        // All actions but enable/disable need the search engine to be ready.
-        if (!empty($searchmanagererror)) {
-            throw $searchmanagererror;
-        }
+    // All the indexing actions.
+    if (in_array($action, ['delete', 'indexall', 'reindexall', 'deleteall'])) {
 
-        // Show confirm prompt for all these actions as they may be inadvisable, or may cause
-        // an interruption in search functionality, on production systems.
-        if (!optional_param('confirm', 0, PARAM_INT)) {
-            // Display confirmation prompt.
-            $a = null;
-            if ($areaid) {
-                $a = html_writer::tag('strong', $area->get_visible_name());
-            }
+        // All of these actions require that indexing is enabled.
+        if ($indexingenabled) {
 
-            $actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
-            if ($areaid) {
-                $actionparams['areaid'] = $areaid;
+            // For all of these actions, we strictly need a manager instance.
+            if (isset($searchmanagererror)) {
+                throw $searchmanagererror;
             }
-            $actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
-            $cancelurl = new moodle_url('/admin/searchareas.php');
-            echo $OUTPUT->header();
-            echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
+
+            // Show confirm prompt for all these actions as they may be inadvisable, or may cause
+            // an interruption in search functionality, on production systems.
+            if (!optional_param('confirm', 0, PARAM_INT)) {
+                // Display confirmation prompt.
+                $a = null;
+                if ($areaid) {
+                    $a = html_writer::tag('strong', $area->get_visible_name());
+                }
+
+                $actionparams = ['sesskey' => sesskey(), 'action' => $action, 'confirm' => 1];
+                if ($areaid) {
+                    $actionparams['areaid'] = $areaid;
+                }
+                $actionurl = new moodle_url('/admin/searchareas.php', $actionparams);
+                $cancelurl = new moodle_url('/admin/searchareas.php');
+                echo $OUTPUT->header();
+                echo $OUTPUT->confirm(get_string('confirm_' . $action, 'search', $a),
                     new single_button($actionurl, get_string('continue'), 'post', true),
                     new single_button($cancelurl, get_string('cancel'), 'get'));
-            echo $OUTPUT->footer();
-            exit;
+                echo $OUTPUT->footer();
+                exit;
+            } else {
+                // Confirmed, so run the required action.
+                require_sesskey();
+
+                switch ($action) {
+                    case 'delete':
+                        $searchmanager->delete_index($areaid);
+                        \core\notification::success(get_string('searchindexdeleted', 'admin'));
+                        break;
+                    case 'indexall':
+                        $searchmanager->index();
+                        \core\notification::success(get_string('searchindexupdated', 'admin'));
+                        break;
+                    case 'reindexall':
+                        $searchmanager->index(true);
+                        \core\notification::success(get_string('searchreindexed', 'admin'));
+                        break;
+                    case 'deleteall':
+                        $searchmanager->delete_index();
+                        \core\notification::success(get_string('searchalldeleted', 'admin'));
+                        break;
+                    default:
+                        break;
+                }
+
+                // Redirect back to the main page after taking action.
+                redirect(new moodle_url('/admin/searchareas.php'));
+            }
         }
-    }
+    } else if (in_array($action, ['enable', 'disable'])) {
+        // Toggling search areas requires no confirmation.
+        require_sesskey();
 
-    // We are now taking an actual action, so require sesskey.
-    require_sesskey();
-
-    switch ($action) {
-        case 'enable':
-            $area->set_enabled(true);
-            \core\notification::add(get_string('searchareaenabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'disable':
-            $area->set_enabled(false);
-            \core\notification::add(get_string('searchareadisabled', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'delete':
-            $search = \core_search\manager::instance();
-            $search->delete_index($areaid);
-            \core\notification::add(get_string('searchindexdeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'indexall':
-            $searchmanager->index();
-            \core\notification::add(get_string('searchindexupdated', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'reindexall':
-            $searchmanager->index(true);
-            \core\notification::add(get_string('searchreindexed', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        case 'deleteall':
-            $searchmanager->delete_index();
-            \core\notification::add(get_string('searchalldeleted', 'admin'), \core\output\notification::NOTIFY_SUCCESS);
-            break;
-        default:
-            throw new moodle_exception('invalidaction');
-            break;
-    }
+        switch ($action) {
+            case 'enable':
+                $area->set_enabled(true);
+                \core\notification::success(get_string('searchareaenabled', 'admin'));
+                break;
+            case 'disable':
+                $area->set_enabled(false);
+                core\notification::success(get_string('searchareadisabled', 'admin'));
+                break;
+            default:
+                break;
+        }
 
-    // Redirect back to the main page after taking action.
-    redirect(new moodle_url('/admin/searchareas.php'));
+        redirect(new moodle_url('/admin/searchareas.php'));
+    } else {
+        // Invalid action.
+        throw new moodle_exception('invalidaction');
+    }
 }
 
-echo $OUTPUT->header();
 
-$searchareas = \core_search\manager::get_search_areas_list();
-if (empty($searchmanagererror)) {
-    $areasconfig = $searchmanager->get_areas_config($searchareas);
+// Display.
+if (isset($searchmanager) && $indexingenabled) {
+    \core\notification::info(get_string('indexinginfo', 'admin'));
+} else if (isset($searchmanager)) {
+    $params = (object) [
+        'url' => (new moodle_url("/admin/settings.php?section=manageglobalsearch#admin-searchindexwhendisabled"))->out(false)
+    ];
+    \core\notification::error(get_string('indexwhendisabledfullnotice', 'search', $params));
 } else {
-    $areasconfig = false;
-}
-
-if (!empty($searchmanagererror)) {
+    // In non-action cases, init errors are translated and displayed to the user as error notifications.
     $errorstr = get_string($searchmanagererror->errorcode, $searchmanagererror->module, $searchmanagererror->a);
-    echo $OUTPUT->notification($errorstr, \core\output\notification::NOTIFY_ERROR);
-} else {
-    echo $OUTPUT->notification(get_string('indexinginfo', 'admin'), \core\output\notification::NOTIFY_INFO);
+    \core\notification::error($errorstr);
 }
 
+echo $OUTPUT->header();
+
 $table = new html_table();
 $table->id = 'core-search-areas';
+$table->head = [
+    get_string('searcharea', 'search'),
+    get_string('enable'),
+    get_string('newestdocindexed', 'admin'),
+    get_string('searchlastrun', 'admin'),
+    get_string('searchindexactions', 'admin')
+];
 
-$table->head = array(get_string('searcharea', 'search'), get_string('enable'), get_string('newestdocindexed', 'admin'),
-    get_string('searchlastrun', 'admin'), get_string('searchindexactions', 'admin'));
-
+$searchareas = \core_search\manager::get_search_areas_list();
+$areasconfig = isset($searchmanager) ? $searchmanager->get_areas_config($searchareas) : false;
 foreach ($searchareas as $area) {
     $areaid = $area->get_area_id();
     $columns = array(new html_table_cell($area->get_visible_name()));
@@ -144,7 +170,7 @@ foreach ($searchareas as $area) {
             new pix_icon('t/hide', get_string('disable'), 'moodle', array('title' => '', 'class' => 'iconsmall')),
             null, array('title' => get_string('disable')));
 
-        if ($areasconfig) {
+        if ($areasconfig && $indexingenabled) {
             $columns[] = $areasconfig[$areaid]->lastindexrun;
 
             if ($areasconfig[$areaid]->indexingstart) {
@@ -173,7 +199,11 @@ foreach ($searchareas as $area) {
             $columns[] = html_writer::alist($actions, ['class' => 'unstyled list-unstyled']);
 
         } else {
-            $blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
+            if (!$areasconfig) {
+                $blankrow = new html_table_cell(get_string('searchnotavailable', 'admin'));
+            } else {
+                $blankrow = new html_table_cell(get_string('indexwhendisabledshortnotice', 'search'));
+            }
             $blankrow->colspan = 3;
             $columns[] = $blankrow;
         }
@@ -192,10 +222,7 @@ foreach ($searchareas as $area) {
 }
 
 // Cross-search area tasks.
-$options = array();
-if (!empty($searchmanagererror)) {
-    $options['disabled'] = true;
-}
+$options = (isset($searchmanager) && $indexingenabled) ? [] : ['disabled' => true];
 echo $OUTPUT->box_start('search-areas-actions');
 echo $OUTPUT->single_button(admin_searcharea_action_url('indexall'), get_string('searchupdateindex', 'admin'), 'get', $options);
 echo $OUTPUT->single_button(admin_searcharea_action_url('reindexall'), get_string('searchreindexindex', 'admin'), 'get', $options);
@@ -204,7 +231,7 @@ echo $OUTPUT->box_end();
 
 echo html_writer::table($table);
 
-if (empty($searchmanagererror)) {
+if (isset($searchmanager)) {
     // Show information about queued index requests for specific contexts.
     $searchrenderer = $PAGE->get_renderer('core_search');
     echo $searchrenderer->render_index_requests_info($searchmanager->get_index_requests_info());
index b9a03e5..75e2b42 100644 (file)
@@ -65,3 +65,20 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
     And I should not see "User Two"
     And I should not see "User Three"
     And I should not see "User Four"
+
+  Scenario: Filter user accounts by enrolled in any course
+    When I set the following fields to these values:
+      | id_anycourses | Yes |
+    And I press "Add filter"
+    Then I should see "User One"
+    And I should see "User Two"
+    And I should see "User Three"
+    And I should not see "User Four"
+    And I press "Remove all filters"
+    And I set the following fields to these values:
+      | id_anycourses | No |
+    And I press "Add filter"
+    And I should not see "User One"
+    And I should not see "User Two"
+    And I should not see "User Three"
+    And I should see "User Four"
index 224e76b..aea7fd6 100644 (file)
@@ -134,11 +134,13 @@ class tool_customlang_renderer extends plugin_renderer_base {
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'translatorsubmitted', 'value'=>1));
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'sesskey', 'value'=>sesskey()));
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'p', 'value'=>$translator->currentpage));
-        $save1   = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecontinue', 'value'=>get_string('savecontinue', 'tool_customlang')));
-        $save2   = html_writer::empty_tag('input', array('type'=>'submit', 'name'=>'savecheckin', 'value'=>get_string('savecheckin', 'tool_customlang')));
-        $output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
+        $save1   = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecontinue',
+            'value' => get_string('savecontinue', 'tool_customlang'), 'class' => 'btn btn-secondary'));
+        $save2   = html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'savecheckin',
+            'value' => get_string('savecheckin', 'tool_customlang'), 'class' => 'btn btn-secondary'));
+        $output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
         $output .= html_writer::table($table);
-        $output .= html_writer::tag('fieldset', $save1.$save2, array('class'=>'buttonsbar'));
+        $output .= html_writer::tag('fieldset', $save1 . ' ' . $save2, array('class' => 'buttonsbar'));
         $output .= html_writer::end_tag('div');
         $output .= html_writer::end_tag('form');
 
index 40dc1e7..b8622d8 100644 (file)
@@ -193,6 +193,7 @@ $string['purposeupdated'] = 'Purpose updated';
 $string['replyto'] = 'Reply to';
 $string['requestactions'] = 'Actions';
 $string['requestby'] = 'Requested by';
+$string['requestbydetail'] = 'Requested by:';
 $string['requestcomments'] = 'Comments';
 $string['requestcomments_help'] = 'This box enables you to enter any further details about your data request.';
 $string['requestemailintro'] = 'You have received a data request:';
index beed5ac..45cedc6 100644 (file)
                 "comments": "I would like to download all of my daughter's personal data",
                 "statuslabelclass": "label-default",
                 "statuslabel": "Pending",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 2,
                 "comments": "Please give me all of the information you have about me...",
                 "statuslabelclass": "label-warning",
                 "statuslabel": "Awaiting completion",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 3,
                 "comments": "Please delete all of my son's personal data.",
                 "statuslabelclass": "label-success",
                 "statuslabel": "Complete",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 4,
                 "comments": "I would like to request for my personal data to be deleted from your site. Thanks!",
                 "statuslabelclass": "label-danger",
                 "statuslabel": "Rejected",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 5,
                 "comments": "Please let me download my data",
                 "statuslabelclass": "label-info",
                 "statuslabel": "Processing",
-                "timecreated" : 1517902435
+                "timecreated" : 1517902435,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             }
         ]
     }
                 <th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th>
                 <th scope="col">{{#str}}user, tool_dataprivacy{{/str}}</th>
                 <th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th>
+                <th scope="col">{{#str}}requestby, tool_dataprivacy{{/str}}</th>
                 <th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th>
                 <th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th>
             </tr>
               }}>
                 <td>{{typenameshort}}</td>
                 <td><a href="{{foruser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{foruser.fullname}}</a></td>
-                <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}</td>
+                <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
+                <td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
                 <td>
                     <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
                 </td>
index 1bdcbdb..2b654b2 100644 (file)
                 "comments": "I would like to download all of my daughter's personal data",
                 "statuslabelclass": "label-default",
                 "statuslabel": "Pending",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 2,
                 "comments": "Give me all of the information you have about me, or else...",
                 "statuslabelclass": "label-warning",
                 "statuslabel": "Awaiting completion",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 3,
                 "comments": "Please delete all of my son's personal data.",
                 "statuslabelclass": "label-success",
                 "statuslabel": "Complete",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 4,
                 "comments": "Delete my data or I'm coming for you...",
                 "statuslabelclass": "label-danger",
                 "statuslabel": "Rejected",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             },
             {
                 "id": 5,
                 "comments": "Please let me download my data",
                 "statuslabelclass": "label-info",
                 "statuslabel": "Processing",
-                "timecreated" : 1517902087
+                "timecreated" : 1517902087,
+                "requestedbyuser" : {
+                    "fullname": "Martha Smith",
+                    "profileurl": "#"
+                }
             }
         ]
     }
             <tr>
                 <th scope="col">{{#str}}requesttype, tool_dataprivacy{{/str}}</th>
                 <th scope="col">{{#str}}daterequested, tool_dataprivacy{{/str}}</th>
+                <th scope="col">{{#str}}requestby, tool_dataprivacy{{/str}}</th>
                 <th scope="col">{{#str}}requeststatus, tool_dataprivacy{{/str}}</th>
                 <th scope="col" colspan="2">{{#str}}message, tool_dataprivacy{{/str}}</th>
             </tr>
               }} data-status="{{status}}"{{!
               }}>
                 <td>{{typename}}</td>
-                <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}</td>
+                <td>{{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}</td>
+                <td><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></td>
                 <td>
                     <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
                 </td>
index 531e394..c8e9dc1 100644 (file)
         "canreview": true,
         "reviewurl": "#",
         "timecreated": 1517561224,
+        "requestedbyuser" : {
+            "fullname": "Martha Smith",
+            "profileurl": "#"
+        },
         "statuslabel": "Pending",
         "statuslabelclass": "label-default",
         "messagehtml": "<p>Hello,</p><p>I would like to download all of my personal data.</p><p>Thanks!</p>"
             <a href="mailto:{{foruser.email}}">{{foruser.email}}</a>
             <div class="clearfix m-t-1 m-b-1">
             <span class="pull-left m-r-1">
-                <strong>{{#str}}daterequesteddetail, tool_dataprivacy{{/str}}</strong> {{#userdate}} {{timecreated}}, {{#str}} strftimedate {{/str}} {{/userdate}}
+                <strong>{{#str}}daterequesteddetail, tool_dataprivacy{{/str}}</strong> {{#userdate}} {{timecreated}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}
             </span>
                 <span class="pull-left m-r-1">
                 <strong>{{#str}}statusdetail, tool_dataprivacy{{/str}}</strong>
                 <span class="label {{statuslabelclass}}">{{statuslabel}}</span>
             </span>
+            <span class="pull-left m-r-1">
+                <strong>{{#str}}requestbydetail, tool_dataprivacy{{/str}}</strong>
+                <span><a href="{{requestedbyuser.profileurl}}" title="{{#str}}viewprofile{{/str}}">{{requestedbyuser.fullname}}</a></span>
+            </span>
             </div>
             {{#canreview}}
                 <!--a href="{{reviewurl}}" class="btn btn-default">{{#str}}reviewdata, tool_dataprivacy{{/str}}</a-->
index 1e434a7..d480e57 100644 (file)
@@ -142,7 +142,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         global $DB;
 
         $purpose1 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'PT1H', 'lawfulbases' => 'gdpr_art_6_1_a']);
-        $purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1000Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
+        $purpose2 = api::create_purpose((object)['name' => 'p1', 'retentionperiod' => 'P1Y', 'lawfulbases' => 'gdpr_art_6_1_b']);
         $cat = api::create_category((object)['name' => 'a']);
 
         $record = (object)[
@@ -165,8 +165,12 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
 
         $course1 = $this->getDataGenerator()->create_course();
 
-        // Old course.
-        $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => '2']);
+        // Course finished last week (so purpose1 retention period does delete stuff but purpose2 retention period does not).
+        $dt = new \DateTime();
+        $di = new \DateInterval('P7D');
+        $dt->sub($di);
+
+        $course2 = $this->getDataGenerator()->create_course(['startdate' => '1', 'enddate' => $dt->getTimestamp()]);
         $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
         $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
 
index 5c2e7b9..32f6fea 100644 (file)
@@ -124,7 +124,7 @@ class edit_table extends XMLDBAction {
         $o.= '    <input type="hidden" name ="action" value="edit_table_save" />';
         $o.= '    <input type="hidden" name ="sesskey" value="' . sesskey() .'" />';
         $o.= '    <input type="hidden" name ="postaction" value="edit_table" />';
-        $o.= '    <table id="formelements" class="boxaligncenter">';
+        $o .= '    <table id="formelements">';
         // If the table is being used, we cannot rename it
         if ($structure->getTableUses($table->getName())) {
             $o.= '      <tr valign="top"><td>Name:</td><td><input type="hidden" name ="name" value="' . s($table->getName()) . '" />' . s($table->getName()) .'</td></tr>';
@@ -243,7 +243,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $field->readableInfo() . '</td>';
                 // Print table row
-                $o .= '<tr class="r' . $row . '"><td class="table cell">' . $f . $b . $r . '</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $f . $b . $r . '</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
@@ -296,7 +296,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $key->readableInfo() . '</td>';
                 // Print table row
-            $o .= '<tr class="r' . $row . '"><td class="table cell">' . $k . $b . $r .'</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $k . $b . $r .'</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
@@ -337,7 +337,7 @@ class edit_table extends XMLDBAction {
                 // The readable info
                 $r = '</td><td class="readableinfo cell">' . $index->readableInfo() . '</td>';
                 // Print table row
-            $o .= '<tr class="r' . $row . '"><td class="table cell">' . $i . $b . $r .'</tr>';
+                $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $i . $b . $r .'</tr>';
                 $row = ($row + 1) % 2;
             }
             $o .= '</table>';
index 1fdd4cb..f053c08 100644 (file)
@@ -115,7 +115,7 @@ class edit_xml_file extends XMLDBAction {
                 $o.= '    <input type="hidden" name ="path" value="' . s($structure->getPath()) .'" />';
                 $o.= '    <input type="hidden" name ="version" value="' . s($structure->getVersion()) .'" />';
                 $o.= '    <input type="hidden" name ="sesskey" value="' . sesskey() .'" />';
-                $o.= '    <table id="formelements" class="boxaligncenter">';
+                $o .= '    <table id="formelements">';
                 $o.= '      <tr valign="top"><td>Path:</td><td>' . s($structure->getPath()) . '</td></tr>';
                 $o.= '      <tr valign="top"><td>Version:</td><td>' . s($structure->getVersion()) . '</td></tr>';
                 $o.= '      <tr valign="top"><td><label for="comment" accesskey="c">Comment:</label></td><td><textarea name="comment" rows="3" cols="80" id="comment">' . $structure->getComment() . '</textarea></td></tr>';
@@ -216,7 +216,7 @@ class edit_xml_file extends XMLDBAction {
                          }
                         $b .= '</td>';
                         // Print table row
-                        $o .= '<tr class="r' . $row . '"><td class="table cell">' . $t . $b . '</tr>';
+                        $o .= '<tr class="r' . $row . '"><td class="cell firstcol">' . $t . $b . '</tr>';
                         $row = ($row + 1) % 2;
                     }
                     $o .= '</table>';
index e975201..d337572 100644 (file)
@@ -162,7 +162,7 @@ class view_table_php extends XMLDBAction {
         $o.= '    <input type="hidden" name ="dir" value="' . str_replace($CFG->dirroot, '', $dirpath) . '" />';
         $o.= '    <input type="hidden" name ="table" value="' . s($tableparam) . '" />';
         $o.= '    <input type="hidden" name ="action" value="view_table_php" />';
-        $o.= '    <table id="formelements" class="boxaligncenter" cellpadding="5">';
+        $o .= '    <table id="formelements" cellpadding="5">';
         $o.= '      <tr><td><label for="menucommand" accesskey="c">' . $this->str['selectaction'] .' </label>' . html_writer::select($popcommands, 'command', $commandparam, false) . '&nbsp;<label for="menufieldkeyindex" accesskey="f">' . $this->str['selectfieldkeyindex'] . ' </label>' .html_writer::select($popfields, 'fieldkeyindex', $origfieldkeyindexparam, false) . '</td></tr>';
         $o.= '      <tr><td colspan="2" align="center"><input type="submit" value="' .$this->str['view'] . '" /></td></tr>';
         $o.= '    </table>';
index 549e103..bcb26df 100644 (file)
@@ -709,12 +709,12 @@ class auth_plugin_db extends auth_plugin_base {
         raise_memory_limit(MEMORY_HUGE);
 
         if (empty($this->config->table)) {
-            echo $OUTPUT->notification('External table not specified.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbnoexttable', 'auth_db'), 'notifyproblem');
             return;
         }
 
         if (empty($this->config->fielduser)) {
-            echo $OUTPUT->notification('External user field not specified.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbnouserfield', 'auth_db'), 'notifyproblem');
             return;
         }
 
@@ -735,7 +735,7 @@ class auth_plugin_db extends auth_plugin_base {
             error_reporting($CFG->debug);
             ob_end_flush();
 
-            echo $OUTPUT->notification('Cannot connect the database.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbcannotconnect', 'auth_db'), 'notifyproblem');
             return;
         }
 
@@ -744,17 +744,17 @@ class auth_plugin_db extends auth_plugin_base {
                                 WHERE {$this->config->fielduser} <> 'random_unlikely_username'"); // Any unlikely name is ok here.
 
         if (!$rs) {
-            echo $OUTPUT->notification('Can not read external table.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbcannotreadtable', 'auth_db'), 'notifyproblem');
 
         } else if ($rs->EOF) {
-            echo $OUTPUT->notification('External table is empty.', 'notifyproblem');
+            echo $OUTPUT->notification(get_string('auth_dbtableempty', 'auth_db'), 'notifyproblem');
             $rs->close();
 
         } else {
             $fields_obj = $rs->FetchObj();
             $columns = array_keys((array)$fields_obj);
 
-            echo $OUTPUT->notification('External table contains following columns:<br />'.implode(', ', $columns), 'notifysuccess');
+            echo $OUTPUT->notification(get_string('auth_dbcolumnlist', 'auth_db', implode(', ', $columns)), 'notifysuccess');
             $rs->close();
         }
 
index 216fd31..8f77a3c 100644 (file)
@@ -68,5 +68,11 @@ $string['auth_dbuser'] = 'Username with read access to the database';
 $string['auth_dbuser_key'] = 'DB user';
 $string['auth_dbuserstoadd'] = 'User entries to add: {$a}';
 $string['auth_dbuserstoremove'] = 'User entries to remove: {$a}';
+$string['auth_dbnoexttable'] = 'External table not specified.';
+$string['auth_dbnouserfield'] = 'External user field not specified.';
+$string['auth_dbcannotconnect'] = 'Cannot connect to external database.';
+$string['auth_dbcannotreadtable'] = 'Cannot read external table.';
+$string['auth_dbtableempty'] = 'External table is empty.';
+$string['auth_dbcolumnlist'] = 'External table contains the following columns:<br />{$a}';
 $string['pluginname'] = 'External database';
 $string['privacy:metadata'] = 'The External database authentication plugin does not store any personal data.';
index 1bcce5f..e5cc3f4 100644 (file)
@@ -244,7 +244,7 @@ class restore_stepslib_date_testcase extends restore_date_testcase {
         // Testing the restore of an overridden grade.
         list($course, $assign) = $this->create_course_and_module('assign', []);
         $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]);
-        $assignobj = new testable_assign(context_module::instance($cm->id), $cm, $course);
+        $assignobj = new mod_assign_testable_assign(context_module::instance($cm->id), $cm, $course);
         $submission = $assignobj->get_user_submission($USER->id, true);
         $grade = $assignobj->get_user_grade($USER->id, true);
         $grade->grade = 75;
@@ -300,7 +300,7 @@ class restore_stepslib_date_testcase extends restore_date_testcase {
                 'completionusegrade' => 1 // Student must receive a grade to complete this activity.
             ]);
         $cm = $DB->get_record('course_modules', ['course' => $course->id, 'instance' => $assign->id]);
-        $assignobj = new testable_assign(context_module::instance($cm->id), $cm, $course);
+        $assignobj = new mod_assign_testable_assign(context_module::instance($cm->id), $cm, $course);
         $submission = $assignobj->get_user_submission($USER->id, true);
         $grade = $assignobj->get_user_grade($USER->id, true);
         $grade->grade = 75;
@@ -410,4 +410,4 @@ class restore_stepslib_date_testcase extends restore_date_testcase {
             $this->assertEquals($dates['originaldate'], $dates['restoredate']);
         }
     }
-}
\ No newline at end of file
+}
index 2ff89e6..bc09bc5 100644 (file)
@@ -1061,7 +1061,26 @@ class restore_ui_stage_process extends restore_ui_stage {
                 if (!empty($info->role_mappings->mappings)) {
                     $context = context_course::instance($this->ui->get_controller()->get_courseid());
                     $assignableroles = get_assignable_roles($context, ROLENAME_ALIAS, false);
-                    $html .= $renderer->role_mappings($info->role_mappings->mappings, $assignableroles);
+
+                    // Get current role mappings.
+                    $currentroles = role_fix_names(get_all_roles(), $context);
+                    // Get backup role mappings.
+                    $rolemappings = $info->role_mappings->mappings;
+
+                    array_map(function($rolemapping) use ($currentroles) {
+                        foreach ($currentroles as $role) {
+                            // Find matching archetype to determine the backup's shortname for label display.
+                            if ($rolemapping->archetype == $role->archetype) {
+                                $rolemapping->name = $rolemapping->shortname;
+                                break;
+                            }
+                        }
+                        if ($rolemapping->name == null) {
+                            $rolemapping->name = get_string('undefinedrolemapping', 'backup', $rolemapping->archetype);
+                        }
+                    }, $rolemappings);
+
+                    $html .= $renderer->role_mappings($rolemappings, $assignableroles);
                 }
                 break;
             default:
index 58e5477..8e74142 100644 (file)
@@ -20,7 +20,8 @@ M.core_completion.init = function(Y) {
             var current = args.state.get('value');
             var modulename = args.modulename.get('value'),
                 altstr,
-                iconkey;
+                iconkey,
+                button = args.image.get('parentNode');
 
 
             if (current == 1) {
@@ -32,6 +33,7 @@ M.core_completion.init = function(Y) {
                 iconkey = 'i/completion-manual-n';
                 args.state.set('value', 1);
             }
+            button.set('title', altstr);
 
             require(['core/templates', 'core/notification'], function(Templates, Notification) {
                 Templates.renderPix(iconkey, 'core', altstr).then(function(html) {
index b3cd970..b78acb6 100644 (file)
@@ -551,7 +551,8 @@ class core_course_renderer extends plugin_renderer_base {
                 $output .= html_writer::empty_tag('input', array(
                     'type' => 'hidden', 'name' => 'completionstate', 'value' => $newstate));
                 $output .= html_writer::tag('button',
-                    $this->output->pix_icon('i/completion-' . $completionicon, $imgalt), array('class' => 'btn btn-link'));
+                    $this->output->pix_icon('i/completion-' . $completionicon, $imgalt),
+                        array('class' => 'btn btn-link', 'title' => $imgalt));
                 $output .= html_writer::end_tag('div');
                 $output .= html_writer::end_tag('form');
             } else {
index ad195b1..0cbbe98 100644 (file)
@@ -43,7 +43,9 @@
 defined('MOODLE_INTERNAL') || die();
 
 function xmldb_enrol_paypal_upgrade($oldversion) {
-    global $CFG;
+    global $DB;
+
+    $dbman = $DB->get_manager();
 
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
@@ -57,5 +59,21 @@ function xmldb_enrol_paypal_upgrade($oldversion) {
     // Automatically generated Moodle v3.5.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2018053000) {
+
+        // Define field instanceid to be added to enrol_paypal.
+        // For some reason, some Moodle instances that are upgraded from old versions do not have this field.
+        $table = new xmldb_table('enrol_paypal');
+        $field = new xmldb_field('instanceid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0', 'userid');
+
+        // Conditionally launch add field instanceid.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Paypal savepoint reached.
+        upgrade_plugin_savepoint(true, 2018053000, 'enrol', 'paypal');
+    }
+
     return true;
 }
index 04ee0a4..61566dd 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018051400;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2018053000;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018050800;        // Requires this Moodle version
 $plugin->component = 'enrol_paypal';    // Full name of the plugin (used for diagnostics)
index d579bf6..5f5b3ff 100644 (file)
@@ -32,6 +32,22 @@ defined('MOODLE_INTERNAL') || die();
 
 class filter_emoticon extends moodle_text_filter {
 
+    /**
+     * Internal cache used for replacing. Multidimensional array;
+     * - dimension 1: language,
+     * - dimension 2: theme.
+     * @var array
+     */
+    protected static $emoticontexts = array();
+
+    /**
+     * Internal cache used for replacing. Multidimensional array;
+     * - dimension 1: language,
+     * - dimension 2: theme.
+     * @var array
+     */
+    protected static $emoticonimgs = array();
+
     /**
      * Apply the filter to the text
      *
@@ -49,7 +65,7 @@ class filter_emoticon extends moodle_text_filter {
             return $text;
         }
         if (in_array($options['originalformat'], explode(',', get_config('filter_emoticon', 'formats')))) {
-            $this->replace_emoticons($text);
+            return $this->replace_emoticons($text);
         }
         return $text;
     }
@@ -62,51 +78,73 @@ class filter_emoticon extends moodle_text_filter {
      * Replace emoticons found in the text with their images
      *
      * @param string $text to modify
-     * @return void
+     * @return string the modified result
      */
-    protected function replace_emoticons(&$text) {
+    protected function replace_emoticons($text) {
         global $CFG, $OUTPUT, $PAGE;
-        static $emoticontexts = array();    // internal cache used for replacing
-        static $emoticonimgs = array();     // internal cache used for replacing
 
         $lang = current_language();
         $theme = $PAGE->theme->name;
 
-        if (!isset($emoticontexts[$lang][$theme]) or !isset($emoticonimgs[$lang][$theme])) {
+        if (!isset(self::$emoticontexts[$lang][$theme]) or !isset(self::$emoticonimgs[$lang][$theme])) {
             // prepare internal caches
             $manager = get_emoticon_manager();
             $emoticons = $manager->get_emoticons();
-            $emoticontexts[$lang][$theme] = array();
-            $emoticonimgs[$lang][$theme] = array();
+            self::$emoticontexts[$lang][$theme] = array();
+            self::$emoticonimgs[$lang][$theme] = array();
             foreach ($emoticons as $emoticon) {
-                $emoticontexts[$lang][$theme][] = $emoticon->text;
-                $emoticonimgs[$lang][$theme][] = $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
+                self::$emoticontexts[$lang][$theme][] = $emoticon->text;
+                self::$emoticonimgs[$lang][$theme][] = $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
             }
             unset($emoticons);
         }
 
-        if (empty($emoticontexts[$lang][$theme])) { // No emoticons defined, nothing to process here
-            return;
+        if (empty(self::$emoticontexts[$lang][$theme])) { // No emoticons defined, nothing to process here.
+            return $text;
         }
 
-        // detect all the <script> zones to take out
-        $excludes = array();
-        preg_match_all('/<script language(.+?)<\/script>/is', $text, $listofexcludes);
+        // Detect all zones that we should not handle (including the nested tags).
+        $processing = preg_split('/(<\/?(?:span|script)[^>]*>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
 
-        // take out all the <script> zones from text
-        foreach (array_unique($listofexcludes[0]) as $key => $value) {
-            $excludes['<+'.$key.'+>'] = $value;
-        }
-        if ($excludes) {
-            $text = str_replace($excludes, array_keys($excludes), $text);
-        }
+        // Initialize the results.
+        $resulthtml = "";
+        $exclude = 0;
 
-        // this is the meat of the code - this is run every time
-        $text = str_replace($emoticontexts[$lang][$theme], $emoticonimgs[$lang][$theme], $text);
+        // Define the patterns that mark the start of the forbidden zones.
+        $excludepattern = array('/^<script/is', '/^<span[^>]+class="nolink[^"]*"/is');
 
-        // Recover all the <script> zones to text
-        if ($excludes) {
-            $text = str_replace(array_keys($excludes), $excludes, $text);
+        // Loop through the fragments.
+        foreach ($processing as $fragment) {
+            // If we are not ignoring, we MUST test if we should.
+            if ($exclude == 0) {
+                foreach ($excludepattern as $exp) {
+                    if (preg_match($exp, $fragment)) {
+                        $exclude = $exclude + 1;
+                        break;
+                    }
+                }
+            }
+            if ($exclude > 0) {
+                // If we are ignoring the fragment, then we must check if we may have reached the end of the zone.
+                if (strpos($fragment, '</span') !== false || strpos($fragment, '</script') !== false) {
+                    $exclude -= 1;
+                    // This is needed because of a double increment at the first element.
+                    if ($exclude == 1) {
+                        $exclude -= 1;
+                    }
+                } else if (strpos($fragment, '<span') !== false || strpos($fragment, '<script') !== false) {
+                    // If we find a nested tag we increase the exclusion level.
+                    $exclude = $exclude + 1;
+                }
+            } else if (strpos($fragment, '<span') === false ||
+                       strpos($fragment, '</span') === false) {
+                // This is the meat of the code - this is run every time.
+                // This code only runs for fragments that are not ignored (including the tags themselves).
+                $fragment = str_replace(self::$emoticontexts[$lang][$theme], self::$emoticonimgs[$lang][$theme], $fragment);
+            }
+            $resulthtml .= $fragment;
         }
+
+        return $resulthtml;
     }
 }
index da3576a..10ee76e 100644 (file)
@@ -34,29 +34,124 @@ require_once($CFG->dirroot . '/filter/emoticon/filter.php'); // Include the code
 class filter_emoticon_testcase extends advanced_testcase {
 
     /**
-     * Verify configured target formats are observed. Just that.
+     * Tests the filter doesn't affect nolink classes.
+     *
+     * @dataProvider filter_emoticon_provider
      */
-    public function test_filter_emoticon_formats() {
-
-        $this->resetAfterTest(true); // We are modifying the config.
+    public function test_filter_emoticon($input, $format, $expected) {
+        $this->resetAfterTest();
 
         $filter = new testable_filter_emoticon();
+        $this->assertEquals($expected, $filter->filter($input, [
+                'originalformat' => $format,
+            ]));
+    }
+
+    /**
+     * The data provider for filter emoticon tests.
+     *
+     * @return  array
+     */
+    public function filter_emoticon_provider() {
+        $grr = '(grr)';
+        return [
+            'FORMAT_MOODLE is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_MOODLE,
+                'expected' => $grr,
+            ],
+            'FORMAT_MARKDOWN is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_MARKDOWN,
+                'expected' => $grr,
+            ],
+            'FORMAT_PLAIN is not filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_PLAIN,
+                'expected' => $grr,
+            ],
+            'FORMAT_HTML is filtered' => [
+                'input' => $grr,
+                'format' => FORMAT_HTML,
+                'expected' => $this->get_converted_content_for_emoticon($grr),
+            ],
+            'Script tag should not be processed' => [
+                'input' => "<script language='javascript'>alert('{$grr}');</script>",
+                'format' => FORMAT_HTML,
+                'expected' => "<script language='javascript'>alert('{$grr}');</script>",
+            ],
+            'Basic nolink should not be processed' => [
+                'input' => '<span class="nolink">(n)</span>',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink">(n)</span>',
+            ],
+            'Nested nolink should not be processed' => [
+                'input' => '<span class="nolink"><span>(n)</span>(n)</span>',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink"><span>(n)</span>(n)</span>',
+            ],
+            'Nested nolink should not be processed but following emoticon' => [
+                'input' => '<span class="nolink"><span>(n)</span>(n)</span>(n)',
+                'format' => FORMAT_HTML,
+                'expected' => '<span class="nolink"><span>(n)</span>(n)</span>' . $this->get_converted_content_for_emoticon('(n)'),
+            ],
+        ];
+    }
+
+    /**
+     * Translate the text for a single emoticon into the rendered value.
+     *
+     * @param   string  $text The text to translate.
+     * @return  string
+     */
+    public function get_converted_content_for_emoticon($text) {
+        global $OUTPUT;
+        $manager = get_emoticon_manager();
+        $emoticons = $manager->get_emoticons();
+        foreach ($emoticons as $emoticon) {
+            if ($emoticon->text == $text) {
+                return $OUTPUT->render($manager->prepare_renderable_emoticon($emoticon));
+            }
+        }
+
+        return $text;
+    }
 
-        // Verify texts not matching target formats aren't filtered.
+    /**
+     * Tests the filter doesn't break anything if activated but invalid format passed.
+     *
+     */
+    public function test_filter_invalidformat() {
+        global $PAGE;
+        $this->resetAfterTest();
+
+        $filter = new testable_filter_emoticon();
+        $input = '(grr)';
         $expected = '(grr)';
-        $options = array('originalformat' => FORMAT_MOODLE); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
 
-        $options = array('originalformat' => FORMAT_MARKDOWN); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $this->assertEquals($expected, $filter->filter($input, [
+            'originalformat' => 'ILLEGALFORMAT',
+        ]));
+    }
+
+    /**
+     * Tests the filter doesn't break anything if activated but no emoticons available.
+     *
+     */
+    public function test_filter_emptyemoticons() {
+        global $CFG;
+        $this->resetAfterTest();
+        // Empty the emoticons array.
+        $CFG->emoticons = null;
 
-        $options = array('originalformat' => FORMAT_PLAIN); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $filter = new filter_emoticon(context_system::instance(), array('originalformat' => FORMAT_HTML));
+
+        $input = '(grr)';
+        $expected = '(grr)';
 
-        // And texts matching target formats are filtered.
-        $expected = '<img class="icon emoticon" alt="angry" title="angry" src="https://www.example.com/moodle/theme/image.php/_s/boost/core/1/s/angry" />';
-        $options = array('originalformat' => FORMAT_HTML); // Only FORMAT_HTML is filtered, see {@link testable_filter_emoticon}.
-        $this->assertEquals($expected, $filter->filter('(grr)', $options));
+        $this->assertEquals($expected, $filter->filter($input, [
+            'originalformat' => FORMAT_HTML,
+        ]));
     }
 }
 
@@ -65,6 +160,9 @@ class filter_emoticon_testcase extends advanced_testcase {
  */
 class testable_filter_emoticon extends filter_emoticon {
     public function __construct() {
+        // Reset static emoticon caches.
+        parent::$emoticontexts = array();
+        parent::$emoticonimgs = array();
         // Use this context for filtering.
         $this->context = context_system::instance();
         // Define FORMAT_HTML as only one filtering in DB.
index 3a1ce5e..b898b99 100644 (file)
@@ -274,13 +274,13 @@ echo $OUTPUT->container_start('buttons mdl-align');
 if ($moving) {
     echo $OUTPUT->single_button(new moodle_url('index.php', array('id'=>$course->id)), get_string('cancel'), 'get');
 } else {
-    echo $OUTPUT->single_button(new moodle_url('category.php', array('courseid'=>$course->id)), get_string('addcategory', 'grades'), 'get');
-    echo $OUTPUT->single_button(new moodle_url('item.php', array('courseid'=>$course->id)), get_string('additem', 'grades'), 'get');
-
+    echo $OUTPUT->single_button(new moodle_url('item.php', array('courseid' => $course->id)), get_string('additem',
+        'grades'), 'get');
     if (!empty($CFG->enableoutcomes)) {
         echo $OUTPUT->single_button(new moodle_url('outcomeitem.php', array('courseid'=>$course->id)), get_string('addoutcomeitem', 'grades'), 'get');
     }
-
+    echo $OUTPUT->single_button(new moodle_url('category.php', array('courseid' => $course->id)), get_string('addcategory',
+        'grades'), 'get');
     //echo $OUTPUT->(new moodle_url('index.php', array('id'=>$course->id, 'action'=>'autosort')), get_string('autosort', 'grades'), 'get');
 }
 
index 3680d03..880a8c5 100644 (file)
@@ -632,7 +632,7 @@ $string['invalidsection'] = 'Invalid section.';
 $string['invaliduserchangeme'] = 'Username "changeme" is reserved -- you cannot create an account with it.';
 $string['ipblocked'] = 'This site is not available currently.';
 $string['ipblocker'] = 'IP blocker';
-$string['ipblockersyntax'] = 'Put every entry on one line. Valid entries are either full IP address (such as <b>192.168.10.1</b>) which matches a single host; or partial address (such as <b>192.168</b>) which matches any address starting with those numbers; or CIDR notation (such as <b>231.54.211.0/20</b>); or a range of IP addresses (such as <b>231.3.56.10-20</b>) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. Blank lines are ignored.';
+$string['ipblockersyntax'] = 'Put every entry on one line. Valid entries are either full IP address (such as <b>192.168.10.1</b>) which matches a single host; or partial address (such as <b>192.168</b>) which matches any address starting with those numbers; or CIDR notation (such as <b>231.54.211.0/20</b>); or a range of IP addresses (such as <b>231.3.56.10-20</b>) where the range applies to the last part of the address. Text domain names (like \'example.com\') are not supported. Blank lines, and text following a "#" character are ignored.';
 $string['iplookup'] = 'IP address lookup';
 $string['iplookupgeoplugin'] = '<a href="http://www.geoplugin.com">geoPlugin</a> service is currently being used to look up geographical information. For more accurate results we recommend installing a local copy of the MaxMind GeoLite database.';
 $string['iplookupinfo'] = 'By default Moodle uses the free online NetGeo (The Internet Geographic Database) server to lookup location of IP addresses, unfortunately this database is not maintained anymore and may return <em>wildly incorrect</em> data.
index dc711e8..abaf88e 100644 (file)
@@ -326,6 +326,7 @@ $string['timetaken'] = 'Time taken';
 $string['title'] = 'Title';
 $string['totalcategorysearchresults'] = 'Total categories: {$a}';
 $string['totalcoursesearchresults'] = 'Total courses: {$a}';
+$string['undefinedrolemapping'] = 'Role mapping undefined for: \'{$a}\' archetype';
 $string['unnamedsection'] = 'Unnamed section';
 $string['userinfo'] = 'Userinfo';
 $string['module'] = 'Module';
index 7c1b469..f178887 100644 (file)
@@ -26,6 +26,7 @@ $string['actfilterhdr'] = 'Active filters';
 $string['addfilter'] = 'Add filter';
 $string['anycategory'] = 'any category';
 $string['anycourse'] = 'any course';
+$string['anycourses'] = 'Enrolled in any course';
 $string['anyfield'] = 'any field';
 $string['anyrole'] = 'any role';
 $string['anyvalue'] = 'any value';
index 3889226..15b8463 100644 (file)
@@ -36,6 +36,7 @@
  */
 
 $string['application/epub_zip'] = 'EPUB ebook';
+$string['application/json'] = '{$a->MIMETYPE2} text';
 $string['application/msword'] = 'Word document';
 $string['application/pdf'] = 'PDF document';
 $string['application/vnd.moodle.backup'] = 'Moodle backup';
index db7c5d6..09917b6 100644 (file)
@@ -75,6 +75,8 @@ $string['checkdir'] = 'Check dir';
 $string['checkdiradvice'] = 'Ensure the data directory exists and is writable.';
 $string['incourse'] = 'in course {$a}';
 $string['index'] = 'Index';
+$string['indexwhendisabledfullnotice'] = 'Indexing is currently not permitted when search is disabled. To enable this, please see the <a href="{$a->url}">searchindexwhendisabled</a> setting.';
+$string['indexwhendisabledshortnotice'] = 'Indexing is not available.';
 $string['invalidindexerror'] = 'Index directory either contains an invalid index, or nothing at all.';
 $string['ittook'] = 'It took';
 $string['matchingfile'] = 'Matched from file <span class="filename">{$a}</span>';
index b14741e..c3e39cd 100644 (file)
@@ -3614,14 +3614,15 @@ class admin_setting_configiplist extends admin_setting_configtextarea {
      */
     public function validate($data) {
         if(!empty($data)) {
-            $ips = explode("\n", $data);
+            $lines = explode("\n", $data);
         } else {
             return true;
         }
         $result = true;
         $badips = array();
-        foreach($ips as $ip) {
-            $ip = trim($ip);
+        foreach ($lines as $line) {
+            $tokens = explode('#', $line);
+            $ip = trim($tokens[0]);
             if (empty($ip)) {
                 continue;
             }
index ab06031..fd51f0b 100644 (file)
Binary files a/lib/amd/build/tag.min.js and b/lib/amd/build/tag.min.js differ
index 1f1d3fb..1609564 100644 (file)
@@ -150,8 +150,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                 ]).done(function(s) {
                     var el = $('<div><form id="combinetags_form" class="form-inline">' +
                         '<p class="description"></p><p class="options"></p>' +
-                        '<p class="mdl-align"><input type="submit" id="combinetags_submit"/>' +
-                        '<input type="button" id="combinetags_cancel"/></p>' +
+                        '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="combinetags_submit"/>' +
+                        '<input type="button" class="btn btn-secondary" id="combinetags_cancel"/></p>' +
                         '</form></div>');
                     el.find('.description').html(s[1]);
                     el.find('#combinetags_submit').attr('value', s[2]);
@@ -223,8 +223,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                         '<input type="hidden" name="sesskey" value="' + M.cfg.sesskey + '"/>' +
                         '<p><label for="id_tagslist">' + s[1] + '</label>' +
                         '<input type="text" id="id_tagslist" name="tagslist"/></p>' +
-                        '<p class="mdl-align"><input type="submit" id="addtags_submit"/>' +
-                        '<input type="button" id="addtags_cancel"/></p>' +
+                        '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="addtags_submit"/>' +
+                        '<input type="button" class="btn btn-secondary" id="addtags_cancel"/></p>' +
                         '</form></div>');
                     el.find('#addtags_form').attr('action', window.location.href);
                     el.find('#addtags_submit').attr('value', s[2]);
@@ -295,8 +295,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/str'
                             '<input id="addtagcoll_name" type="text"/></p>' +
                             '<p><label for="addtagcoll_searchable"></label>: ' +
                             '<input id="addtagcoll_searchable" type="checkbox" value="1" checked/></p>' +
-                            '<p class="mdl-align"><input type="submit" id="addtagcoll_submit"/>' +
-                            '<input type="button" id="addtagcoll_cancel"/></p>' +
+                            '<p class="mdl-align"><input type="submit" class="btn btn-primary" id="addtagcoll_submit"/>' +
+                            '<input type="button" class="btn btn-secondary" id="addtagcoll_cancel"/></p>' +
                             '</form></div>');
                         el.find('label[for="addtagcoll_name"]').html(s[1]);
                         el.find('label[for="addtagcoll_searchable"]').html(s[2]);
index 23c0191..4a84e55 100644 (file)
@@ -139,6 +139,7 @@ abstract class core_filetypes {
             'jpg' => array('type' => 'image/jpeg', 'icon' => 'jpeg', 'groups' => array('image', 'web_image'), 'string' => 'image'),
             'jqz' => array('type' => 'text/xml', 'icon' => 'markup'),
             'js' => array('type' => 'application/x-javascript', 'icon' => 'text', 'groups' => array('web_file')),
+            'json' => array('type' => 'application/json', 'icon' => 'text'),
             'latex' => array('type' => 'application/x-latex', 'icon' => 'text'),
             'm' => array('type' => 'text/plain', 'icon' => 'sourcecode'),
             'mbz' => array('type' => 'application/vnd.moodle.backup', 'icon' => 'moodle'),
index 0f151ec..4656462 100644 (file)
@@ -1,4 +1,4 @@
 .atto_charmap_selector button {
-    width: 2em;
-    padding: 0 3px;
+    width: 2.18rem;
+    margin: 0.1rem;
 }
index 30e0e37..422e2d8 100644 (file)
Binary files a/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-debug.js and b/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-debug.js differ
index 77b7449..339dad4 100644 (file)
Binary files a/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-min.js and b/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button-min.js differ
index 30e0e37..422e2d8 100644 (file)
Binary files a/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button.js and b/lib/editor/atto/plugins/charmap/yui/build/moodle-atto_charmap-button/moodle-atto_charmap-button.js differ
index 6f549b4..397f1fe 100644 (file)
@@ -377,7 +377,7 @@ Y.namespace('M.atto_charmap').Button = Y.Base.create('button', Y.M.editor_atto.E
             '<div class="{{CSS.CHARMAP}}">' +
                 '{{#each CHARMAP}}' +
                     '{{#if this.[2]}}' +
-                    '<button class="{{../../CSS.BUTTON}}" ' +
+                    '<button class="btn btn-default btn-sm {{../../CSS.BUTTON}}" ' +
                         'aria-label="{{get_string this.[3] ../../component}}" ' +
                         'title="{{get_string this.[3] ../../component}}" ' +
                         'data-character="{{this.[0]}}" ' +
index f6081a0..8df2d6b 100644 (file)
@@ -1,16 +1,3 @@
-.atto_equation_library .yui3-tabview-list {
-    border: none;
-}
-
-.atto_equation_library .yui3-tab-selected .yui3-tab-label,
-.yui3-skin-sam #atto_equation_library .yui3-tab-selected .yui3-tab-label:focus,
-.yui3-skin-sam #atto_equation_library .yui3-tab-selected .yui3-tab-label:hover {
-    background: none;
-    color: black;
-    border-top-left-radius: 4px;
-    border-top-right-radius: 4px;
-}
-
 .atto_equation_library button {
     margin: 0.25%;
     min-width: 12%;
index ae1d92c..fdc94ce 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js differ
index a5ff287..00d0781 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js differ
index f98b58c..1274071 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js differ
index 849a967..6db2cb8 100644 (file)
@@ -60,29 +60,34 @@ var COMPONENTNAME = 'atto_equation',
                 '<textarea class="fullwidth {{CSS.EQUATION_TEXT}}" ' +
                         'id="{{elementid}}_{{CSS.EQUATION_TEXT}}" rows="8"></textarea><br/>' +
                 '<label for="{{elementid}}_{{CSS.EQUATION_PREVIEW}}">{{get_string "preview" component}}</label>' +
-                '<div describedby="{{elementid}}_cursorinfo" class="well well-small fullwidth {{CSS.EQUATION_PREVIEW}}" ' +
+                '<div describedby="{{elementid}}_cursorinfo" class="well well-small p-1 fullwidth {{CSS.EQUATION_PREVIEW}}" ' +
                         'id="{{elementid}}_{{CSS.EQUATION_PREVIEW}}"></div>' +
                 '<div id="{{elementid}}_cursorinfo">{{get_string "cursorinfo" component}}</div>' +
                 '<div class="mdl-align">' +
                     '<br/>' +
-                    '<button class="{{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' +
+                    '<button class="btn btn-default {{CSS.SUBMIT}}">{{get_string "saveequation" component}}</button>' +
                 '</div>' +
             '</form>',
         LIBRARY: '' +
             '<div class="{{CSS.LIBRARY}}">' +
-                '<ul>' +
+                '<ul class="root nav nav-tabs m-b-1" role="tablist">' +
                     '{{#each library}}' +
-                        '<li><a href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' +
-                            '{{get_string groupname ../component}}' +
-                        '</a></li>' +
+                        '<li  class="nav-item">' +
+                            '<a class="nav-link" href="#{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}" ' +
+                                ' role="tab" data-toggle="tab">' +
+                                '{{get_string groupname ../component}}' +
+                            '</a>' +
+                        '</li>' +
                     '{{/each}}' +
                 '</ul>' +
-                '<div class="{{CSS.LIBRARY_GROUPS}}">' +
+                '<div class="tab-content m-b-1 {{CSS.LIBRARY_GROUPS}}">' +
                     '{{#each library}}' +
-                        '<div id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' +
+                        '<div data-medium-type="{{CSS.LINK}}" class="tab-pane" ' +
+                        'id="{{../elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}_{{@key}}">' +
                             '<div role="toolbar">' +
                             '{{#split "\n" elements}}' +
-                                '<button tabindex="-1" data-tex="{{this}}" aria-label="{{this}}" title="{{this}}">' +
+                                '<button class="btn btn-default" tabindex="-1" data-tex="{{this}}"' +
+                                    'aria-label="{{this}}" title="{{this}}">' +
                                     '{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}' +
                                 '</button>' +
                             '{{/split}}' +
@@ -220,13 +225,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         var content = this._getDialogueContent();
         dialogue.set('bodyContent', content);
 
-        var library = content.one(SELECTORS.LIBRARY);
+        content.one('.nav-item:first-child .nav-link').getDOMNode().click();
 
-        var tabview = new Y.TabView({
-            srcNode: library
-        });
-
-        tabview.render();
         dialogue.show();
         // Notify the filters about the modified nodes.
         require(['core/event'], function(event) {
index a913c73..8656f0e 100644 (file)
@@ -6,7 +6,7 @@
 }
 
 .atto_image_preview_box {
-    max-height: 200px;
+    max-height: 150px;
     margin-bottom: 1em;
     overflow: auto;
 }
     cursor: pointer;
 }
 
-.atto_image_size {
-    display: inline-block;
-}
-
-.atto_image_size input[type=checkbox] {
-    margin-left: 1em;
-    margin-right: 1em;
-}
-
-.atto_image_size input[type=text] {
-    width: 3em;
-}
-
-.atto_image_size label {
-    display: inline-block;
-}
-
 .atto_image_button_text-top {
     vertical-align: text-top;
     margin: 0 0.5em;
index 69ab2fd..899c1a8 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js differ
index 78edf53..bbe3e39 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js differ
index 18416c3..293794c 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js differ
index f9c0d60..5cc1626 100644 (file)
@@ -94,52 +94,79 @@ var CSS = {
 
     TEMPLATE = '' +
             '<form class="atto_form">' +
-                '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
-                '<input class="fullwidth {{CSS.INPUTURL}}" type="url" id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
-                '<br/>' +
 
                 // Add the repository browser button.
                 '{{#if showFilepicker}}' +
-                    '<button class="{{CSS.IMAGEBROWSER}}" type="button">{{get_string "browserepositories" component}}</button>' +
+                    '<div class="m-b-1">' +
+                        '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
+                        '<div class="input-group input-append w-100">' +
+                            '<input class="form-control {{CSS.INPUTURL}}" type="url" ' +
+                            'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
+                            '<span class="input-group-append">' +
+                                '<button class="btn btn-default {{CSS.IMAGEBROWSER}}" type="button">' +
+                                '{{get_string "browserepositories" component}}</button>' +
+                            '</span>' +
+                        '</div>' +
+                    '</div>' +
+                '{{else}}' +
+                    '<div class="m-b-1">' +
+                        '<label for="{{elementid}}_{{CSS.INPUTURL}}">{{get_string "enterurl" component}}</label>' +
+                        '<input class="form-control fullwidth {{CSS.INPUTURL}}" type="url" ' +
+                        'id="{{elementid}}_{{CSS.INPUTURL}}" size="32"/>' +
+                    '</div>' +
                 '{{/if}}' +
 
                 // Add the Alt box.
-                '<div style="display:none" role="alert" class="warning {{CSS.IMAGEALTWARNING}}">' +
+                '<div style="display:none" role="alert" class="alert alert-warning m-b-1 {{CSS.IMAGEALTWARNING}}">' +
                     '{{get_string "presentationoraltrequired" component}}' +
                 '</div>' +
+                '<div class="m-b-1">' +
                 '<label for="{{elementid}}_{{CSS.INPUTALT}}">{{get_string "enteralt" component}}</label>' +
-                '<input class="fullwidth {{CSS.INPUTALT}}" type="text" value="" id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
-                '<br/>' +
+                '<input class="form-control fullwidth {{CSS.INPUTALT}}" type="text" value="" ' +
+                'id="{{elementid}}_{{CSS.INPUTALT}}" size="32"/>' +
 
                 // Add the presentation select box.
-                '<input type="checkbox" class="{{CSS.IMAGEPRESENTATION}}" id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
-                '<label class="sameline" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
+                '<div class="form-check">' +
+                '<input type="checkbox" class="form-check-input {{CSS.IMAGEPRESENTATION}}" ' +
+                    'id="{{elementid}}_{{CSS.IMAGEPRESENTATION}}"/>' +
+                '<label class="form-check-label" for="{{elementid}}_{{CSS.IMAGEPRESENTATION}}">' +
                     '{{get_string "presentation" component}}' +
                 '</label>' +
-                '<br/>' +
+                '</div>' +
+                '</div>' +
 
                 // Add the size entry boxes.
-                '<label class="sameline" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
-                '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="{{CSS.INPUTSIZE}}">' +
+                '<div class="m-b-1">' +
+                '<label class="" for="{{elementid}}_{{CSS.INPUTSIZE}}">{{get_string "size" component}}</label>' +
+                '<div id="{{elementid}}_{{CSS.INPUTSIZE}}" class="form-inline {{CSS.INPUTSIZE}}">' +
                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTWIDTH}}">{{get_string "width" component}}</label>' +
-                '<input type="text" class="{{CSS.INPUTWIDTH}} input-mini" id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x ' +
+                '<input type="text" class="form-control m-r-1 input-mini {{CSS.INPUTWIDTH}}" ' +
+                'id="{{elementid}}_{{CSS.INPUTWIDTH}}" size="4"/> x' +
 
                 // Add the height entry box.
                 '<label class="accesshide" for="{{elementid}}_{{CSS.INPUTHEIGHT}}">{{get_string "height" component}}</label>' +
-                '<input type="text" class="{{CSS.INPUTHEIGHT}} input-mini" id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
+                '<input type="text" class="form-control m-l-1 input-mini {{CSS.INPUTHEIGHT}}" ' +
+                'id="{{elementid}}_{{CSS.INPUTHEIGHT}}" size="4"/>' +
 
                 // Add the constrain checkbox.
-                '<input type="checkbox" class="{{CSS.INPUTCONSTRAIN}} sameline" id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
-                '<label for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">{{get_string "constrain" component}}</label>' +
+                '<div class="form-check m-l-2">' +
+                '<input type="checkbox" class="form-check-input {{CSS.INPUTCONSTRAIN}}" ' +
+                'id="{{elementid}}_{{CSS.INPUTCONSTRAIN}}"/>' +
+                '<label class="form-check-label" for="{{elementid}}_{{CSS.INPUTCONSTRAIN}}">' +
+                '{{get_string "constrain" component}}</label>' +
+                '</div>' +
+                '</div>' +
                 '</div>' +
 
                 // Add the alignment selector.
-                '<label class="sameline" for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
-                '<select class="{{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
+                '<div class="form-inline m-b-1">' +
+                '<label class="for="{{elementid}}_{{CSS.INPUTALIGNMENT}}">{{get_string "alignment" component}}</label>' +
+                '<select class="custom-select {{CSS.INPUTALIGNMENT}}" id="{{elementid}}_{{CSS.INPUTALIGNMENT}}">' +
                     '{{#each alignments}}' +
                         '<option value="{{value}}">{{get_string str ../component}}</option>' +
                     '{{/each}}' +
                 '</select>' +
+                '</div>' +
                 // Hidden input to store custom styles.
                 '<input type="hidden" class="{{CSS.INPUTCUSTOMSTYLE}}"/>' +
                 '<br/>' +
@@ -151,7 +178,7 @@ var CSS = {
                 '</div>' +
 
                 // Add the submit button and close the form.
-                '<button class="{{CSS.INPUTSUBMIT}}" type="submit">{{get_string "saveimage" component}}</button>' +
+                '<button class="btn btn-default {{CSS.INPUTSUBMIT}}" type="submit">{{get_string "saveimage" component}}</button>' +
                 '</div>' +
             '</form>',
 
index 7aa1907..d1c971e 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-debug.js differ
index f7b3aeb..4f1d046 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button-min.js differ
index 7aa1907..d1c971e 100644 (file)
Binary files a/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js and b/lib/editor/atto/plugins/link/yui/build/moodle-atto_link-button/moodle-atto_link-button.js differ
index 8226483..51112dd 100644 (file)
@@ -41,20 +41,32 @@ var COMPONENTNAME = 'atto_link',
     },
     TEMPLATE = '' +
             '<form class="atto_form">' +
-                '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
-                '<input class="fullwidth url {{CSS.URLINPUT}}" type="url" id="{{elementid}}_atto_link_urlentry" size="32"/><br/>' +
-
-                // Add the repository browser button.
                 '{{#if showFilepicker}}' +
-                    '<button class="openlinkbrowser">{{get_string "browserepositories" component}}</button>' +
-                    '<br/>' +
+                    '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
+                    '<div class="input-group input-append w-100 m-b-1">' +
+                        '<input class="form-control url {{CSS.URLINPUT}}" type="url" ' +
+                        'id="{{elementid}}_atto_link_urlentry"/>' +
+                        '<span class="input-group-append">' +
+                            '<button class="btn btn-default openlinkbrowser" type="button">' +
+                            '{{get_string "browserepositories" component}}</button>' +
+                        '</span>' +
+                    '</div>' +
+                '{{else}}' +
+                    '<div class="m-b-1">' +
+                        '<label for="{{elementid}}_atto_link_urlentry">{{get_string "enterurl" component}}</label>' +
+                        '<input class="form-control fullwidth url {{CSS.URLINPUT}}" type="url" ' +
+                        'id="{{elementid}}_atto_link_urlentry" size="32"/>' +
+                    '</div>' +
                 '{{/if}}' +
-                '<input type="checkbox" class="newwindow" id="{{elementid}}_{{CSS.NEWWINDOW}}"/>' +
-                '<label class="sameline" for="{{elementid}}_{{CSS.NEWWINDOW}}">{{get_string "openinnewwindow" component}}</label>' +
-                '<br/>' +
+                '<div class="form-check">' +
+                    '<input type="checkbox" class="form-check-input newwindow" id="{{elementid}}_{{CSS.NEWWINDOW}}"/>' +
+                    '<label class="form-check-label" for="{{elementid}}_{{CSS.NEWWINDOW}}">' +
+                    '{{get_string "openinnewwindow" component}}' +
+                    '</label>' +
+                '</div>' +
                 '<div class="mdl-align">' +
                     '<br/>' +
-                    '<button type="submit" class="submit">{{get_string "createlink" component}}</button>' +
+                    '<button type="submit" class="btn btn-default submit">{{get_string "createlink" component}}</button>' +
                 '</div>' +
             '</form>';
 Y.namespace('M.atto_link').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
index ee5a759..f1ecb10 100644 (file)
@@ -1,106 +1,4 @@
-.atto_form.atto_media #video input,
-.atto_form.atto_media #audio input,
-.atto_form.atto_media #link input {
-    box-sizing: border-box;
-    height: inherit;
-}
-
 .atto_form.atto_media > .tab-content {
     max-height: 45vh;
     overflow-x: hidden;
-    padding-left: 20px;
-    padding-right: 20px;
-    margin-left: -20px;
-    margin-right: -21px;
-}
-
-.atto_form.atto_media [id$="-advanced-settings"] label {
-    margin-right: 10px;
-}
-
-.atto_form.atto_media label {
-    display: inline-block;
-}
-
-.atto_form.atto_media label > span {
-    display: inline-block;
-    min-width: 6em;
-}
-
-.atto_form.atto_media .atto_media_track_lang_entry,
-.atto_form.atto_media .atto_media_track_label_entry {
-    width: 168px;
-}
-
-.atto_form.atto_media .atto_media_track_source {
-    margin-bottom: 10px;
-}
-
-.atto_form.atto_media select {
-    margin-right: 10px;
-}
-
-.atto_form.atto_media [id$="-tracks"] input[type=checkbox] {
-    margin-left: 10px;
-}
-
-.atto_form.atto_media .atto_media_track ~ .atto_media_track {
-    margin-top: 5px;
-    padding-top: 10px;
-    border-top: 1px solid #e5e5e5;
-}
-
-.atto_form.atto_media label.fullwidth {
-    width: 100%;
-}
-
-.atto_media_postersize {
-    display: inline-block;
-}
-
-.atto_media_postersize input[type=text] {
-    width: 3em;
-}
-
-input[size].atto_media_url_entry {
-    width: calc(100% - 15px);
-}
-
-.openmediabrowser {
-    margin-top: -4px;
-}
-
-.addcomponent,
-.removecomponent {
-    font-weight: bold;
-    margin-right: 10px;
-}
-
-.trackhelp {
-    text-align: right;
-}
-
-.atto_form.atto_media .atto_media_source > label {
-    width: calc(100% - 153px);
-}
-
-.atto_form.atto_media .atto_media_track_lang_entry,
-.atto_form.atto_media .atto_media_track_label_entry {
-    width: 116px;
-}
-
-.langlabel {
-    width: 42%;
-}
-
-.labellabel {
-    width: 44%;
-}
-
-.defaultlabel {
-    width: 14%;
-}
-
-[data-medium-type=link] label {
-    width: 100%;
-}
+}
\ No newline at end of file
index 603896a..541685a 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-debug.js differ
index 0049c93..cfdd7c1 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button-min.js differ
index 603896a..541685a 100644 (file)
Binary files a/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js and b/lib/editor/atto/plugins/media/yui/build/moodle-atto_media-button/moodle-atto_media-button.js differ
index f5a63c0..b406076 100644 (file)
@@ -114,7 +114,7 @@ var COMPONENTNAME = 'atto_media',
     TEMPLATES = {
         ROOT: '' +
             '<form class="mform atto_form atto_media" id="{{elementid}}_atto_media_form">' +
-                '<ul class="root nav nav-tabs" role="tablist">' +
+                '<ul class="root nav nav-tabs m-b-1" role="tablist">' +
                     '<li data-medium-type="{{CSS.LINK}}" class="nav-item">' +
                         '<a class="nav-link active" href="#{{elementid}}_{{CSS.LINK}}" role="tab" data-toggle="tab">' +
                             '{{get_string "link" component}}' +
@@ -144,17 +144,15 @@ var COMPONENTNAME = 'atto_media',
                 '</div>' +
                 '<div class="mdl-align">' +
                     '<br/>' +
-                    '<button class="submit" type="submit">{{get_string "createmedia" component}}</button>' +
+                    '<button class="btn btn-default submit" type="submit">{{get_string "createmedia" component}}</button>' +
                 '</div>' +
             '</form>',
         TAB_PANES: {
             LINK: '' +
                 '{{renderPartial "form_components.source" context=this id=CSS.LINK_SOURCE}}' +
-                '<label>' +
-                    'Enter name' +
-                    '<input class="fullwidth {{CSS.NAME_INPUT}}" type="text" id="{{elementid}}_link_nameentry"' +
-                        'size="32" required="true"/>' +
-                '</label>',
+                '<label for="{{elementid}}_link_nameentry">{{get_string "entername" component}}</label>' +
+                '<input class="form-control fullwidth {{CSS.NAME_INPUT}}" type="text" id="{{elementid}}_link_nameentry"' +
+                        'size="32" required="true"/>',
             VIDEO: '' +
                 '{{renderPartial "form_components.source" context=this id=CSS.MEDIA_SOURCE entersourcelabel="videosourcelabel"' +
                     ' addcomponentlabel="addsource" multisource="true" addsourcehelp=helpStrings.addsource}}' +
@@ -200,13 +198,19 @@ var COMPONENTNAME = 'atto_media',
         FORM_COMPONENTS: {
             SOURCE: '' +
                 '<div class="{{CSS.SOURCE}} {{id}}">' +
-                    '<label>' +
+                    '<div class="m-b-1">' +
+                        '<label for="url-input">' +
                         '{{#entersourcelabel}}{{get_string ../entersourcelabel ../component}}{{/entersourcelabel}}' +
-                        '{{^entersourcelabel}}{{get_string "entersource" ../component}}{{/entersourcelabel}}</a>' +
-                        '<br/>' +
-                        '<input class="{{CSS.URL_INPUT}}" type="url" size="32"/>' +
-                    '</label>' +
-                    '<button class="openmediabrowser" type="button">{{get_string "browserepositories" component}}</button>' +
+                        '{{^entersourcelabel}}{{get_string "entersource" ../component}}{{/entersourcelabel}}' +
+                        '</label>' +
+                        '<div class="input-group input-append w-100">' +
+                            '<input id="url-input" class="form-control {{CSS.URL_INPUT}}" type="url" size="32"/>' +
+                            '<span class="input-group-append">' +
+                                '<button class="btn btn-default openmediabrowser" type="button">' +
+                                '{{get_string "browserepositories" component}}</button>' +
+                            '</span>' +
+                        '</div>' +
+                    '</div>' +
                     '{{#multisource}}' +
                         '{{renderPartial "form_components.add_component" context=../this label=../addcomponentlabel ' +
                             ' help=../addsourcehelp}}' +
@@ -229,44 +233,42 @@ var COMPONENTNAME = 'atto_media',
                 '</div>',
             DISPLAY_OPTIONS: '' +
                 '<div class="{{CSS.DISPLAY_OPTIONS}}">' +
-                    '<label>' +
-                        '{{get_string "size" component}}' +
-                        '<div class={{CSS.POSTER_SIZE}}>' +
-                            '<label>' +
-                                '<span class="accesshide">{{get_string "videowidth" component}}</span>' +
-                                '<input type="text" class="{{CSS.WIDTH_INPUT}} input-mini" size="4"/>' +
-                            '</label>' +
+                    '<div class="m-b-1">' +
+                        '<label>{{get_string "size" component}}</label>' +
+                        '<div class="form-inline {{CSS.POSTER_SIZE}}">' +
+                            '<label class="accesshide">{{get_string "videowidth" component}}</label>' +
+                            '<input type="text" class="form-control m-r-1 {{CSS.WIDTH_INPUT}} input-mini" size="4"/>' +
                             ' x ' +
-                            '<label>' +
-                                '<span class="accesshide">{{get_string "videoheight" component}}</span>' +
-                                '<input type="text" class="{{CSS.HEIGHT_INPUT}} input-mini" size="4"/>' +
-                            '</label>' +
+                            '<label class="accesshide">{{get_string "videoheight" component}}</label>' +
+                            '<input type="text" class="form-control m-l-1 {{CSS.HEIGHT_INPUT}} input-mini" size="4"/>' +
                         '</div>' +
-                    '</label>' +
+                    '</div>' +
                     '<div class="clearfix"></div>' +
                     '{{renderPartial "form_components.source" context=this id=CSS.POSTER_SOURCE entersourcelabel="poster"}}' +
                 '<div>',
             ADVANCED_SETTINGS: '' +
                 '<div class="{{CSS.ADVANCED_SETTINGS}}">' +
-                    '<label>' +
-                        '<input type="checkbox" checked="true" class="{{CSS.MEDIA_CONTROLS_TOGGLE}}"/>' +
-                        '{{get_string "controls" component}}' +
-                    '</label>' +
-                    '<label>' +
-                        '<input type="checkbox" class="{{CSS.MEDIA_AUTOPLAY_TOGGLE}}"/>' +
-                        '{{get_string "autoplay" component}}' +
-                    '</label>' +
-                    '<label>' +
-                        '<input type="checkbox" class="{{CSS.MEDIA_MUTE_TOGGLE}}"/>' +
-                        '{{get_string "mute" component}}' +
-                    '</label>' +
-                    '<label>' +
-                        '<input type="checkbox" class="{{CSS.MEDIA_LOOP_TOGGLE}}"/>' +
-                        '{{get_string "loop" component}}' +
-                    '</label>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" checked="true" class="form-check-input {{CSS.MEDIA_CONTROLS_TOGGLE}}"' +
+                        'id="media-controls-toggle"/>' +
+                        '<label class="form-check-label" for="media-controls-toggle">{{get_string "controls" component}}</label>' +
+                    '</div>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_AUTOPLAY_TOGGLE}}"' +
+                        'id="media-autoplay-toggle"/>' +
+                        '<label class="form-check-label" for="media-autoplay-toggle">{{get_string "autoplay" component}}</label>' +
+                    '</div>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_MUTE_TOGGLE}}" id="media-mute-toggle"/>' +
+                        '<label class="form-check-label" for="media-mute-toggle">{{get_string "mute" component}}</label>' +
+                    '</div>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" class="form-check-input {{CSS.MEDIA_LOOP_TOGGLE}}" id="media-loop-toggle"/>' +
+                        '<label class="form-check-label" for="media-loop-toggle">{{get_string "loop" component}}</label>' +
+                    '</div>' +
                 '</div>',
             TRACK_TABS: '' +
-                '<ul class="nav nav-tabs">' +
+                '<ul class="nav nav-tabs mb-3">' +
                     '<li data-track-kind="{{CSS.TRACK_SUBTITLES}}" class="nav-item">' +
                         '<a class="nav-link active" href="#{{elementid}}_{{id}}_{{CSS.TRACK_SUBTITLES}}"' +
                             ' role="tab" data-toggle="tab">' +
@@ -328,11 +330,11 @@ var COMPONENTNAME = 'atto_media',
                     '</div>' +
                 '</div>',
             TRACK: '' +
-                '<div class="{{CSS.TRACK}}">' +
+                '<div class="m-b-1 {{CSS.TRACK}}">' +
                     '{{renderPartial "form_components.source" context=this id=CSS.TRACK_SOURCE entersourcelabel=sourcelabel}}' +
-                    '<label class="langlabel">' +
-                        '<span>{{get_string "srclang" component}}</span>' +
-                        '<select class="{{CSS.TRACK_LANG_INPUT}}">' +
+                    '<div class="form-group">' +
+                        '<label class="w-100" for="lang-input">{{get_string "srclang" component}}</label>' +
+                        '<select id="lang-input" class="custom-select {{CSS.TRACK_LANG_INPUT}}">' +
                             '<optgroup label="{{get_string "languagesinstalled" component}}">' +
                                 '{{#langsinstalled}}' +
                                     '<option value="{{code}}" {{#default}}selected="selected"{{/default}}>{{lang}}</option>' +
@@ -342,15 +344,15 @@ var COMPONENTNAME = 'atto_media',
                                 '{{#langsavailable}}<option value="{{code}}">{{lang}}</option>{{/langsavailable}}' +
                             '</optgroup>' +
                         '</select>' +
-                    '</label>' +
-                    '<label class="labellabel">' +
-                        '<span>{{get_string "label" component}}</span>' +
-                        '<input class="{{CSS.TRACK_LABEL_INPUT}}" type="text"/>' +
-                    '</label>' +
-                    '<label class="defaultlabel">' +
-                        '<input type="checkbox" class="{{CSS.TRACK_DEFAULT_SELECT}}"/>' +
-                        '{{get_string "default" component}}' +
-                    '</label>' +
+                    '</div>' +
+                    '<div class="form-group">' +
+                        '<label class="w-100" for="track-input">{{get_string "label" component}}</label>' +
+                        '<input id="track-input" class="form-control {{CSS.TRACK_LABEL_INPUT}}" type="text"/>' +
+                    '</div>' +
+                    '<div class="form-check">' +
+                        '<input type="checkbox" class="form-check-input {{CSS.TRACK_DEFAULT_SELECT}}"/>' +
+                        '<label class="form-check-label">{{get_string "default" component}}</label>' +
+                    '</div>' +
                     '{{renderPartial "form_components.add_component" context=this label=addcomponentlabel}}' +
                 '</div>'
         },
index fedd668..3565c9b 100644 (file)
Binary files a/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js and b/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-debug.js differ
index 4208609..54e9fb4 100644 (file)
Binary files a/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js and b/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording-min.js differ
index fedd668..3565c9b 100644 (file)
Binary files a/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js and b/lib/editor/atto/plugins/recordrtc/yui/build/moodle-atto_recordrtc-recording/moodle-atto_recordrtc-recording.js differ
index 7c2abc4..e61cbec 100644 (file)
@@ -75,6 +75,7 @@ M.atto_recordrtc.audiomodule = {
                 // Empty the array containing the previously recorded chunks.
                 cm.chunks = [];
                 cm.blobSize = 0;
+                cm.uploadBtn.detach('click');
 
                 // Initialize common configurations.
                 var commonConfig = {
index 4069294..3dfb629 100644 (file)
@@ -74,6 +74,7 @@ M.atto_recordrtc.videomodule = {
                 // Empty the array containing the previously recorded chunks.
                 cm.chunks = [];
                 cm.blobSize = 0;
+                cm.uploadBtn.detach('click');
 
                 // Initialize common configurations.
                 var commonConfig = {
index ac4e854..1c68da3 100644 (file)
@@ -3,8 +3,8 @@ div.editor_atto_content th,
 div.editor_atto_content caption {
     border: 1px dashed #bbb;
     position: relative;
-    min-width: 30px;
-    height: 13px;
+    min-width: 2rem;
+    height: 2rem;
 }
 
 div.editor_atto_content caption {
@@ -12,30 +12,18 @@ div.editor_atto_content caption {
 }
 
 div.availablecolors {
-    max-width: 55%;
-    display: inline-block;
-    vertical-align: middle;
+    background-color: #eee;
+    padding: 0.5rem;
 }
-
-div.availablecolors label:not(.hideborder) {
-    border: 1px solid #ddd;
-}
-
-div.availablecolors label {
-    border-radius: 4px;
-    display: inline-block;
-    font-size: 0.1em;
-    padding: 2px;
-    padding-left: 22px;
-}
-
-div.availablecolors label input[type="radio"] {
-    float: none;
-    margin: 0;
-    margin-left: -15px;
-}
-
-input[name="bordersize"],
-input[name="width"] {
-    margin-right: 0.3em;
+div.availablecolors .tablebordercolor,
+div.availablecolors .tablebackgroundcolor {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 2rem;
+    height: 2rem;
+    text-align: center;
+    vertical-align: sub;
+    border-radius: 25%;
+    margin-right: 0.2rem;
 }
index 1083781..1f3e5d4 100644 (file)
@@ -86,13 +86,13 @@ Feature: Atto tables
     And I click on "Table" "button"
     When I click on "Edit table" "link"
     # Check that the background colour is set correctly.
-    Then the field with xpath "//label/input[@name='backgroundColour' and @value='#FFFFFF']" matches value "1"
+    Then the field with xpath "//div[@class='tablebackgroundcolor']/input[@name='backgroundColour' and @value='#FFFFFF']" matches value "1"
     And the field "Table width (in %)" matches value "100"
     And the field "Borders" matches value "Around table"
     And the field "Style of borders" matches value "dashed"
     And the field "Size of borders" matches value "2"
     # Check that the border colour is set correctly.
-    And the field with xpath "//label/input[@name='borderColour' and @value='#FFFFFF']" matches value "1"
+    And the field with xpath "//div[@class='tablebordercolor']/input[@name='borderColour' and @value='#FFFFFF']" matches value "1"
 
   @javascript
   Scenario: Create a table with background colour and width with border settings off
index c8db3c5..8624a7b 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-debug.js differ
index 5b9e915..3039fcd 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button-min.js differ
index c8db3c5..8624a7b 100644 (file)
Binary files a/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js and b/lib/editor/atto/plugins/table/yui/build/moodle-atto_table-button/moodle-atto_table-button.js differ
index f33bc87..5a36fa9 100644 (file)
@@ -41,132 +41,175 @@ var COMPONENT = 'atto_table',
     },
     TEMPLATE = '' +
         '<form class="{{CSS.FORM}}">' +
+            '<div class="m-b-1 form-group row-fluid">' +
+            '<div class="col-sm-4 span4">' +
             '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
-            '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
-            '<br/>' +
-            '<br/>' +
-            '<label for="{{elementid}}_atto_table_captionposition" class="sameline">' +
+            '</div><div class="col-sm-8 span8">' +
+            '<input type="text" class="form-control {{CSS.CAPTION}}" id="{{elementid}}_atto_table_caption" required />' +
+            '</div>' +
+            '</div>' +
+            '<div class="m-b-1 form-group row-fluid">' +
+            '<div class="col-sm-4 span4">' +
+            '<label for="{{elementid}}_atto_table_captionposition">' +
             '{{get_string "captionposition" component}}</label>' +
-            '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
+            '</div><div class="col-sm-8 span8">' +
+            '<select class="custom-select {{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' +
                 '<option value=""></option>' +
                 '<option value="top">{{get_string "top" "editor"}}</option>' +
                 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' +
             '</select>' +
-            '<br/>' +
-            '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
-            '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
+            '</div>' +
+            '</div>' +
+            '<div class="m-b-1 form-group row-fluid">' +
+            '<div class="col-sm-4 span4">' +
+            '<label for="{{elementid}}_atto_table_headers">{{get_string "headers" component}}</label>' +
+            '</div><div class="col-sm-8 span8">' +
+            '<select class="custom-select {{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' +
                 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
                 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
                 '<option value="both">{{get_string "both" component}}' + '</option>' +
             '</select>' +
-            '<br/>' +
+            '</div>' +
+            '</div>' +
             '{{#if nonedit}}' +
-                '<label for="{{elementid}}_atto_table_rows" class="sameline">{{get_string "numberofrows" component}}</label>' +
-                '<input class="{{CSS.ROWS}}" type="number" value="3" ' +
+                '<div class="m-b-1 form-group row-fluid">' +
+                '<div class="col-sm-4 span4">' +
+                '<label for="{{elementid}}_atto_table_rows">{{get_string "numberofrows" component}}</label>' +
+                '</div><div class="col-sm-8 span8">' +
+                '<input class="form-control w-auto {{CSS.ROWS}}" type="number" value="3" ' +
                 'id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
-                '<br/>' +
+                '</div>' +
+                '</div>' +
+                '<div class="m-b-1 form-group row-fluid">' +
+                '<div class="col-sm-4 span4">' +
                 '<label for="{{elementid}}_atto_table_columns" ' +
-                'class="sameline">{{get_string "numberofcolumns" component}}</label>' +
-                '<input class="{{CSS.COLUMNS}}" type="number" value="3" id="{{elementid}}_atto_table_columns"' +
+                '>{{get_string "numberofcolumns" component}}</label>' +
+                '</div><div class="col-sm-8 span8">' +
+                '<input class="form-control w-auto {{CSS.COLUMNS}}" type="number" value="3" ' +
+                    'id="{{elementid}}_atto_table_columns"' +
                 'size="8" min="1" max="20"/>' +
-                '<br/>' +
+                '</div>' +
+                '</div>' +
             '{{/if}}' +
             '{{#if allowStyling}}' +
                 '<fieldset>' +
                 '<legend class="mdl-align">{{get_string "appearance" component}}</legend>' +
                 '{{#if allowBorders}}' +
-                    '<label for="{{elementid}}_atto_table_borders" class="sameline">{{get_string "borders" component}}</label>' +
-                    '<select name="borders" class="{{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_borders">{{get_string "borders" component}}</label>' +
+                    '</div><div class="col-sm-8 span8">' +
+                    '<select name="borders" class="custom-select {{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' +
                         '<option value="default">{{get_string "themedefault" component}}' + '</option>' +
                         '<option value="outer">{{get_string "outer" component}}' + '</option>' +
                         '<option value="all">{{get_string "all" component}}' + '</option>' +
                     '</select>' +
-                    '<br>' +
-                    '<label for="{{elementid}}_atto_table_borderstyle" class="sameline">' +
+                    '</div>' +
+                    '</div>' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_borderstyle">' +
                     '{{get_string "borderstyles" component}}</label>' +
-                    '<select name="borderstyles" class="{{CSS.BORDERSTYLE}}" id="{{elementid}}_atto_table_borderstyle">' +
+                    '</div><div class="col-sm-8 span8">' +
+                    '<select name="borderstyles" class="custom-select {{CSS.BORDERSTYLE}}" ' +
+                        'id="{{elementid}}_atto_table_borderstyle">' +
                         '{{#each borderStyles}}' +
                             '<option value="' + '{{this}}' + '">' + '{{get_string this ../component}}' + '</option>' +
                         '{{/each}}' +
                     '</select>' +
-                    '<br>' +
-                    '<label for="{{elementid}}_atto_table_bordersize" class="sameline">' +
+                    '</div>' +
+                    '</div>' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_bordersize">' +
                     '{{get_string "bordersize" component}}</label>' +
-                    '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" class="{{CSS.BORDERSIZE}}"' +
+                    '</div><div class="col-sm-8 span8">' +
+                    '<div class="form-inline">' +
+                    '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" ' +
+                    'class="form-control w-auto m-r-1 {{CSS.BORDERSIZE}}"' +
                     'type="number" value="1" size="8" min="1" max="50"/>' +
-                    '<label style="display: inline-block;">{{CSS.BORDERSIZEUNIT}}</label>' +
-                    '<br>' +
-                    '<label for="{{elementid}}_atto_table_bordercolour" class="sameline">' +
+                    '<label>{{CSS.BORDERSIZEUNIT}}</label>' +
+                    '</div>' +
+                    '</div>' +
+                    '</div>' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_bordercolour">' +
                     '{{get_string "bordercolour" component}}</label>' +
+                    '</div><div class="col-sm-8 span8">' +
                     '<div id="{{elementid}}_atto_table_bordercolour"' +
-                    'class="{{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
-                        '<label class="hideborder" for="{{../elementid}}_atto_table_bordercolour_-1"' +
-                        'style="background-color:transparent;color:transparent">' +
-
+                    'class="form-inline {{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
+                        '<div class="tablebordercolor" style="background-color:transparent;color:transparent">' +
                             '<input id="{{../elementid}}_atto_table_bordercolour_-1"' +
-                            'type="radio" name="borderColour" value="none" checked="checked"' +
+                            'type="radio" class="m-0" name="borderColour" value="none" checked="checked"' +
                             'title="{{get_string "themedefault" component}}"></input>' +
-
-                            '{{get_string "themedefault" component}}' +
-                        '</label>' +
+                            '<label for="{{../elementid}}_atto_table_bordercolour_-1" class="accesshide">' +
+                            '{{get_string "themedefault" component}}</label>' +
+                        '</div>' +
                         '{{#each availableColours}}' +
-                            '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
-                            'style="background-color:{{this}};color:{{this}}">' +
-
+                            '<div class="tablebordercolor" style="background-color:{{this}};color:{{this}}">' +
                                 '<input id="{{../elementid}}_atto_table_bordercolour_{{@index}}"' +
-                                'type="radio" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +
-
-                                '{{this}}' +
-                            '</label>' +
+                                'type="radio" class="m-0" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' +
+                                '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}" class="accesshide">' +
+                                '{{this}}</label>' +
+                            '</div>' +
                         '{{/each}}' +
                     '</div>' +
-                    '<br>' +
+                    '</div>' +
+                    '</div>' +
                 '{{/if}}' +
                 '{{#if allowBackgroundColour}}' +
-                    '<label for="{{elementid}}_atto_table_backgroundcolour" class="sameline">' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_backgroundcolour">' +
                     '{{get_string "backgroundcolour" component}}</label>' +
+                    '</div><div class="col-sm-8 span8">' +
                     '<div id="{{elementid}}_atto_table_backgroundcolour"' +
-                    'class="{{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
-                        '<label class="hideborder" for="{{../elementid}}_atto_table_backgroundcolour_-1"' +
-                        'style="background-color:transparent;color:transparent">' +
-
+                    'class="form-inline {{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' +
+                        '<div class="tablebackgroundcolor" style="background-color:transparent;color:transparent">' +
                             '<input id="{{../elementid}}_atto_table_backgroundcolour_-1"' +
-                            'type="radio" name="backgroundColour" value="none" checked="checked"' +
+                            'type="radio" class="m-0" name="backgroundColour" value="none" checked="checked"' +
                             'title="{{get_string "themedefault" component}}"></input>' +
-
-                            '{{get_string "themedefault" component}}' +
-                        '</label>' +
+                            '<label for="{{../elementid}}_atto_table_backgroundcolour_-1" class="accesshide">' +
+                            '{{get_string "themedefault" component}}</label>' +
+                        '</div>' +
 
                         '{{#each availableColours}}' +
-                            '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
-                            'style="background-color:{{this}};color:{{this}}">' +
-
+                            '<div class="tablebackgroundcolor" style="background-color:{{this}};color:{{this}}">' +
                                 '<input id="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' +
-                                'type="radio" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +
-
-                                '{{this}}' +
-                            '</label>' +
+                                'type="radio" class="m-0" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' +
+                                '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}" class="accesshide">' +
+                                '{{this}}</label>' +
+                            '</div>' +
                         '{{/each}}' +
                     '</div>' +
-                    '<br>' +
+                    '</div>' +
+                    '</div>' +
                 '{{/if}}' +
                 '{{#if allowWidth}}' +
-                    '<label for="{{elementid}}_atto_table_width" class="sameline">' +
+                    '<div class="m-b-1 form-group row-fluid">' +
+                    '<div class="col-sm-4 span4">' +
+                    '<label for="{{elementid}}_atto_table_width">' +
                     '{{get_string "width" component}}</label>' +
-                    '<input name="width" id="{{elementid}}_atto_table_width" class="{{CSS.WIDTH}}" size="8" ' +
+                    '</div><div class="col-sm-8 span8">' +
+                    '<div class="form-inline">' +
+                    '<input name="width" id="{{elementid}}_atto_table_width" ' +
+                        'class="form-control w-auto m-r-1 {{CSS.WIDTH}}" size="8" ' +
                         'type="number" min="0" max="100"/>' +
-                    '<label style="display: inline-block;">{{CSS.WIDTHUNIT}}</label>' +
-                    '<br>' +
+                    '<label>{{CSS.WIDTHUNIT}}</label>' +
+                    '</div>' +
+                    '</div>' +
+                    '</div>' +
                 '{{/if}}' +
                 '</fieldset>' +
             '{{/if}}' +
             '<div class="mdl-align">' +
             '<br/>' +
             '{{#if edit}}' +
-                '<button class="submit" type="submit">{{get_string "updatetable" component}}</button>' +
+                '<button class="btn btn-default submit" type="submit">{{get_string "updatetable" component}}</button>' +
             '{{/if}}' +
             '{{#if nonedit}}' +
-                '<button class="submit" type="submit">{{get_string "createtable" component}}</button>' +
+                '<button class="btn btn-default submit" type="submit">{{get_string "createtable" component}}</button>' +
             '{{/if}}' +
             '</div>' +
         '</form>',
index 84c777e..f2b74e1 100644 (file)
@@ -107,10 +107,10 @@ div.editor_atto_toolbar div.atto_group {
     margin: 4px;
 }
 
-.atto_form label.sameline {
+/*.atto_form label.sameline {
     display: inline-block;
     min-width: 10em;
-}
+}*/
 
 .atto_form textarea.fullwidth,
 .atto_form input.fullwidth {
@@ -118,14 +118,13 @@ div.editor_atto_toolbar div.atto_group {
 }
 
 .atto_form {
-    padding-left: 30px;
-    padding-right: 30px;
+    padding: 0.5rem;
 }
 
-.atto_form label {
+/*.atto_form label {
     display: block;
     margin: 0 0 5px 0;
-}
+}*/
 
 .atto_control {
     position: absolute;
index af8a77d..d8875b5 100644 (file)
@@ -89,7 +89,8 @@ LICENSE
 /**
  * This class was heavily modified in order to get usefull spreadsheet emulation ;-)
  * skodak
- *
+ * This class was modified to allow comparison operators (<, <=, ==, >=, >)
+ * and synonyms functions (for the 'if' function). See MDL-14274 for more details.
  */
 
 class EvalMath {
@@ -113,7 +114,8 @@ class EvalMath {
         'average'=>array(-1), 'max'=>array(-1),  'min'=>array(-1),
         'mod'=>array(2),      'pi'=>array(0),    'power'=>array(2),
         'round'=>array(1, 2), 'sum'=>array(-1), 'rand_int'=>array(2),
-        'rand_float'=>array(0));
+        'rand_float'=>array(0), 'ifthenelse'=>array(3));
+    var $fcsynonyms = array('if' => 'ifthenelse');
 
     var $allowimplicitmultiplication;
 
@@ -207,20 +209,25 @@ class EvalMath {
         $stack = new EvalMathStack;
         $output = array(); // postfix form of expression, to be passed to pfx()
         $expr = trim(strtolower($expr));
-
-        $ops   = array('+', '-', '*', '/', '^', '_');
+        // MDL-14274: new operators for comparison added.
+        $ops   = array('+', '-', '*', '/', '^', '_', '>', '<', '<=', '>=', '==');
         $ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
-        $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
+        $ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2, '>'=>3, '<'=>3, '<='=>3, '>='=>3, '=='=>3); // operator precedence
 
         $expecting_op = false; // we use this in syntax-checking the expression
                                // and determining when a - is a negation
 
-        if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
+        if (preg_match("/[^\w\s+*^\/()\.,-<>=]/", $expr, $matches)) { // make sure the characters are all good
             return $this->trigger(get_string('illegalcharactergeneral', 'mathslib', $matches[0]));
         }
 
         while(1) { // 1 Infinite Loop ;)
-            $op = substr($expr, $index, 1); // get the first character at the current index
+            // MDL-14274 Test two character operators.
+            $op = substr($expr, $index, 2);
+            if (!in_array($op, $ops)) {
+                // MDL-14274 Get one character operator.
+                $op = substr($expr, $index, 1); // get the first character at the current index
+            }
             // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
             $ex = preg_match('/^('.self::$namepat.'\(?|\d+(?:\.\d*)?(?:(e[+-]?)\d*)?|\.\d+|\()/', substr($expr, $index), $match);
             //===============
@@ -245,7 +252,7 @@ class EvalMath {
                 }
                 // many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
                 $stack->push($op); // finally put OUR operator onto the stack
-                $index++;
+                $index += strlen($op);
                 $expecting_op = false;
             //===============
             } elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
@@ -265,7 +272,9 @@ class EvalMath {
                             $a->given = $arg_count;
                             return $this->trigger(get_string('wrongnumberofarguments', 'mathslib', $a));
                         }
-                    } elseif (array_key_exists($fnn, $this->fc)) {
+                    } elseif ($this->get_native_function_name($fnn)) {
+                        $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
+
                         $counts = $this->fc[$fnn];
                         if (in_array(-1, $counts) and $arg_count > 0) {}
                         elseif (!in_array($arg_count, $counts)) {
@@ -309,7 +318,9 @@ class EvalMath {
                 $expecting_op = true;
                 $val = $match[1];
                 if (preg_match('/^('.self::$namepat.')\($/', $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
-                    if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f) or array_key_exists($matches[1], $this->fc)) { // it's a func
+                    if (in_array($matches[1], $this->fb) or
+                                array_key_exists($matches[1], $this->f) or
+                                $this->get_native_function_name($matches[1])){ // it's a func
                         $stack->push($val);
                         $stack->push(1);
                         $stack->push('(');
@@ -331,6 +342,7 @@ class EvalMath {
                     $stack->pop();// 1
                     $fn = $stack->pop();
                     $fnn = $matches[1]; // get the function name
+                    $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
                     $counts = $this->fc[$fnn];
                     if (!in_array(0, $counts)){
                         $a= new stdClass();
@@ -368,7 +380,20 @@ class EvalMath {
         }
         return $output;
     }
-
+    /**
+     *
+     * @param string $fnn
+     * @return string|boolean false if function name unknown.
+     */
+    function get_native_function_name($fnn) {
+        if (array_key_exists($fnn, $this->fcsynonyms)) {
+            return $this->fcsynonyms[$fnn];
+        } else if (array_key_exists($fnn, $this->fc)) {
+            return $fnn;
+        } else {
+            return false;
+        }
+    }
     // evaluate postfix notation
     function pfx($tokens, $vars = array()) {
 
@@ -387,7 +412,8 @@ class EvalMath {
                     $fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
                     if ($fnn == 'ln') $fnn = 'log';
                     eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
-                } elseif (array_key_exists($fnn, $this->fc)) { // calc emulation function
+                } elseif ($this->get_native_function_name($fnn)) { // calc emulation function
+                    $fnn = $this->get_native_function_name($fnn); // Resolve synonyms.
                     // get args
                     $args = array();
                     for ($i = $count-1; $i >= 0; $i--) {
@@ -407,7 +433,7 @@ class EvalMath {
                     $stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
                 }
             // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
-            } elseif (in_array($token, array('+', '-', '*', '/', '^'), true)) {
+            } elseif (in_array($token, array('+', '-', '*', '/', '^', '>', '<', '==', '<=', '>='), true)) {
                 if (is_null($op2 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
                 if (is_null($op1 = $stack->pop())) return $this->trigger(get_string('internalerror', 'mathslib'));
                 switch ($token) {
@@ -422,6 +448,16 @@ class EvalMath {
                         $stack->push($op1/$op2); break;
                     case '^':
                         $stack->push(pow($op1, $op2)); break;
+                    case '>':
+                        $stack->push((int)($op1 > $op2)); break;
+                    case '<':
+                        $stack->push((int)($op1 < $op2)); break;
+                    case '==':
+                        $stack->push((int)($op1 == $op2)); break;
+                    case '<=':
+                        $stack->push((int)($op1 <= $op2)); break;
+                    case '>=':
+                        $stack->push((int)($op1 >= $op2)); break;
                 }
             // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
             } elseif ($token == "_") {
@@ -483,7 +519,21 @@ class EvalMathStack {
 
 // spreadsheet functions emulation
 class EvalMathFuncs {
-
+    /**
+     * MDL-14274 new conditional function.
+     * @param boolean $condition boolean for conditional.
+     * @param variant $then value if condition is true.
+     * @param unknown $else value if condition is false.
+     * @author Juan Pablo de Castro <juan.pablo.de.castro@gmail.com>
+     * @return unknown
+     */
+    static function ifthenelse($condition, $then, $else) {
+        if ($condition == true) {
+            return $then;
+        } else {
+            return $else;
+        }
+    }
     static function average() {
         $args = func_get_args();
         return (call_user_func_array(array('self', 'sum'), $args) / count($args));
index e9b1a80..c9935c5 100644 (file)
@@ -18,3 +18,7 @@ To see all changes diff against version 1.1, available from:
 http://www.phpclasses.org/browse/package/2695.html
 
 skodak, Tim Hunt
+
+Changes by Juan Pablo de Castro (MDL-14274):
+* operators >,<,>=,<=,== added.
+* function if[thenelse](condition, true_value, false_value)
index ff38252..ef612b4 100644 (file)
@@ -116,7 +116,7 @@ class MoodleExcelWorkbook {
             header('Pragma: no-cache');
         }
 
-        if (core_useragent::is_ie()) {
+        if (core_useragent::is_ie() || core_useragent::is_edge()) {
             $filename = rawurlencode($filename);
         } else {
             $filename = s($filename);
index 53f07db..a8c844a 100644 (file)
@@ -2115,7 +2115,7 @@ function send_temp_file($path, $filename, $pathisstring=false) {
     }
 
     // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
-    if (core_useragent::is_ie()) {
+    if (core_useragent::is_ie() || core_useragent::is_edge()) {
         $filename = urlencode($filename);
     }
 
@@ -2264,7 +2264,7 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring
     }
 
     // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
-    if (core_useragent::is_ie()) {
+    if (core_useragent::is_ie() || core_useragent::is_edge()) {
         $filename = rawurlencode($filename);
     }
 
index aa2e866..8753695 100644 (file)
@@ -56,10 +56,7 @@ class calc_formula {
             return;
         }
         $formula = substr($formula, 1);
-        if (strpos($formula, '=') !== false) {
-            $this->_error = "too many '='";
-            return;
-        }
+
         $this->_nfx = $this->_em->nfx($formula);
         if ($this->_nfx == false) {
             $this->_error = $this->_em->last_error;
index 446807e..34fe56b 100644 (file)
@@ -8888,7 +8888,7 @@ function cleardoubleslashes ($path) {
 }
 
 /**
- * Is current ip in give list?
+ * Is the current ip in a given list?
  *
  * @param string $list
  * @return bool
@@ -8903,8 +8903,9 @@ function remoteip_in_list($list) {
     }
 
     $list = explode("\n", $list);
-    foreach ($list as $subnet) {
-        $subnet = trim($subnet);
+    foreach ($list as $line) {
+        $tokens = explode('#', $line);
+        $subnet = trim($tokens[0]);
         if (address_in_subnet($clientip, $subnet)) {
             $inlist = true;
             break;
index fe3b1d2..6d37bcf 100644 (file)
@@ -81,6 +81,47 @@ class core_mathslib_testcase extends basic_testcase {
         $this->assertSame(8, $formula->evaluate());
     }
 
+    public function test_conditional_functions() {
+        $formula = new calc_formula('=ifthenelse(1,2,3)');
+        $this->assertSame(2, (int)$formula->evaluate());
+
+        $formula = new calc_formula('=ifthenelse(0,2,3)');
+        $this->assertSame(3, (int) $formula->evaluate());
+
+        $formula = new calc_formula('=ifthenelse(2<3,2,3)');
+        $this->assertSame(2, (int) $formula->evaluate());
+
+        // Test synonim if.
+        $formula = new calc_formula('=if(1,2,3)');
+        $this->assertSame(2, (int)$formula->evaluate());
+
+        $formula = new calc_formula('=if(0,2,3)');
+        $this->assertSame(3, (int) $formula->evaluate());
+
+        $formula = new calc_formula('=if(2<3,2,3)');
+        $this->assertSame(2, (int) $formula->evaluate());
+    }
+
+    public function test_conditional_operators() {
+        $formula = new calc_formula('=2==2');
+        $this->assertSame(1, $formula->evaluate());
+
+        $formula = new calc_formula('=2>3');
+        $this->assertSame(0, $formula->evaluate());
+        $formula = new calc_formula('=2<3');
+        $this->assertSame(1, $formula->evaluate());
+
+        $formula = new calc_formula('=(2<=3)');
+        $this->assertSame(1, $formula->evaluate());
+
+        $formula = new calc_formula('=(2<=3)*10');
+        $this->assertSame(10, $formula->evaluate());
+
+        $formula = new calc_formula('=(2>=3)*10');
+        $this->assertSame(0, $formula->evaluate());
+        $formula = new calc_formula('=2<3*10');
+        $this->assertSame(10, $formula->evaluate());
+    }
     /**
      * Tests the min and max functions.
      */
index e81dee0..34d481f 100644 (file)
@@ -90,10 +90,10 @@ class migrate_message_data extends \core\task\adhoc_task {
                             $this->migrate_data($userid, $otheruserid);
                             $transaction->allow_commit();
                         } catch (\Throwable $e) {
-                            $updatepreference = false;
+                            throw $e;
+                        } finally {
+                            $lock->release();
                         }
-
-                        $lock->release();
                     } else {
                         // Couldn't get a lock, move on to next user but make sure we don't update user preference so
                         // we still try again.
@@ -172,7 +172,7 @@ class migrate_message_data extends \core\task\adhoc_task {
         $tabledata->useridto = $notification->useridto;
         $tabledata->subject = $notification->subject;
         $tabledata->fullmessage = $notification->fullmessage;
-        $tabledata->fullmessageformat = $notification->fullmessageformat;
+        $tabledata->fullmessageformat = $notification->fullmessageformat ?? FORMAT_MOODLE;
         $tabledata->fullmessagehtml = $notification->fullmessagehtml;
         $tabledata->smallmessage = $notification->smallmessage;
         $tabledata->component = $notification->component;
@@ -210,7 +210,7 @@ class migrate_message_data extends \core\task\adhoc_task {
         $tabledata->conversationid = $conversationid;
         $tabledata->subject = $message->subject;
         $tabledata->fullmessage = $message->fullmessage;
-        $tabledata->fullmessageformat = $message->fullmessageformat;
+        $tabledata->fullmessageformat = $message->fullmessageformat ?? FORMAT_MOODLE;
         $tabledata->fullmessagehtml = $message->fullmessagehtml;
         $tabledata->smallmessage = $message->smallmessage;
         $tabledata->timecreated = $message->timecreated;
index b67fdc8..37fea75 100644 (file)
Binary files a/message/output/popup/amd/build/message_popover_controller.min.js and b/message/output/popup/amd/build/message_popover_controller.min.js differ
index 4d099a4..b241a56 100644 (file)
Binary files a/message/output/popup/amd/build/notification_popover_controller.min.js and b/message/output/popup/amd/build/notification_popover_controller.min.js differ
index 499980b..6ea5608 100644 (file)
@@ -57,7 +57,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str',
         this.initialLoad = false;
 
         // Let's find out how many unread messages there are.
-        this.loadUnreadMessageCount();
+        this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();
     };
 
     /**
@@ -139,21 +139,6 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str',
         this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');
     };
 
-    /**
-     * Ask the server how many unread messages are left, render the value
-     * as a badge on the menu toggle and update the aria labels on the menu
-     * toggle.
-     *
-     * @method loadUnreadMessageCount
-     */
-    MessagePopoverController.prototype.loadUnreadMessageCount = function() {
-        MessageRepo.countUnreadConversations({useridto: this.userId}).then(function(count) {
-            this.unreadCount = count;
-            this.renderUnreadCount();
-            this.updateButtonAriaLabel();
-        }.bind(this)).catch(Notification.exception);
-    };
-
     /**
      * Render the message data with the appropriate template and add it to the DOM.
      *
index de45501..9aa7630 100644 (file)
@@ -60,7 +60,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',
         this.initialLoad = false;
 
         // Let's find out how many unread notifications there are.
-        this.loadUnreadNotificationCount();
+        this.unreadCount = this.root.find(SELECTORS.COUNT_CONTAINER).html();
     };
 
     /**
@@ -188,21 +188,6 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',
         this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');
     };
 
-    /**
-     * Ask the server how many unread notifications are left, render the value
-     * as a badge on the menu toggle and update the aria labels on the menu
-     * toggle.
-     *
-     * @method loadUnreadNotificationCount
-     */
-    NotificationPopoverController.prototype.loadUnreadNotificationCount = function() {
-        NotificationRepo.countUnread({useridto: this.userId}).then(function(count) {
-            this.unreadCount = count;
-            this.renderUnreadCount();
-            this.updateButtonAriaLabel();
-        }.bind(this)).catch(DebugNotification.exception);
-    };
-
     /**
      * Find the notification element for the given id.
      *
@@ -239,8 +224,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',
 
             // Link to mark read page before loading the actual link.
             notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', {
-                redirecturl: notification.contexturl,
                 notificationid: notification.id,
+                redirecturl: notification.contexturl
             });
 
             var promise = Templates.render('message_popup/notification_content_item', notification)
index 4c4f910..3253109 100644 (file)
@@ -14,8 +14,6 @@
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-defined('MOODLE_INTERNAL') || die();
-
 /**
  * Contains standard functions for message_popup.
  *
@@ -24,6 +22,8 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+defined('MOODLE_INTERNAL') || die();
+
 /**
  * Renders the popup.
  *
@@ -45,8 +45,10 @@ function message_popup_render_navbar_output(\renderer_base $renderer) {
 
     // Add the messages popover.
     if (!empty($CFG->messaging)) {
+        $unreadcount = \core_message\api::count_unread_conversations($USER);
         $context = [
             'userid' => $USER->id,
+            'unreadcount' => $unreadcount,
             'urls' => [
                 'seeall' => (new moodle_url('/message/index.php'))->out(),
                 'writeamessage' => (new moodle_url('/message/index.php', ['contactsfirst' => 1]))->out(),
@@ -59,8 +61,10 @@ function message_popup_render_navbar_output(\renderer_base $renderer) {
     // Add the notifications popover.
     $enabled = \core_message\api::is_processor_enabled("popup");
     if ($enabled) {
+        $unreadcount = \message_popup\api::count_unread_popup_notifications($USER->id);
         $context = [
             'userid' => $USER->id,
+            'unreadcount' => $unreadcount,
             'urls' => [
                 'seeall' => (new moodle_url('/message/output/popup/notifications.php'))->out(),
                 'preferences' => (new moodle_url('/message/notificationpreferences.php', ['userid' => $USER->id]))->out(),
index 0355993..75b363d 100644 (file)
@@ -45,7 +45,7 @@
     {{$togglelabel}}{{#str}} showmessagewindownonew, message {{/str}}{{/togglelabel}}
     {{$togglecontent}}
         {{#pix}} t/message, core, {{#str}} togglemessagemenu, message {{/str}} {{/pix}}
-        <div class="count-container hidden" data-region="count-container"></div>
+        <div class="count-container {{^unreadcount}}hidden{{/unreadcount}}" data-region="count-container">{{unreadcount}}</div>
     {{/togglecontent}}
 
     {{$containerlabel}}{{#str}} notificationwindow, message {{/str}}{{/containerlabel}}
index 97efbff..602d106 100644 (file)
@@ -45,7 +45,7 @@
     {{$togglelabel}}{{#str}} shownotificationwindownonew, message {{/str}}{{/togglelabel}}
     {{$togglecontent}}
         {{#pix}} i/notifications, core, {{#str}} togglenotificationmenu, message {{/str}} {{/pix}}
-        <div class="count-container hidden" data-region="count-container"></div>
+        <div class="count-container {{^unreadcount}}hidden{{/unreadcount}}" data-region="count-container">{{unreadcount}}</div>
     {{/togglecontent}}
 
     {{$containerlabel}}{{#str}} notificationwindow, message {{/str}}{{/containerlabel}}
index 72c1d0a..3ec3009 100644 (file)
@@ -268,6 +268,62 @@ class core_message_migrate_message_data_task_testcase extends advanced_testcase
         }
     }
 
+    /**
+     * Test migrating a legacy message that contains null as the format.
+     */
+    public function test_migrating_message_null_format() {
+        global $DB;
+
+        // Create users to test with.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $this->create_legacy_message_or_notification($user1->id, $user2->id, null, false, null, null);
+
+        // Now, let's execute the task for user 1.
+        $task = new \core_message\task\migrate_message_data();
+        $task->set_custom_data(
+            [
+                'userid' => $user1->id
+            ]
+        );
+        $task->execute();
+
+        $messages = $DB->get_records('messages');
+        $this->assertCount(1, $messages);
+
+        $message = reset($messages);
+        $this->assertEquals(FORMAT_MOODLE, $message->fullmessageformat);
+    }
+
+    /**
+     * Test migrating a legacy notification that contains null as the format.
+     */
+    public function test_migrating_notification_null_format() {
+        global $DB;
+
+        // Create users to test with.
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $this->create_legacy_message_or_notification($user1->id, $user2->id, null, true, null, null);
+
+        // Now, let's execute the task for user 1.
+        $task = new \core_message\task\migrate_message_data();
+        $task->set_custom_data(
+            [
+                'userid' => $user1->id
+            ]
+        );
+        $task->execute();
+
+        $notifications = $DB->get_records('notifications');
+        $this->assertCount(1, $notifications);
+
+        $notification = reset($notifications);
+        $this->assertEquals(FORMAT_MOODLE, $notification->fullmessageformat);
+    }
+
     /**
      * Creates a legacy message or notification to be used for testing.
      *
@@ -276,11 +332,12 @@ class core_message_migrate_message_data_task_testcase extends advanced_testcase
      * @param int $timecreated
      * @param bool $notification
      * @param int|null $timeread The time the message/notification was read, null if it hasn't been.
+     * @param string|int|null $format The format of the message.
      * @return int The id of the message (in either the message or message_read table)
      * @throws dml_exception
      */
     private function create_legacy_message_or_notification($useridfrom, $useridto, $timecreated = null,
-            $notification = false, $timeread = null) {
+            $notification = false, $timeread = null, $format = FORMAT_PLAIN) {
         global $DB;
 
         $tabledata = new \stdClass();
@@ -312,7 +369,7 @@ class core_message_migrate_message_data_task_testcase extends advanced_testcase
         $tabledata->useridto = $useridto;
         $tabledata->subject = 'Subject ' . $timecreated;
         $tabledata->fullmessage = 'Full message ' . $timecreated;
-        $tabledata->fullmessageformat = FORMAT_PLAIN;
+        $tabledata->fullmessageformat = $format;
         $tabledata->fullmessagehtml = 'Full message HTML ' . $timecreated;
         $tabledata->smallmessage = 'Small message ' . $timecreated;
         $tabledata->timecreated = $timecreated;
index ad5ba63..3f2e075 100644 (file)
@@ -277,7 +277,8 @@ class assign_grading_table extends table_sql implements renderable {
                                  s.status = :submitted AND
                                  (s.timemodified >= g.timemodified OR g.timemodified IS NULL OR g.grade IS NULL';
 
-                if ($this->assignment->get_grade_item()->gradetype == GRADE_TYPE_SCALE) {
+                // Assignment grade is set to the negative grade scale id when scales are used.
+                if ($this->assignment->get_instance()->grade < 0) {
                     // Scale grades are set to -1 when not graded.
                     $where .= ' OR g.grade = -1';
                 }
@@ -1121,11 +1122,10 @@ class assign_grading_table extends table_sql implements renderable {
 
         if ($row->allowsubmissionsfromdate) {
             $userdate = userdate($row->allowsubmissionsfromdate);
-            $o = $this->output->container($userdate, 'allowsubmissionsfromdate');
+            $o = ($this->is_downloading()) ? $userdate : $this->output->container($userdate, 'allowsubmissionsfromdate');
         }
 
         return $o;
-
     }
 
     /**
@@ -1139,11 +1139,10 @@ class assign_grading_table extends table_sql implements renderable {
 
         if ($row->duedate) {
             $userdate = userdate($row->duedate);
-            $o = $this->output->container($userdate, 'duedate');
+            $o = ($this->is_downloading()) ? $userdate : $this->output->container($userdate, 'duedate');
         }
 
         return $o;
-
     }
 
     /**
@@ -1157,11 +1156,10 @@ class assign_grading_table extends table_sql implements renderable {
 
         if ($row->cutoffdate) {
             $userdate = userdate($row->cutoffdate);
-            $o = $this->output->container($userdate, 'cutoffdate');
+            $o = ($this->is_downloading()) ? $userdate : $this->output->container($userdate, 'cutoffdate');
         }
 
         return $o;
-
     }
 
     /**
index db8958a..c454974 100644 (file)
@@ -189,6 +189,40 @@ class mod_assign_locallib_testcase extends advanced_testcase {
         }
     }
 
+    /**
+     * Test filter by requires grading.
+     *
+     * This is specifically checking an assignment with no grade to make sure we do not
+     * get an exception thrown when rendering the grading table for this type of assignment.
+     */
+    public function test_gradingtable_filter_by_requiresgrading_no_grade() {
+        global $PAGE;
+
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+        $this->setUser($teacher);
+        $assign = $this->create_instance($course, [
+                'assignsubmission_onlinetext_enabled' => 1,
+                'assignfeedback_comments_enabled' => 0,
+                'grade' => GRADE_TYPE_NONE
+            ]);
+
+        $PAGE->set_url(new moodle_url('/mod/assign/view.php', array(
+            'id' => $assign->get_course_module()->id,
+            'action' => 'grading',
+        )));
+
+        // Render the table with the requires grading filter.
+        $gradingtable = new assign_grading_table($assign, 1, ASSIGN_FILTER_REQUIRE_GRADING, 0, true);
+        $output = $assign->get_renderer()->render($gradingtable);
+
+        // Test that the filter function does not throw errors for assignments with no grade.
+        $this->assertContains(get_string('nothingtodisplay'), $output);
+    }
+
+
     /**
      * Test submissions with extension date.
      */
@@ -2748,7 +2782,7 @@ class mod_assign_locallib_testcase extends advanced_testcase {
 
         $cm = get_coursemodule_from_instance('assign', $assign->get_instance()->id);
         $context = context_module::instance($cm->id);
-        $assign = new testable_assign($context, $cm, $course);
+        $assign = new mod_assign_testable_assign($context, $cm, $course);
 
         // Check that other teachers can't view this submission.
         $this->setUser($otherteacher);
index df0a726..84652e4 100644 (file)
@@ -350,7 +350,7 @@ if (!empty($forum)) {      // User is starting a new discussion in a forum
                 $event->add_record_snapshot('forum_discussions', $discussion);
                 $event->trigger();
 
-                redirect("view.php?f=$discussion->forum");
+                redirect(new moodle_url('/mod/forum/view.php', ['f' => $discussion->forum]));
 
             } else if (forum_delete_post($post, has_capability('mod/forum:deleteanypost', $modcontext),
                 $course, $cm, $forum)) {
@@ -845,7 +845,7 @@ if ($mform_post->is_cancelled()) {
 
     } else { // Adding a new discussion.
         // The location to redirect to after successfully posting.
-        $redirectto = new moodle_url('view.php', array('f' => $fromform->forum));
+        $redirectto = new moodle_url('/mod/forum/view.php', array('f' => $fromform->forum));
 
         $fromform->mailnow = empty($fromform->mailnow) ? 0 : 1;
 
index c105fd7..7400a71 100644 (file)
@@ -64,5 +64,7 @@ $string['printheading'] = 'Display page name';
 $string['printheadingexplain'] = 'Display page name above content?';
 $string['printintro'] = 'Display page description';
 $string['printintroexplain'] = 'Display page description above content?';
+$string['printlastmodified'] = 'Display last modified date';
+$string['printlastmodifiedexplain'] = 'Display last modified date below content?';
 $string['privacy:metadata'] = 'The Page resource plugin does not store any personal data.';
 $string['search:activity'] = 'Page';
index 67135ad..3b6f596 100644 (file)
@@ -113,6 +113,7 @@ function page_add_instance($data, $mform = null) {
     }
     $displayoptions['printheading'] = $data->printheading;
     $displayoptions['printintro']   = $data->printintro;
+    $displayoptions['printlastmodified'] = $data->printlastmodified;
     $data->displayoptions = serialize($displayoptions);
 
     if ($mform) {
@@ -162,6 +163,7 @@ function page_update_instance($data, $mform) {
     }
     $displayoptions['printheading'] = $data->printheading;
     $displayoptions['printintro']   = $data->printintro;
+    $displayoptions['printlastmodified'] = $data->printlastmodified;
     $data->displayoptions = serialize($displayoptions);
 
     $data->content       = $data->page['text'];
@@ -495,6 +497,7 @@ function page_dndupload_handle($uploadinfo) {
     $data->popupwidth = $config->popupwidth;
     $data->printheading = $config->printheading;
     $data->printintro = $config->printintro;
+    $data->printlastmodified = $config->printlastmodified;
 
     return page_add_instance($data, null);
 }
index a411f1a..b8ff4ab 100644 (file)
@@ -92,6 +92,8 @@ class mod_page_mod_form extends moodleform_mod {
         $mform->setDefault('printheading', $config->printheading);
         $mform->addElement('advcheckbox', 'printintro', get_string('printintro', 'page'));
         $mform->setDefault('printintro', $config->printintro);
+        $mform->addElement('advcheckbox', 'printlastmodified', get_string('printlastmodified', 'page'));
+        $mform->setDefault('printlastmodified', $config->printlastmodified);
 
         // add legacy files flag only if used
         if (isset($this->current->legacyfiles) and $this->current->legacyfiles != RESOURCELIB_LEGACYFILES_NO) {
@@ -113,26 +115,36 @@ class mod_page_mod_form extends moodleform_mod {
         $mform->setDefault('revision', 1);
     }
 
-    function data_preprocessing(&$default_values) {
+    /**
+     * Enforce defaults here.
+     *
+     * @param array $defaultvalues Form defaults
+     * @return void
+     **/
+    public function data_preprocessing(&$defaultvalues) {
         if ($this->current->instance) {
             $draftitemid = file_get_submitted_draft_itemid('page');
-            $default_values['page']['format'] = $default_values['contentformat'];
-            $default_values['page']['text']   = file_prepare_draft_area($draftitemid, $this->context->id, 'mod_page', 'content', 0, page_get_editor_options($this->context), $default_values['content']);
-            $default_values['page']['itemid'] = $draftitemid;
+            $defaultvalues['page']['format'] = $defaultvalues['contentformat'];
+            $defaultvalues['page']['text']   = file_prepare_draft_area($draftitemid, $this->context->id, 'mod_page',
+                    'content', 0, page_get_editor_options($this->context), $defaultvalues['content']);
+            $defaultvalues['page']['itemid'] = $draftitemid;
         }
-        if (!empty($default_values['displayoptions'])) {
-            $displayoptions = unserialize($default_values['displayoptions']);
+        if (!empty($defaultvalues['displayoptions'])) {
+            $displayoptions = unserialize($defaultvalues['displayoptions']);
             if (isset($displayoptions['printintro'])) {
-                $default_values['printintro'] = $displayoptions['printintro'];
+                $defaultvalues['printintro'] = $displayoptions['printintro'];
             }
             if (isset($displayoptions['printheading'])) {
-                $default_values['printheading'] = $displayoptions['printheading'];
+                $defaultvalues['printheading'] = $displayoptions['printheading'];
+            }
+            if (isset($displayoptions['printlastmodified'])) {
+                $defaultvalues['printlastmodified'] = $displayoptions['printlastmodified'];
             }
             if (!empty($displayoptions['popupwidth'])) {
-                $default_values['popupwidth'] = $displayoptions['popupwidth'];
+                $defaultvalues['popupwidth'] = $displayoptions['popupwidth'];
             }
             if (!empty($displayoptions['popupheight'])) {
-                $default_values['popupheight'] = $displayoptions['popupheight'];
+                $defaultvalues['popupheight'] = $displayoptions['popupheight'];
             }
         }
     }
index 6424204..a1c9e06 100644 (file)
@@ -43,6 +43,8 @@ if ($ADMIN->fulltree) {
         get_string('printheading', 'page'), get_string('printheadingexplain', 'page'), 1));
     $settings->add(new admin_setting_configcheckbox('page/printintro',
         get_string('printintro', 'page'), get_string('printintroexplain', 'page'), 0));
+    $settings->add(new admin_setting_configcheckbox('page/printlastmodified',
+        get_string('printlastmodified', 'page'), get_string('printlastmodifiedexplain', 'page'), 1));
     $settings->add(new admin_setting_configselect('page/display',
         get_string('displayselect', 'page'), get_string('displayselectexplain', 'page'), RESOURCELIB_DISPLAY_OPEN, $displayoptions));
     $settings->add(new admin_setting_configtext('page/popupwidth',
diff --git a/mod/page/tests/behat/page_appearance.feature b/mod/page/tests/behat/page_appearance.feature
new file mode 100644 (file)
index 0000000..efcee45
--- /dev/null
@@ -0,0 +1,62 @@
+@mod @mod_page
+Feature: Configure page appearance
+  In order to change the appearance of the page resource
+  As an admin
+  I need to configure the page appearance settings
+
+  Background:
+    Given the following "courses" exist:
+      | shortname | fullname   |
+      | C1        | Course 1 |
+    And the following "activities" exist:
+      | activity | name       | intro      | course | idnumber |
+      | page     | PageName1  | PageDesc1  | C1     | PAGE1    |
+    And I log in as "admin"
+
+  @javascript
+  Scenario: Hide and display the page name
+    Given I am on "Course 1" course homepage
+    When I follow "PageName1"
+    Then I should see "PageName1" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display page name" "checkbox"
+    And I press "Save and display"
+    Then I should not see "PageName1" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display page name" "checkbox"
+    And I press "Save and display"
+    Then I should see "PageName1" in the "region-main" "region"
+
+  @javascript
+  Scenario: Display and hide the page description
+    Given I am on "Course 1" course homepage
+    When I follow "PageName1"
+    Then I should not see "PageDesc1" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display page description" "checkbox"
+    And I press "Save and display"
+    Then I should see "PageDesc1" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display page description" "checkbox"
+    And I press "Save and display"
+    Then I should not see "PageDesc1" in the "region-main" "region"
+
+  @javascript
+  Scenario: Display and hide the last modified date
+    Given I am on "Course 1" course homepage
+    When I follow "PageName1"
+    Then I should see "Last modified:" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display last modified date" "checkbox"
+    And I press "Save and display"
+    Then I should not see "Last modified:" in the "region-main" "region"
+    And I navigate to "Edit settings" in current page administration
+    And I follow "Appearance"
+    When I click on "Display last modified date" "checkbox"
+    And I press "Save and display"
+    Then I should see "Last modified:" in the "region-main" "region"
index 9ca99ee..2121a9e 100644 (file)
@@ -57,6 +57,9 @@ class mod_page_generator extends testing_module_generator {
         if (!isset($record->printintro)) {
             $record->printintro = 0;
         }
+        if (!isset($record->printlastmodified)) {
+            $record->printlastmodified = 1;
+        }
 
         return parent::create_instance($record, (array)$options);
     }
index 93ffa65..6033a00 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018051400;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2018051401;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018050800;    // Requires this Moodle version
 $plugin->component = 'mod_page';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index 149e0b1..4297a7f 100644 (file)
@@ -88,7 +88,9 @@ $formatoptions->context = $context;
 $content = format_text($content, $page->contentformat, $formatoptions);
 echo $OUTPUT->box($content, "generalbox center clearfix");
 
-$strlastmodified = get_string("lastmodified");
-echo "<div class=\"modified\">$strlastmodified: ".userdate($page->timemodified)."</div>";
+if (!isset($options['printlastmodified']) || !empty($options['printlastmodified'])) {
+    $strlastmodified = get_string("lastmodified");
+    echo html_writer::div("$strlastmodified: " . userdate($page->timemodified), 'modified');
+}
 
 echo $OUTPUT->footer();
index 6eb80f6..6f2391b 100644 (file)
@@ -16,7 +16,7 @@
       </FIELDS>
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
-        <KEY NAME="questionusageid-slot" TYPE="foreign-unique" FIELDS="questionusageid, slot" REFTABLE="question_attempts" REFFIELDS="questionusageid, slot"/>
+        <KEY NAME="questionusageid-slot" TYPE="foreign" FIELDS="questionusageid, slot" REFTABLE="question_attempts" REFFIELDS="questionusageid, slot"/>
       </KEYS>
     </TABLE>
   </TABLES>
index 81efd89..0cc8919 100644 (file)
@@ -42,21 +42,42 @@ function xmldb_quiz_overview_upgrade($oldversion) {
     // Automatically generated Moodle v3.4.0 release upgrade line.
     // Put any upgrade step following this.
 
-    if ($oldversion < 2018021800) {
+    // Automatically generated Moodle v3.5.0 release upgrade line.
+    // Put any upgrade step following this.
+
+    // Upgrade 2018021800 (now removed) incorrectly added this key
+    // with a unique constraint, which breaks things because those
+    // columns are not, in fact, unique. So drop (if it exists) then recreate.
+    if ($oldversion < 2018061800) {
+
+        // Define key questionusageid-slot (foreign) to be dropped form quiz_overview_regrades.
+        $table = new xmldb_table('quiz_overview_regrades');
+
+        // There is no key_exists, so test the equivalent index.
+        $oldindex = new xmldb_index('questionusageid-slot', XMLDB_KEY_UNIQUE, array('questionusageid', 'slot'));
+
+        // Launch drop key questionusageid-slot.
+        if ($dbman->index_exists($table, $oldindex)) {
+            $key = new xmldb_key('questionusageid-slot', XMLDB_KEY_FOREIGN, array('questionusageid', 'slot'), 'question_attempts', array('questionusageid', 'slot'));
+            $dbman->drop_key($table, $key);
+        }
 
-        // Define key questionusageid-slot (foreign-unique) to be added to quiz_overview_regrades.
+        // Overview savepoint reached.
+        upgrade_plugin_savepoint(true, 2018061800, 'quiz', 'overview');
+    }
+
+    if ($oldversion < 2018061801) {
+
+        // Define key questionusageid-slot (foreign) to be added to quiz_overview_regrades.
         $table = new xmldb_table('quiz_overview_regrades');
-        $key = new xmldb_key('questionusageid-slot', XMLDB_KEY_FOREIGN_UNIQUE, array('questionusageid', 'slot'), 'question_attempts', array('questionusageid', 'slot'));
+        $key = new xmldb_key('questionusageid-slot', XMLDB_KEY_FOREIGN, array('questionusageid', 'slot'), 'question_attempts', array('questionusageid', 'slot'));
 
         // Launch add key questionusageid-slot.
         $dbman->add_key($table, $key);
 
         // Overview savepoint reached.
-        upgrade_plugin_savepoint(true, 2018021800, 'quiz', 'overview');
+        upgrade_plugin_savepoint(true, 2018061801, 'quiz', 'overview');
     }
 
-    // Automatically generated Moodle v3.5.0 release upgrade line.
-    // Put any upgrade step following this.
-
     return true;
 }
index b3a68de..76d0534 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version  = 2018051400;
+$plugin->version  = 2018061801;
 $plugin->requires = 2018050800;
 $plugin->component = 'quiz_overview';
index 855499e..3d6c1c7 100644 (file)
@@ -27,8 +27,9 @@ namespace mod_scorm\privacy;
 defined('MOODLE_INTERNAL') || die();
 
 use core_privacy\local\metadata\collection;
-use core_privacy\local\request\contextlist;
 use core_privacy\local\request\approved_contextlist;
+use core_privacy\local\request\contextlist;
+use core_privacy\local\request\helper;
 use core_privacy\local\request\transform;
 use core_privacy\local\request\writer;
 
@@ -122,10 +123,18 @@ class provider implements
             return;
         }
 
-        $userid = $contextlist->get_user()->id;
-        list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
+        $user = $contextlist->get_user();
+        $userid = $user->id;
+        // Get SCORM data.
+        foreach ($contexts as $contextid) {
+            $context = \context::instance_by_id($contextid);
+            $data = helper::get_context_data($context, $user);
+            writer::with_context($context)->export_data([], $data);
+            helper::export_context_files($context, $user);
+        }
 
         // Get scoes_track data.
+        list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
         $sql = "SELECT ss.id,
                        ss.attempt,
                        ss.element,
@@ -152,14 +161,17 @@ class provider implements
         }
         $scoestracks->close();
 
-        // The scoes_track data is organised in: {Course name}/{SCORM activity name}/attempt-X.json.
+        // The scoes_track data is organised in: {Course name}/{SCORM activity name}/{My attempts}/{Attempt X}/data.json
         // where X is the attempt number.
         array_walk($alldata, function($attemptsdata, $contextid) {
             $context = \context::instance_by_id($contextid);
             array_walk($attemptsdata, function($data, $attempt) use ($context) {
-                writer::with_context($context)->export_related_data(
-                    [],
-                    'attempt-'.$attempt,
+                $subcontext = [
+                    get_string('myattempts', 'scorm'),
+                    get_string('attempt', 'scorm'). " $attempt"
+                ];
+                writer::with_context($context)->export_data(
+                    $subcontext,
                     (object)['scoestrack' => $data]
                 );
             });
@@ -199,13 +211,15 @@ class provider implements
         }
         $aiccsessions->close();
 
-        // The aicc_session data is organised in: {Course name}/{SCORM activity name}/aiccsession.json.
+        // The aicc_session data is organised in: {Course name}/{SCORM activity name}/{My AICC sessions}/data.json
         // In this case, the attempt hasn't been included in the json file because it can be null.
         array_walk($alldata, function($data, $contextid) {
             $context = \context::instance_by_id($contextid);
-            writer::with_context($context)->export_related_data(
-                [],
-                'aiccsession',
+            $subcontext = [
+                get_string('myaiccsessions', 'scorm')
+            ];
+            writer::with_context($context)->export_data(
+                $subcontext,
                 (object)['sessions' => $data]
             );
         });
index c541a07..c693dc3 100644 (file)
@@ -208,6 +208,8 @@ $string['interactions'] = 'Interactions';
 $string['masteryoverride'] = 'Mastery score overrides status';
 $string['masteryoverride_help'] = 'If enabled and a mastery score is provided, when LMSFinish is called and a raw score has been set, status will be recalculated using the raw score and mastery score and any status provided by the SCORM (including "incomplete") will be overridden.';
 $string['masteryoverridedesc'] = 'This preference sets the default for the mastery score override setting';
+$string['myattempts'] = 'My attempts';
+$string['myaiccsessions'] = 'My AICC sessions';
 $string['repositorynotsupported'] = 'This repository does not support linking directly to an imsmanifest.xml file.';
 $string['trackid'] = 'Id';
 $string['trackid_help'] = 'This is the identifier set by your SCORM package for this question, the SCORM specification doesn\'t allow the full question text to be provided.';
index 096988d..00811b7 100644 (file)
@@ -79,11 +79,18 @@ class mod_scorm_testcase extends provider_testcase {
         // Validate exported data for student0 (without any AICC/SCORM attempt).
         $this->setUser($this->student0);
         $writer = writer::with_context($this->context);
+
         $this->export_context_data_for_user($this->student0->id, $this->context, 'mod_scorm');
-        $data = $writer->get_related_data([], 'attempt-1');
+        $subcontextattempt1 = [
+            get_string('myattempts', 'scorm'),
+            get_string('attempt', 'scorm'). " 1"
+        ];
+        $subcontextaicc = [
+            get_string('myaiccsessions', 'scorm')
+        ];
+        $data = $writer->get_data($subcontextattempt1);
         $this->assertEmpty($data);
-        $this->export_context_data_for_user($this->student0->id, $this->context, 'mod_scorm');
-        $data = $writer->get_related_data([], 'aiccsession');
+        $data = $writer->get_data($subcontextaicc);
         $this->assertEmpty($data);
 
         // Validate exported data for student1.
@@ -92,17 +99,28 @@ class mod_scorm_testcase extends provider_testcase {
         $writer = writer::with_context($this->context);
         $this->assertFalse($writer->has_any_data());
         $this->export_context_data_for_user($this->student1->id, $this->context, 'mod_scorm');
-        $data = $writer->get_related_data([], 'attempt-1');
+
+        $data = $writer->get_data([]);
+        $this->assertEquals('SCORM1', $data->name);
+
+        $data = $writer->get_data($subcontextattempt1);
         $this->assertCount(1, (array) $data);
         $this->assertCount(2, (array) reset($data));
-        $data = $writer->get_related_data([], 'attempt-2');
+        $subcontextattempt2 = [
+            get_string('myattempts', 'scorm'),
+            get_string('attempt', 'scorm'). " 2"
+        ];
+        $data = $writer->get_data($subcontextattempt2);
         $this->assertCount(2, (array) reset($data));
         // The student1 has only 2 scoes_track attempts.
-        $data = $writer->get_related_data([], 'attempt-3');
+        $subcontextattempt3 = [
+            get_string('myattempts', 'scorm'),
+            get_string('attempt', 'scorm'). " 3"
+        ];
+        $data = $writer->get_data($subcontextattempt3);
         $this->assertEmpty($data);
         // The student1 has only 1 aicc_session.
-        $this->export_context_data_for_user($this->student1->id, $this->context, 'mod_scorm');
-        $data = $writer->get_related_data([], 'aiccsession');
+        $data = $writer->get_data($subcontextaicc);
         $this->assertCount(1, (array) $data);
     }
 
@@ -189,7 +207,8 @@ class mod_scorm_testcase extends provider_testcase {
 
         // Setup test data.
         $course = $this->getDataGenerator()->create_course();
-        $scorm = $this->getDataGenerator()->create_module('scorm', array('course' => $course->id));
+        $params = array('course' => $course->id, 'name' => 'SCORM1');
+        $scorm = $this->getDataGenerator()->create_module('scorm', $params);
         $this->context = \context_module::instance($scorm->cmid);
 
         // Users enrolments.
index 2406c03..423d8c2 100644 (file)
@@ -177,6 +177,10 @@ if ($id) {
 
     $groupmode = groups_get_activity_groupmode($cm);
 
+    // This is where people will land when they change groups using the drop-down selector.
+    // Set the activity group so tabs and content are shown correctly.
+    $currentgroup = groups_get_activity_group($cm, true);
+
     if ($wiki->wikimode == 'individual' && ($groupmode == SEPARATEGROUPS || $groupmode == VISIBLEGROUPS)) {
         list($gid, $uid) = explode('-', $groupanduser);
     } else if ($wiki->wikimode == 'individual') {
index 8d0b4f6..17246d5 100644 (file)
@@ -156,15 +156,15 @@ class core_question_renderer extends plugin_renderer_base {
      * @return HTML fragment.
      */
     protected function number($number) {
+        if (trim($number) === '') {
+            return '';
+        }
         $numbertext = '';
-        if (is_numeric($number)) {
+        if (trim($number) === 'i') {
+            $numbertext = get_string('information', 'question');
+        } else {
             $numbertext = get_string('questionx', 'question',
                     html_writer::tag('span', $number, array('class' => 'qno')));
-        } else if ($number == 'i') {
-            $numbertext = get_string('information', 'question');
-        }
-        if (!$numbertext) {
-            return '';
         }
         return html_writer::tag('h3', $numbertext, array('class' => 'no'));
     }
index 0614f60..6877d45 100644 (file)
@@ -1243,3 +1243,22 @@ class question_test_recordset extends moodle_recordset {
         $this->records = null;
     }
 }
+
+/**
+ * Helper class for tests that help to test core_question_renderer.
+ *
+ * @copyright  2018 Huong Nguyen <huongnv13@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class testable_core_question_renderer extends core_question_renderer {
+
+    /**
+     * Test the private number function.
+     *
+     * @param null|string $number
+     * @return HTML
+     */
+    public function number($number) {
+        return parent::number($number);
+    }
+}
index 9e800b1..db1f72e 100644 (file)
@@ -131,4 +131,20 @@ class question_engine_test extends advanced_testcase {
     public function test_is_manual_grade_in_range_ungraded() {
         $this->assertTrue(question_engine::is_manual_grade_in_range(1, 2));
     }
+
+    public function test_render_question_number() {
+        global $PAGE;
+        $renderer = new testable_core_question_renderer($PAGE, 'core_question');
+
+        // Test with number is i character.
+        $this->assertEquals('<h3 class="no">Information</h3>', $renderer->number('i'));
+        // Test with number is empty string.
+        $this->assertEquals('', $renderer->number(''));
+        // Test with number is 0.
+        $this->assertEquals('<h3 class="no">Question <span class="qno">0</span></h3>', $renderer->number(0));
+        // Test with number is numeric.
+        $this->assertEquals('<h3 class="no">Question <span class="qno">1</span></h3>', $renderer->number(1));
+        // Test with number is string.
+        $this->assertEquals('<h3 class="no">Question <span class="qno">1 of 2</span></h3>', $renderer->number('1 of 2'));
+    }
 }
index 0a724eb..5ab0b76 100644 (file)
@@ -47,7 +47,7 @@ class qtype_shortanswer_renderer extends qtype_renderer {
             'value' => $currentanswer,
             'id' => $inputname,
             'size' => 80,
-            'class' => 'form-control',
+            'class' => 'form-control d-inline',
         );
 
         if ($options->readonly) {
index 638aff7..02f7ea8 100644 (file)
@@ -76,9 +76,10 @@ if ($contextid) {
         $customdata['withincourseid'] = $coursecontext->instanceid;
     }
 
-    // Get available ordering options from search engine.
-    $customdata['orderoptions'] = $search->get_engine()->get_supported_orders($context);
 }
+// Get available ordering options from search engine.
+$customdata['orderoptions'] = $search->get_engine()->get_supported_orders($context);
+
 $mform = new \core_search\output\form\search(null, $customdata);
 
 $data = $mform->get_data();
index 744dae9..25379ce 100644 (file)
@@ -166,7 +166,7 @@ class provider implements
             $params['itemid'] = $itemid;
         }
         if ($userid) {
-            $params['userid'] = $userid;
+            $params['tiuserid'] = $userid;
         }
         $DB->delete_records('tag_instance', $params);
     }
index ccbda47..56c8ae9 100644 (file)
@@ -121,6 +121,35 @@ class core_tag_privacy_testcase extends provider_testcase {
         $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance'));
     }
 
+    /**
+     * Test method delete_item_tags() with userid.
+     */
+    public function test_delete_item_tags_with_userid() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+        // Create a course to tag.
+        $course = $this->getDataGenerator()->create_course();
+        $context = context_course::instance($course->id);
+
+        // Create a user to perform tagging.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Tag courses.
+        core_tag_tag::set_item_tags('core_course', 'course', $course->id, $context, ['Tag 1', 'Tag 2'], $user->id);
+        $expectedtagcount = $DB->count_records('tag_instance');
+
+        // Delete tags for course. Use wrong userid.
+        core_tag\privacy\provider::delete_item_tags($context, 'core_course', 'course', null, 1);
+        $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance'));
+
+        $expectedtagcount -= 2;
+        // Delete tags for course. Use correct userid.
+        core_tag\privacy\provider::delete_item_tags($context, 'core_course', 'course', null, $user->id);
+        $this->assertEquals($expectedtagcount, $DB->count_records('tag_instance'));
+    }
+
     /**
      * Test method delete_item_tags_select().
      */
index fc90019..fe36c9b 100644 (file)
Binary files a/theme/boost/amd/build/drawer.min.js and b/theme/boost/amd/build/drawer.min.js differ
index 6b48b55..cc9042b 100644 (file)
@@ -32,6 +32,8 @@ define(['jquery', 'core/custom_interaction_events', 'core/log'],
         SECTION: '.list-group-item[href*="#section-"]'
     };
 
+    var small = $(document).width() < 768;
+
     /**
      * Constructor for the Drawer.
      *
@@ -52,6 +54,10 @@ define(['jquery', 'core/custom_interaction_events', 'core/log'],
             var hidden = trigger.attr('aria-expanded') == 'false';
             var side = trigger.attr('data-side');
             var body = $(SELECTORS.BODY);
+            var preference = trigger.attr('data-preference');
+            if (small) {
+                M.util.set_user_preference(preference, 'false');
+            }
 
             drawer.on('mousewheel DOMMouseScroll', this.preventPageScroll);
 
@@ -64,7 +70,6 @@ define(['jquery', 'core/custom_interaction_events', 'core/log'],
         }.bind(this));
 
         this.registerEventListeners();
-        var small = $(document).width() < 768;
         if (small) {
             this.closeAll();
         }
@@ -83,7 +88,9 @@ define(['jquery', 'core/custom_interaction_events', 'core/log'],
             body.removeClass('drawer-open-' + side);
             drawer.attr('aria-hidden', 'true');
             drawer.addClass('closed');
-            M.util.set_user_preference(preference, 'false');
+            if (!small) {
+                M.util.set_user_preference(preference, 'false');
+            }
         });
     };
 
@@ -100,6 +107,9 @@ define(['jquery', 'core/custom_interaction_events', 'core/log'],
         var body = $(SELECTORS.BODY);
         var side = trigger.attr('data-side');
         var preference = trigger.attr('data-preference');
+        if (small) {
+            M.util.set_user_preference(preference, 'false');
+        }
 
         body.addClass('drawer-ease');
         var open = trigger.attr('aria-expanded') == 'true';
@@ -110,14 +120,18 @@ define(['jquery', 'core/custom_interaction_events', 'core/log'],
             drawer.focus();
             body.addClass('drawer-open-' + side);
             drawer.removeClass('closed');
-            M.util.set_user_preference(preference, 'true');
+            if (!small) {
+                M.util.set_user_preference(preference, 'true');
+            }
         } else {
             // Close.
             body.removeClass('drawer-open-' + side);
             trigger.attr('aria-expanded', 'false');
             drawer.attr('aria-hidden', 'true');
             drawer.addClass('closed');
-            M.util.set_user_preference(preference, 'false');
+            if (!small) {
+                M.util.set_user_preference(preference, 'false');
+            }
         }
     };
 
@@ -153,7 +167,6 @@ define(['jquery', 'core/custom_interaction_events', 'core/log'],
         }.bind(this));
 
         $(SELECTORS.SECTION).click(function() {
-            var small = $(document).width() < 768;
             if (small) {
                 this.closeAll();
             }
index bf755b7..379e50b 100644 (file)
 
 namespace theme_boost\privacy;
 
+use \core_privacy\local\metadata\collection;
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * The boost theme does not store any data.
+ * The boost theme stores a user preference data.
  *
  * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements
+    // This plugin has data.
+    \core_privacy\local\metadata\provider,
+    // This plugin has some sitewide user preferences to export.
+    \core_privacy\local\request\user_preference_provider {
+
+    /** The user preference for the navigation drawer. */
+    const DRAWER_OPEN_NAV = 'drawer-open-nav';
+
+    /**
+     * Returns meta data about this system.
+     *
+     * @param  collection $items The initialised item collection to add items to.
+     * @return collection A listing of user data stored through this system.
+     */
+    public static function get_metadata(collection $items) : collection {
+        $items->add_user_preference(self::DRAWER_OPEN_NAV, 'privacy:metadata:preference:draweropennav');
+        return $items;
+    }
 
     /**
-     * Get the language string identifier with the component's language
-     * file to explain why this plugin stores no data.
+     * Store all user preferences for the plugin.
      *
-     * @return  string
+     * @param int $userid The userid of the user whose data is to be exported.
      */
-    public static function get_reason() : string {
-        return 'privacy:metadata';
+    public static function export_user_preferences(int $userid) {
+        $draweropennavpref = get_user_preferences(self::DRAWER_OPEN_NAV, null, $userid);
+
+        if (isset($draweropennavpref)) {
+            $preferencestring = get_string('privacy:drawernavclosed', 'theme_boost');
+            if ($draweropennavpref == 'true') {
+                $preferencestring = get_string('privacy:drawernavopen', 'theme_boost');
+            }
+            \core_privacy\local\request\writer::export_user_preference(
+                'theme_boost',
+                self::DRAWER_OPEN_NAV,
+                $draweropennavpref,
+                $preferencestring
+            );
+        }
     }
 }
index 2f3b990..627ccf7 100644 (file)
@@ -49,3 +49,6 @@ $string['rawscss_desc'] = 'Use this field to provide SCSS or CSS code which will
 $string['rawscsspre'] = 'Raw initial SCSS';
 $string['rawscsspre_desc'] = 'In this field you can provide initialising SCSS code, it will be injected before everything else. Most of the time you will use this setting to define variables.';
 $string['region-side-pre'] = 'Right';
+$string['privacy:metadata:preference:draweropennav'] = 'The user\'s preference for hiding or showing the drawer menu navigation.';
+$string['privacy:drawernavclosed'] = 'The current preference for the navigation drawer is closed.';
+$string['privacy:drawernavopen'] = 'The current preference for the navigation drawer is open.';
index 3442487..4d01c21 100644 (file)
@@ -127,9 +127,9 @@ $tag-font-weight: bold !default;
     background-size: ($input-height / 2) ($input-height / 2);
 }
 
-.form-check-input {
-    position: static;
-}
+// .form-check-input {
+//     position: static;
+// }
 
 @mixin tag-variant($color) {
     background-color: $color;
index af0e685..53e5411 100644 (file)
@@ -2108,3 +2108,10 @@ $footer-link-color: $bg-inverse-link-color !default;
 .open.atto_menu > .dropdown-menu {
     display: block;
 }
+div.editor_atto_toolbar button .icon {
+    color: $gray-700;
+}
+.w-auto {
+    width: auto;
+}
+
index fde60b3..c368d44 100644 (file)
@@ -709,10 +709,6 @@ span.editinstructions {
     &.even {
         background-color: $table-bg-accent;
     }
-
-    &:hover {
-        background-color: $table-bg-hover;
-    }
 }
 
 .courses > .paging.paging-morelink {
index 1f2467a..26557f8 100644 (file)
@@ -126,13 +126,6 @@ body:not(.jsenabled) .langmenu:hover > .dropdown-menu,
     overflow-y: auto;
 }
 
-// Extends bootstrapbase/less/bootstrap/type.less
-// to enable ol lists to use a larger number set.
-
-ol {
-    margin: 0 0 $line-height-base / 2 2.5em;
-}
-
 // Set menus in the fixed header to scroll vertically when they are longer than the page.
 .navbar.fixed-top .dropdown .dropdown-menu {
     max-height: calc(100vh - #{$navbar-height});
index 48d72ed..ca9d291 100644 (file)
                 <div class="fp-license form-group row">
                     <label class="form-control-label col-4">{{#str}}chooselicense, repository{{/str}}</label>
                     <div class="col-8">
-                        <select class="form-control"></select>
+                        <select class="custom-select"></select>
                     </div>
                 </div>
                 <div class="fp-path form-group row">
                     <label class="form-control-label col-4">{{#str}}path, repository{{/str}}</label>
                     <div class="col-8">
-                        <select class="form-control"></select>
+                        <select class="custom-select"></select>
                     </div>
                 </div>
                 <div class="fp-original form-group row">
index 7002e76..c9d6cae 100644 (file)
@@ -22,7 +22,7 @@
                 </div>
                 <div class="fp-login-select form-group">
                     <label class="form-control-label"></label>
-                    <select class="form-control"></select>
+                    <select class="custom-select"></select>
                 </div>
                 <div class="fp-login-input form-group">
                     <label class="form-control-label"></label>
index 110a5e8..d27da6e 100644 (file)
@@ -41,7 +41,7 @@
             </div>
             <div class="fp-setlicense form-group row">
                 <label class="col-form-label">{{#str}}chooselicense, repository{{/str}}</label>
-                <select class="form-control"></select>
+                <select class="custom-select"></select>
             </div>
             <div class="form-group row">
                 <div class="fp-select-buttons">
index 4caf07b..b3aec8e 100644 (file)
@@ -18,7 +18,7 @@
                 </div>
                 <div class="fp-setlicense control-group">
                     <label>{{#str}}chooselicense, repository{{/str}}</label>
-                    <select class="form-control"></select>
+                    <select class="custom-select"></select>
                 </div>
             </div>
         </form>
index c01bff2..8adcac2 100644 (file)
@@ -1,6 +1,6 @@
 {{< core_form/element-template-inline }}
     {{$element}}
-        <div class="fdate_time_selector d-flex">
+        <div class="fdate_time_selector d-flex align-items-center">
         {{#element.elements}}
             {{{separator}}}
             {{{html}}}
index fb4d48f..225c4ee 100644 (file)
@@ -1,6 +1,6 @@
 {{< core_form/element-template }}
     {{$element}}
-        <div class="fdate_time_selector d-flex flex-wrap">
+        <div class="fdate_time_selector d-flex flex-wrap align-items-center">
         {{#element.elements}}
             {{{separator}}}
             {{{html}}}
diff --git a/theme/boost/tests/privacy_test.php b/theme/boost/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..6df617f
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Privacy tests for theme_boost.
+ *
+ * @package    theme_boost
+ * @category   test
+ * @copyright  2018 Adrian Greeve <adriangreeve.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+use \theme_boost\privacy\provider;
+
+/**
+ * Unit tests for theme_boost/classes/privacy/policy
+ *
+ * @copyright  2018 Adrian Greeve <adriangreeve.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class theme_boost_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Test for provider::test_export_user_preferences().
+     */
+    public function test_export_user_preferences() {
+        $this->resetAfterTest();
+
+        // Test setup.
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+
+        // Add a user home page preference for the User.
+        set_user_preference(provider::DRAWER_OPEN_NAV, 'false', $user);
+
+        // Test the user preferences export contains 1 user preference record for the User.
+        provider::export_user_preferences($user->id);
+        $contextuser = context_user::instance($user->id);
+        $writer = \core_privacy\local\request\writer::with_context($contextuser);
+        $this->assertTrue($writer->has_any_data());
+
+        $exportedpreferences = $writer->get_user_preferences('theme_boost');
+        $this->assertCount(1, (array) $exportedpreferences);
+        $this->assertEquals('false', $exportedpreferences->{provider::DRAWER_OPEN_NAV}->value);
+        $this->assertEquals(get_string('privacy:drawernavclosed', 'theme_boost'),
+                $exportedpreferences->{provider::DRAWER_OPEN_NAV}->description);
+
+        // Add a user home page preference for the User.
+        set_user_preference(provider::DRAWER_OPEN_NAV, 'true', $user);
+
+        // Test the user preferences export contains 1 user preference record for the User.
+        provider::export_user_preferences($user->id);
+        $contextuser = context_user::instance($user->id);
+        $writer = \core_privacy\local\request\writer::with_context($contextuser);
+        $this->assertTrue($writer->has_any_data());
+
+        $exportedpreferences = $writer->get_user_preferences('theme_boost');
+        $this->assertCount(1, (array) $exportedpreferences);
+        $this->assertEquals('true', $exportedpreferences->{provider::DRAWER_OPEN_NAV}->value);
+        $this->assertEquals(get_string('privacy:drawernavopen', 'theme_boost'),
+                $exportedpreferences->{provider::DRAWER_OPEN_NAV}->description);
+    }
+}
index 8d4842e..b7a2223 100644 (file)
@@ -25,6 +25,9 @@
 
 
 // Due to lessphp limitations, the following classes must be declared statically without use of a loop.
+.p-0 {
+    padding: 0 !important;
+}
 .p-l-1 {
     padding-left: 1 * @baseFontSize !important;
 }
     padding: 1 * @baseFontSize !important;
 }
 
+.m-0 {
+    margin: 0 !important;
+}
+
 .m-l-1 {
     margin-left: 1 * @baseFontSize !important;
 }
 .bg-faded {
     background-color: @grayLighter;
 }
+
+.w-100 {
+    width: 100%;
+}
index 770d95c..9c26f9a 100644 (file)
@@ -578,7 +578,6 @@ span.editinstructions {
 .courses .coursebox.even {
     background-color: @tableBackgroundAccent;
 }
-.courses .coursebox:hover,
 .course_category_tree .courses > .paging.paging-morelink:hover {
     background-color: @tableBackgroundHover;
 }
index ce2775a..cd03f9b 100644 (file)
@@ -162,7 +162,9 @@ form {
 #page-mod-forum-search .c1 {
     .form-horizontal .controls
 }
-
+.form-inline label:not(.sr-only):not(.accesshide) + select {
+    margin-left: 0.5rem;
+}
 
 .formsettingheading {
     .form-horizontal .help-block
index 4bb1976..ce1163d 100644 (file)
@@ -6480,7 +6480,6 @@ span.editinstructions {
 .courses .coursebox.even {
   background-color: #f9f9f9;
 }
-.courses .coursebox:hover,
 .course_category_tree .courses > .paging.paging-morelink:hover {
   background-color: #f5f5f5;
 }
@@ -16726,6 +16725,9 @@ form {
 #page-mod-forum-search .c1:first-child {
   *padding-left: 200px;
 }
+.form-inline label:not(.sr-only):not(.accesshide) + select {
+  margin-left: 0.5rem;
+}
 .formsettingheading {
   margin-bottom: 0;
 }
@@ -21836,6 +21838,9 @@ ul.indented-list {
 .card-block {
   padding: 1.25rem;
 }
+.p-0 {
+  padding: 0 !important;
+}
 .p-l-1 {
   padding-left: 14px !important;
 }
@@ -21859,6 +21864,9 @@ ul.indented-list {
 .p-a-1 {
   padding: 14px !important;
 }
+.m-0 {
+  margin: 0 !important;
+}
 .m-l-1 {
   margin-left: 14px !important;
 }
@@ -22066,3 +22074,6 @@ ul.indented-list {
 .bg-faded {
   background-color: #eee;
 }
+.w-100 {
+  width: 100%;
+}
diff --git a/user/filters/anycourses.php b/user/filters/anycourses.php
new file mode 100644 (file)
index 0000000..39022af
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is filter is used to see which students are enroled on any courses
+ *
+ * @package   core_user
+ * @copyright 2014 Krister Viirsaar
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User filter to distinguish users with no or any enroled courses.
+ * @copyright 2014 Krister Viirsaar
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_filter_anycourses extends user_filter_yesno {
+
+    /**
+     * Returns the condition to be used with SQL
+     *
+     * @param array $data filter settings
+     * @return array sql string and $params
+     */
+    public function get_sql_filter($data) {
+        $value = $data['value'];
+
+        $not = $value ? '' : 'NOT';
+
+        return array("EXISTS ( SELECT userid FROM {user_enrolments} ) AND " .
+            " id $not IN ( SELECT userid FROM {user_enrolments} )", array());
+    }
+}
+
index 83852a2..2264671 100644 (file)
@@ -31,6 +31,7 @@ require_once($CFG->dirroot.'/user/filters/courserole.php');
 require_once($CFG->dirroot.'/user/filters/globalrole.php');
 require_once($CFG->dirroot.'/user/filters/profilefield.php');
 require_once($CFG->dirroot.'/user/filters/yesno.php');
+require_once($CFG->dirroot.'/user/filters/anycourses.php');
 require_once($CFG->dirroot.'/user/filters/cohort.php');
 require_once($CFG->dirroot.'/user/filters/user_filter_forms.php');
 require_once($CFG->dirroot.'/user/filters/checkbox.php');
@@ -63,10 +64,11 @@ class user_filtering {
         }
 
         if (empty($fieldnames)) {
-            $fieldnames = array('realname' => 0, 'lastname' => 1, 'firstname' => 1, 'username' => 1, 'email' => 1, 'city' => 1, 'country' => 1,
-                                'confirmed' => 1, 'suspended' => 1, 'profile' => 1, 'courserole' => 1, 'systemrole' => 1,
-                                'cohort' => 1, 'firstaccess' => 1, 'lastaccess' => 1, 'neveraccessed' => 1, 'timemodified' => 1,
-                                'nevermodified' => 1, 'auth' => 1, 'mnethostid' => 1, 'idnumber' => 1);
+            $fieldnames = array('realname' => 0, 'lastname' => 1, 'firstname' => 1, 'username' => 1, 'email' => 1, 'city' => 1,
+                                'country' => 1, 'confirmed' => 1, 'suspended' => 1, 'profile' => 1, 'courserole' => 1,
+                                'anycourses' => 1, 'systemrole' => 1, 'cohort' => 1, 'firstaccess' => 1, 'lastaccess' => 1,
+                                'neveraccessed' => 1, 'timemodified' => 1, 'nevermodified' => 1, 'auth' => 1, 'mnethostid' => 1,
+                                'idnumber' => 1);
         }
 
         $this->_fields  = array();
@@ -142,6 +144,8 @@ class user_filtering {
             case 'suspended':   return new user_filter_yesno('suspended', get_string('suspended', 'auth'), $advanced, 'suspended');
             case 'profile':     return new user_filter_profilefield('profile', get_string('profilefields', 'admin'), $advanced);
             case 'courserole':  return new user_filter_courserole('courserole', get_string('courserole', 'filters'), $advanced);
+            case 'anycourses':
+                return new user_filter_anycourses('anycourses', get_string('anycourses', 'filters'), $advanced, 'user_enrolments');
             case 'systemrole':  return new user_filter_globalrole('systemrole', get_string('globalrole', 'role'), $advanced);
             case 'firstaccess': return new user_filter_date('firstaccess', get_string('firstaccess', 'filters'), $advanced, 'firstaccess');
             case 'lastaccess':  return new user_filter_date('lastaccess', get_string('lastaccess'), $advanced, 'lastaccess');
index 52ddf3b..d81ce8b 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018061400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018062100.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.6dev (Build: 20180614)'; // Human-friendly version name
+$release  = '3.6dev (Build: 20180621)'; // Human-friendly version name
 
 $branch   = '36';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.