Merge branch 'MDL-62783' of https://github.com/timhunt/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Jun 2018 22:25:33 +0000 (00:25 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Wed, 27 Jun 2018 22:25:33 +0000 (00:25 +0200)
33 files changed:
admin/searchareas.php
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
filter/emoticon/filter.php
filter/emoticon/tests/filter_test.php
lang/en/admin.php
lang/en/search.php
lib/environmentlib.php
lib/evalmath/evalmath.class.php
lib/evalmath/readme_moodle.txt
lib/excellib.class.php
lib/filelib.php
lib/mathslib.php
lib/tests/mathslib_test.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
question/type/shortanswer/renderer.php
theme/boost/scss/moodle/course.scss
theme/boost/templates/core_form/element-date_time_selector-inline.mustache
theme/boost/templates/core_form/element-date_time_selector.mustache
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/style/moodle.css

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 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 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 0103646..880a8c5 100644 (file)
@@ -529,6 +529,7 @@ $string['environmentrequireversion'] = 'version {$a->needed} is required and you
 $string['environmentsettingok'] = 'recommended setting detected';
 $string['environmentshouldfixsetting'] = 'PHP setting should be changed.';
 $string['environmentxmlerror'] = 'Error reading environment data ({$a->error_code})';
+$string['environmentmariadbwrongdbtype'] = 'Wrong <code>$CFG->dbtype</code>: you need to change it in your <code>config.php</code> file, from \'<code>mysql</code>\' to \'<code>mariadb</code>\'.';
 $string['errordeletingconfig'] = 'An error occurred while deleting the configuration records for plugin \'{$a}\'.';
 $string['errorsetting'] = 'Could not save setting:';
 $string['errorwithsettings'] = 'Some settings were not changed due to an error.';
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 270d79c..51ea48d 100644 (file)
@@ -1048,6 +1048,19 @@ function environment_check_database($version, $env_select) {
         return $result;
     }
 
+    // Check if the DB Vendor has been properly configured.
+    // Hack: this is required when playing with MySQL and MariaDB since they share the same PHP module and base DB classes,
+    // whilst they are slowly evolving using separate directions though MariaDB is still an "almost" drop-in replacement.
+    $dbvendorismysql = ($current_vendor === 'mysql');
+    $dbtypeismariadb = (stripos($dbinfo['description'], 'mariadb') !== false);
+    if ($dbvendorismysql && $dbtypeismariadb) {
+        $result->setStatus(false);
+        $result->setLevel($level);
+        $result->setInfo($current_vendor . ' (' . $dbinfo['description'] . ')');
+        $result->setFeedbackStr('environmentmariadbwrongdbtype');
+        return $result;
+    }
+
 /// And finally compare them, saving results
     if (version_compare($current_version, $needed_version, '>=')) {
         $result->setStatus(true);
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 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 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 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 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 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}}}
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 32cad6e..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;
 }