Merge branch 'MDL-44454_old_code' of https://github.com/andyjdavis/moodle
authorSam Hemelryk <sam@moodle.com>
Mon, 7 Apr 2014 23:35:34 +0000 (11:35 +1200)
committerSam Hemelryk <sam@moodle.com>
Mon, 7 Apr 2014 23:35:34 +0000 (11:35 +1200)
139 files changed:
admin/index.php
course/lib.php
course/recent.php
enrol/otherusers.php
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js
filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js
filter/glossary/yui/src/autolinker/js/autolinker.js
filter/glossary/yui/src/autolinker/meta/autolinker.json
filter/mathjaxloader/filter.php [new file with mode: 0644]
filter/mathjaxloader/lang/en/filter_mathjaxloader.php [new file with mode: 0644]
filter/mathjaxloader/readme_moodle.txt [new file with mode: 0644]
filter/mathjaxloader/settings.php [new file with mode: 0644]
filter/mathjaxloader/styles.css [new file with mode: 0644]
filter/mathjaxloader/version.php [new file with mode: 0644]
filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-debug.js [new file with mode: 0644]
filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-min.js [new file with mode: 0644]
filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader.js [new file with mode: 0644]
filter/mathjaxloader/yui/src/loader/build.json [new file with mode: 0644]
filter/mathjaxloader/yui/src/loader/js/loader.js [new file with mode: 0644]
filter/mathjaxloader/yui/src/loader/meta/loader.json [new file with mode: 0644]
lang/en/role.php
lib/ajax/blocks.php
lib/blocklib.php
lib/classes/plugin_manager.php
lib/db/access.php
lib/db/upgrade.php
lib/editor/atto/db/install.php [new file with mode: 0644]
lib/editor/atto/db/upgrade.php [new file with mode: 0644]
lib/editor/atto/lang/en/editor_atto.php
lib/editor/atto/lib.php
lib/editor/atto/plugins/backcolor/styles.css [new file with mode: 0644]
lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-debug.js
lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-min.js
lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button.js
lib/editor/atto/plugins/backcolor/yui/src/button/js/button.js
lib/editor/atto/plugins/equation/lib.php
lib/editor/atto/plugins/equation/settings.php
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/equation/yui/src/button/meta/button.json
lib/editor/atto/plugins/fontcolor/styles.css [new file with mode: 0644]
lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-debug.js
lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-min.js
lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button.js
lib/editor/atto/plugins/fontcolor/yui/src/button/js/button.js
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/table/lang/en/atto_table.php
lib/editor/atto/plugins/table/lib.php
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/editor/atto/version.php
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js
lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js
lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu-min.js
lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js
lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js
lib/editor/atto/yui/src/editor/js/editor-plugin-buttons.js
lib/editor/atto/yui/src/editor/js/editor-plugin.js
lib/editor/atto/yui/src/editor/js/editor.js
lib/editor/atto/yui/src/editor/js/menu.js
lib/editor/atto/yui/src/editor/js/selection.js
lib/editor/atto/yui/src/editor/js/styling.js
lib/enrollib.php
lib/outputlib.php
lib/outputrenderers.php
lib/tests/behat/behat_hooks.php
lib/tests/blocklib_test.php
lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js
lib/yui/build/moodle-core-blocks/moodle-core-blocks.js
lib/yui/build/moodle-core-event/moodle-core-event-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-event/moodle-core-event-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-event/moodle-core-event.js [new file with mode: 0644]
lib/yui/src/blocks/js/blocks.js
lib/yui/src/event/build.json [new file with mode: 0644]
lib/yui/src/event/js/event.js [new file with mode: 0644]
lib/yui/src/event/meta/event.json [new file with mode: 0644]
mod/assign/lib.php
mod/chat/lib.php
mod/feedback/lib.php
mod/glossary/lib.php
mod/quiz/editlib.php
mod/quiz/lib.php
mod/scorm/datamodels/scorm_12.js.php
mod/scorm/lang/en/scorm.php
mod/scorm/settings.php
mod/wiki/lib.php
mod/wiki/pagelib.php
mod/wiki/renderer.php
mod/wiki/search.php
mod/wiki/tests/behat/wiki_search.feature [new file with mode: 0644]
mod/workshop/lib.php
mod/workshop/submission.php
my/index.php
my/indexsys.php
report/log/classes/renderable.php
report/log/classes/table_log.php
report/log/index.php
report/loglive/classes/renderable.php [new file with mode: 0644]
report/loglive/classes/renderer.php [new file with mode: 0644]
report/loglive/classes/renderer_ajax.php [new file with mode: 0644]
report/loglive/classes/table_log.php [new file with mode: 0644]
report/loglive/classes/table_log_ajax.php [new file with mode: 0644]
report/loglive/index.php
report/loglive/lang/en/report_loglive.php
report/loglive/lib.php
report/loglive/loglive_ajax.php [new file with mode: 0644]
report/loglive/settings.php
report/loglive/styles.css
report/loglive/tests/behat/loglive_report.feature [new file with mode: 0644]
report/loglive/version.php
report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-debug.js [new file with mode: 0644]
report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-min.js [new file with mode: 0644]
report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs.js [new file with mode: 0644]
report/loglive/yui/src/fetchlogs/build.json [new file with mode: 0644]
report/loglive/yui/src/fetchlogs/js/fetchlogs.js [new file with mode: 0644]
report/loglive/yui/src/fetchlogs/meta/fetchlogs.json [new file with mode: 0644]
theme/base/config.php
theme/base/style/core.css
theme/base/style/pagelayout.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/renderers/core_renderer.php
theme/bootstrapbase/style/moodle.css
theme/canvas/style/pagelayout.css
user/profile.php
user/profilesys.php

index d31eeac..4947c5e 100644 (file)
@@ -237,6 +237,13 @@ if (!core_tables_exist()) {
 // Check version of Moodle code on disk compared with database
 // and upgrade if possible.
 
+if (!$cache) {
+    // Do not try to do anything fancy in non-cached mode,
+    // this prevents themes from fetching data from non-existent tables.
+    $PAGE->set_pagelayout('maintenance');
+    $PAGE->set_popup_notification_allowed(false);
+}
+
 $stradministration = get_string('administration');
 $PAGE->set_context(context_system::instance());
 
@@ -267,9 +274,6 @@ if (!$cache and $version > $CFG->version) {  // upgrade
     // We then purge the regular caches.
     purge_all_caches();
 
-    $PAGE->set_pagelayout('maintenance');
-    $PAGE->set_popup_notification_allowed(false);
-
     /** @var core_admin_renderer $output */
     $output = $PAGE->get_renderer('core', 'admin');
 
@@ -347,8 +351,6 @@ if (!$cache and $version > $CFG->version) {  // upgrade
         // Always verify plugin dependencies!
         $failed = array();
         if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
-            $PAGE->set_pagelayout('maintenance');
-            $PAGE->set_popup_notification_allowed(false);
             $reloadurl = new moodle_url('/admin/index.php', array('confirmupgrade' => 1, 'confirmrelease' => 1, 'cache' => 0));
             echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
             die();
@@ -382,8 +384,6 @@ if (!$cache and moodle_needs_upgrading()) {
         if (!$confirmplugins) {
             $strplugincheck = get_string('plugincheck');
 
-            $PAGE->set_pagelayout('maintenance');
-            $PAGE->set_popup_notification_allowed(false);
             $PAGE->navbar->add($strplugincheck);
             $PAGE->set_title($strplugincheck);
             $PAGE->set_heading($strplugincheck);
@@ -421,8 +421,6 @@ if (!$cache and moodle_needs_upgrading()) {
         // Make sure plugin dependencies are always checked.
         $failed = array();
         if (!core_plugin_manager::instance()->all_plugins_ok($version, $failed)) {
-            $PAGE->set_pagelayout('maintenance');
-            $PAGE->set_popup_notification_allowed(false);
             $reloadurl = new moodle_url('/admin/index.php', array('cache' => 0));
             echo $output->unsatisfied_dependencies_page($version, $failed, $reloadurl);
             die();
index 6d54403..6726f03 100644 (file)
@@ -2597,14 +2597,14 @@ function update_course($data, $editoroptions = NULL) {
 
     // Check we don't have a duplicate shortname.
     if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) {
-        if ($DB->record_exists('course', array('shortname' => $data->shortname))) {
+        if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) {
             throw new moodle_exception('shortnametaken', '', '', $data->shortname);
         }
     }
 
     // Check we don't have a duplicate idnumber.
     if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) {
-        if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) {
+        if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) {
             throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
         }
     }
index 192ccc2..11e3d5c 100644 (file)
@@ -119,9 +119,6 @@ if ($param->modid === 'all') {
     $sections = array($sectionnum => $sections[$sectionnum]);
 }
 
-
-$modinfo->get_groups(); // load all my groups and cache it in modinfo
-
 $activities = array();
 $index = 0;
 
index 063d5b4..50c2bcb 100644 (file)
@@ -35,7 +35,7 @@ $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST);
 $context = context_course::instance($course->id, MUST_EXIST);
 
 require_login($course);
-require_capability('moodle/role:assign', $context);
+require_capability('moodle/course:reviewotherusers', $context);
 
 if ($course->id == SITEID) {
     redirect("$CFG->wwwroot/");
index 0a18679..92b7fe5 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-debug.js differ
index 5d6f83b..a895bd0 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker-min.js differ
index 0a18679..92b7fe5 100644 (file)
Binary files a/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js and b/filter/glossary/yui/build/moodle-filter_glossary-autolinker/moodle-filter_glossary-autolinker.js differ
index 3217f4a..8879603 100644 (file)
@@ -70,6 +70,8 @@ Y.extend(AUTOLINKER, Y.Base, {
                     alertpanel = new M.core.alert({title:data.entries[key].concept,
                         message:definition, modal:false, yesLabel: M.util.get_string('ok', 'moodle')});
                     alertpanel.show();
+                    Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(alertpanel.get('boundingBox')))});
+
                     Y.Node.one('#id_yuialertconfirm-' + alertpanel.get('COUNT')).focus();
                 }
 
index ac4042e..065cb2e 100644 (file)
@@ -7,6 +7,7 @@
         "json-parse",
         "event-delegate",
         "overlay",
+        "moodle-core-event",
         "moodle-core-notification-alert"
     ]
   }
diff --git a/filter/mathjaxloader/filter.php b/filter/mathjaxloader/filter.php
new file mode 100644 (file)
index 0000000..a22e5bf
--- /dev/null
@@ -0,0 +1,171 @@
+<?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 filter provides automatic support for MathJax
+ *
+ * @package    filter_mathjaxloader
+ * @copyright  2013 Damyon Wiese (damyon@moodle.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Mathjax filtering
+ */
+class filter_mathjaxloader extends moodle_text_filter {
+
+    /*
+     * Perform a mapping of the moodle language code to the equivalent for MathJax.
+     *
+     * @param string $moodlelangcode - The moodle language code - e.g. en_pirate
+     * @return string The MathJax language code.
+     */
+    public function map_language_code($moodlelangcode) {
+        $mathjaxlangcodes = array('br',
+                                  'cdo',
+                                  'cs',
+                                  'da',
+                                  'de',
+                                  'en',
+                                  'eo',
+                                  'es',
+                                  'fa',
+                                  'fi',
+                                  'fr',
+                                  'gl',
+                                  'he',
+                                  'ia',
+                                  'it',
+                                  'ja',
+                                  'ko',
+                                  'lb',
+                                  'mk',
+                                  'nl',
+                                  'oc',
+                                  'pl',
+                                  'pt',
+                                  'pt-br',
+                                  'ru',
+                                  'sl',
+                                  'sv',
+                                  'tr',
+                                  'uk',
+                                  'zh-hans');
+        $exceptions = array('cz' => 'cs');
+
+        // First see if this is an exception.
+        if (isset($exceptions[$moodlelangcode])) {
+            $moodlelangcode = $exceptions[$moodlelangcode];
+        }
+
+        // Now look for an exact lang string match.
+        if (in_array($moodlelangcode, $mathjaxlangcodes)) {
+            return $moodlelangcode;
+        }
+
+        // Now try shortening the moodle lang string.
+        $moodlelangcode = preg_replace('/-.*/', '', $moodlelangcode);
+        // Look for a match on the shortened string.
+        if (in_array($moodlelangcode, $mathjaxlangcodes)) {
+            return $moodlelangcode;
+        }
+        // All failed - use english.
+        return 'en';
+    }
+
+    /*
+     * Add the javascript to enable mathjax processing on this page.
+     *
+     * @param moodle_page $page The current page.
+     * @param context $context The current context.
+     */
+    public function setup($page, $context) {
+        global $CFG;
+        // This only requires execution once per request.
+        static $jsinitialised = false;
+
+        if (empty($jsinitialised)) {
+            if (strpos($CFG->httpswwwroot, 'https:') === 0) {
+                $url = get_config('filter_mathjaxloader', 'httpsurl');
+            } else {
+                $url = get_config('filter_mathjaxloader', 'httpurl');
+            }
+            $lang = $this->map_language_code(current_language());
+            $url = new moodle_url($url, array('delayStartupUntil' => 'configured'));
+
+            $moduleconfig = array(
+                'name' => 'mathjax',
+                'fullpath' => $url
+            );
+
+            $page->requires->js_module($moduleconfig);
+
+            $config = get_config('filter_mathjaxloader', 'mathjaxconfig');
+
+            $params = array('mathjaxconfig' => $config, 'lang' => $lang);
+
+            $page->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.configure', array($params));
+
+            $jsinitialised = true;
+        }
+    }
+
+    /*
+     * This function wraps the filtered text in a span, that mathjaxloader is configured to process.
+     *
+     * @param string $text The text to filter.
+     * @param array $options The filter options.
+     */
+    public function filter($text, array $options = array()) {
+        global $PAGE;
+
+        $legacy = get_config('filter_mathjaxloader', 'texfiltercompatibility');
+        $extradelimiters = explode(',', get_config('filter_mathjaxloader', 'additionaldelimiters'));
+        if ($legacy) {
+            // This replaces any of the tex filter maths delimiters with the default for inline maths in MathJAX "\( blah \)".
+            // E.g. "<tex.*> blah </tex>".
+            $text = preg_replace('|<(/?) *tex( [^>]*)?>|u', '[\1tex]', $text);
+            // E.g. "[tex.*] blah [/tex]".
+            $text = str_replace('[tex]', '\\(', $text);
+            $text = str_replace('[/tex]', '\\)', $text);
+            // E.g. "$$ blah $$".
+            $text = preg_replace('|\$\$[\S\s]\$\$|u', '\\(\1\\)', $text);
+            // E.g. "\[ blah \]".
+            $text = str_replace('\\[', '\\(', $text);
+            $text = str_replace('\\]', '\\)', $text);
+        }
+
+        $hasinline = strpos($text, '\\(') !== false && strpos($text, '\\)') !== false;
+        $hasdisplay = (strpos($text, '$$') !== false) ||
+                      (strpos($text, '\\[') !== false && strpos($text, '\\]') !== false);
+
+        $hasextra = false;
+
+        foreach ($extradelimiters as $extra) {
+            if ($extra && strpos($text, $extra) !== false) {
+                $hasextra = true;
+                break;
+            }
+        }
+        if ($hasinline || $hasdisplay || $hasextra) {
+            $PAGE->requires->yui_module('moodle-filter_mathjaxloader-loader', 'M.filter_mathjaxloader.typeset');
+            return '<span class="nolink"><span class="filter_mathjaxloader_equation">' . $text . '</span></span>';
+        }
+        return $text;
+    }
+}
diff --git a/filter/mathjaxloader/lang/en/filter_mathjaxloader.php b/filter/mathjaxloader/lang/en/filter_mathjaxloader.php
new file mode 100644 (file)
index 0000000..8ba3583
--- /dev/null
@@ -0,0 +1,45 @@
+<?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/>.
+
+/**
+ * Strings for component 'filter_mathjaxloader', language 'en'.
+ *
+ * @package    filter_mathjaxloader
+ * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['filtername'] = 'MathJax';
+$string['additionaldelimiters'] = 'Additional equation delimiters';
+$string['additionaldelimiters_help'] = 'MathJax filter parses text for equations contained within delimiter characters.
+
+The list of recognised delimiter characters can be added to here (e.g. AsciiMath uses `). Delimiters can contain multiple characters and multiple delimiters can be separated with commas.';
+$string['httpurl'] = 'HTTP MathJax URL';
+$string['httpurl_help'] = 'Full URL to MathJax library. Used when the page is loaded via http.';
+$string['httpsurl'] = 'HTTPS MathJax URL';
+$string['httpsurl_help'] = 'Full URL to MathJax library. Used when the page is loaded via https (secure). ';
+$string['texfiltercompatibility'] = 'Tex filter compatibility';
+$string['texfiltercompatibility_help'] = 'The MathJax filter can be used as a replacement for the Tex filter.
+
+To support all the delimiters supported by the Tex filter MathJax will be configured to display all equations "inline" with the tex.';
+$string['localinstall'] = 'Local MathJax installation';
+$string['localinstall_help'] = 'The default MathJAX configuration uses the CDN version of MathJAX, but MathJAX can be installed locally if required.
+
+Some reasons this might be useful are to save on bandwidth - or because of local proxy restrictions.
+
+To use a local installation of MathJAX, first download the full MathJax library from http://www.mathjax.org/. Then install it on a web server. Finally update the MathJax filter settings httpurl and/or httpsurl to point to the local MathJax.js url.';
+$string['mathjaxsettings'] = 'MathJax configuration';
+$string['mathjaxsettings_desc'] = 'The default MathJAX configuration should be appropriate for most users, but MathJax is highly configurable and any of the standard MathJax configuration options can be added here.';
diff --git a/filter/mathjaxloader/readme_moodle.txt b/filter/mathjaxloader/readme_moodle.txt
new file mode 100644 (file)
index 0000000..a46c895
--- /dev/null
@@ -0,0 +1,15 @@
+Description of MathJAX library integration in Moodle
+=========================================================================================
+
+License: Apache 2.0
+Source: http://www.mathjax.org
+
+Moodle maintainer: Damyon Wiese
+
+=========================================================================================
+This library is not shipped with Moodle, but this filter is provided, which can be used to
+correctly load MathJax into a page from the CDN. Alternatively you can download the entire
+library and install it locally, then use this filter to load that local version.
+
+The only changes required to this filter to handle different MathJax versions is to update
+the default CDN urls in settings.php - and update the list of language mappings - in filter.php.
diff --git a/filter/mathjaxloader/settings.php b/filter/mathjaxloader/settings.php
new file mode 100644 (file)
index 0000000..deeb975
--- /dev/null
@@ -0,0 +1,86 @@
+<?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/>.
+
+/**
+ * MathJAX filter settings
+ *
+ * @package    filter_mathjaxloader
+ * @copyright  2014 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+if ($ADMIN->fulltree) {
+    $item = new admin_setting_heading('filter_mathjaxloader/localinstall',
+                                      new lang_string('localinstall', 'filter_mathjaxloader'),
+                                      new lang_string('localinstall_help', 'filter_mathjaxloader'));
+    $settings->add($item);
+
+    $item = new admin_setting_configtext('filter_mathjaxloader/httpurl',
+                                         new lang_string('httpurl', 'filter_mathjaxloader'),
+                                         new lang_string('httpurl_help', 'filter_mathjaxloader'),
+                                         'http://cdn.mathjax.org/mathjax/2.3-latest/MathJax.js',
+                                         PARAM_RAW);
+    $settings->add($item);
+
+    $item = new admin_setting_configtext('filter_mathjaxloader/httpsurl',
+                                         new lang_string('httpsurl', 'filter_mathjaxloader'),
+                                         new lang_string('httpsurl_help', 'filter_mathjaxloader'),
+                                         'https://c328740.ssl.cf1.rackcdn.com/mathjax/2.3-latest/MathJax.js',
+                                         PARAM_RAW);
+    $settings->add($item);
+
+    $item = new admin_setting_configcheckbox('filter_mathjaxloader/texfiltercompatibility',
+                                             new lang_string('texfiltercompatibility', 'filter_mathjaxloader'),
+                                             new lang_string('texfiltercompatibility_help', 'filter_mathjaxloader'),
+                                             0);
+    $settings->add($item);
+
+    $default = '
+MathJax.Hub.Config({
+    config: ["MMLorHTML.js", "Safe.js"],
+    jax: ["input/TeX","input/MathML","output/HTML-CSS","output/NativeMML"],
+    extensions: ["tex2jax.js","mml2jax.js","MathMenu.js","MathZoom.js"],
+    TeX: {
+        extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
+    },
+    menuSettings: {
+        zoom: "Double-Click",
+        mpContext: true,
+        mpMouse: true
+    },
+    errorSettings: { message: ["!"] },
+    skipStartupTypeset: true,
+    messageStyle: "none"
+});
+';
+
+    $item = new admin_setting_configtextarea('filter_mathjaxloader/mathjaxconfig',
+                                             new lang_string('mathjaxsettings','filter_mathjaxloader'),
+                                             new lang_string('mathjaxsettings_desc', 'filter_mathjaxloader'),
+                                             $default);
+
+    $settings->add($item);
+
+    $item = new admin_setting_configtext('filter_mathjaxloader/additionaldelimiters',
+                                         new lang_string('additionaldelimiters', 'filter_mathjaxloader'),
+                                         new lang_string('additionaldelimiters_help', 'filter_mathjaxloader'),
+                                         '',
+                                         PARAM_RAW);
+    $settings->add($item);
+
+}
diff --git a/filter/mathjaxloader/styles.css b/filter/mathjaxloader/styles.css
new file mode 100644 (file)
index 0000000..f095292
--- /dev/null
@@ -0,0 +1,3 @@
+.jsenabled #MathJax_ZoomFrame {
+    position: absolute;
+}
diff --git a/filter/mathjaxloader/version.php b/filter/mathjaxloader/version.php
new file mode 100644 (file)
index 0000000..666c4b8
--- /dev/null
@@ -0,0 +1,29 @@
+<?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/>.
+
+/**
+ * MathJax filter version information
+ *
+ * @package    filter_mathjaxloader
+ * @copyright  2014 Damyon Wiese (damyon@moodle.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$plugin->version  = 2013110500;
+$plugin->requires = 2013110500;  // Requires this Moodle version
+$plugin->component= 'filter_mathjaxloader';
diff --git a/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-debug.js b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-debug.js
new file mode 100644 (file)
index 0000000..15f0c75
Binary files /dev/null and b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-debug.js differ
diff --git a/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-min.js b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-min.js
new file mode 100644 (file)
index 0000000..a5cd7f1
Binary files /dev/null and b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader-min.js differ
diff --git a/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader.js b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader.js
new file mode 100644 (file)
index 0000000..15f0c75
Binary files /dev/null and b/filter/mathjaxloader/yui/build/moodle-filter_mathjaxloader-loader/moodle-filter_mathjaxloader-loader.js differ
diff --git a/filter/mathjaxloader/yui/src/loader/build.json b/filter/mathjaxloader/yui/src/loader/build.json
new file mode 100644 (file)
index 0000000..b262702
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "name": "moodle-filter_mathjaxloader-loader",
+    "builds": {
+        "moodle-filter_mathjaxloader-loader": {
+            "jsfiles": [
+                "loader.js"
+            ]
+        }
+    }
+}
diff --git a/filter/mathjaxloader/yui/src/loader/js/loader.js b/filter/mathjaxloader/yui/src/loader/js/loader.js
new file mode 100644 (file)
index 0000000..fb7fe1b
--- /dev/null
@@ -0,0 +1,115 @@
+// 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/>.
+
+/**
+ * Mathjax JS Loader.
+ *
+ * @package    filter_mathjaxloader
+ * @copyright  2014 Damyon Wiese  <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+M.filter_mathjaxloader = M.filter_mathjaxloader || {
+
+    /**
+     * The users current language - this can't be set until MathJax is loaded - so we need to store it.
+     * @property _lang
+     * @type String
+     * @default ''
+     * @private
+     */
+    _lang: '',
+
+    /**
+     * Boolean used to prevent configuring MathJax twice.
+     * @property _configured
+     * @type Boolean
+     * @default ''
+     * @private
+     */
+    _configured: false,
+
+    /**
+     * Called by the filter when it is active on any page.
+     * This does not load MathJAX yet - it addes the configuration to the head incase it gets loaded later.
+     * It also subscribes to the filter-content-updated event so MathJax can respond to content loaded by Ajax.
+     *
+     * @method typeset
+     * @param {Object} params List of configuration params containing mathjaxconfig (text) and lang
+     */
+    configure: function(params) {
+
+        // Add a js configuration object to the head.
+        // See "http://docs.mathjax.org/en/latest/dynamic.html#ajax-mathjax"
+        var script = document.createElement("script");
+        script.type = "text/x-mathjax-config";
+        script[(window.opera ? "innerHTML" : "text")] = params.mathjaxconfig;
+        document.getElementsByTagName("head")[0].appendChild(script);
+
+        // Save the lang config until MathJax is actually loaded.
+        this._lang = params.lang;
+
+        // Listen for events triggered when new text is added to a page that needs
+        // processing by a filter.
+        Y.on(M.core.event.FILTER_CONTENT_UPDATED, this.contentUpdated, this);
+    },
+
+    /**
+     * Set the correct language for the MathJax menus. Only do this once.
+     *
+     * @method setLocale
+     * @private
+     */
+    _setLocale: function() {
+        if (!this._configured) {
+            MathJax.Localization.setLocale(this._lang);
+            MathJax.Hub.Configured();
+            this._configured = true;
+        }
+    },
+
+    /**
+     * Called by the filter when an equation is found while rendering the page.
+     *
+     * @method typeset
+     */
+    typeset: function() {
+        if (!this._configured) {
+            var self = this;
+            Y.use('mathjax', function() {
+                self._setLocale();
+                Y.all('.filter_mathjaxloader_equation').each(function(node) {
+                    MathJax.Hub.Queue(["Typeset", MathJax.Hub, node.getDOMNode()]);
+                });
+            });
+        }
+    },
+
+    /**
+     * Handle content updated events - typeset the new content.
+     * @method contentUpdated
+     * @param Y.Event - Custom event with "nodes" indicating the root of the updated nodes.
+     */
+    contentUpdated: function(event) {
+        var self = this;
+        Y.use('mathjax', function() {
+            self._setLocale();
+            event.nodes.each(function (node) {
+                node.all('.filter_mathjaxloader_equation').each(function(node) {
+                    MathJax.Hub.Queue(["Typeset", MathJax.Hub, node.getDOMNode()]);
+                });
+            });
+        });
+    }
+};
diff --git a/filter/mathjaxloader/yui/src/loader/meta/loader.json b/filter/mathjaxloader/yui/src/loader/meta/loader.json
new file mode 100644 (file)
index 0000000..eaca422
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "moodle-filter_mathjaxloader-loader": {
+        "requires": [
+            "moodle-core-event"
+        ]
+    }
+}
index b3cbdec..6033ac9 100644 (file)
@@ -144,6 +144,7 @@ $string['course:movesections'] = 'Move sections';
 $string['course:publish'] = 'Publish a course into hub';
 $string['course:request'] = 'Request new courses';
 $string['course:reset'] = 'Reset course';
+$string['course:reviewotherusers'] = 'Review other users';
 $string['course:sectionvisibility'] = 'Control section visibility';
 $string['course:setcurrentsection'] = 'Set current section';
 $string['course:update'] = 'Update course settings';
index 7b2c820..545b82f 100644 (file)
@@ -56,17 +56,14 @@ $PAGE->set_context(context::instance_by_id($contextid));
 // Setting layout to replicate blocks configuration for the page we edit.
 $PAGE->set_pagelayout($pagelayout);
 $PAGE->set_subpage($subpage);
+$PAGE->blocks->add_custom_regions_for_pagetype($pagetype);
 $pagetype = explode('-', $pagetype);
 switch ($pagetype[0]) {
     case 'my':
-        // My Home page needs to have 'content' block region set up.
         $PAGE->set_blocks_editing_capability('moodle/my:manageblocks');
-        $PAGE->blocks->add_region('content');
         break;
     case 'user':
         if ($pagelayout == 'mydashboard') {
-            // User profile pages also need the 'content' block region set up.
-            $PAGE->blocks->add_region('content');
             // If it's not the current user's profile, we need a different capability.
             if ($PAGE->context->contextlevel == CONTEXT_USER && $PAGE->context->instanceid != $USER->id) {
                 $PAGE->set_blocks_editing_capability('moodle/user:manageblocks');
index 07f31a5..fbf37ac 100644 (file)
@@ -394,12 +394,30 @@ class block_manager {
     /**
      * Add a region to a page
      *
-     * @param string $region add a named region where blocks may appear on the
-     * current page. This is an internal name, like 'side-pre', not a string to
-     * display in the UI.
+     * @param string $region add a named region where blocks may appear on the current page.
+     *      This is an internal name, like 'side-pre', not a string to display in the UI.
+     * @param bool $custom True if this is a custom block region, being added by the page rather than the theme layout.
      */
-    public function add_region($region) {
+    public function add_region($region, $custom = true) {
+        global $SESSION;
         $this->check_not_yet_loaded();
+        if ($custom) {
+            if (array_key_exists($region, $this->regions)) {
+                // This here is EXACTLY why we should not be adding block regions into a page. It should
+                // ALWAYS be done in a theme layout.
+                debugging('A custom region conflicts with a block region in the theme.', DEBUG_DEVELOPER);
+            }
+            // We need to register this custom region against the page type being used.
+            // This allows us to check, when performing block actions, that unrecognised regions can be worked with.
+            $type = $this->page->pagetype;
+            if (!isset($SESSION->custom_block_regions)) {
+                $SESSION->custom_block_regions = array($type => array($region));
+            } else if (!isset($SESSION->custom_block_regions[$type])) {
+                $SESSION->custom_block_regions[$type] = array($region);
+            } else if (!in_array($region, $SESSION->custom_block_regions[$type])) {
+                $SESSION->custom_block_regions[$type][] = $region;
+            }
+        }
         $this->regions[$region] = 1;
     }
 
@@ -409,9 +427,23 @@ class block_manager {
      *
      * @param array $regions this utility method calls add_region for each array element.
      */
-    public function add_regions($regions) {
+    public function add_regions($regions, $custom = true) {
         foreach ($regions as $region) {
-            $this->add_region($region);
+            $this->add_region($region, $custom);
+        }
+    }
+
+    /**
+     * Finds custom block regions associated with a page type and registers them with this block manager.
+     *
+     * @param string $pagetype
+     */
+    public function add_custom_regions_for_pagetype($pagetype) {
+        global $SESSION;
+        if (isset($SESSION->custom_block_regions[$pagetype])) {
+            foreach ($SESSION->custom_block_regions[$pagetype] as $customregion) {
+                $this->add_region($customregion, false);
+            }
         }
     }
 
@@ -746,7 +778,7 @@ class block_manager {
      * @param string $subpagepattern optional. Passed to @see add_block()
      */
     public function add_blocks($blocks, $pagetypepattern = NULL, $subpagepattern = NULL, $showinsubcontexts=false, $weight=0) {
-        $this->add_regions(array_keys($blocks));
+        $this->add_regions(array_keys($blocks), false);
         foreach ($blocks as $region => $regionblocks) {
             $weight = 0;
             foreach ($regionblocks as $blockname) {
index d06464b..580265a 100644 (file)
@@ -1012,7 +1012,7 @@ class core_plugin_manager {
 
             'filter' => array(
                 'activitynames', 'algebra', 'censor', 'emailprotect',
-                'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
+                'emoticon', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
                 'urltolink', 'data', 'glossary'
             ),
 
index afc8067..99f2b28 100644 (file)
@@ -813,6 +813,17 @@ $capabilities = array(
         )
     ),
 
+    'moodle/course:reviewotherusers' => array(
+
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW,
+        ),
+        'clonepermissionsfrom' => 'moodle/role:assign'
+    ),
+
     'moodle/course:bulkmessaging' => array(
 
         'riskbitmask' => RISK_SPAM,
index 298cb26..c0d4059 100644 (file)
@@ -3197,31 +3197,34 @@ function xmldb_main_upgrade($oldversion) {
             }
         }
 
-        list($insql, $inparams) = $DB->get_in_or_equal($themes, SQL_PARAMS_NAMED);
+        // Check we actually have themes to remove.
+        if (count($themes) > 0) {
+            list($insql, $inparams) = $DB->get_in_or_equal($themes, SQL_PARAMS_NAMED);
 
-        // Replace the theme usage.
-        $DB->set_field_select('course', 'theme', 'clean', "theme $insql", $inparams);
-        $DB->set_field_select('course_categories', 'theme', 'clean', "theme $insql", $inparams);
-        $DB->set_field_select('user', 'theme', 'clean', "theme $insql", $inparams);
-        $DB->set_field_select('mnet_host', 'theme', 'clean', "theme $insql", $inparams);
+            // Replace the theme usage.
+            $DB->set_field_select('course', 'theme', 'clean', "theme $insql", $inparams);
+            $DB->set_field_select('course_categories', 'theme', 'clean', "theme $insql", $inparams);
+            $DB->set_field_select('user', 'theme', 'clean', "theme $insql", $inparams);
+            $DB->set_field_select('mnet_host', 'theme', 'clean', "theme $insql", $inparams);
 
-        // Replace the theme configs.
-        if (in_array(get_config('core', 'theme'), $themes)) {
-            set_config('theme', 'clean');
-        }
-        if (in_array(get_config('core', 'thememobile'), $themes)) {
-            set_config('thememobile', 'clean');
-        }
-        if (in_array(get_config('core', 'themelegacy'), $themes)) {
-            set_config('themelegacy', 'clean');
-        }
-        if (in_array(get_config('core', 'themetablet'), $themes)) {
-            set_config('themetablet', 'clean');
-        }
+            // Replace the theme configs.
+            if (in_array(get_config('core', 'theme'), $themes)) {
+                set_config('theme', 'clean');
+            }
+            if (in_array(get_config('core', 'thememobile'), $themes)) {
+                set_config('thememobile', 'clean');
+            }
+            if (in_array(get_config('core', 'themelegacy'), $themes)) {
+                set_config('themelegacy', 'clean');
+            }
+            if (in_array(get_config('core', 'themetablet'), $themes)) {
+                set_config('themetablet', 'clean');
+            }
 
-        // Hacky emulation of plugin uninstallation.
-        foreach ($themes as $theme) {
-            unset_all_config_for_plugin('theme_' . $theme);
+            // Hacky emulation of plugin uninstallation.
+            foreach ($themes as $theme) {
+                unset_all_config_for_plugin('theme_' . $theme);
+            }
         }
 
         // Main savepoint reached.
diff --git a/lib/editor/atto/db/install.php b/lib/editor/atto/db/install.php
new file mode 100644 (file)
index 0000000..ad89040
--- /dev/null
@@ -0,0 +1,50 @@
+<?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/>.
+
+/**
+ * Atto upgrade script.
+ *
+ * @package    editor_atto
+ * @copyright  2014 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Make the Atto the default editor for upgrades from 26.
+ *
+ * @return bool
+ */
+function xmldb_editor_atto_install() {
+    global $CFG;
+
+    // Make Atto the default.
+    $currenteditors = $CFG->texteditors;
+    $neweditors = array();
+
+    $list = explode(',', $currenteditors);
+    array_push($neweditors, 'atto');
+    foreach ($list as $editor) {
+        if ($editor != 'atto') {
+            array_push($neweditors, $editor);
+        }
+    }
+
+    set_config('texteditors', implode(',', $neweditors));
+
+    return true;
+}
diff --git a/lib/editor/atto/db/upgrade.php b/lib/editor/atto/db/upgrade.php
new file mode 100644 (file)
index 0000000..fc738c1
--- /dev/null
@@ -0,0 +1,55 @@
+<?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/>.
+
+/**
+ * Atto upgrade script.
+ *
+ * @package    editor_atto
+ * @copyright  2014 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Run all Atto upgrade steps between the current DB version and the current version on disk.
+ * @param int $oldversion The old version of atto in the DB.
+ * @return bool
+ */
+function xmldb_editor_atto_upgrade($oldversion) {
+    global $CFG, $DB;
+
+    $dbman = $DB->get_manager();
+
+    if ($oldversion < 2014032800) {
+        // Make Atto the default.
+        $currenteditors = $CFG->texteditors;
+        $neweditors = array();
+
+        $list = explode(',', $currenteditors);
+        array_push($neweditors, 'atto');
+        foreach ($list as $editor) {
+            if ($editor != 'atto') {
+                array_push($neweditors, $editor);
+            }
+        }
+
+        set_config('texteditors', implode(',', $neweditors));
+        upgrade_plugin_savepoint(true, 2014032800, 'editor', 'atto');
+    }
+
+    return true;
+}
index c77afe5..e9739a9 100644 (file)
@@ -33,3 +33,6 @@ $string['subplugintype_atto_plural'] = 'Atto plugins';
 $string['settings'] = 'Atto toolbar settings';
 $string['toolbarconfig'] = 'Toolbar config';
 $string['toolbarconfig_desc'] = 'The list of plugins and the order they are displayed can be configured here. The configuration consists of groups (one per line) followed by the ordered list of plugins for that group. The group is separated from the plugins with an equals sign and the plugins are separated with commas. The group names must be unique and should indicate what the buttons have in common. Button and group names should not be repeated and may only contain alphanumeric characters.';
+$string['editor_command_keycode'] = 'Cmd + {$a}';
+$string['editor_control_keycode'] = 'Ctrl + {$a}';
+$string['plugin_title_shortcut'] = '{$a->title} [{$a->shortcut}]';
index fafa9df..5838292 100644 (file)
@@ -122,6 +122,11 @@ class atto_texteditor extends texteditor {
             $jsplugins[] = array('group'=>$group, 'plugins'=>$groupplugins);
         }
 
+        $PAGE->requires->strings_for_js(array(
+                'editor_command_keycode',
+                'editor_control_keycode',
+                'plugin_title_shortcut',
+            ), 'editor_atto');
         $PAGE->requires->yui_module($modules,
                                     'Y.M.editor_atto.Editor.init',
                                     array($this->get_init_params($elementid, $options, $fpoptions, $jsplugins)));
diff --git a/lib/editor/atto/plugins/backcolor/styles.css b/lib/editor/atto/plugins/backcolor/styles.css
new file mode 100644 (file)
index 0000000..2d0a90c
--- /dev/null
@@ -0,0 +1,3 @@
+.atto_backcolor_button .dropdown-menu {
+    min-width: inherit;
+}
index 7327354..971d480 100644 (file)
Binary files a/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-debug.js and b/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-debug.js differ
index 870db41..16a998f 100644 (file)
Binary files a/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-min.js and b/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button-min.js differ
index 7327354..971d480 100644 (file)
Binary files a/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button.js and b/lib/editor/atto/plugins/backcolor/yui/build/moodle-atto_backcolor-button/moodle-atto_backcolor-button.js differ
index 26eefc2..fdf92b8 100644 (file)
@@ -31,9 +31,7 @@
  * @extends M.editor_atto.EditorPlugin
  */
 
-var doc = document,
-    BackColor = 'BackColor',
-    colors = [
+var colors = [
         {
             name: 'white',
             color: '#FFFFFF'
@@ -86,53 +84,8 @@ Y.namespace('M.atto_backcolor').Button = Y.Base.create('button', Y.M.editor_atto
      * @private
      */
     _changeStyle: function(e, color) {
-        if (window.getSelection) {
-            // Test for IE9 and non-IE browsers.
-            try {
-                if (!doc.execCommand(BackColor, false, color)) {
-                    this._fallbackChangeStyle(color);
-                }
-            } catch (ex) {
-                this._fallbackChangeStyle(color);
-            }
-        } else if (doc.selection && doc.selection.createRange) {
-            // Test for IE8 or less.
-            range = doc.selection.createRange();
-            range.execCommand(BackColor, false, color);
-        }
-
-        // Mark as updated
-        this.markUpdated();
-    },
-
-    /**
-     * Change the background color.
-     *
-     * This function is an alternative use for IE browsers.
-     *
-     * @method _fallbackChangeStyle
-     * @param {string} color The color for the background.
-     * @chainable
-     * @private
-     */
-    _fallbackChangeStyle: function (color) {
-        var selection = window.getSelection(),
-            range;
-
-        if (selection.rangeCount && selection.getRangeAt) {
-            range = selection.getRangeAt(0);
-        }
-        doc.designMode = "on";
-        if (range) {
-            selection.removeAllRanges();
-            selection.addRange(range);
-        }
-
-        if (!doc.execCommand("HiliteColor", false, color)) {
-            doc.execCommand(BackColor, false, color);
-        }
-        doc.designMode = "off";
-
-        return this;
+        this.get('host').formatSelectionInlineStyle({
+            backgroundColor: color
+        });
     }
 });
index 3e2816e..87865f2 100644 (file)
@@ -81,6 +81,8 @@ function atto_equation_params_for_js($elementid, $options, $fpoptions) {
                 'elements' => get_config('atto_equation', 'librarygroup4'),
             ));
 
-    return array('texfilteractive' => $texfilteractive, 'contextid' => $context->id, 'library' => $library,
-        'texdocsurl' => get_docs_url('Using_TeX_Notation'));
+    return array('texfilteractive' => $texfilteractive,
+                 'contextid' => $context->id,
+                 'library' => $library,
+                 'texdocsurl' => get_docs_url('Using_TeX_Notation'));
 }
index 9aec16a..11d75bc 100644 (file)
@@ -71,9 +71,9 @@ if ($ADMIN->fulltree) {
 \neq
 ';
     $setting = new admin_setting_configtextarea('atto_equation/librarygroup1',
-                                                    $name,
-                                                    $desc,
-                                                    $default);
+                                                $name,
+                                                $desc,
+                                                $default);
     $settings->add($setting);
 
     // Group 2
@@ -96,9 +96,9 @@ if ($ADMIN->fulltree) {
 \Leftrightarrow
 ';
     $setting = new admin_setting_configtextarea('atto_equation/librarygroup2',
-                                                    $name,
-                                                    $desc,
-                                                    $default);
+                                                $name,
+                                                $desc,
+                                                $default);
     $settings->add($setting);
 
     // Group 3
@@ -141,9 +141,9 @@ if ($ADMIN->fulltree) {
 \Omega
 ';
     $setting = new admin_setting_configtextarea('atto_equation/librarygroup3',
-                                                    $name,
-                                                    $desc,
-                                                    $default);
+                                                $name,
+                                                $desc,
+                                                $default);
     $settings->add($setting);
 
     // Group 4
@@ -161,8 +161,9 @@ if ($ADMIN->fulltree) {
 \left| \begin{matrix} a_1 & a_2 \\ a_3 & a_4 \end{matrix} \right|
 ';
     $setting = new admin_setting_configtextarea('atto_equation/librarygroup4',
-                                                    $name,
-                                                    $desc,
-                                                    $default);
+                                                $name,
+                                                $desc,
+                                                $default);
     $settings->add($setting);
+
 }
index 00abb9c..77ae288 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 c043250..85204f1 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 76df89c..1153695 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 a5af93d..9d8185a 100644 (file)
@@ -46,6 +46,10 @@ var COMPONENTNAME = 'atto_equation',
         SUBMIT: '.' + CSS.SUBMIT,
         LIBRARY_BUTTON: '.' + CSS.LIBRARY + ' button'
     },
+    DELIMITERS = {
+        START: '\\(',
+        END: '\\)'
+    },
     TEMPLATES = {
         FORM: '' +
             '<form class="atto_form">' +
@@ -70,7 +74,7 @@ var COMPONENTNAME = 'atto_equation',
                     '{{#each library}}' +
                         '<div id="{{elementid}}_{{../CSS.LIBRARY_GROUP_PREFIX}}{{@key}}">' +
                         '{{#split "\n" elements}}' +
-                            '<button data-tex="{{this}}" title="{{this}}">$${{this}}$$</button>' +
+                            '<button data-tex="{{this}}" title="{{this}}">{{../../DELIMITERS.START}}{{this}}{{../../DELIMITERS.END}}</button>' +
                         '{{/split}}' +
                         '</div>' +
                     '{{/each}}' +
@@ -109,7 +113,17 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
      */
     _content: null,
 
+    /**
+     * The source equation we are editing in the text.
+     *
+     * @property _sourceEquation
+     * @type String
+     * @private
+     */
+    _sourceEquation: '',
+
     initializer: function() {
+        // If there is a tex filter active - enable this button.
         if (this.get('texfilteractive')) {
             // Add the button to the toolbar.
             this.addButton({
@@ -125,7 +139,14 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
                     this.unHighlightButtons();
                 }
             }, this);
+
+            // We need to convert these to a non dom node based format.
+            this.editor.all('tex').each(function (texNode) {
+                var replacement = Y.Node.create('<span>' + DELIMITERS.START + ' ' + texNode.get('text') + ' ' + DELIMITERS.END + '</span>');
+                texNode.replace(replacement);
+            });
         }
+
     },
 
     /**
@@ -158,6 +179,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
 
         tabview.render();
         dialogue.show();
+        // Trigger any JS filters to reprocess the new nodes.
+        Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(dialogue.get('boundingBox')))});
 
         var equation = this._resolveEquation();
         if (equation) {
@@ -179,7 +202,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         // Find the equation in the surrounding text.
         var selectedNode = this.get('host').getSelectionParentNode(),
             text,
-            equation;
+            equation,
+            patterns = [], i;
 
         // Note this is a document fragment and YUI doesn't like them.
         if (!selectedNode) {
@@ -188,14 +212,27 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
 
         text = Y.one(selectedNode).get('text');
         // We use space or not space because . does not match new lines.
-        pattern = /\$\$[\S\s]*\$\$/;
-        equation = pattern.exec(text);
-        if (equation && equation.length) {
-            equation = equation.pop();
-            // Replace the equation.
-            equation = equation.substring(2, equation.length - 2);
-            return equation;
+        // $$ blah $$.
+        patterns.push(/\$\$([\S\s]*)\$\$/);
+        // E.g. "\( blah \)".
+        patterns.push(/\\\(([\S\s]*)\\\)/);
+        // E.g. "\[ blah \]".
+        patterns.push(/\\\[([\S\s]*)\\\]/);
+        // E.g. "[tex] blah [/tex]".
+        patterns.push(/\[tex\]([\S\s]*)\[\/tex\]/);
+
+        for (i = 0; i < patterns.length; i++) {
+            pattern = patterns[i];
+            equation = pattern.exec(text);
+            if (equation && equation.length) {
+                // Remember the inner match so we can replace it later.
+                this.sourceEquation = equation = equation[1];
+
+                return equation;
+            }
         }
+
+        this.sourceEquation = '';
         return false;
     },
 
@@ -210,11 +247,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         var input,
             selectedNode,
             text,
-            pattern,
-            equation,
-            value;
+            value,
+            host;
 
-        var host = this.get('host');
+        host = this.get('host');
 
         e.preventDefault();
         this.getDialogue({
@@ -227,18 +263,16 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         if (value !== '') {
             host.setSelection(this._currentSelection);
 
-            value = '$$ ' + value.trim() + ' $$';
-            selectedNode = Y.one(host.getSelectionParentNode());
-            text = selectedNode.get('text');
-            pattern = /\$\$[\S\s]*\$\$/;
-            equation = pattern.exec(text);
-            if (equation && equation.length) {
+            if (this.sourceEquation.length) {
                 // Replace the equation.
-                equation = equation.pop();
-                text = text.replace(equation, '$$' + value + '$$');
+                selectedNode = Y.one(host.getSelectionParentNode());
+                text = selectedNode.get('text');
+
+                text = text.replace(this.sourceEquation, value);
                 selectedNode.set('text', text);
             } else {
                 // Insert the new equation.
+                value = DELIMITERS.START + ' ' + value + ' ' + DELIMITERS.END;
                 host.insertContentAtFocusPoint(value);
             }
 
@@ -247,6 +281,29 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         }
     },
 
+    /**
+     * Smart throttle, only call a function every delay milli seconds,
+     * and always run the last call. Y.throttle does not work here,
+     * because it calls the function immediately, the first time, and then
+     * ignores repeated calls within X seconds. This does not guarantee
+     * that the last call will be executed (which is required here).
+     *
+     * @param {function} fn
+     * @param {Number} delay Delay in milliseconds
+     * @method _throttle
+     * @private
+     */
+    _throttle: function(fn, delay) {
+        var timer = null;
+        return function () {
+            var context = this, args = arguments;
+            clearTimeout(timer);
+            timer = setTimeout(function () {
+              fn.apply(context, args);
+            }, delay);
+        };
+    },
+
     /**
      * Update the preview div to match the current equation.
      *
@@ -262,8 +319,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
             currentPos = textarea.get('selectionStart'),
             prefix = '',
             cursorLatex = '\\square ',
-            isChar;
-
+            isChar,
+            params;
 
         if (e) {
             e.preventDefault();
@@ -277,25 +334,33 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         while (equation.charAt(currentPos) === '\\' && currentPos > 0) {
             currentPos -= 1;
         }
-        isChar = /[\w\{\}]/;
+        isChar = /[a-zA-Z\{\}]/;
         while (isChar.test(equation.charAt(currentPos)) && currentPos < equation.length) {
             currentPos += 1;
         }
         // Save the cursor position - for insertion from the library.
         this._lastCursorPos = currentPos;
         equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
+
+        var previewNode = this._content.one(SELECTORS.EQUATION_PREVIEW);
+        equation = DELIMITERS.START + ' ' + equation + ' ' + DELIMITERS.END;
+        // Make an ajax request to the filter.
         url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
         params = {
             sesskey: M.cfg.sesskey,
             contextid: this.get('contextid'),
             action: 'filtertext',
-            text: '$$ ' + equation + ' $$'
+            text: equation
         };
 
-        preview = Y.io(url, { sync: true,
-                              data: params });
+        preview = Y.io(url, {
+            sync: true,
+            data: params
+        });
+
         if (preview.status === 200) {
-            this._content.one(SELECTORS.EQUATION_PREVIEW).setHTML(preview.responseText);
+            previewNode.setHTML(preview.responseText);
+            Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(previewNode))});
         }
     },
 
@@ -320,9 +385,9 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         }));
 
         this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, this);
-        this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', this._updatePreview, this);
-        this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', this._updatePreview, this);
-        this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', this._updatePreview, this);
+        this._content.one(SELECTORS.EQUATION_TEXT).on('valuechange', this._throttle(this._updatePreview, 500), this);
+        this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', this._throttle(this._updatePreview, 500), this);
+        this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', this._throttle(this._updatePreview, 500), this);
         this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this);
 
         return this._content;
@@ -390,7 +455,7 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
             out = '';
             parts = str.trim().split(delimiter);
             while (parts.length > 0) {
-                current = parts.shift();
+                current = parts.shift().trim();
                 out += options.fn(current);
             }
 
@@ -400,7 +465,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
             elementid: this.get('host').get('elementid'),
             component: COMPONENTNAME,
             library: library,
-            CSS: CSS
+            CSS: CSS,
+            DELIMITERS: DELIMITERS
         });
 
         var url = M.cfg.wwwroot + '/lib/editor/atto/plugins/equation/ajax.php';
@@ -433,6 +499,7 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         texfilteractive: {
             value: false
         },
+
         /**
          * The contextid to use when generating this preview.
          *
@@ -462,5 +529,6 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         texdocsurl: {
             value: null
         }
+
     }
 });
index c0d749f..6103cb3 100644 (file)
@@ -2,6 +2,7 @@
     "moodle-atto_equation-button": {
         "requires": [
             "moodle-editor_atto-plugin",
+            "moodle-core-event",
             "io",
             "event-valuechange",
             "tabview"
diff --git a/lib/editor/atto/plugins/fontcolor/styles.css b/lib/editor/atto/plugins/fontcolor/styles.css
new file mode 100644 (file)
index 0000000..7333969
--- /dev/null
@@ -0,0 +1,3 @@
+.atto_fontcolor_button .dropdown-menu {
+    min-width: inherit;
+}
index bd0465e..9c7b078 100644 (file)
Binary files a/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-debug.js and b/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-debug.js differ
index 0017bcb..9eb70b3 100644 (file)
Binary files a/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-min.js and b/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button-min.js differ
index bd0465e..9c7b078 100644 (file)
Binary files a/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button.js and b/lib/editor/atto/plugins/fontcolor/yui/build/moodle-atto_fontcolor-button/moodle-atto_fontcolor-button.js differ
index 51990c2..f6aed22 100644 (file)
@@ -86,9 +86,8 @@ Y.namespace('M.atto_fontcolor').Button = Y.Base.create('button', Y.M.editor_atto
      * @private
      */
     _changeStyle: function(e, color) {
-        document.execCommand('forecolor', 0, color);
-
-        // Mark as updated
-        this.markUpdated();
+        this.get('host').formatSelectionInlineStyle({
+            color: color
+        });
     }
 });
index 3fb7392..0dd910f 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 2668335..fbb7a71 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 3fb7392..0dd910f 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 77c7991..873caf3 100644 (file)
@@ -409,8 +409,8 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             input.set('value', params.url);
 
             // Auto set the width and height.
-            self._form.one('.' + CSS.INPUTWIDTH).set('value', '');
-            self._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
+            this._form.one('.' + CSS.INPUTWIDTH).set('value', '');
+            this._form.one('.' + CSS.INPUTHEIGHT).set('value', '');
 
             // Load the preview image.
             this._loadPreviewImage(params.url);
index 64190e5..225ba4a 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['createtable'] = 'Create table';
-$string['pluginname'] = 'Table';
-$string['numberofcolumns'] = 'Number of columns';
-$string['numberofrows'] = 'Number of rows';
+$string['addcolumnafter'] = 'Insert column after';
+$string['addrowafter'] = 'Insert row after';
+$string['both'] = 'Both';
 $string['caption'] = 'Caption';
-$string['headers'] = 'Define headers on';
 $string['columns'] = 'Columns';
-$string['rows'] = 'Rows';
-$string['both'] = 'Both';
+$string['createtable'] = 'Create table';
+$string['deletecolumn'] = 'Delete column';
+$string['deleterow'] = 'Delete row';
 $string['edittable'] = 'Edit table';
-$string['addrowafter'] = 'Insert row after current cell';
-$string['addcolumnafter'] = 'Insert column after current cell';
-$string['moverowup'] = 'Move row up';
-$string['moverowdown'] = 'Move row down';
+$string['headers'] = 'Define headers on';
 $string['movecolumnleft'] = 'Move column left';
 $string['movecolumnright'] = 'Move column right';
-$string['deleterow'] = 'Delete row';
-$string['deletecolumn'] = 'Delete column';
+$string['moverowdown'] = 'Move row down';
+$string['moverowup'] = 'Move row up';
+$string['numberofcolumns'] = 'Number of columns';
+$string['numberofrows'] = 'Number of rows';
+$string['pluginname'] = 'Table';
+$string['rows'] = 'Rows';
+$string['updatetable'] = 'Update table';
index 500421a..f42dc43 100644 (file)
@@ -31,6 +31,7 @@ function atto_table_strings_for_js() {
     global $PAGE;
 
     $PAGE->requires->strings_for_js(array('createtable',
+                                          'updatetable',
                                           'headers',
                                           'caption',
                                           'columns',
index 8e9c9a5..532bb36 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 5fd06f0..d6d6a33 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 8e9c9a5..532bb36 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 5cc18aa..ef461cf 100644 (file)
  */
 
 var COMPONENT = 'atto_table',
+    EDITTEMPLATE = '' +
+        '<form class="{{CSS.FORM}}">' +
+            '<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_headers" class="sameline">{{get_string "headers" component}}</label>' +
+            '<select class="{{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 class="mdl-align">' +
+                '<br/>' +
+                '<button class="submit" type="submit">{{get_string "updatetable" component}}</button>' +
+            '</div>' +
+        '</form>',
     TEMPLATE = '' +
-        '<form class="atto_form">' +
+        '<form class="{{CSS.FORM}}">' +
             '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
-            '<input class="caption fullwidth" id="{{elementid}}_atto_table_caption" required />' +
+            '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' +
+            '<br/>' +
             '<br/>' +
             '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
-            '<select class="headers" id="{{elementid}}_atto_table_headers">' +
+            '<select class="{{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/>' +
             '<label for="{{elementid}}_atto_table_rows" class="sameline">{{get_string "numberofrows" component}}</label>' +
-            '<input class="rows" type="number" value="3" id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
+            '<input class="{{CSS.ROWS}}" type="number" value="3" id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
             '<br/>' +
             '<label for="{{elementid}}_atto_table_columns" class="sameline">{{get_string "numberofcolumns" component}}</label>' +
-            '<input class="columns" type="number" value="3" id="{{elementid}}_atto_table_columns" size="8" min="1" max="20"/>' +
+            '<input class="{{CSS.COLUMNS}}" type="number" value="3" id="{{elementid}}_atto_table_columns" size="8" min="1" max="20"/>' +
             '<br/>' +
             '<div class="mdl-align">' +
                 '<br/>' +
-                '<button class="submit" type="submit">{{get_string "createtable" component}}</button>' +
+                '<button class="{{CSS.SUBMIT}}" type="submit">{{get_string "createtable" component}}</button>' +
             '</div>' +
         '</form>',
-    CONTEXTMENUTEMPLATE = '' +
-        '<ul>' +
-            '<li><a href="#" data-change="addcolumnafter">{{get_string "addcolumnafter" component}}</a></li>' +
-            '<li><a href="#" data-change="addrowafter">{{get_string "addrowafter" component}}</a></li>' +
-            '<li><a href="#" data-change="moverowup">{{get_string "moverowup" component}}</a></li>' +
-            '<li><a href="#" data-change="moverowdown">{{get_string "moverowdown" component}}</a></li>' +
-            '<li><a href="#" data-change="movecolumnleft">{{get_string "movecolumnleft" component}}</a></li>' +
-            '<li><a href="#" data-change="movecolumnright">{{get_string "movecolumnright" component}}</a></li>' +
-            '<li><a href="#" data-change="deleterow">{{get_string "deleterow" component}}</a></li>' +
-            '<li><a href="#" data-change="deletecolumn">{{get_string "deletecolumn" component}}</a></li>' +
-        '</ul>';
-
     CSS = {
+        CAPTION: 'caption',
+        HEADERS: 'headers',
+        ROWS: 'rows',
+        COLUMNS: 'columns',
+        SUBMIT: 'submit',
+        FORM: 'atto_form'
+    },
+    SELECTORS = {
+        CAPTION: '.' + CSS.CAPTION,
+        HEADERS: '.' + CSS.HEADERS,
+        ROWS: '.' + CSS.ROWS,
+        COLUMNS: '.' + CSS.COLUMNS,
+        SUBMIT: '.' + CSS.SUBMIT,
+        FORM: '.atto_form'
     };
 
 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
@@ -100,6 +121,15 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      */
     _lastTarget: null,
 
+    /**
+     * The list of menu items.
+     *
+     * @property _menuOptions
+     * @type Object
+     * @private
+     */
+    _menuOptions: null,
+
     initializer: function() {
         this.addButton({
             icon: 'e/table',
@@ -147,30 +177,49 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @private
      */
     _displayTableEditor: function(e) {
-        var selection = this.get('host').getSelectionParentNode(),
-            cell;
-
-        if (!selection) {
-            // We don't have a current selection at all, so show the standard dialogue.
-            return this._displayDialogue(e);
-        }
-
-        // Check all of the table cells found in the selection.
-        Y.one(selection).ancestors('th, td', true).each(function(node) {
-            if (this.editor.contains(node)) {
-                cell = node;
-            }
-        }, this);
-
+        var cell = this._getSuitableTableCell();
         if (cell) {
             // Add the cell to the EventFacade to save duplication in when showing the menu.
             e.tableCell = cell;
             return this._showTableMenu(e);
         }
-
         return this._displayDialogue(e);
     },
 
+    /**
+     * Returns whether or not the parameter node exists within the editor.
+     *
+     * @method _stopAtContentEditableFilter
+     * @param  {Node} node
+     * @private
+     * @return {boolean} whether or not the parameter node exists within the editor.
+     */
+    _stopAtContentEditableFilter: function(node) {
+        this.editor.contains(node);
+    },
+
+    /**
+     * Return the edit table dialogue content, attaching any required
+     * events.
+     *
+     * @method _getEditDialogueContent
+     * @private
+     * @return {Node} The content to place in the dialogue.
+     */
+    _getEditDialogueContent: function() {
+        var template = Y.Handlebars.compile(EDITTEMPLATE);
+        this._content = Y.Node.create(template({
+                CSS: CSS,
+                elementid: this.get('host').get('elementid'),
+                component: COMPONENT
+            }));
+
+        // Handle table setting.
+        this._content.one('.submit').on('click', this._updateTable, this);
+
+        return this._content;
+    },
+
     /**
      * Return the dialogue content for the tool, attaching any required
      * events.
@@ -193,6 +242,154 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         return this._content;
     },
 
+    /**
+     * Given the current selection, return a table cell suitable for table editing
+     * purposes, i.e. the first table cell selected, or the first cell in the table
+     * that the selection exists in, or null if not within a table.
+     *
+     * @method _getSuitableTableCell
+     * @private
+     * @return {Node} suitable target cell, or null if not within a table
+     */
+    _getSuitableTableCell: function() {
+        var targetcell = null,
+            host = this.get('host');
+
+        host.getSelectedNodes().some(function (node) {
+            if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) {
+                targetcell = node;
+
+                var caption = node.ancestor('caption', true, this._stopAtContentEditableFilter);
+                if (caption) {
+                    var table = caption.get('parentNode');
+                    if (table) {
+                        targetcell = table.one('td, th');
+                    }
+                }
+
+                // Once we've found a cell to target, we shouldn't need to keep looking.
+                return true;
+            }
+        });
+
+        if (targetcell) {
+            var selection = host.getSelectionFromNode(targetcell);
+            host.setSelection(selection);
+        }
+
+        return targetcell;
+    },
+
+    /**
+     * Change a node from one type to another, copying all attributes and children.
+     *
+     * @method _changeNodeType
+     * @param {Y.Node} node
+     * @param {String} new node type
+     * @private
+     * @chainable
+     */
+    _changeNodeType: function(node, newType) {
+        var newNode = Y.Node.create('<' + newType + '></' + newType + '>');
+        newNode.setAttrs(node.getAttrs());
+        node.get('childNodes').each(function(child) {
+            newNode.append(child.remove());
+        });
+        node.replace(newNode);
+        return newNode;
+    },
+
+    /**
+     * Handle updating an existing table.
+     *
+     * @method _updateTable
+     * @param {EventFacade} e
+     * @private
+     */
+    _updateTable: function(e) {
+        var caption,
+            headers,
+            table,
+            captionnode;
+
+        e.preventDefault();
+        // Hide the dialogue.
+        this.getDialogue({
+            focusAfterHide: null
+        }).hide();
+
+        // Add/update the caption.
+        caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION);
+        headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
+
+        table = this._lastTarget.ancestor('table');
+
+        captionnode = table.one('caption');
+        if (!captionnode) {
+            captionnode = Y.Node.create('<caption></caption');
+            table.insert(captionnode, 0);
+        }
+        captionnode.setHTML(caption.get('value'));
+
+        // Add the row headers.
+        if (headers.get('value') === 'rows' || headers.get('value') === 'both') {
+            table.all('tr').each(function (row) {
+                var cells = row.all('th, td'),
+                    firstCell = cells.shift(),
+                    newCell;
+
+                if (firstCell.get('tagName') === 'TD') {
+                    // Cell is a td but should be a th - change it.
+                    newCell = this._changeNodeType(firstCell, 'th');
+                    newCell.setAttribute('scope', 'row');
+                } else {
+                    firstCell.setAttribute('scope', 'row');
+                }
+
+                // Now make sure all other cells in the row are td.
+                cells.each(function (cell) {
+                    if (cell.get('tagName') === 'TH') {
+                        newCell = this._changeNodeType(cell, 'td');
+                        newCell.removeAttribute('scope');
+                    }
+                }, this);
+
+            }, this);
+        }
+        // Add the col headers. These may overrule the row headers in the first cell.
+        if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
+            var rows = table.all('tr'),
+                firstRow = rows.shift(),
+                newCell;
+
+            firstRow.all('td, th').each(function (cell) {
+                if (cell.get('tagName') === 'TD') {
+                    // Cell is a td but should be a th - change it.
+                    newCell = this._changeNodeType(cell, 'th');
+                    newCell.setAttribute('scope', 'col');
+                } else {
+                    cell.setAttribute('scope', 'col');
+                }
+            }, this);
+            // Change all the cells in the rest of the table to tds (unless they are row headers).
+            rows.each(function(row) {
+                var cells = row.all('th, td');
+
+                if (headers.get('value') === 'both') {
+                    // Ignore the first cell because it's a row header.
+                    cells.shift();
+                }
+                cells.each(function(cell) {
+                    if (cell.get('tagName') === 'TH') {
+                        newCell = this._changeNodeType(cell, 'td');
+                        newCell.removeAttribute('scope');
+                    }
+                }, this);
+
+            }, this);
+        }
+    },
+
     /**
      * Handle creation of a new table.
      *
@@ -215,10 +412,10 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             focusAfterHide: null
         }).hide();
 
-        caption = e.currentTarget.ancestor('.atto_form').one('.caption');
-        rows = e.currentTarget.ancestor('.atto_form').one('.rows');
-        cols = e.currentTarget.ancestor('.atto_form').one('.columns');
-        headers = e.currentTarget.ancestor('.atto_form').one('.headers');
+        caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTORS.CAPTIONCAPTION);
+        rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS);
+        cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS);
+        headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS);
 
         // Set the selection.
         this.get('host').setSelection(this._currentSelection);
@@ -258,6 +455,102 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         this.markUpdated();
     },
 
+    /**
+     * Search for all the cells in the current, next and previous columns.
+     *
+     * @method _findColumnCells
+     * @private
+     * @return {Object} containing current, prev and next {Y.NodeList}s
+     */
+    _findColumnCells: function() {
+        var columnindex = this._getColumnIndex(this._lastTarget),
+            rows = this._lastTarget.ancestor('table').all('tr'),
+            currentcells = new Y.NodeList(),
+            prevcells = new Y.NodeList(),
+            nextcells = new Y.NodeList();
+
+        rows.each(function(row) {
+            var cells = row.all('td, th'),
+                cell = cells.item(columnindex),
+                cellprev = cells.item(columnindex-1),
+                cellnext = cells.item(columnindex+1);
+            currentcells.push(cell);
+            if (cellprev) {
+                prevcells.push(cellprev);
+            }
+            if (cellnext) {
+                nextcells.push(cellnext);
+            }
+        });
+
+        return {
+            current: currentcells,
+            prev: prevcells,
+            next: nextcells
+        };
+    },
+
+    /**
+     * Hide the entries in the context menu that don't make sense with the
+     * current selection.
+     *
+     * @method _hideInvalidEntries
+     * @param {Y.Node} node - The node containing the menu.
+     * @private
+     */
+    _hideInvalidEntries: function(node) {
+        // Moving rows.
+        var table = this._lastTarget.ancestor('table'),
+            row = this._lastTarget.ancestor('tr'),
+            rows = table.all('tr'),
+            rowindex = rows.indexOf(row),
+            prevrow = rows.item(rowindex - 1),
+            prevrowhascells = prevrow ? prevrow.one('td') : null;
+
+        if (!row || !prevrowhascells) {
+            node.one('[data-change="moverowup"]').hide();
+        } else {
+            node.one('[data-change="moverowup"]').show();
+        }
+
+        var nextrow = rows.item(rowindex + 1),
+            rowhascell = row ? row.one('td') : false;
+
+        if (!row || !nextrow || !rowhascell) {
+            node.one('[data-change="moverowdown"]').hide();
+        } else {
+            node.one('[data-change="moverowdown"]').show();
+        }
+
+        // Moving columns.
+        var cells = this._findColumnCells();
+        if (cells.prev.filter('td').size() > 0) {
+            node.one('[data-change="movecolumnleft"]').show();
+        } else {
+            node.one('[data-change="movecolumnleft"]').hide();
+        }
+
+        var colhascell = cells.current.filter('td').size() > 0;
+        if ((cells.next.size() > 0) && colhascell) {
+            node.one('[data-change="movecolumnright"]').show();
+        } else {
+            node.one('[data-change="movecolumnright"]').hide();
+        }
+
+        // Delete col
+        if (cells.current.filter('td').size() > 0) {
+            node.one('[data-change="deletecolumn"]').show();
+        } else {
+            node.one('[data-change="deletecolumn"]').hide();
+        }
+        // Delete row
+        if (!row || !row.one('td')) {
+            node.one('[data-change="deleterow"]').hide();
+        } else {
+            node.one('[data-change="deleterow"]').show();
+        }
+    },
+
     /**
      * Display the table menu.
      *
@@ -271,30 +564,70 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         var boundingBox;
 
         if (!this._contextMenu) {
-            var template = Y.Handlebars.compile(CONTEXTMENUTEMPLATE),
-                content = Y.Node.create(template({
-                    elementid: this.get('host').get('elementid'),
-                    component: COMPONENT
-                }));
+            this._menuOptions = [
+                {
+                    text: M.util.get_string("addcolumnafter", COMPONENT),
+                    data: {
+                        change: "addcolumnafter"
+                    }
+                }, {
+                    text: M.util.get_string("addrowafter", COMPONENT),
+                    data: {
+                        change: "addrowafter"
+                    }
+                }, {
+                    text: M.util.get_string("moverowup", COMPONENT),
+                    data: {
+                        change: "moverowup"
+                    }
+                }, {
+                    text: M.util.get_string("moverowdown", COMPONENT),
+                    data: {
+                        change: "moverowdown"
+                    }
+                }, {
+                    text: M.util.get_string("movecolumnleft", COMPONENT),
+                    data: {
+                        change: "movecolumnleft"
+                    }
+                }, {
+                    text: M.util.get_string("movecolumnright", COMPONENT),
+                    data: {
+                        change: "movecolumnright"
+                    }
+                }, {
+                    text: M.util.get_string("deleterow", COMPONENT),
+                    data: {
+                        change: "deleterow"
+                    }
+                }, {
+                    text: M.util.get_string("deletecolumn", COMPONENT),
+                    data: {
+                        change: "deletecolumn"
+                    }
+                }
+            ];
 
             this._contextMenu = new Y.M.editor_atto.Menu({
-                headerText: M.util.get_string('edittable', 'atto_table'),
-                bodyContent: content
+                items: this._menuOptions
             });
 
             // Add event handlers for table control menus.
             boundingBox = this._contextMenu.get('boundingBox');
             boundingBox.delegate('click', this._handleTableChange, 'a', this);
-            boundingBox.delegate('key', this._handleTableChange, 'down:enter,space', 'a', this);
         }
+
         boundingBox = this._contextMenu.get('boundingBox');
 
         // We store the cell of the last click (the control node is transient).
         this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
 
+        this._hideInvalidEntries(boundingBox);
+
         // Show the context menu, and align to the current position.
         this._contextMenu.show();
         this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
+        this._contextMenu.set('focusAfterHide', this.buttons[this.name]);
 
         // If there are any anchors in the bounding box, focus on the first.
         if (boundingBox.one('a')) {
@@ -312,8 +645,9 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
     _handleTableChange: function(e) {
         e.preventDefault();
 
+        this._contextMenu.set('focusAfterHide', this.get('host').editor);
         // Hide the context menu.
-        this._contextMenu.hide();
+        this._contextMenu.hide(e);
 
         // Make our changes.
         switch (e.target.getData('change')) {
@@ -329,6 +663,9 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             case 'deletecolumn':
                 this._deleteColumn();
                 break;
+            case 'edittable':
+                this._editTable();
+                break;
             case 'moverowdown':
                 this._moveRowDown();
                 break;
@@ -392,11 +729,9 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
     _deleteRow: function() {
         var row = this._lastTarget.ancestor('tr');
 
-        if (row) {
-            // We do not remove rows with no cells (all headers).
-            if (row.one('td')) {
-                row.remove(true);
-            }
+        if (row && row.one('td')) {
+            // Only delete rows with at least one non-header cell.
+            row.remove(true);
         }
 
         // Clean the HTML.
@@ -410,8 +745,8 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @private
      */
     _moveRowUp: function() {
-        var row = this._lastTarget.ancestor('tr');
-        var prevrow = row.previous('tr');
+        var row = this._lastTarget.ancestor('tr'),
+            prevrow = row.previous('tr');
         if (!row || !prevrow) {
             return;
         }
@@ -428,30 +763,13 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @private
      */
     _moveColumnLeft: function() {
-        var columnindex = this._getColumnIndex(this._lastTarget);
-        var rows = this._lastTarget.ancestor('table').all('tr');
-        var columncells = new Y.NodeList();
-        var prevcells = new Y.NodeList();
-        var hastd = false;
+        var cells = this._findColumnCells();
 
-        rows.each(function(row) {
-            var cells = row.all('td, th');
-            var cell = cells.item(columnindex),
-                cellprev = cells.item(columnindex-1);
-            columncells.push(cell);
-            if (cellprev) {
-                if (cellprev.get('tagName') === 'TD') {
-                    hastd = true;
-                }
-                prevcells.push(cellprev);
-            }
-        });
-
-        if (hastd && prevcells.size() > 0) {
+        if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) {
             var i = 0;
-            for (i = 0; i < columncells.size(); i++) {
-                var cell = columncells.item(i);
-                var prevcell = prevcells.item(i);
+            for (i = 0; i < cells.current.size(); i++) {
+                var cell = cells.current.item(i),
+                    prevcell = cells.prev.item(i);
 
                 cell.swap(prevcell);
             }
@@ -460,6 +778,36 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         this.markUpdated();
     },
 
+    /**
+     * Add a caption to the table if it doesn't have one.
+     *
+     * @method _addCaption
+     * @private
+     */
+    _addCaption: function() {
+        var table = this._lastTarget.ancestor('table'),
+            caption = table.one('caption');
+
+        if (!caption) {
+            table.insert(Y.Node.create('<caption>&nbsp;</caption>'), 1);
+        }
+    },
+
+    /**
+     * Remove a caption from the table if has one.
+     *
+     * @method _removeCaption
+     * @private
+     */
+    _removeCaption: function() {
+        var table = this._lastTarget.ancestor('table'),
+            caption = table.one('caption');
+
+        if (caption) {
+            caption.remove(true);
+        }
+    },
+
     /**
      * Move column right.
      *
@@ -467,30 +815,16 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @private
      */
     _moveColumnRight: function() {
-        var columnindex = this._getColumnIndex(this._lastTarget);
-        var rows = this._lastTarget.ancestor('table').all('tr');
-        var columncells = new Y.NodeList();
-        var nextcells = new Y.NodeList();
-        var hastd = false;
-
-        rows.each(function(row) {
-            var cells = row.all('td, th');
-            var cell = cells.item(columnindex),
-                cellnext = cells.item(columnindex+1);
-            if (cell.get('tagName') === 'TD') {
-                hastd = true;
-            }
-            columncells.push(cell);
-            if (cellnext) {
-                nextcells.push(cellnext);
-            }
-        });
+        var cells = this._findColumnCells();
 
-        if (hastd && nextcells.size() > 0) {
+        // Check we have some tds in this column, and one exists to the right.
+        if ( (cells.next.size() > 0) &&
+                (cells.current.size() === cells.next.size()) &&
+                (cells.current.filter('td').size() > 0)) {
             var i = 0;
-            for (i = 0; i < columncells.size(); i++) {
-                var cell = columncells.item(i);
-                var nextcell = nextcells.item(i);
+            for (i = 0; i < cells.current.size(); i++) {
+                var cell = cells.current.item(i),
+                    nextcell = cells.next.item(i);
 
                 cell.swap(nextcell);
             }
@@ -506,9 +840,9 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @private
      */
     _moveRowDown: function() {
-        var row = this._lastTarget.ancestor('tr');
-        var nextrow = row.next('tr');
-        if (!row || !nextrow) {
+        var row = this._lastTarget.ancestor('tr'),
+            nextrow = row.next('tr');
+        if (!row || !nextrow || !row.one('td')) {
             return;
         }
 
@@ -517,6 +851,43 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
         this.markUpdated();
     },
 
+    /**
+     * Edit table (show the dialogue).
+     *
+     * @method _editTable
+     * @private
+     */
+    _editTable: function() {
+        var dialogue = this.getDialogue({
+            headerContent: M.util.get_string('edittable', COMPONENT),
+            focusAfterHide: false
+        });
+
+        // Set the dialogue content, and then show the dialogue.
+        var node = this._getEditDialogueContent(),
+            captioninput = node.one(SELECTORS.CAPTION),
+            headersinput = node.one(SELECTORS.HEADERS),
+            table = this._lastTarget.ancestor('table'),
+            captionnode = table.one('caption');
+
+        if (captionnode) {
+            captioninput.set('value', captionnode.getHTML());
+        } else {
+            captioninput.set('value', '');
+        }
+
+        var headersvalue = 'columns';
+        if (table.one('th[scope="row"]')) {
+            headersvalue = 'rows';
+            if (table.one('th[scope="col"]')) {
+                headersvalue = 'both';
+            }
+        }
+        headersinput.set('value', headersvalue);
+        dialogue.set('bodyContent', node).show();
+    },
+
+
     /**
      * Delete the current column.
      *
@@ -524,10 +895,11 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @private
      */
     _deleteColumn: function() {
-        var columnindex = this._getColumnIndex(this._lastTarget);
-        var rows = this._lastTarget.ancestor('table').all('tr');
-        var columncells = new Y.NodeList();
-        var hastd = false;
+        var columnindex = this._getColumnIndex(this._lastTarget),
+            table = this._lastTarget.ancestor('table'),
+            rows = table.all('tr'),
+            columncells = new Y.NodeList(),
+            hastd = false;
 
         rows.each(function(row) {
             var cells = row.all('td, th');
@@ -538,6 +910,7 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             columncells.push(cell);
         });
 
+        // Do not delete all the headers.
         if (hastd) {
             columncells.remove(true);
         }
@@ -595,20 +968,29 @@ Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.Edi
      * @private
      */
     _addColumnAfter: function() {
-        var columnindex = this._getColumnIndex(this._lastTarget);
+        var cells = this._findColumnCells(),
+            before = true,
+            clonecells = cells.next;
+        if (cells.next.size() <= 0) {
+            before = false;
+            clonecells = cells.current;
+        }
 
-        var tablecell = this._lastTarget.ancestor('table');
-        var rows = tablecell.all('tr');
-        Y.each(rows, function(row) {
-            // Clone the first cell from the row so it has the same type/attributes (e.g. scope).
-            var newcell = row.one('td, th').cloneNode(true);
+        Y.each(clonecells, function(cell) {
+            var newcell = cell.cloneNode();
             // Clear the content of the cell.
             newcell.setHTML('&nbsp;');
 
-            row.insert(newcell, columnindex + 1);
+            if (before) {
+                cell.get('parentNode').insert(newcell, cell);
+            } else {
+                cell.get('parentNode').insert(newcell, cell);
+                cell.swap(newcell);
+            }
         }, this);
 
         // Clean the HTML.
         this.markUpdated();
     }
+
 });
index 008dd7c..c416a6f 100644 (file)
@@ -98,15 +98,6 @@ div.editor_atto_toolbar div.atto_group {
     height: 16px;
 }
 
-.atto_menu {
-    background: white;
-    padding: 1px;
-    white-space: inherit;
-}
-
-.atto_menu a {
-    color: black;
-}
 .atto_menuentry {
     clear: left;
 }
@@ -167,33 +158,6 @@ div.editor_atto_content:hover .atto_control {
 .yui3-menu-hidden {
     display: none;
 }
-.editor_atto_controlmenu li {
-    list-style-type: none;
-    padding: 0;
-    margin: 0;
-}
-.editor_atto_controlmenu ul {
-    padding: 0;
-    margin: 0;
-}
-.editor_atto_controlmenu .moodle-dialogue-hd,
-.editor_atto_controlmenu .moodle-dialogue-ft {
-    display: none;
-}
-
-.editor_atto_controlmenu ul li a:active,
-.editor_atto_controlmenu ul li a:hover {
-    background-color: #0088cc;
-    color: white;
-}
-.editor_atto_controlmenu ul li a {
-    color: #333;
-    padding: 2px;
-    padding-left: 6px;
-    padding-right: 6px;
-    border-radius: 2px;
-    display: block;
-}
 
 /* Get broken images back in firefox */
 .editor_atto_content img:-moz-broken {
@@ -202,41 +166,17 @@ div.editor_atto_content:hover .atto_control {
     min-height:24px;
 }
 
+/* Atto menu styling */
 .moodle-dialogue-base .editor_atto_menu .moodle-dialogue-content .moodle-dialogue-bd {
-    position: absolute;
-    text-align:left;
+    padding: 0;
     z-index: 1000;
-    display: block;
-    background-color: #fff;
-    border: 1px solid #ccc;
-    -webkit-border-radius: 5px;
-    -moz-border-radius: 5px;
-    border-radius: 5px;
-    -webkit-box-shadow: 5px 5px 20px 0 #666666;
-    -moz-box-shadow: 5px 5px 20px 0 #666666;
-    box-shadow: 5px 5px 20px 0 #666666;
-    padding: 0px;
-}
-
-.editor_atto_menu .menu {
-  margin: 0px;
 }
 
-.editor_atto_menu .menu a {
-    display: block;
-    padding: 4px 1em 4px 1em;
-    color: #333333;
+.editor_atto_menu .dropdown-menu > li > a {
+  padding: 3px 14px;
 }
 
-.editor_atto_menu .menu a:hover {
-    color: #ffffff;background-color: #0088cc;
-}
-.editor_atto_menu .menu a.hidden {
-    display: none;
-}
-.editor_atto_menu .menu img {
-    vertical-align: middle;
-}
-.editor_atto_menu .menu > li {
-    display: block;
+.editor_atto_menu .open ul.dropdown-menu {
+    padding-top: 5px;
+    padding-bottom: 5px;
 }
index c2112cb..3476671 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014012800;        // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2014032800;        // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2013110500;        // Requires this Moodle version.
 $plugin->component = 'editor_atto';  // Full name of the plugin (used for diagnostics).
index 5c7a78c..2f8ac9c 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-debug.js differ
index 8e72ac4..5efbe55 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor-min.js differ
index 0c14f5f..477a74c 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js and b/lib/editor/atto/yui/build/moodle-editor_atto-editor/moodle-editor_atto-editor.js differ
index b2014f1..89f6576 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu-debug.js differ
index 77fb95d..d1b8901 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu-min.js differ
index b2014f1..93462f3 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu.js and b/lib/editor/atto/yui/build/moodle-editor_atto-menu/moodle-editor_atto-menu.js differ
index 28b4239..910bcd2 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-debug.js differ
index f8d557e..c3911c4 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin-min.js differ
index 4a51f51..957c316 100644 (file)
Binary files a/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js and b/lib/editor/atto/yui/build/moodle-editor_atto-plugin/moodle-editor_atto-plugin.js differ
index 923a8ad..80dc303 100644 (file)
@@ -35,20 +35,7 @@ var MENUTEMPLATE = '' +
             '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" '+
                 'style="background-color:{{config.menuColor}};" src="{{config.iconurl}}" />' +
             '<img class="icon" aria-hidden="true" role="presentation" width="16" height="16" src="{{image_url "t/expanded" "moodle"}}"/>' +
-        '</button>',
-    MENUDIALOGUE = '' +
-        '<div class="{{config.buttonClass}} atto_menu" ' +
-            'style="min-width:{{config.innerOverlayWidth}};">' +
-            '<ul class="menu">' +
-                '{{#each config.items}}' +
-                    '<li role="presentation" class="atto_menuentry">' +
-                        '<a href="#" role="menuitem" data-index="{{@index}}">' +
-                            '{{{text}}}' +
-                        '</a>' +
-                    '</li>' +
-                '{{/each}}' +
-            '</ul>' +
-        '</div>';
+        '</button>';
 
 var DISABLED = 'disabled',
     HIGHLIGHT = 'highlight',
@@ -141,6 +128,19 @@ EditorPluginButtons.prototype = {
      */
     _menuHideHandlers: null,
 
+    /**
+     * A textual description of the primary keyboard shortcut for this
+     * plugin.
+     *
+     * This will be null if no keyboard shortcut has been registered.
+     *
+     * @property _primaryKeyboardShortcut
+     * @protected
+     * @type String
+     * @default null
+     */
+    _primaryKeyboardShortcut: null,
+
     /**
      * Add a button for this plugin to the toolbar.
      *
@@ -150,6 +150,10 @@ EditorPluginButtons.prototype = {
      * @param {string} [config.icon] The icon identifier.
      * @param {string} [config.iconComponent='core'] The icon component.
      * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard.
+     * @param {string} [config.keyDescription] An optional description for the keyboard shortcuts.
+     * If not specified, this is automatically generated based on config.keys.
+     * If multiple key bindings are supplied to config.keys, then only the first is used.
+     * If set to false, then no description is added to the title.
      * @param {string} [config.tags] The tags that trigger this button to be highlighted.
      * @param {boolean} [config.tagMatchRequiresAll=false] Working in combination with the tags parameter, highlight
      * this button when any match is good enough.
@@ -180,6 +184,7 @@ EditorPluginButtons.prototype = {
         } else {
             buttonClass = buttonClass + '_' + config.buttonName;
         }
+        config.buttonClass = buttonClass;
 
         // Normalize icon configuration.
         config = this._normalizeIcon(config);
@@ -217,7 +222,19 @@ EditorPluginButtons.prototype = {
 
         // Handle button click via shortcut key.
         if (config.keys) {
+            if (typeof config.keyDescription !== 'undefined') {
+                // A keyboard shortcut description was specified - use it.
+                this._primaryKeyboardShortcut[buttonClass] = config.keyDescription;
+            }
             this._addKeyboardListener(config.callback, config.keys, buttonClass);
+
+            if (this._primaryKeyboardShortcut[buttonClass]) {
+                // If we have a valid keyboard shortcut description, then set it with the title.
+                button.setAttribute('title', M.util.get_string('plugin_title_shortcut', 'editor_atto', {
+                        title: title,
+                        shortcut: this._primaryKeyboardShortcut[buttonClass]
+                    }));
+            }
         }
 
         // Handle highlighting of the button.
@@ -323,6 +340,7 @@ EditorPluginButtons.prototype = {
         } else {
             buttonClass = buttonClass + '_' + config.buttonName;
         }
+        config.buttonClass = buttonClass;
 
         // Normalize icon configuration.
         config = this._normalizeIcon(config);
@@ -357,7 +375,7 @@ EditorPluginButtons.prototype = {
         // Add the standard click handler to the menu.
         this._buttonHandlers.push(
             this.toolbar.delegate('click', this._showToolbarMenu, '.' + buttonClass, this, config),
-            this.toolbar.delegate('key', this._showToolbarMenu, '40, 32, enter', '.' + buttonClass, this, config)
+            this.toolbar.delegate('key', this._showToolbarMenuAndFocus, '40, 32, enter', '.' + buttonClass, this, config)
         );
 
         // Add the button reference to the buttons array for later reference.
@@ -403,45 +421,15 @@ EditorPluginButtons.prototype = {
             }
             config.overlayWidth = parseInt(config.overlayWidth, 10) + 'em';
 
-            // Create the actual button.
-            var template = Y.Handlebars.compile(MENUDIALOGUE),
-                menu = Y.Node.create(template({
-                    config: config
-                }));
-
-            // Create the dialogue.
-            this.menus[config.buttonClass] = new M.core.dialogue({
-                bodyContent: menu,
-                width: null,
-                visible: false,
-                center: false,
-                closeButton: false,
-                responsive: false,
-                extraClasses: ['editor_atto_menu']
-            });
-
-            menuDialogue = this.menus[config.buttonClass];
-
-            // Hide the header node entirely.
-            menuDialogue.headerNode.hide();
-
-            // Handle menu item selection.
-            this._buttonHandlers.push(
-                menu.delegate('click', this._chooseMenuItem, '.atto_menuentry a', this, config, menuDialogue),
-
-                // Select the menu item on space, and enter.
-                menu.delegate('key', this._chooseMenuItem, '32, enter', '.atto_menuentry', this, config, menuDialogue),
+            this.menus[config.buttonClass] = new Y.M.editor_atto.Menu(config);
 
-                // Move up and down the menu on up/down.
-                menu.delegate('key', this._menuKeyboardNavigation, 'down:38,40', '.menu', this),
-
-                // Hide the menu on left/right.
-                menu.delegate('key', this._hideMenu, 'down:37,39', '.menu', this, menuDialogue)
-            );
+            this.menus[config.buttonClass].get('contentBox').delegate('click',
+                    this._chooseMenuItem, '.atto_menuentry a', this, config);
         }
 
         // Ensure that we focus on this button next time.
         var creatorButton = this.buttons[config.buttonName];
+        creatorButton.focus();
         this.get('host')._setTabFocus(creatorButton);
 
         // Get a reference to the menu dialogue.
@@ -449,36 +437,31 @@ EditorPluginButtons.prototype = {
 
         // Focus on the button by default after hiding this menu.
         menuDialogue.set('focusAfterHide', creatorButton);
+        menuDialogue.on('focusAfterHide', function(e) {
+            console.log(e);
+        });
 
         // Display the menu.
         menuDialogue.show();
 
         // Position it next to the button which opened it.
         menuDialogue.align(this.buttons[config.buttonName], [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
-
-        // And focus on the first element in the menu.
-        menuDialogue.get('boundingBox').one('a').focus();
-
-        // Hide the menu when clicking outside of it.
-        this._menuHideHandlers.push(
-            menuDialogue.get('boundingBox').on('focusoutside', this._hideMenu, this, menuDialogue)
-        );
     },
 
     /**
-     * Hide a menu, removing all of the event handlers which trigger the hide.
+     * Display a toolbar menu and focus upon the first item.
      *
-     * @method _hideMenu
+     * @method _showToolbarMenuAndFocus
      * @param {EventFacade} e
-     * @param {M.core.dialogue} menuDialogue The Dialogue to hide.
+     * @param {object} config The configuration for the whole toolbar.
+     * @param {Number} [config.overlayWidth=14] The width of the menu
      * @private
      */
-    _hideMenu: function(e, menuDialogue) {
-        if (menuDialogue.get('preventHideMenu') === true) {
-            return;
-        }
-        menuDialogue.hide();
-        new Y.EventHandle(this._menuHideHandlers).detach();
+    _showToolbarMenuAndFocus: function(e, config) {
+        this._showToolbarMenu(e, config);
+
+        // Focus on the first element in the menu.
+        this.menus[config.buttonClass].get('boundingBox').one('a').focus();
     },
 
     /**
@@ -491,12 +474,14 @@ EditorPluginButtons.prototype = {
      * @private
      */
     _chooseMenuItem: function(e, config, menuDialogue) {
-            // Get the index from the clicked anchor.
+        // Get the index from the clicked anchor.
         var index = e.target.ancestor('a', true).getData('index'),
 
             // And the normalized callback configuration.
             buttonConfig = this._normalizeCallback(config.items[index], config.globalItemConfig);
 
+            menuDialogue = this.menus[config.buttonClass];
+
         // Prevent the dialogue to be closed because of some browser weirdness.
         menuDialogue.set('preventHideMenu', true);
 
@@ -506,9 +491,10 @@ EditorPluginButtons.prototype = {
         // Cancel the hide menu prevention.
         menuDialogue.set('preventHideMenu', false);
 
+        console.log('Menu item chosen');
         // Set the focus after hide so that focus is returned to the editor and changes are made correctly.
         menuDialogue.set('focusAfterHide', this.get('host').editor);
-        this._hideMenu(e, menuDialogue);
+        menuDialogue.hide(e);
     },
 
     /**
@@ -666,6 +652,9 @@ EditorPluginButtons.prototype = {
 
         } else {
             keys = this._getKeyEvent() + keyConfig + this._getDefaultMetaKey();
+            if (typeof this._primaryKeyboardShortcut[buttonName] === 'undefined') {
+                this._primaryKeyboardShortcut[buttonName] = this._getDefaultMetaKeyDescription(keyConfig);
+            }
 
         }
 
@@ -807,71 +796,6 @@ EditorPluginButtons.prototype = {
         return this;
     },
 
-    /**
-     * Implement arrow-key navigation for the items in a toolbar menu.
-     *
-     * @method _menuKeyboardNavigation
-     * @param {EventFacade} e The keyboard event.
-     * @private
-     */
-    _menuKeyboardNavigation: function(e) {
-        // Prevent the default browser behaviour.
-        e.preventDefault();
-
-        // Get a list of all buttons in the menu.
-        var buttons = e.currentTarget.all('a[role="menuitem"]');
-
-        // On cursor moves we loops through the buttons.
-        var found = false,
-            index = 0,
-            direction = 1,
-            checkCount = 0,
-            current = e.target.ancestor('a[role="menuitem"]', true);
-
-        // Determine which button is currently selected.
-        while (!found && index < buttons.size()) {
-            if (buttons.item(index) === current) {
-                found = true;
-            } else {
-                index++;
-            }
-        }
-
-        if (!found) {
-            Y.log("Unable to find this menu item in the menu", 'debug', LOGNAME);
-            return;
-        }
-
-        if (e.keyCode === 38) {
-            // Moving up so reverse the direction.
-            direction = -1;
-        }
-
-        // Try to find the next
-        do {
-            index += direction;
-            if (index < 0) {
-                index = buttons.size() - 1;
-            } else if (index >= buttons.size()) {
-                // Handle wrapping.
-                index = 0;
-            }
-            next = buttons.item(index);
-
-            // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
-            checkCount++;
-            // Loop while:
-            // * we are not in a loop and have not already checked every button; and
-            // * we are on a different button; and
-            // * the next menu item is not hidden.
-        } while (checkCount < buttons.size() && next !== current && next.hasAttribute('hidden'));
-
-        if (next) {
-            next.focus();
-        }
-    },
-
-
     /**
      * Get the default meta key to use with keyboard events.
      *
@@ -890,6 +814,23 @@ EditorPluginButtons.prototype = {
         }
     },
 
+    /**
+     * Get the user-visible description of the meta key to use with keyboard events.
+     *
+     * On a Mac, this will be 'Command' ; otherwise it will be 'Control'.
+     *
+     * @method _getDefaultMetaKeyDescription
+     * @return {string}
+     * @private
+     */
+    _getDefaultMetaKeyDescription: function(keyCode) {
+        if (Y.UA.os === 'macintosh') {
+            return M.util.get_string('editor_command_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase());
+        } else {
+            return M.util.get_string('editor_control_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase());
+        }
+    },
+
     /**
      * Get the standard key event to use for keyboard events.
      *
index 9a6f8fa..5c1f78d 100644 (file)
@@ -81,6 +81,7 @@ Y.extend(EditorPlugin, Y.Base, {
         this.buttonNames = [];
         this.buttonStates = {};
         this.menus = {};
+        this._primaryKeyboardShortcut = [];
         this._buttonHandlers = [];
         this._menuHideHandlers = [];
     },
index 039a4b8..be2043f 100644 (file)
@@ -101,7 +101,7 @@ Y.extend(Editor, Y.Base, {
         'video'
     ],
 
-    PLACEHOLDER_FONTNAME: 'yui-tmp',
+    PLACEHOLDER_CLASS: 'atto-tmp-class',
     ALL_NODES_SELECTOR: '[style],font[face]',
     FONT_FAMILY: 'fontFamily',
 
index 5c907d9..d055868 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+var MENUDIALOGUE = '' +
+        '<div class="open {{config.buttonClass}} atto_menu" ' +
+            'style="min-width:{{config.innerOverlayWidth}};">' +
+            '<ul class="dropdown-menu">' +
+                '{{#each config.items}}' +
+                    '<li role="presentation" class="atto_menuentry">' +
+                        '<a href="#" role="menuitem" data-index="{{@index}}" {{#each data}}data-{{@key}}="{{this}}"{{/each}}>' +
+                            '{{{text}}}' +
+                        '</a>' +
+                    '</li>' +
+                '{{/each}}' +
+            '</ul>' +
+        '</div>';
+
 /**
  * A Menu for the Atto editor used in Moodle.
  *
@@ -41,24 +55,169 @@ Menu = function() {
 
 Y.extend(Menu, M.core.dialogue, {
 
+    /**
+     * A list of the menu handlers which have been attached here.
+     *
+     * @property _menuHandlers
+     * @type Array
+     * @private
+     */
+    _menuHandlers: null,
+
     initializer: function(config) {
-        var body, headertext, bb;
-        Menu.superclass.initializer.call(this, config);
+        var headertext,
+            bb;
+
+        this._menuHandlers = [];
+
+        // Create the actual button.
+        var template = Y.Handlebars.compile(MENUDIALOGUE),
+            menu = Y.Node.create(template({
+                config: config
+            }));
+        this.set('bodyContent', menu);
 
         bb = this.get('boundingBox');
         bb.addClass('editor_atto_controlmenu');
-
-        // Close the menu when clicked outside (excluding the button that opened the menu).
-        body = this.bodyNode;
+        bb.addClass('editor_atto_menu');
+        bb.one('.moodle-dialogue-wrap')
+            .removeClass('moodle-dialogue-wrap')
+            .addClass('moodle-dialogue-content');
 
         headertext = Y.Node.create('<h3/>')
                 .addClass('accesshide')
                 .setHTML(this.get('headerText'));
-        body.prepend(headertext);
+        this.get('bodyContent').prepend(headertext);
 
-        this.get('hideOn');
-    }
+        // Hide the header and footer node entirely.
+        this.headerNode.hide();
+        this.footerNode.hide();
+
+        this._setupHandlers();
+    },
+
+    /**
+     * Setup the Event handlers.
+     *
+     * @method _setupHandlers
+     * @private
+     */
+    _setupHandlers: function() {
+        var contentBox = this.get('contentBox');
+        // Handle menu item selection.
+        this._menuHandlers.push(
+            // Select the menu item on space, and enter.
+            contentBox.delegate('key', this._chooseMenuItem, '32, enter', '.atto_menuentry', this),
+
+            // Move up and down the menu on up/down.
+            contentBox.delegate('key', this._handleKeyboardEvent, 'down:38,40', '.dropdown-menu', this),
+
+            // Hide the menu when clicking outside of it.
+            contentBox.on('focusoutside', this.hide, this),
+
+            // Hide the menu on left/right, and escape keys.
+            contentBox.delegate('key', this.hide, 'down:37,39,esc', '.dropdown-menu', this)
+        );
+    },
+
+    /**
+     * Simulate other types of menu selection.
+     *
+     * @method _chooseMenuItem
+     * @param {EventFacade} e
+     */
+    _chooseMenuItem: function(e) {
+        e.target.simulate('click');
+        e.preventDefault();
+    },
+
+    /**
+     * Hide a menu, removing all of the event handlers which trigger the hide.
+     *
+     * @method hide
+     * @param {EventFacade} e
+     */
+    hide: function(e) {
+        if (this.get('preventHideMenu') === true) {
+            return;
+        }
+
+        // We must prevent the default action (left/right/escape) because
+        // there are other listeners on the toolbar which will focus on the
+        // editor.
+        if (e) {
+            e.preventDefault();
+        }
+
+        return Menu.superclass.hide.call(this, arguments);
+    },
+
+    /**
+     * Implement arrow-key navigation for the items in a toolbar menu.
+     *
+     * @method _handleKeyboardEvent
+     * @param {EventFacade} e The keyboard event.
+     * @static
+     */
+    _handleKeyboardEvent: function(e) {
+        // Prevent the default browser behaviour.
+        e.preventDefault();
+
+        // Get a list of all buttons in the menu.
+        var buttons = e.currentTarget.all('a[role="menuitem"]');
+
+        // On cursor moves we loops through the buttons.
+        var found = false,
+            index = 0,
+            direction = 1,
+            checkCount = 0,
+            current = e.target.ancestor('a[role="menuitem"]', true);
+
+        // Determine which button is currently selected.
+        while (!found && index < buttons.size()) {
+            if (buttons.item(index) === current) {
+                found = true;
+            } else {
+                index++;
+            }
+        }
 
+        if (!found) {
+            Y.log("Unable to find this menu item in the menu", 'debug', LOGNAME);
+            return;
+        }
+
+        if (e.keyCode === 38) {
+            // Moving up so reverse the direction.
+            direction = -1;
+        }
+
+        // Try to find the next
+        do {
+            index += direction;
+            if (index < 0) {
+                index = buttons.size() - 1;
+            } else if (index >= buttons.size()) {
+                // Handle wrapping.
+                index = 0;
+            }
+            next = buttons.item(index);
+
+            // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
+            checkCount++;
+            // Loop while:
+            // * we are not in a loop and have not already checked every button; and
+            // * we are on a different button; and
+            // * the next menu item is not hidden.
+        } while (checkCount < buttons.size() && next !== current && next.hasAttribute('hidden'));
+
+        if (next) {
+            next.focus();
+        }
+
+        e.preventDefault();
+        e.stopImmediatePropagation();
+    }
 }, {
     NAME: "menu",
     ATTRS: {
@@ -87,17 +246,6 @@ Y.Base.modifyAttrs(Menu, {
         value: 'auto'
     },
 
-    /**
-     * TODO - chekc that this is needed
-     * The footer content.
-     *
-     * @attribute footerContent
-     * @default ''
-     */
-    footerContent: {
-        value: ''
-    },
-
     /**
      * When to hide this menu.
      *
@@ -114,6 +262,62 @@ Y.Base.modifyAttrs(Menu, {
                 eventName: 'clickoutside'
             }
         ]
+    },
+
+    /**
+     * The default list of extra classes for this menu.
+     *
+     * @attribute extraClasses
+     * @type Array
+     * @default editor_atto_menu
+     */
+    extraClasses: {
+        value: [
+            'editor_atto_menu'
+        ]
+    },
+
+    /**
+     * Override the responsive nature of the core dialogues.
+     *
+     * @attribute responsive
+     * @type boolean
+     * @default false
+     */
+    responsive: {
+        value: false
+    },
+
+    /**
+     * The default visibility of the menu.
+     *
+     * @attribute visible
+     * @type boolean
+     * @default false
+     */
+    visible: {
+        value: false
+    },
+
+    /**
+     * Whether to centre the menu.
+     *
+     * @attribute center
+     * @type boolean
+     * @default false
+     */
+    center: {
+        value: false
+    },
+
+    /**
+     * Hide the close button.
+     * @attribute closeButton
+     * @type boolean
+     * @default false
+     */
+    closeButton: {
+        value: false
     }
 });
 
index 383f825..10e90c5 100644 (file)
@@ -342,107 +342,6 @@ EditorSelection.prototype = {
         selection.setRanges(ranges);
     },
 
-    /**
-     * Change the formatting for the current selection.
-     *
-     * Also changes the selection to the newly formatted block (allows applying multiple styles to a block).
-     *
-     * @method formatSelectionBlock
-     * @param {String} [blocktag] Change the block level tag to this. Empty string, means do not change the tag.
-     * @param {Object} [attributes] The keys and values for attributes to be added/changed in the block tag.
-     * @return {Node|boolean} The Node that was formatted if a change was made, otherwise false.
-     */
-    formatSelectionBlock: function(blocktag, attributes) {
-        // First find the nearest ancestor of the selection that is a block level element.
-        var selectionparentnode = this.getSelectionParentNode(),
-            boundary,
-            cell,
-            nearestblock,
-            newcontent,
-            match,
-            replacement;
-
-        if (!selectionparentnode) {
-            // No selection, nothing to format.
-            return false;
-        }
-
-        boundary = this.editor;
-
-        selectionparentnode = Y.one(selectionparentnode);
-
-        // If there is a table cell in between the selectionparentnode and the boundary,
-        // move the boundary to the table cell.
-        // This is because we might have a table in a div, and we select some text in a cell,
-        // want to limit the change in style to the table cell, not the entire table (via the outer div).
-        cell = selectionparentnode.ancestor(function (node) {
-            var tagname = node.get('tagName');
-            if (tagname) {
-                tagname = tagname.toLowerCase();
-            }
-            return (node === boundary) ||
-                   (tagname === 'td') ||
-                   (tagname === 'th');
-        }, true);
-
-        if (cell) {
-            // Limit the scope to the table cell.
-            boundary = cell;
-        }
-
-        nearestblock = selectionparentnode.ancestor(this.BLOCK_TAGS.join(', '), true);
-        if (nearestblock) {
-            // Check that the block is contained by the boundary.
-            match = nearestblock.ancestor(function (node) {
-                return node === boundary;
-            }, false);
-
-            if (!match) {
-                nearestblock = false;
-            }
-        }
-
-        // No valid block element - make one.
-        if (!nearestblock) {
-            // There is no block node in the content, wrap the content in a p and use that.
-            newcontent = Y.Node.create('<p></p>');
-            boundary.get('childNodes').each(function (child) {
-                newcontent.append(child.remove());
-            });
-            boundary.append(newcontent);
-            nearestblock = newcontent;
-        }
-
-        // Guaranteed to have a valid block level element contained in the contenteditable region.
-        // Change the tag to the new block level tag.
-        if (blocktag && blocktag !== '') {
-            // Change the block level node for a new one.
-            replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');
-            // Copy all attributes.
-            replacement.setAttrs(nearestblock.getAttrs());
-            // Copy all children.
-            nearestblock.get('childNodes').each(function (child) {
-                child.remove();
-                replacement.append(child);
-            });
-
-            nearestblock.replace(replacement);
-            nearestblock = replacement;
-        }
-
-        // Set the attributes on the block level tag.
-        if (attributes) {
-            nearestblock.setAttrs(attributes);
-        }
-
-        // Change the selection to the modified block. This makes sense when we might apply multiple styles
-        // to the block.
-        var selection = this.getSelectionFromNode(nearestblock);
-        this.setSelection(selection);
-
-        return nearestblock;
-    },
-
     /**
      * Inserts the given HTML into the editable content at the currently focused point.
      *
index 318fa2b..dfb58d1 100644 (file)
@@ -86,55 +86,138 @@ EditorStyling.prototype = {
      * @param {Array} toggleclasses - Class names to be toggled on or off.
      */
     toggleInlineSelectionClass: function(toggleclasses) {
+        var classname = toggleclasses.join(" ");
+        var originalSelection = this.getSelection();
+        var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
+
+        cssApplier.toggleSelection();
+
+        this.setSelection(originalSelection);
+    },
+
+    /**
+     * Change the formatting for the current selection.
+     *
+     * This will set inline styles on the current selection.
+     *
+     * @method toggleInlineSelectionClass
+     * @param {Array} styles - Style attributes to set on the nodes.
+     */
+    formatSelectionInlineStyle: function(styles) {
+        var classname = this.PLACEHOLDER_CLASS;
+        var originalSelection = this.getSelection();
+        var cssApplier = rangy.createCssClassApplier(classname, {normalize: true});
+
+        cssApplier.applyToSelection();
+
+        this.editor.all('.' + classname).each(function (node) {
+            node.removeClass(classname).setStyles(styles);
+        }, this);
+
+        this.setSelection(originalSelection);
+    },
+
+    /**
+     * Change the formatting for the current selection.
+     *
+     * Also changes the selection to the newly formatted block (allows applying multiple styles to a block).
+     *
+     * @method formatSelectionBlock
+     * @param {String} [blocktag] Change the block level tag to this. Empty string, means do not change the tag.
+     * @param {Object} [attributes] The keys and values for attributes to be added/changed in the block tag.
+     * @return {Node|boolean} The Node that was formatted if a change was made, otherwise false.
+     */
+    formatSelectionBlock: function(blocktag, attributes) {
+        // First find the nearest ancestor of the selection that is a block level element.
         var selectionparentnode = this.getSelectionParentNode(),
-            nodes,
-            items = [],
-            parentspan,
-            currentnode,
-            newnode,
-            i = 0;
+            boundary,
+            cell,
+            nearestblock,
+            newcontent,
+            match,
+            replacement;
 
         if (!selectionparentnode) {
             // No selection, nothing to format.
-            return;
+            return false;
         }
 
-        // Add a bogus fontname as the browsers handle inserting fonts into multiple blocks correctly.
-        document.execCommand('fontname', false, this.PLACEHOLDER_FONTNAME);
-        nodes = this.editor.all(this.ALL_NODES_SELECTOR);
+        boundary = this.editor;
 
-        // Create a list of all nodes that have our bogus fontname.
-        nodes.each(function(node, index) {
-            if (node.getStyle(this.FONT_FAMILY) === this.PLACEHOLDER_FONTNAME) {
-                node.setStyle(this.FONT_FAMILY, '');
-                if (!node.compareTo(this.editor)) {
-                    items.push(Y.Node.getDOMNode(nodes.item(index)));
-                }
-            }
-        });
-
-        // Replace the fontname tags with spans
-        for (i = 0; i < items.length; i++) {
-            currentnode = Y.one(items[i]);
-
-            // Check for an existing span to add the nolink class to.
-            parentspan = currentnode.ancestor('.editor_atto_content span');
-            if (!parentspan) {
-                newnode = Y.Node.create('<span></span>');
-                newnode.append(items[i].innerHTML);
-                currentnode.replace(newnode);
-
-                currentnode = newnode;
-            } else {
-                currentnode = parentspan;
+        selectionparentnode = Y.one(selectionparentnode);
+
+        // If there is a table cell in between the selectionparentnode and the boundary,
+        // move the boundary to the table cell.
+        // This is because we might have a table in a div, and we select some text in a cell,
+        // want to limit the change in style to the table cell, not the entire table (via the outer div).
+        cell = selectionparentnode.ancestor(function (node) {
+            var tagname = node.get('tagName');
+            if (tagname) {
+                tagname = tagname.toLowerCase();
             }
+            return (node === boundary) ||
+                   (tagname === 'td') ||
+                   (tagname === 'th');
+        }, true);
 
-            // Toggle the classes on the spans.
-            for (var j = 0; j < toggleclasses.length; j++) {
-                currentnode.toggleClass(toggleclasses[j]);
+        if (cell) {
+            // Limit the scope to the table cell.
+            boundary = cell;
+        }
+
+        nearestblock = selectionparentnode.ancestor(this.BLOCK_TAGS.join(', '), true);
+        if (nearestblock) {
+            // Check that the block is contained by the boundary.
+            match = nearestblock.ancestor(function (node) {
+                return node === boundary;
+            }, false);
+
+            if (!match) {
+                nearestblock = false;
             }
         }
+
+        // No valid block element - make one.
+        if (!nearestblock) {
+            // There is no block node in the content, wrap the content in a p and use that.
+            newcontent = Y.Node.create('<p></p>');
+            boundary.get('childNodes').each(function (child) {
+                newcontent.append(child.remove());
+            });
+            boundary.append(newcontent);
+            nearestblock = newcontent;
+        }
+
+        // Guaranteed to have a valid block level element contained in the contenteditable region.
+        // Change the tag to the new block level tag.
+        if (blocktag && blocktag !== '') {
+            // Change the block level node for a new one.
+            replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');
+            // Copy all attributes.
+            replacement.setAttrs(nearestblock.getAttrs());
+            // Copy all children.
+            nearestblock.get('childNodes').each(function (child) {
+                child.remove();
+                replacement.append(child);
+            });
+
+            nearestblock.replace(replacement);
+            nearestblock = replacement;
+        }
+
+        // Set the attributes on the block level tag.
+        if (attributes) {
+            nearestblock.setAttrs(attributes);
+        }
+
+        // Change the selection to the modified block. This makes sense when we might apply multiple styles
+        // to the block.
+        var selection = this.getSelectionFromNode(nearestblock);
+        this.setSelection(selection);
+
+        return nearestblock;
     }
+
 };
 
 Y.Base.mix(Y.M.editor_atto.Editor, [EditorStyling]);
index a4ed9ed..6ba6498 100644 (file)
@@ -461,7 +461,7 @@ function enrol_add_course_navigation(navigation_node $coursenode, $course) {
      // Deal somehow with users that are not enrolled but still got a role somehow
     if ($course->id != SITEID) {
         //TODO, create some new UI for role assignments at course level
-        if (has_capability('moodle/role:assign', $coursecontext)) {
+        if (has_capability('moodle/course:reviewotherusers', $coursecontext)) {
             $url = new moodle_url('/enrol/otherusers.php', array('id'=>$course->id));
             $usersnode->add(get_string('notenrolledusers', 'enrol'), $url, navigation_node::TYPE_SETTING, null, 'otherusers', new pix_icon('i/assignroles', ''));
         }
index 8adf217..9327c9d 100644 (file)
@@ -380,6 +380,13 @@ class theme_config {
      */
     public $lessvariablescallback = null;
 
+    /**
+     * Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php
+     * Defaults to {@link core_renderer::blocks_for_region()}
+     * @var string
+     */
+    public $blockrendermethod = null;
+
     /**
      * Load the config.php file for a particular theme, and return an instance
      * of this class. (That is, this is a factory method.)
@@ -448,7 +455,8 @@ class theme_config {
         $configurable = array('parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'javascripts', 'javascripts_footer',
                               'parents_exclude_javascripts', 'layouts', 'enable_dock', 'enablecourseajax', 'supportscssoptimisation',
                               'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'hidefromselector', 'doctype',
-                              'yuicssmodules', 'blockrtlmanipulations', 'lessfile', 'extralesscallback', 'lessvariablescallback');
+                              'yuicssmodules', 'blockrtlmanipulations', 'lessfile', 'extralesscallback', 'lessvariablescallback',
+                              'blockrendermethod');
 
         foreach ($config as $key=>$value) {
             if (in_array($key, $configurable)) {
@@ -1844,7 +1852,7 @@ class theme_config {
     public function setup_blocks($pagelayout, $blockmanager) {
         $layoutinfo = $this->layout_info_for_page($pagelayout);
         if (!empty($layoutinfo['regions'])) {
-            $blockmanager->add_regions($layoutinfo['regions']);
+            $blockmanager->add_regions($layoutinfo['regions'], false);
             $blockmanager->set_default_region($layoutinfo['defaultregion']);
         }
     }
@@ -1899,6 +1907,32 @@ class theme_config {
     public function get_theme_name() {
         return get_string('pluginname', 'theme_'.$this->name);
     }
+
+    /**
+     * Returns the block render method.
+     *
+     * It is set by the theme via:
+     *     $THEME->blockrendermethod = '...';
+     *
+     * It can be one of two values, blocks or blocks_for_region.
+     * It should be set to the method being used by the theme layouts.
+     *
+     * @return string
+     */
+    public function get_block_render_method() {
+        if ($this->blockrendermethod) {
+            // Return the specified block render method.
+            return $this->blockrendermethod;
+        }
+        // Its not explicitly set, check the parent theme configs.
+        foreach ($this->parent_configs as $config) {
+            if (isset($config->blockrendermethod)) {
+                return $config->blockrendermethod;
+            }
+        }
+        // Default it to blocks.
+        return 'blocks';
+    }
 }
 
 /**
index 991c9ea..e1795bd 100644 (file)
@@ -3246,6 +3246,28 @@ EOD;
         return html_writer::tag($tag, $content, $attributes);
     }
 
+    /**
+     * Renders a custom block region.
+     *
+     * Use this method if you want to add an additional block region to the content of the page.
+     * Please note this should only be used in special situations.
+     * We want to leave the theme is control where ever possible!
+     *
+     * This method must use the same method that the theme uses within its layout file.
+     * As such it asks the theme what method it is using.
+     * It can be one of two values, blocks or blocks_for_region (deprecated).
+     *
+     * @param string $regionname The name of the custom region to add.
+     * @return string HTML for the block region.
+     */
+    public function custom_block_region($regionname) {
+        if ($this->page->theme->get_block_render_method() === 'blocks') {
+            return $this->blocks($regionname);
+        } else {
+            return $this->blocks_for_region($regionname);
+        }
+    }
+
     /**
      * Returns the CSS classes to apply to the body tag.
      *
index e8caf66..91b55e7 100644 (file)
@@ -478,7 +478,11 @@ class behat_hooks extends behat_base {
             if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) {
 
                 // Getting the debugging info and the backtrace.
-                $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny');
+                $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error');
+                // If errorinfoboxes is empty, try find notifytiny (original) class.
+                if (empty($errorinfoboxes)) {
+                    $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny');
+                }
                 $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
                     $this->get_debug_text($errorinfoboxes[1]->getHtml());
 
index fbadf5d..49b064a 100644 (file)
@@ -43,6 +43,7 @@ class core_blocklib_testcase extends advanced_testcase {
         parent::setUp();
         $this->testpage = new moodle_page();
         $this->testpage->set_context(context_system::instance());
+        $this->testpage->set_pagetype('phpunit-block-test');
         $this->blockmanager = new testable_block_manager($this->testpage);
     }
 
@@ -69,7 +70,7 @@ class core_blocklib_testcase extends advanced_testcase {
 
     public function test_add_region() {
         // Exercise SUT.
-        $this->blockmanager->add_region('a-region-name');
+        $this->blockmanager->add_region('a-region-name', false);
         // Validate.
         $this->assertEquals(array('a-region-name'), $this->blockmanager->get_regions());
     }
@@ -78,15 +79,15 @@ class core_blocklib_testcase extends advanced_testcase {
         // Set up fixture.
         $regions = array('a-region', 'another-region');
         // Exercise SUT.
-        $this->blockmanager->add_regions($regions);
+        $this->blockmanager->add_regions($regions, false);
         // Validate.
         $this->assertEquals($regions, $this->blockmanager->get_regions(), '', 0, 10, true);
     }
 
     public function test_add_region_twice() {
         // Exercise SUT.
-        $this->blockmanager->add_region('a-region-name');
-        $this->blockmanager->add_region('another-region');
+        $this->blockmanager->add_region('a-region-name', false);
+        $this->blockmanager->add_region('another-region', false);
         // Validate.
         $this->assertEquals(array('a-region-name', 'another-region'), $this->blockmanager->get_regions(), '', 0, 10, true);
     }
@@ -95,6 +96,63 @@ class core_blocklib_testcase extends advanced_testcase {
      * @expectedException coding_exception
      */
     public function test_cannot_add_region_after_loaded() {
+        // Set up fixture.
+        $this->blockmanager->mark_loaded();
+        // Exercise SUT.
+        $this->blockmanager->add_region('too-late', false);
+    }
+
+    /**
+     * Testing adding a custom region.
+     */
+    public function test_add_custom_region() {
+        global $SESSION;
+        // Exercise SUT.
+        $this->blockmanager->add_region('a-custom-region-name');
+        // Validate.
+        $this->assertEquals(array('a-custom-region-name'), $this->blockmanager->get_regions());
+        $this->assertTrue(isset($SESSION->custom_block_regions));
+        $this->assertArrayHasKey('phpunit-block-test', $SESSION->custom_block_regions);
+        $this->assertTrue(in_array('a-custom-region-name', $SESSION->custom_block_regions['phpunit-block-test']));
+
+    }
+
+    /**
+     * Test adding two custom regions using add_regions method.
+     */
+    public function test_add_custom_regions() {
+        global $SESSION;
+        // Set up fixture.
+        $regions = array('a-region', 'another-custom-region');
+        // Exercise SUT.
+        $this->blockmanager->add_regions($regions);
+        // Validate.
+        $this->assertEquals($regions, $this->blockmanager->get_regions(), '', 0, 10, true);
+        $this->assertTrue(isset($SESSION->custom_block_regions));
+        $this->assertArrayHasKey('phpunit-block-test', $SESSION->custom_block_regions);
+        $this->assertTrue(in_array('another-custom-region', $SESSION->custom_block_regions['phpunit-block-test']));
+    }
+
+    /**
+     * Test adding two custom block regions.
+     */
+    public function test_add_custom_region_twice() {
+        // Exercise SUT.
+        $this->blockmanager->add_region('a-custom-region-name');
+        $this->blockmanager->add_region('another-custom-region');
+        // Validate.
+        $this->assertEquals(
+            array('a-custom-region-name', 'another-custom-region'),
+            $this->blockmanager->get_regions(),
+            '', 0, 10, true
+        );
+    }
+
+    /**
+     * Test to ensure that we cannot add a region after the blocks have been loaded.
+     * @expectedException coding_exception
+     */
+    public function test_cannot_add_custom_region_after_loaded() {
         // Set up fixture.
         $this->blockmanager->mark_loaded();
         // Exercise SUT.
@@ -103,7 +161,7 @@ class core_blocklib_testcase extends advanced_testcase {
 
     public function test_set_default_region() {
         // Set up fixture.
-        $this->blockmanager->add_region('a-region-name');
+        $this->blockmanager->add_region('a-region-name', false);
         // Exercise SUT.
         $this->blockmanager->set_default_region('a-region-name');
         // Validate.
@@ -149,7 +207,7 @@ class core_blocklib_testcase extends advanced_testcase {
         $page->set_subpage($subpage);
 
         $blockmanager = new testable_block_manager($page);
-        $blockmanager->add_regions($regions);
+        $blockmanager->add_regions($regions, false);
         $blockmanager->set_default_region($regions[0]);
 
         return array($page, $blockmanager);
index e8828e2..a907328 100644 (file)
Binary files a/lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-debug.js differ
index 83c014a..c4223e0 100644 (file)
Binary files a/lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks-min.js differ
index 605161a..476975f 100644 (file)
Binary files a/lib/yui/build/moodle-core-blocks/moodle-core-blocks.js and b/lib/yui/build/moodle-core-blocks/moodle-core-blocks.js differ
diff --git a/lib/yui/build/moodle-core-event/moodle-core-event-debug.js b/lib/yui/build/moodle-core-event/moodle-core-event-debug.js
new file mode 100644 (file)
index 0000000..9bb3479
Binary files /dev/null and b/lib/yui/build/moodle-core-event/moodle-core-event-debug.js differ
diff --git a/lib/yui/build/moodle-core-event/moodle-core-event-min.js b/lib/yui/build/moodle-core-event/moodle-core-event-min.js
new file mode 100644 (file)
index 0000000..b016868
Binary files /dev/null and b/lib/yui/build/moodle-core-event/moodle-core-event-min.js differ
diff --git a/lib/yui/build/moodle-core-event/moodle-core-event.js b/lib/yui/build/moodle-core-event/moodle-core-event.js
new file mode 100644 (file)
index 0000000..d2c481f
Binary files /dev/null and b/lib/yui/build/moodle-core-event/moodle-core-event.js differ
index da6e26d..f01d006 100644 (file)
@@ -16,7 +16,8 @@ CSS = {
     SKIPBLOCK : 'skip-block',
     SKIPBLOCKTO : 'skip-block-to',
     MYINDEX : 'page-my-index',
-    REGIONMAIN : 'region-main'
+    REGIONMAIN : 'region-main',
+    BLOCKSMOVING : 'blocks-moving'
 };
 
 var SELECTOR = {
@@ -164,6 +165,9 @@ Y.extend(DRAGBLOCK, M.core.dragdrop, {
         if (drag.get('node').next() && drag.get('node').next().hasClass(CSS.SKIPBLOCKTO)) {
             this.skipnodebottom = drag.get('node').next();
         }
+
+        // Add the blocks-moving class so that the theme can respond if need be.
+        Y.one('body').addClass(CSS.BLOCKSMOVING);
     },
 
     drop_over : function(e) {
@@ -208,11 +212,13 @@ Y.extend(DRAGBLOCK, M.core.dragdrop, {
         }
     },
 
-    drop_end : function() {
+    drag_end : function() {
         // clear variables
         this.skipnodetop = null;
         this.skipnodebottom = null;
         this.dragsourceregion = null;
+        // Remove the blocks moving class once the drag-drop is over.
+        Y.one('body').removeClass(CSS.BLOCKSMOVING);
     },
 
     drag_dropmiss : function(e) {
@@ -343,6 +349,9 @@ M.core.blockdraganddrop.is_using_blocks_render_method = function() {
         var goodregions = Y.all('.block-region[data-blockregion]').size();
         var allregions = Y.all('.block-region').size();
         this._isusingnewblocksmethod = (allregions === goodregions);
+        if (goodregions > 0 && allregions > 0) {
+            Y.log('Both core_renderer::blocks and core_renderer::blocks_for_region have been used.', 'warn', 'moodle-core_blocks');
+        }
     }
     return this._isusingnewblocksmethod;
 };
@@ -357,8 +366,10 @@ M.core.blockdraganddrop.is_using_blocks_render_method = function() {
  */
 M.core.blockdraganddrop.init = function(params) {
     if (this.is_using_blocks_render_method()) {
+        Y.log('Block drag and drop initialised for the blocks method.', 'info', 'moodle-core_blocks');
         new MANAGER(params);
     } else {
+        Y.log('Block drag and drop initialised with the legacy manager (blocks_for_region used).', 'info', 'moodle-core_blocks');
         new DRAGBLOCK(params);
     }
 };
diff --git a/lib/yui/src/event/build.json b/lib/yui/src/event/build.json
new file mode 100644 (file)
index 0000000..6271245
--- /dev/null
@@ -0,0 +1,10 @@
+{
+    "name": "moodle-core-event",
+    "builds": {
+        "moodle-core-event": {
+            "jsfiles": [
+                "event.js"
+            ]
+        }
+    }
+}
diff --git a/lib/yui/src/event/js/event.js b/lib/yui/src/event/js/event.js
new file mode 100644 (file)
index 0000000..39fe4f6
--- /dev/null
@@ -0,0 +1,67 @@
+// 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/>.
+
+/**
+ * @module moodle-core-event
+ */
+
+var LOGNAME = 'moodle-core-event';
+
+/**
+ * List of published global JS events in Moodle. This is a collection
+ * of global events that can be subscribed to, or fired from any plugin.
+ *
+ * @namespace M.core
+ * @class event
+ */
+M.core = M.core || {};
+
+M.core.event = {
+    /**
+     * This event is triggered when a page has added dynamic nodes to a page
+     * that should be processed by the filter system. An example is loading
+     * user text that could have equations in it. MathJax can typeset the equations
+     * but only if it is notified that there are new nodes in the page that need processing.
+     * To trigger this event use M.core.Event.fire(M.core.Event.FILTER_CONTENT_UPDATED, {nodes: list});
+     *
+     * @event "filter-content-updated"
+     * @param nodes {Y.NodeList} List of nodes added to the DOM.
+     */
+    FILTER_CONTENT_UPDATED: "filter-content-updated"
+};
+
+
+var eventDefaultConfig = {
+    emitFacade: true,
+    defaultFn: function(e) {
+        Y.log('Event fired: ' + e.type, 'debug', LOGNAME);
+    },
+    preventedFn: function(e) {
+        Y.log('Event prevented: ' + e.type, 'debug', LOGNAME);
+    },
+    stoppedFn: function(e) {
+        Y.log('Event stopped: ' + e.type, 'debug', LOGNAME);
+    }
+};
+
+// Publish all the events with a standard config.
+var key;
+for (key in M.core.event) {
+    if (M.core.event.hasOwnProperty(key)) {
+        Y.publish(M.core.event[key], eventDefaultConfig);
+    }
+}
+
+// Publish events with a custom config here.
diff --git a/lib/yui/src/event/meta/event.json b/lib/yui/src/event/meta/event.json
new file mode 100644 (file)
index 0000000..89a7d38
--- /dev/null
@@ -0,0 +1,7 @@
+{
+    "moodle-core-event": {
+        "requires": [
+            "event-custom"
+        ]
+    }
+}
index 42dff0a..240cc6a 100644 (file)
@@ -517,7 +517,7 @@ function assign_print_recent_activity($course, $viewfullnames, $timestart) {
     $show    = array();
     $grader  = array();
 
-    $showrecentsubmissions = get_config('mod_assign', 'showrecentsubmissions');
+    $showrecentsubmissions = get_config('assign', 'showrecentsubmissions');
 
     foreach ($submissions as $submission) {
         if (!array_key_exists($submission->cmid, $modinfo->get_cms())) {
@@ -553,19 +553,14 @@ function assign_print_recent_activity($course, $viewfullnames, $timestart) {
                 continue;
             }
 
-            if (is_null($modinfo->get_groups())) {
-                // Load all my groups and cache it in modinfo.
-                $modinfo->groups = groups_get_user_groups($course->id);
-            }
-
             // This will be slow - show only users that share group with me in this cm.
-            if (empty($modinfo->groups[$cm->id])) {
+            if (!$modinfo->get_groups($cm->groupingid)) {
                 continue;
             }
             $usersgroups =  groups_get_all_groups($course->id, $submission->userid, $cm->groupingid);
             if (is_array($usersgroups)) {
                 $usersgroups = array_keys($usersgroups);
-                $intersect = array_intersect($usersgroups, $modinfo->groups[$cm->id]);
+                $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid));
                 if (empty($intersect)) {
                     continue;
                 }
@@ -665,17 +660,9 @@ function assign_get_recent_mod_activity(&$activities,
     $accessallgroups = has_capability('moodle/site:accessallgroups', $cmcontext);
     $viewfullnames   = has_capability('moodle/site:viewfullnames', $cmcontext);
 
-    if (is_null($modinfo->get_groups())) {
-        // Load all my groups and cache it in modinfo.
-        $modinfo->groups = groups_get_user_groups($course->id);
-    }
 
-    $showrecentsubmissions = get_config('mod_assign', 'showrecentsubmissions');
+    $showrecentsubmissions = get_config('assign', 'showrecentsubmissions');
     $show = array();
-    $usersgroups = groups_get_all_groups($course->id, $USER->id, $cm->groupingid);
-    if (is_array($usersgroups)) {
-        $usersgroups = array_keys($usersgroups);
-    }
     foreach ($submissions as $submission) {
         if ($submission->userid == $USER->id) {
             $show[] = $submission;
@@ -696,11 +683,13 @@ function assign_get_recent_mod_activity(&$activities,
             }
 
             // This will be slow - show only users that share group with me in this cm.
-            if (empty($modinfo->groups[$cm->id])) {
+            if (!$modinfo->get_groups($cm->groupingid)) {
                 continue;
             }
+            $usersgroups =  groups_get_all_groups($course->id, $submission->userid, $cm->groupingid);
             if (is_array($usersgroups)) {
-                $intersect = array_intersect($usersgroups, $modinfo->groups[$cm->id]);
+                $usersgroups = array_keys($usersgroups);
+                $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid));
                 if (empty($intersect)) {
                     continue;
                 }
index c7e9d48..026b7f6 100644 (file)
@@ -289,12 +289,8 @@ function chat_print_recent_activity($course, $viewfullnames, $timestart) {
             continue;
         }
 
-        if (is_null($modinfo->groups)) {
-            $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
-        }
-
         // verify groups in separate mode
-        if (!$mygroupids = $modinfo->groups[$cm->groupingid]) {
+        if (!$mygroupids = $modinfo->get_groups($cm->groupingid)) {
             continue;
         }
 
index 18d87a0..f924139 100644 (file)
@@ -385,7 +385,7 @@ function feedback_get_recent_mod_activity(&$activities, &$index,
 
     $sqlargs = array();
 
-    $userfields = user_picture::fields('u', null, 'userid');
+    $userfields = user_picture::fields('u', null, 'useridagain');
     $sql = " SELECT fk . * , fc . * , $userfields
                 FROM {feedback_completed} fc
                     JOIN {feedback} fk ON fk.id = fc.feedback
@@ -423,11 +423,6 @@ function feedback_get_recent_mod_activity(&$activities, &$index,
     $viewfullnames   = has_capability('moodle/site:viewfullnames', $cm_context);
     $groupmode       = groups_get_activity_groupmode($cm, $course);
 
-    if (is_null($modinfo->groups)) {
-        // load all my groups and cache it in modinfo
-        $modinfo->groups = groups_get_user_groups($course->id);
-    }
-
     $aname = format_string($cm->name, true);
     foreach ($feedbackitems as $feedbackitem) {
         if ($feedbackitem->userid != $USER->id) {
@@ -440,7 +435,7 @@ function feedback_get_recent_mod_activity(&$activities, &$index,
                     continue;
                 }
                 $usersgroups = array_keys($usersgroups);
-                $intersect = array_intersect($usersgroups, $modinfo->groups[$cm->id]);
+                $intersect = array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid));
                 if (empty($intersect)) {
                     continue;
                 }
@@ -459,19 +454,7 @@ function feedback_get_recent_mod_activity(&$activities, &$index,
         $tmpactivity->content->feedbackid = $feedbackitem->id;
         $tmpactivity->content->feedbackuserid = $feedbackitem->userid;
 
-        $userfields = explode(',', user_picture::fields());
-        $tmpactivity->user = new stdClass();
-        foreach ($userfields as $userfield) {
-            if ($userfield == 'id') {
-                $tmpactivity->user->{$userfield} = $feedbackitem->userid; // aliased in SQL above
-            } else {
-                if (!empty($feedbackitem->{$userfield})) {
-                    $tmpactivity->user->{$userfield} = $feedbackitem->{$userfield};
-                } else {
-                    $tmpactivity->user->{$userfield} = null;
-                }
-            }
-        }
+        $tmpactivity->user = user_picture::unalias($feedbackitem, null, 'useridagain');
         $tmpactivity->user->fullname = fullname($feedbackitem, $viewfullnames);
 
         $activities[$index++] = $tmpactivity;
index 2793603..6f48e37 100644 (file)
@@ -335,13 +335,16 @@ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $co
     $cm = $modinfo->cms[$cmid];
     $context = context_module::instance($cm->id);
 
-    if (!has_capability('mod/glossary:view', $context)) {
+    if (!$cm->uservisible) {
         return;
     }
 
     $viewfullnames = has_capability('moodle/site:viewfullnames', $context);
+    // Groups are not yet supported for glossary. See MDL-10728 .
+    /*
     $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
     $groupmode = groups_get_activity_groupmode($cm, $course);
+     */
 
     $params['timestart'] = $timestart;
 
@@ -361,17 +364,24 @@ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $co
         $groupjoin   = '';
     }
 
+    $approvedselect = "";
+    if (!has_capability('mod/glossary:approve', $context)) {
+        $approvedselect = " AND ge.approved = 1 ";
+    }
+
     $params['timestart'] = $timestart;
     $params['glossaryid'] = $cm->instance;
 
-    $ufields = user_picture::fields('u', null, 'useridagain');
+    $ufields = user_picture::fields('u', null, 'userid');
     $entries = $DB->get_records_sql("
-              SELECT ge.id AS entryid, ge.*, $ufields
+              SELECT ge.id AS entryid, ge.glossaryid, ge.concept, ge.definition, ge.approved,
+                     ge.timemodified, $ufields
                 FROM {glossary_entries} ge
                 JOIN {user} u ON u.id = ge.userid
                      $groupjoin
                WHERE ge.timemodified > :timestart
                  AND ge.glossaryid = :glossaryid
+                     $approvedselect
                      $userselect
                      $groupselect
             ORDER BY ge.timemodified ASC", $params);
@@ -381,6 +391,8 @@ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $co
     }
 
     foreach ($entries as $entry) {
+        // Groups are not yet supported for glossary. See MDL-10728 .
+        /*
         $usersgroups = null;
         if ($entry->userid != $USER->id) {
             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
@@ -392,14 +404,15 @@ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $co
                         $usersgroups = array();
                     }
                 }
-                if (!array_intersect($usersgroups, $modinfo->get_groups($cm->id))) {
+                if (!array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid))) {
                     continue;
                 }
             }
         }
+         */
 
         $tmpactivity                       = new stdClass();
-        $tmpactivity->user                 = user_picture::unalias($entry, null, 'useridagain');
+        $tmpactivity->user                 = user_picture::unalias($entry, null, 'userid');
         $tmpactivity->user->fullname       = fullname($tmpactivity->user, $viewfullnames);
         $tmpactivity->type                 = 'glossary';
         $tmpactivity->cmid                 = $cm->id;
@@ -411,6 +424,7 @@ function glossary_get_recent_mod_activity(&$activities, &$index, $timestart, $co
         $tmpactivity->content->entryid     = $entry->entryid;
         $tmpactivity->content->concept     = $entry->concept;
         $tmpactivity->content->definition  = $entry->definition;
+        $tmpactivity->content->approved    = $entry->approved;
 
         $activities[$index++] = $tmpactivity;
     }
@@ -440,14 +454,20 @@ function glossary_print_recent_mod_activity($activity, $courseid, $detail, $modn
     echo html_writer::start_tag('div', array('class'=>'glossary-activity-content'));
     echo html_writer::start_tag('div', array('class'=>'glossary-activity-entry'));
 
-    $urlparams = array('g' => $activity->glossaryid, 'mode' => 'entry', 'hook' => $activity->content->entryid);
-    echo html_writer::tag('a', strip_tags($activity->content->concept),
-        array('href' => new moodle_url('/mod/glossary/view.php', $urlparams)));
+    if (isset($activity->content->approved) && !$activity->content->approved) {
+        $urlparams = array('g' => $activity->glossaryid, 'mode' => 'approval', 'hook' => $activity->content->concept);
+        $class = array('class' => 'dimmed_text');
+    } else {
+        $urlparams = array('g' => $activity->glossaryid, 'mode' => 'entry', 'hook' => $activity->content->entryid);
+        $class = array();
+    }
+    echo html_writer::link(new moodle_url('/mod/glossary/view.php', $urlparams),
+            strip_tags($activity->content->concept), $class);
     echo html_writer::end_tag('div');
 
     $url = new moodle_url('/user/view.php', array('course'=>$courseid, 'id'=>$activity->user->id));
     $name = $activity->user->fullname;
-    $link = html_writer::link($url, $name);
+    $link = html_writer::link($url, $name, $class);
 
     echo html_writer::start_tag('div', array('class'=>'user'));
     echo $link .' - '. userdate($activity->timestamp);
index b4ea3c8..4793292 100644 (file)
@@ -301,6 +301,10 @@ function _quiz_move_question($quiz, $slotnumber, $shift) {
     $otherslot = $DB->get_record('quiz_slots',
             array('quizid' => $quiz->id, 'slot' => $slotnumber + $shift));
     if (!$otherslot) {
+        // Must be first or last question being moved further that way if we can.
+        if ($shift + $slot->page > 0) {
+            $DB->set_field('quiz_slots', 'page', $slot->page + $shift, array('id' => $slot->id));
+        }
         return;
     }
 
@@ -469,6 +473,8 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
     $returnurl = $pageurl->out_as_local_url(false);
     $questiontotalcount = count($order);
 
+    $lastquestion = new stdClass();
+    $lastquestion->slot = 0; // Used to get the add page here buttons right.
     foreach ($order as $count => $qnum) { // Note: $qnum is acutally slot number, if it is not 0.
 
         $reordercheckbox = '';
@@ -499,7 +505,7 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                         '</span><div class="pagecontent">';
                 $pageopen = true;
             }
-            if ($qnum == 0  && $count < $questiontotalcount) {
+            if ($qnum == 0) {
                 // This is the second successive page break. Tell the user the page is empty.
                 echo '<div class="pagestatus">';
                 print_string('noquestionsonpage', 'quiz');
@@ -564,9 +570,6 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                 if ($count != 0) {
                     if (!$hasattempts) {
                         $upbuttonclass = '';
-                        if ($count >= $lastindex - 1) {
-                            $upbuttonclass = 'upwithoutdown';
-                        }
                         echo $OUTPUT->action_icon($pageurl->out(true,
                                 array('up' => $question->slot, 'sesskey' => sesskey())),
                                 new pix_icon('t/up', $strmoveup),
@@ -576,15 +579,13 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                     }
 
                 }
-                if ($count < $questiontotalcount - 2) {
-                    if (!$hasattempts) {
-                        echo $OUTPUT->action_icon($pageurl->out(true,
-                                array('down' => $question->slot, 'sesskey' => sesskey())),
-                                new pix_icon('t/down', $strmovedown),
-                                new component_action('click',
-                                        'M.core_scroll_manager.save_scroll_action'),
-                                array('title' => $strmovedown));
-                    }
+                if (!$hasattempts) {
+                    echo $OUTPUT->action_icon($pageurl->out(true,
+                            array('down' => $question->slot, 'sesskey' => sesskey())),
+                            new pix_icon('t/down', $strmovedown),
+                            new component_action('click',
+                                    'M.core_scroll_manager.save_scroll_action'),
+                            array('title' => $strmovedown));
                 }
                 if ($allowdelete && ($question->qtype == 'missingtype' ||
                         question_has_capability_on($question, 'use', $question->category))) {
@@ -687,11 +688,11 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                 }
                 echo "</div></div>";
 
-                if (!$reordertool && !$quiz->shufflequestions) {
+                if (!$reordertool && !$quiz->shufflequestions && $count < $questiontotalcount - 1) {
                     echo $OUTPUT->container_start('addpage');
                     $url = new moodle_url($pageurl->out_omit_querystring(),
                             array('cmid' => $quiz->cmid, 'courseid' => $quiz->course,
-                                    'addpage' => $pagecount, 'sesskey' => sesskey()));
+                                    'addpage' => $lastquestion->slot, 'sesskey' => sesskey()));
                     echo $OUTPUT->single_button($url, get_string('addpagehere', 'quiz'), 'post',
                             array('disabled' => $hasattempts,
                             'actions' => array(new component_action('click',
@@ -703,6 +704,10 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
             }
         }
 
+        if ($qnum != 0) {
+            $lastquestion = $question;
+        }
+
     }
     if ($reordertool) {
         echo $reordercontrolsbottom;
index 4cc17c5..f6090c0 100644 (file)
@@ -820,15 +820,10 @@ function quiz_refresh_events($courseid = 0) {
  */
 function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
         $courseid, $cmid, $userid = 0, $groupid = 0) {
-    global $CFG, $COURSE, $USER, $DB;
+    global $CFG, $USER, $DB;
     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 
-    if ($COURSE->id == $courseid) {
-        $course = $COURSE;
-    } else {
-        $course = $DB->get_record('course', array('id' => $courseid));
-    }
-
+    $course = get_course($courseid);
     $modinfo = get_fast_modinfo($course);
 
     $cm = $modinfo->cms[$cmid];
@@ -875,11 +870,6 @@ function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
     $grader          = has_capability('mod/quiz:viewreports', $context);
     $groupmode       = groups_get_activity_groupmode($cm, $course);
 
-    if (is_null($modinfo->groups)) {
-        // Load all my groups and cache it in modinfo.
-        $modinfo->groups = groups_get_user_groups($course->id);
-    }
-
     $usersgroups = null;
     $aname = format_string($cm->name, true);
     foreach ($attempts as $attempt) {
@@ -890,16 +880,10 @@ function quiz_get_recent_mod_activity(&$activities, &$index, $timestart,
             }
 
             if ($groupmode == SEPARATEGROUPS and !$accessallgroups) {
-                if (is_null($usersgroups)) {
-                    $usersgroups = groups_get_all_groups($course->id,
-                            $attempt->userid, $cm->groupingid);
-                    if (is_array($usersgroups)) {
-                        $usersgroups = array_keys($usersgroups);
-                    } else {
-                        $usersgroups = array();
-                    }
-                }
-                if (!array_intersect($usersgroups, $modinfo->groups[$cm->id])) {
+                $usersgroups = groups_get_all_groups($course->id,
+                        $attempt->userid, $cm->groupingid);
+                $usersgroups = array_keys($usersgroups);
+                if (!array_intersect($usersgroups, $modinfo->get_groups($cm->groupingid))) {
                     continue;
                 }
             }
index 9ce5d1d..da262c2 100644 (file)
@@ -30,14 +30,23 @@ if (isset($userdata->status)) {
 if (!isset($currentorg)) {
     $currentorg = '';
 }
+
+// If SCORM 1.2 standard mode is disabled allow higher datamodel limits.
+if (intval(get_config("scorm", "scorm12standard"))) {
+    $cmistring256 = '^[\\u0000-\\uFFFF]{0,255}$';
+    $cmistring4096 = '^[\\u0000-\\uFFFF]{0,4096}$';
+} else {
+    $cmistring256 = '^[\\u0000-\\uFFFF]{0,64000}$';
+    $cmistring4096 = $cmistring256;
+}
 ?>
 //
 // SCORM 1.2 API Implementation
 //
 function SCORMapi1_2() {
     // Standard Data Type Definition
-    CMIString256 = '^[\\u0000-\\uffff]{0,255}$';
-    CMIString4096 = '^[\\u0000-\\uffff]{0,4096}$';
+    CMIString256 = '<?php echo $cmistring256 ?>';
+    CMIString4096 = '<?php echo $cmistring4096 ?>';
     CMITime = '^([0-2]{1}[0-9]{1}):([0-5]{1}[0-9]{1}):([0-5]{1}[0-9]{1})(\.[0-9]{1,2})?$';
     CMITimespan = '^([0-9]{2,4}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,2})?$';
     CMIInteger = '^\\d+$';
index a230a6b..ca6b997 100644 (file)
@@ -314,6 +314,9 @@ $string['results'] = 'Results';
 $string['review'] = 'Review';
 $string['reviewmode'] = 'Review mode';
 $string['rightanswer'] = 'Right answer';
+$string['scorm12standard'] = 'Enable SCORM 1.2 standard mode';
+$string['scorm12standarddesc'] = 'Disabling this setting allows Moodle to store more data than the SCORM 1.2 specification allows.
+If your SCORM packages allow users to enter large amounts of text or if your package tries to store large amounts of data in the suspend_data field disable this.';
 $string['scoes'] = 'Learning objects';
 $string['score'] = 'Score';
 $string['scorm:addinstance'] = 'Add a new SCORM package';
index 280723a..2f7993c 100644 (file)
@@ -123,6 +123,8 @@ if ($ADMIN->fulltree) {
     //admin level settings.
     $settings->add(new admin_setting_heading('scorm/adminsettings', get_string('adminsettings', 'scorm'), ''));
 
+    $settings->add(new admin_setting_configcheckbox('scorm/scorm12standard', get_string('scorm12standard', 'scorm'), get_string('scorm12standarddesc', 'scorm'), 1));
+
     $settings->add(new admin_setting_configcheckbox('scorm/allowtypeexternal', get_string('allowtypeexternal', 'scorm'), '', 0));
 
     $settings->add(new admin_setting_configcheckbox('scorm/allowtypelocalsync', get_string('allowtypelocalsync', 'scorm'), '', 0));
index 592bdd3..2b21413 100644 (file)
@@ -443,7 +443,7 @@ function wiki_pluginfile($course, $cm, $context, $filearea, $args, $forcedownloa
     }
 }
 
-function wiki_search_form($cm, $search = '') {
+function wiki_search_form($cm, $search = '', $subwiki = null) {
     global $CFG, $OUTPUT;
 
     $output = '<div class="wikisearch">';
@@ -454,6 +454,9 @@ function wiki_search_form($cm, $search = '') {
     $output .= '<input id="searchwiki" name="searchstring" type="text" size="18" value="' . s($search, true) . '" alt="search" />';
     $output .= '<input name="courseid" type="hidden" value="' . $cm->course . '" />';
     $output .= '<input name="cmid" type="hidden" value="' . $cm->id . '" />';
+    if (!empty($subwiki->id)) {
+        $output .= '<input name="subwikiid" type="hidden" value="' . $subwiki->id . '" />';
+    }
     $output .= '<input name="searchwikicontent" type="hidden" value="1" />';
     $output .= '<input value="' . get_string('searchwikis', 'wiki') . '" type="submit" />';
     $output .= '</fieldset>';
index 30d4c69..51d09f6 100644 (file)
@@ -110,7 +110,10 @@ abstract class page_wiki {
         $PAGE->set_cm($cm);
         $PAGE->set_activity_record($wiki);
         // the search box
-        $PAGE->set_button(wiki_search_form($cm));
+        if (!empty($subwiki->id)) {
+            $search = optional_param('searchstring', null, PARAM_ALPHANUMEXT);
+            $PAGE->set_button(wiki_search_form($cm, $search, $subwiki));
+        }
     }
 
     /**
@@ -828,6 +831,17 @@ class page_wiki_search extends page_wiki {
         global $PAGE, $CFG;
         $PAGE->set_url($CFG->wwwroot . '/mod/wiki/search.php');
     }
+
+    function print_header() {
+        global $PAGE;
+
+        parent::print_header();
+
+        $wiki = $PAGE->activityrecord;
+        $page = (object)array('title' => $wiki->firstpagetitle);
+        $this->wikioutput->wiki_print_subwiki_selector($wiki, $this->subwiki, $page, 'search');
+    }
+
     function print_content() {
         global $PAGE;
 
index bab331f..560c7c7 100644 (file)
@@ -302,17 +302,27 @@ class mod_wiki_renderer extends plugin_renderer_base {
     public function wiki_print_subwiki_selector($wiki, $subwiki, $page, $pagetype = 'view') {
         global $CFG, $USER;
         require_once($CFG->dirroot . '/user/lib.php');
+        $cm = get_coursemodule_from_instance('wiki', $wiki->id);
+
         switch ($pagetype) {
         case 'files':
-            $baseurl = new moodle_url('/mod/wiki/files.php');
+            $baseurl = new moodle_url('/mod/wiki/files.php',
+                    array('wid' => $wiki->id, 'title' => $page->title, 'pageid' => $page->id));
+            break;
+        case 'search':
+            $search = optional_param('searchstring', null, PARAM_ALPHANUMEXT);
+            $searchcontent = optional_param('searchwikicontent', 0, PARAM_INT);
+            $baseurl = new moodle_url('/mod/wiki/search.php',
+                    array('cmid' => $cm->id, 'courseid' => $cm->course,
+                        'searchstring' => $search, 'searchwikicontent' => $searchcontent));
             break;
         case 'view':
         default:
-            $baseurl = new moodle_url('/mod/wiki/view.php');
+            $baseurl = new moodle_url('/mod/wiki/view.php',
+                    array('wid' => $wiki->id, 'title' => $page->title));
             break;
         }
 
-        $cm = get_coursemodule_from_instance('wiki', $wiki->id);
         $context = context_module::instance($cm->id);
         // @TODO: A plenty of duplicated code below this lines.
         // Create private functions.
@@ -337,11 +347,6 @@ class mod_wiki_renderer extends plugin_renderer_base {
                     }
 
                     echo $this->output->container_start('wiki_right');
-                    $params = array('wid' => $wiki->id, 'title' => $page->title);
-                    if ($pagetype == 'files') {
-                        $params['pageid'] = $page->id;
-                    }
-                    $baseurl->params($params);
                     $name = 'uid';
                     $selected = $subwiki->userid;
                     echo $this->output->single_select($baseurl, $name, $options, $selected, null);
@@ -356,12 +361,6 @@ class mod_wiki_renderer extends plugin_renderer_base {
             if ($wiki->wikimode == 'collaborative') {
                 // We need to print a select to choose a course group
 
-                $params = array('wid'=>$wiki->id, 'title'=>$page->title);
-                if ($pagetype == 'files') {
-                    $params['pageid'] = $page->id;
-                }
-                $baseurl->params($params);
-
                 echo $this->output->container_start('wiki_right');
                 groups_print_activity_menu($cm, $baseurl);
                 echo $this->output->container_end();
@@ -397,11 +396,6 @@ class mod_wiki_renderer extends plugin_renderer_base {
                     }
                 }
                 echo $this->output->container_start('wiki_right');
-                $params = array('wid' => $wiki->id, 'title' => $page->title);
-                if ($pagetype == 'files') {
-                    $params['pageid'] = $page->id;
-                }
-                $baseurl->params($params);
                 $name = 'groupanduser';
                 $selected = $subwiki->groupid . '-' . $subwiki->userid;
                 echo $this->output->single_select($baseurl, $name, $options, $selected, null);
@@ -417,11 +411,6 @@ class mod_wiki_renderer extends plugin_renderer_base {
             if ($wiki->wikimode == 'collaborative') {
                 // We need to print a select to choose a course group
                 // moodle_url will take care of encoding for us
-                $params = array('wid'=>$wiki->id, 'title'=>$page->title);
-                if ($pagetype == 'files') {
-                    $params['pageid'] = $page->id;
-                }
-                $baseurl->params($params);
 
                 echo $this->output->container_start('wiki_right');
                 groups_print_activity_menu($cm, $baseurl);
@@ -444,11 +433,6 @@ class mod_wiki_renderer extends plugin_renderer_base {
                 }
 
                 echo $this->output->container_start('wiki_right');
-                $params = array('wid' => $wiki->id, 'title' => $page->title);
-                if ($pagetype == 'files') {
-                    $params['pageid'] = $page->id;
-                }
-                $baseurl->params($params);
                 $name = 'groupanduser';
                 $selected = $subwiki->groupid . '-' . $subwiki->userid;
                 echo $this->output->single_select($baseurl, $name, $options, $selected, null);
index 76d2c7c..ef1f6c8 100644 (file)
@@ -30,6 +30,8 @@ $search = optional_param('searchstring', null, PARAM_ALPHANUMEXT);
 $courseid = optional_param('courseid', 0, PARAM_INT);
 $searchcontent = optional_param('searchwikicontent', 0, PARAM_INT);
 $cmid = optional_param('cmid', 0, PARAM_INT);
+$subwikiid = optional_param('subwikiid', 0, PARAM_INT);
+$userid = optional_param('uid', 0, PARAM_INT);
 
 if (!$course = $DB->get_record('course', array('id' => $courseid))) {
     print_error('invalidcourseid');
@@ -40,19 +42,37 @@ if (!$cm = get_coursemodule_from_id('wiki', $cmid)) {
 
 require_login($course, true, $cm);
 
-// @TODO: Fix call to wiki_get_subwiki_by_group
-if (!$gid = groups_get_activity_group($cm)) {
-    $gid = 0;
-}
-if (!$subwiki = wiki_get_subwiki_by_group($cm->instance, $gid)) {
-    print_error('incorrectsubwikiid', 'wiki');
-}
-if (!$wiki = wiki_get_wiki($subwiki->wikiid)) {
+// Checking wiki instance
+if (!$wiki = wiki_get_wiki($cm->instance)) {
     print_error('incorrectwikiid', 'wiki');
 }
 
-if (!wiki_user_can_view($subwiki, $wiki)) {
-    print_error('cannotviewfiles', 'wiki');
+if ($subwikiid) {
+    // Subwiki id is specified.
+    $subwiki = wiki_get_subwiki($subwikiid);
+    if (!$subwiki || $subwiki->wikiid != $wiki->id) {
+        print_error('incorrectsubwikiid', 'wiki');
+    }
+} else {
+    // Getting current group id
+    $gid = groups_get_activity_group($cm);
+
+    // Getting current user id
+    if ($wiki->wikimode == 'individual') {
+        $userid = $userid ? $userid : $USER->id;
+    } else {
+        $userid = 0;
+    }
+    if (!$subwiki = wiki_get_subwiki_by_group($cm->instance, $gid, $userid)) {
+        // Subwiki does not exist yet, redirect to the view page (which will redirect to create page if allowed).
+        $params = array('wid' => $wiki->id, 'group' => $gid, 'uid' => $userid, 'title' => $wiki->firstpagetitle);
+        $url = new moodle_url('/mod/wiki/view.php', $params);
+        redirect($url);
+    }
+}
+
+if ($subwiki && !wiki_user_can_view($subwiki, $wiki)) {
+    print_error('cannotviewpage', 'wiki');
 }
 
 $wikipage = new page_wiki_search($wiki, $subwiki, $cm);
diff --git a/mod/wiki/tests/behat/wiki_search.feature b/mod/wiki/tests/behat/wiki_search.feature
new file mode 100644 (file)
index 0000000..ee0a687
--- /dev/null
@@ -0,0 +1,186 @@
+@mod @mod_wiki
+Feature: Users can search wikis
+  In order to find information in wiki
+  As a user
+  I need to be able to search individual and collaborative wikis
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | student1 | Student | 1 | student1@asd.com |
+      | student2 | Student | 2 | student2@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+
+  @javascript
+  Scenario: Searching collaborative wiki
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Wiki" to section "1" and I fill the form with:
+      | Wiki name | Collaborative wiki name |
+      | Description | Collaborative wiki description |
+      | First page name | Collaborative index |
+      | Wiki mode | Collaborative wiki |
+    And I follow "Collaborative wiki name"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | Collaborative teacher1 page [[new page]] |
+    And I press "Save"
+    And I follow "Course 1"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Collaborative wiki name"
+    And I follow "new page"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | New page created by student1 |
+    And I press "Save"
+    When I set the field "searchstring" to "page"
+    And I press "Search wikis"
+    Then I should see "New page created by student1"
+    And I should see "Collaborative teacher1 page"
+    And I set the field "searchstring" to "teacher1"
+    And I press "Search wikis"
+    And I should not see "New page created by student1"
+    And I should see "Collaborative teacher1 page"
+    And I log out
+
+
+  @javascript
+  Scenario: Searching individual wiki
+    Given I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Wiki" to section "1" and I fill the form with:
+      | Wiki name | Individual wiki name |
+      | Description | Individual wiki description |
+      | First page name | Individual index |
+      | Wiki mode | Individual wiki |
+    And I follow "Individual wiki name"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | Individual teacher1 page |
+    And I press "Save"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Individual wiki name"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | Individual student1 page |
+    And I press "Save"
+    When I set the field "searchstring" to "page"
+    And I press "Search wikis"
+    Then I should see "Individual student1 page"
+    And I should not see "Individual teacher1 page"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Individual wiki name"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | Individual student2 page |
+    And I press "Save"
+    And I set the field "searchstring" to "page"
+    And I press "Search wikis"
+    And I should see "Individual student2 page"
+    And I should not see "Individual student1 page"
+    And I should not see "Individual teacher1 page"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Individual wiki name"
+    And I set the field "searchstring" to "page"
+    And I press "Search wikis"
+    And I should see "Individual teacher1 page"
+    And I should not see "Individual student1 page"
+    And I should not see "Individual student2 page"
+    And I set the field "uid" to "Student 1"
+    And I should not see "Individual teacher1 page"
+    And I should see "Individual student1 page"
+    And I should not see "Individual student2 page"
+    And I set the field "uid" to "Student 2"
+    And I should not see "Individual teacher1 page"
+    And I should not see "Individual student1 page"
+    And I should see "Individual student2 page"
+    And I log out
+
+  @javascript
+  Scenario: Searching group wiki
+    Given the following "groups" exist:
+      | name | course | idnumber |
+      | Group1 | C1 | G1 |
+      | Group2 | C1 | G2 |
+    And the following "group members" exist:
+      | user | group |
+      | student1 | G1 |
+      | student2 | G2 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Wiki" to section "1" and I fill the form with:
+      | Wiki name | Group wiki name |
+      | Description | Wiki description |
+      | First page name | Groups index |
+      | Wiki mode | Collaborative wiki |
+      | Group mode | Separate groups |
+    And I follow "Group wiki name"
+    And I set the field "Group" to "All participants"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | All participants teacher1 page |
+    And I press "Save"
+    And I set the field "group" to "Group1"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | Group1 teacher1 page [[new page1]] |
+    And I press "Save"
+    And I set the field "group" to "Group2"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | Group2 teacher1 page [[new page2]] |
+    And I press "Save"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Group wiki name"
+    And I follow "new page1"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | Group1 student1 new page |
+    And I press "Save"
+    When I set the field "searchstring" to "page"
+    And I press "Search wikis"
+    Then I should see "Group1 teacher1 page"
+    And I should not see "Group2 teacher1 page"
+    And I should see "Group1 student1 new page"
+    And I should not see "All participants teacher1 page"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Group wiki name"
+    And I follow "new page2"
+    And I press "Create page"
+    And I set the following fields to these values:
+      | HTML format | Group2 student2 new page |
+    And I press "Save"
+    And I set the field "searchstring" to "page"
+    And I press "Search wikis"
+    And I should not see "Group1 teacher1 page"
+    And I should see "Group2 teacher1 page"
+    And I should not see "Group1 student1 new page"
+    And I should see "Group2 student2 new page"
+    And I should not see "All participants teacher1 page"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Group wiki name"
index e849995..616bc4e 100644 (file)
@@ -379,7 +379,8 @@ function workshop_print_recent_activity($course, $viewfullnames, $timestart) {
              WHERE cm.course = ?
                    AND md.name = 'workshop'
                    AND s.example = 0
-                   AND (s.timemodified > ? OR a.timemodified > ?)";
+                   AND (s.timemodified > ? OR a.timemodified > ?)
+          ORDER BY s.timemodified";
 
     $rs = $DB->get_recordset_sql($sql, array($course->id, $timestart, $timestart));
 
@@ -400,16 +401,14 @@ function workshop_print_recent_activity($course, $viewfullnames, $timestart) {
             continue;
         }
 
-        if ($viewfullnames) {
-            // remember all user names we can use later
-            if (empty($users[$activity->authorid])) {
-                $u = new stdclass();
-                $users[$activity->authorid] = username_load_fields_from_object($u, $activity, 'author');
-            }
-            if ($activity->reviewerid and empty($users[$activity->reviewerid])) {
-                $u = new stdclass();
-                $users[$activity->reviewerid] = username_load_fields_from_object($u, $activity, 'reviewer');
-            }
+        // remember all user names we can use later
+        if (empty($users[$activity->authorid])) {
+            $u = new stdclass();
+            $users[$activity->authorid] = username_load_fields_from_object($u, $activity, 'author');
+        }
+        if ($activity->reviewerid and empty($users[$activity->reviewerid])) {
+            $u = new stdclass();
+            $users[$activity->reviewerid] = username_load_fields_from_object($u, $activity, 'reviewer');
         }
 
         $context = context_module::instance($cm->id);
@@ -421,7 +420,7 @@ function workshop_print_recent_activity($course, $viewfullnames, $timestart) {
             $s->authorid = $activity->authorid;
             $s->timemodified = $activity->submissionmodified;
             $s->cmid = $activity->cmid;
-            if (has_capability('mod/workshop:viewauthornames', $context)) {
+            if ($activity->authorid == $USER->id || has_capability('mod/workshop:viewauthornames', $context)) {
                 $s->authornamevisible = true;
             } else {
                 $s->authornamevisible = false;
@@ -442,18 +441,14 @@ function workshop_print_recent_activity($course, $viewfullnames, $timestart) {
                             break;
                         }
 
-                        if (is_null($modinfo->groups)) {
-                            $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
-                        }
-
                         // this might be slow - show only submissions by users who share group with me in this cm
-                        if (empty($modinfo->groups[$cm->id])) {
+                        if (!$modinfo->get_groups($cm->groupingid)) {
                             break;
                         }
                         $authorsgroups = groups_get_all_groups($course->id, $s->authorid, $cm->groupingid);
                         if (is_array($authorsgroups)) {
                             $authorsgroups = array_keys($authorsgroups);
-                            $intersect = array_intersect($authorsgroups, $modinfo->groups[$cm->id]);
+                            $intersect = array_intersect($authorsgroups, $modinfo->get_groups($cm->groupingid));
                             if (empty($intersect)) {
                                 break;
                             } else {
@@ -478,7 +473,7 @@ function workshop_print_recent_activity($course, $viewfullnames, $timestart) {
             $a->reviewerid = $activity->reviewerid;
             $a->timemodified = $activity->assessmentmodified;
             $a->cmid = $activity->cmid;
-            if (has_capability('mod/workshop:viewreviewernames', $context)) {
+            if ($activity->reviewerid == $USER->id || has_capability('mod/workshop:viewreviewernames', $context)) {
                 $a->reviewernamevisible = true;
             } else {
                 $a->reviewernamevisible = false;
@@ -499,18 +494,14 @@ function workshop_print_recent_activity($course, $viewfullnames, $timestart) {
                             break;
                         }
 
-                        if (is_null($modinfo->groups)) {
-                            $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
-                        }
-
                         // this might be slow - show only submissions by users who share group with me in this cm
-                        if (empty($modinfo->groups[$cm->id])) {
+                        if (!$modinfo->get_groups($cm->groupingid)) {
                             break;
                         }
                         $reviewersgroups = groups_get_all_groups($course->id, $a->reviewerid, $cm->groupingid);
                         if (is_array($reviewersgroups)) {
                             $reviewersgroups = array_keys($reviewersgroups);
-                            $intersect = array_intersect($reviewersgroups, $modinfo->groups[$cm->id]);
+                            $intersect = array_intersect($reviewersgroups, $modinfo->get_groups($cm->groupingid));
                             if (empty($intersect)) {
                                 break;
                             } else {
@@ -537,7 +528,7 @@ function workshop_print_recent_activity($course, $viewfullnames, $timestart) {
         echo $OUTPUT->heading(get_string('recentsubmissions', 'workshop'), 3);
         foreach ($submissions as $id => $submission) {
             $link = new moodle_url('/mod/workshop/submission.php', array('id'=>$id, 'cmid'=>$submission->cmid));
-            if ($viewfullnames and $submission->authornamevisible) {
+            if ($submission->authornamevisible) {
                 $author = $users[$submission->authorid];
             } else {
                 $author = null;
@@ -549,9 +540,10 @@ function workshop_print_recent_activity($course, $viewfullnames, $timestart) {
     if (!empty($assessments)) {
         $shown = true;
         echo $OUTPUT->heading(get_string('recentassessments', 'workshop'), 3);
+        core_collator::asort_objects_by_property($assessments, 'timemodified');
         foreach ($assessments as $id => $assessment) {
             $link = new moodle_url('/mod/workshop/assessment.php', array('asid' => $id));
-            if ($viewfullnames and $assessment->reviewernamevisible) {
+            if ($assessment->reviewernamevisible) {
                 $reviewer = $users[$assessment->reviewerid];
             } else {
                 $reviewer = null;
@@ -642,34 +634,27 @@ function workshop_get_recent_mod_activity(&$activities, &$index, $timestart, $co
     $context         = context_module::instance($cm->id);
     $grader          = has_capability('moodle/grade:viewall', $context);
     $accessallgroups = has_capability('moodle/site:accessallgroups', $context);
-    $viewfullnames   = has_capability('moodle/site:viewfullnames', $context);
     $viewauthors     = has_capability('mod/workshop:viewauthornames', $context);
     $viewreviewers   = has_capability('mod/workshop:viewreviewernames', $context);
 
-    if (is_null($modinfo->groups)) {
-        $modinfo->groups = groups_get_user_groups($course->id); // load all my groups and cache it in modinfo
-    }
-
     $submissions = array(); // recent submissions indexed by submission id
     $assessments = array(); // recent assessments indexed by assessment id
     $users       = array();
 
     foreach ($rs as $activity) {
 
-        if ($viewfullnames) {
-            // remember all user names we can use later
-            if (empty($users[$activity->authorid])) {
-                $u = new stdclass();
-                $additionalfields = explode(',', user_picture::fields());
-                $u = username_load_fields_from_object($u, $activity, 'author', $additionalfields);
-                $users[$activity->authorid] = $u;
-            }
-            if ($activity->reviewerid and empty($users[$activity->reviewerid])) {
-                $u = new stdclass();
-                $additionalfields = explode(',', user_picture::fields());
-                $u = username_load_fields_from_object($u, $activity, 'reviewer', $additionalfields);
-                $users[$activity->reviewerid] = $u;
-            }
+        // remember all user names we can use later
+        if (empty($users[$activity->authorid])) {
+            $u = new stdclass();
+            $additionalfields = explode(',', user_picture::fields());
+            $u = username_load_fields_from_object($u, $activity, 'author', $additionalfields);
+            $users[$activity->authorid] = $u;
+        }
+        if ($activity->reviewerid and empty($users[$activity->reviewerid])) {
+            $u = new stdclass();
+            $additionalfields = explode(',', user_picture::fields());
+            $u = username_load_fields_from_object($u, $activity, 'reviewer', $additionalfields);
+            $users[$activity->reviewerid] = $u;
         }
 
         if ($activity->submissionmodified > $timestart and empty($submissions[$activity->submissionid])) {
@@ -678,7 +663,7 @@ function workshop_get_recent_mod_activity(&$activities, &$index, $timestart, $co
             $s->title = $activity->submissiontitle;
             $s->authorid = $activity->authorid;
             $s->timemodified = $activity->submissionmodified;
-            if (has_capability('mod/workshop:viewauthornames', $context)) {
+            if ($activity->authorid == $USER->id || has_capability('mod/workshop:viewauthornames', $context)) {
                 $s->authornamevisible = true;
             } else {
                 $s->authornamevisible = false;
@@ -700,13 +685,13 @@ function workshop_get_recent_mod_activity(&$activities, &$index, $timestart, $co
                         }
 
                         // this might be slow - show only submissions by users who share group with me in this cm
-                        if (empty($modinfo->groups[$cm->id])) {
+                        if (!$modinfo->get_groups($cm->groupingid)) {
                             break;
                         }
                         $authorsgroups = groups_get_all_groups($course->id, $s->authorid, $cm->groupingid);
                         if (is_array($authorsgroups)) {
                             $authorsgroups = array_keys($authorsgroups);
-                            $intersect = array_intersect($authorsgroups, $modinfo->groups[$cm->id]);
+                            $intersect = array_intersect($authorsgroups, $modinfo->get_groups($cm->groupingid));
                             if (empty($intersect)) {
                                 break;
                             } else {
@@ -731,7 +716,7 @@ function workshop_get_recent_mod_activity(&$activities, &$index, $timestart, $co
             $a->submissiontitle = $activity->submissiontitle;
             $a->reviewerid = $activity->reviewerid;
             $a->timemodified = $activity->assessmentmodified;
-            if (has_capability('mod/workshop:viewreviewernames', $context)) {
+            if ($activity->reviewerid == $USER->id || has_capability('mod/workshop:viewreviewernames', $context)) {
                 $a->reviewernamevisible = true;
             } else {
                 $a->reviewernamevisible = false;
@@ -753,13 +738,13 @@ function workshop_get_recent_mod_activity(&$activities, &$index, $timestart, $co
                         }
 
                         // this might be slow - show only submissions by users who share group with me in this cm
-                        if (empty($modinfo->groups[$cm->id])) {
+                        if (!$modinfo->get_groups($cm->groupingid)) {
                             break;
                         }
                         $reviewersgroups = groups_get_all_groups($course->id, $a->reviewerid, $cm->groupingid);
                         if (is_array($reviewersgroups)) {
                             $reviewersgroups = array_keys($reviewersgroups);
-                            $intersect = array_intersect($reviewersgroups, $modinfo->groups[$cm->id]);
+                            $intersect = array_intersect($reviewersgroups, $modinfo->get_groups($cm->groupingid));
                             if (empty($intersect)) {
                                 break;
                             } else {
index abf0cb5..c889f87 100644 (file)
@@ -40,8 +40,8 @@ if (isguestuser()) {
     print_error('guestsarenotallowed');
 }
 
-$workshop = $DB->get_record('workshop', array('id' => $cm->instance), '*', MUST_EXIST);
-$workshop = new workshop($workshop, $cm, $course);
+$workshoprecord = $DB->get_record('workshop', array('id' => $cm->instance), '*', MUST_EXIST);
+$workshop = new workshop($workshoprecord, $cm, $course);
 
 $PAGE->set_url($workshop->submission_url(), array('cmid' => $cmid, 'id' => $id));
 
@@ -232,7 +232,7 @@ if ($edit) {
         // store the updated values or re-save the new submission (re-saving needed because URLs are now rewritten)
         $DB->update_record('workshop_submissions', $formdata);
         $event = \mod_workshop\event\submission_updated::create($params);
-        $event->add_record_snapshot('workshop', $workshop);
+        $event->add_record_snapshot('workshop', $workshoprecord);
         $event->trigger();
 
         // send submitted content for plagiarism detection
index f2e70dc..07a808a 100644 (file)
@@ -166,6 +166,6 @@ if ($currentpage->userid == 0) {
 
 echo $OUTPUT->header();
 
-echo $OUTPUT->blocks_for_region('content');
+echo $OUTPUT->custom_block_region('content');
 
 echo $OUTPUT->footer();
index 6c49d29..f21e5ac 100644 (file)
@@ -63,6 +63,6 @@ $PAGE->set_subpage($currentpage->id);
 
 echo $OUTPUT->header();
 
-echo $OUTPUT->blocks_for_region('content');
+echo $OUTPUT->custom_block_region('content');
 
 echo $OUTPUT->footer();
index 10e6b7e..7dd474c 100644 (file)
@@ -165,7 +165,7 @@ class report_log_renderable implements renderable {
      * @return array core\log\sql_select_reader object or name.
      */
     public function get_readers($nameonly = false) {
-        if (!isset($this->manager)) {
+        if (!isset($this->logmanager)) {
             $this->logmanager = get_log_manager();
         }
 
index f91e044..cdf25c5 100644 (file)
@@ -129,9 +129,14 @@ class report_log_table_log extends table_sql {
         // Add username who did the action.
         if (!empty($logextra['realuserid'])) {
             $a = new stdClass();
-            $a->realusername = html_writer::link(new moodle_url("/user/view.php?id={$event->userid}&course={$event->courseid}"),
-                    $this->userfullnames[$logextra['realuserid']]);
-            $a->asusername = html_writer::link(new moodle_url("/user/view.php?id={$event->userid}&course={$event->courseid}"),
+            $params = array('id' => $logextra['realuserid']);
+            if ($event->courseid) {
+                $params['course'] = $event->courseid;
+            }
+            $a->realusername = html_writer::link(new moodle_url("/user/view.php", $params),
+                $this->userfullnames[$logextra['realuserid']]);
+            $params['id'] = $event->userid;
+            $a->asusername = html_writer::link(new moodle_url("/user/view.php", $params),
                     $this->userfullnames[$event->userid]);
             $username = get_string('eventloggedas', 'report_log', $a);
         } else if (!empty($event->userid) && !empty($this->userfullnames[$event->userid])) {
@@ -155,8 +160,11 @@ class report_log_table_log extends table_sql {
     public function col_relatedfullnameuser($event) {
         // Add affected user.
         if (!empty($event->relateduserid) && isset($this->userfullnames[$event->relateduserid])) {
-            return html_writer::link(new moodle_url("/user/view.php?id=" . $event->relateduserid . "&course=" .
-                    $event->courseid), $this->userfullnames[$event->relateduserid]);
+            $params = array('id' => $event->relateduserid);
+            if ($event->courseid) {
+                $params['course'] = $event->courseid;
+            }
+            return html_writer::link(new moodle_url("/user/view.php", $params), $this->userfullnames[$event->relateduserid]);
         } else {
             return '-';
         }
@@ -219,9 +227,14 @@ class report_log_table_log extends table_sql {
      */
     public function col_eventname($event) {
         // Event name.
-        $eventname = $event->get_name();
+        if ($this->filterparams->logreader instanceof logstore_legacy\log\store) {
+            // Hack for support of logstore_legacy.
+            $eventname = $event->eventname;
+        } else {
+            $eventname = $event->get_name();
+        }
         if ($url = $event->get_url()) {
-            $eventname = html_writer::link($url, $eventname);
+            $eventname = $this->action_link($url, $eventname, 'action');
         }
         return $eventname;
     }
@@ -261,8 +274,23 @@ class report_log_table_log extends table_sql {
         // Get extra event data for origin and realuserid.
         $logextra = $event->get_logextra();
 
-        $link = new moodle_url("/iplookup/index.php?ip={$logextra['ip']}&user=$event->userid");
-        return html_writer::link($link, $logextra['ip']);
+        $url = new moodle_url("/iplookup/index.php?ip={$logextra['ip']}&user=$event->userid");
+        return $this->action_link($url, $logextra['ip'], 'ip');
+    }
+
+    /**
+     * Method to create a link with popup action.
+     *
+     * @param moodle_url $url The url to open.
+     * @param string $text Anchor text for the link.
+     * @param string $name Name of the popup window.
+     *
+     * @return string html to use.
+     */
+    protected function action_link(moodle_url $url, $text, $name = 'popup') {
+        global $OUTPUT;
+        $link = new action_link($url, $text, new popup_action('click', $url, $name, array('height' => 440, 'width' => 700)));
+        return $OUTPUT->render($link);
     }
 
     /**
@@ -427,11 +455,22 @@ class report_log_table_log extends table_sql {
 
         // Get course shortname and put that in return list.
         if (!empty($courseids)) { // If all logs don't belog to site level then get course info.
-            list($coursesql, $courseparams) = $DB->get_in_or_equal($courseids);
-            $courses = $DB->get_records_sql("SELECT id,shortname FROM {course} WHERE id " . $coursesql, $courseparams);
+            list($coursesql, $courseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+            $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
+            $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
+            $courseparams['contextlevel'] = CONTEXT_COURSE;
+            $sql = "SELECT c.id,c.shortname $ccselect FROM {course} as c
+                   $ccjoin
+                     WHERE c.id " . $coursesql;
+
+            $courses = $DB->get_records_sql($sql, $courseparams);
             foreach ($courses as $courseid => $course) {
                 $url = new moodle_url("/course/view.php", array('id' => $courseid));
-                $this->courseshortnames[$courseid] = html_writer::link($url, format_string($course->shortname));
+                context_helper::preload_from_record($course);
+                $context = context_course::instance($courseid, IGNORE_MISSING);
+                // Method format_string() takes care of missing contexts.
+                $this->courseshortnames[$courseid] = html_writer::link($url, format_string($course->shortname, true,
+                        array('context' => $context)));
             }
         }
     }
index de3c3ef..7035be1 100644 (file)
@@ -148,7 +148,7 @@ $output = $PAGE->get_renderer('report_log');
 
 if (empty($readers)) {
     echo $output->header();
-    echo $output->heading(get_string('noreaderenabled'));
+    echo $output->heading(get_string('nologreaderenabled', 'report_log'));
 } else {
     if (!empty($chooselog)) {
         // Delay creation of table, till called by user with filter.
diff --git a/report/loglive/classes/renderable.php b/report/loglive/classes/renderable.php
new file mode 100644 (file)
index 0000000..06f2eaa
--- /dev/null
@@ -0,0 +1,230 @@
+<?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/>.
+
+/**
+ * Loglive report renderable class.
+ *
+ * @package    report_loglive
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Report loglive renderable class.
+ *
+ * @since      Moodle 2.7
+ * @package    report_loglive
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_loglive_renderable implements renderable {
+
+    /** @const int number of seconds to show logs from, by default. */
+    const CUTOFF = 3600;
+
+    /** @var \core\log\manager log manager */
+    protected $logmanager;
+
+    /** @var string selected log reader pluginname */
+    public $selectedlogreader = null;
+
+    /** @var int page number */
+    public $page;
+
+    /** @var int perpage records to show */
+    public $perpage;
+
+    /** @var stdClass course record */
+    public $course;
+
+    /** @var moodle_url url of report page */
+    public $url;
+
+    /** @var int selected date from which records should be displayed */
+    public $date;
+
+    /** @var string order to sort */
+    public $order;
+
+    /** @var int group id */
+    public $groupid;
+
+    /** @var report_loglive_table_log table log which will be used for rendering logs */
+    public $tablelog;
+
+    /** @var  int refresh rate in seconds */
+    protected $refresh  = 60;
+
+    /**
+     * Constructor.
+     *
+     * @param string $logreader (optional)reader pluginname from which logs will be fetched.
+     * @param stdClass|int $course (optional) course record or id
+     * @param moodle_url|string $url (optional) page url.
+     * @param int $date date (optional) from which records will be fetched.
+     * @param int $page (optional) page number.
+     * @param int $perpage (optional) number of records to show per page.
+     * @param string $order (optional) sortorder of fetched records
+     */
+    public function __construct($logreader = "", $course = 0, $url = "", $date = 0, $page = 0, $perpage = 100,
+                                $order = "timecreated DESC") {
+
+        global $PAGE;
+
+        // Use first reader as selected reader, if not passed.
+        if (empty($logreader)) {
+            $readers = $this->get_readers();
+            if (!empty($readers)) {
+                reset($readers);
+                $logreader = key($readers);
+            } else {
+                $logreader = null;
+            }
+        }
+        $this->selectedlogreader = $logreader;
+
+        // Use page url if empty.
+        if (empty($url)) {
+            $url = new moodle_url($PAGE->url);
+        } else {
+            $url = new moodle_url($url);
+        }
+        $this->url = $url;
+
+        // Use site course id, if course is empty.
+        if (!empty($course) && is_int($course)) {
+            $course = get_course($course);
+        }
+        $this->course = $course;
+
+        if ($date == 0 ) {
+            $date = time() - self::CUTOFF;
+        }
+        $this->date = $date;
+
+        $this->page = $page;
+        $this->perpage = $perpage;
+        $this->order = $order;
+        $this->set_refresh_rate();
+    }
+
+    /**
+     * Get a list of enabled sql_select_reader objects/name
+     *
+     * @param bool $nameonly if true only reader names will be returned.
+     *
+     * @return array core\log\sql_select_reader object or name.
+     */
+    public function get_readers($nameonly = false) {
+        if (!isset($this->logmanager)) {
+            $this->logmanager = get_log_manager();
+        }
+
+        $readers = $this->logmanager->get_readers('core\log\sql_select_reader');
+        if ($nameonly) {
+            foreach ($readers as $pluginname => $reader) {
+                $readers[$pluginname] = $reader->get_name();
+            }
+        }
+        return $readers;
+    }
+
+    /**
+     * Setup table log.
+     */
+    protected function setup_table() {
+        $filter = $this->setup_filters();
+        $this->tablelog = new report_loglive_table_log('report_loglive', $filter);
+        $this->tablelog->define_baseurl($this->url);
+    }
+
+    /**
+     * Setup table log for ajax output.
+     */
+    protected function setup_table_ajax() {
+        $filter = $this->setup_filters();
+        $this->tablelog = new report_loglive_table_log_ajax('report_loglive', $filter);
+        $this->tablelog->define_baseurl($this->url);
+    }
+
+    /**
+     * Setup filters
+     *
+     * @return stdClass filters
+     */
+    protected function setup_filters() {
+        $readers = $this->get_readers();
+
+        // Set up filters.
+        $filter = new \stdClass();
+        if (!empty($this->course)) {
+            $filter->courseid = $this->course->id;
+        } else {
+            $filter->courseid = 0;
+        }
+        $filter->logreader = $readers[$this->selectedlogreader];
+        $filter->date = $this->date;
+        $filter->orderby = $this->order;
+        $filter->anonymous = 0;
+
+        return $filter;
+    }
+
+    /**
+     * Set refresh rate of the live updates.
+     */
+    protected function set_refresh_rate() {
+        if (defined('BEHAT_SITE_RUNNING')) {
+            // Hack for behat tests.
+            $this->refresh = 5;
+        } else {
+            if (defined('REPORT_LOGLIVE_REFRESH')) {
+                // Backward compatibility.
+                $this->refresh = REPORT_LOGLIVE_REFERESH;
+            } else {
+                // Default.
+                $this->refresh = 60;
+            }
+        }
+    }
+
+    /**
+     * Get refresh rate of the live updates.
+     */
+    public function get_refresh_rate() {
+        return $this->refresh;
+    }
+
+    /**
+     * Setup table and return it.
+     *
+     * @param bool $ajax If set to true report_loglive_table_log_ajax is set instead of report_loglive_table_log.
+     *
+     * @return report_loglive_table_log|report_loglive_table_log_ajax table object
+     */
+    public function get_table($ajax = false) {
+        if (empty($this->tablelog)) {
+            if ($ajax) {
+                $this->setup_table_ajax();
+            } else {
+                $this->setup_table();
+            }
+        }
+        return $this->tablelog;
+    }
+}
\ No newline at end of file
diff --git a/report/loglive/classes/renderer.php b/report/loglive/classes/renderer.php
new file mode 100644 (file)
index 0000000..a1919e9
--- /dev/null
@@ -0,0 +1,104 @@
+<?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/>.
+
+/**
+ * Loglive report renderer.
+ *
+ * @package    report_loglive
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Report log renderer's for printing reports.
+ *
+ * @since      Moodle 2.7
+ * @package    report_loglive
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_loglive_renderer extends plugin_renderer_base {
+
+    /**
+     * Return html to render the loglive page..
+     *
+     * @param report_loglive_renderable $reportloglive object of report_log.
+     *
+     * @return string html used to render the page;
+     */
+    public function render_report_loglive_renderable(report_loglive_renderable $reportloglive) {
+        if (empty($reportloglive->selectedlogreader)) {
+            return $this->output->notification(get_string('nologreaderenabled', 'report_loglive'), 'notifyproblem');
+        }
+
+        $table = $reportloglive->get_table();
+        return $this->render_table($table, $reportloglive->perpage);
+    }
+
+    /**
+     * Prints/return reader selector
+     *
+     * @param report_loglive_renderable $reportloglive log report.
+     *
+     * @return string Returns rendered widget
+     */
+    public function reader_selector(report_loglive_renderable $reportloglive) {
+        $readers = $reportloglive->get_readers(true);
+        if (count($readers) <= 1) {
+            // One or no readers found, no need of this drop down.
+            return '';
+        }
+        $select = new single_select($reportloglive->url, 'logreader', $readers, $reportloglive->selectedlogreader, null);
+        $select->set_label(get_string('selectlogreader', 'report_loglive'));
+        return $this->output->render($select);
+    }
+
+    /**
+     * Prints a button to update/resume live updates.
+     *
+     * @param report_loglive_renderable $reportloglive log report.
+     *
+     * @return string Returns rendered widget
+     */
+    public function toggle_liveupdate_button(report_loglive_renderable $reportloglive) {
+        // Add live log controls.
+        if ($reportloglive->page == 0 && $reportloglive->selectedlogreader) {
+            echo html_writer::tag('button' , get_string('pause', 'report_loglive'), array('id' => 'livelogs-pause-button'));
+            $icon = new pix_icon('i/loading_small', 'loading', 'moodle', array('class' => 'spinner'));
+            return $this->output->render($icon);
+        }
+        return '';
+    }
+
+    /**
+     * Get the html for the table.
+     *
+     * @param report_loglive_table_log $table table object.
+     * @param int $perpage entries to display perpage.
+     *
+     * @return string table html
+     */
+    protected function render_table(report_loglive_table_log $table, $perpage) {
+        $o = '';
+        ob_start();
+        $table->out($perpage, true);
+        $o = ob_get_contents();
+        ob_end_clean();
+
+        return $o;
+    }
+}
\ No newline at end of file
diff --git a/report/loglive/classes/renderer_ajax.php b/report/loglive/classes/renderer_ajax.php
new file mode 100644 (file)
index 0000000..123ef15
--- /dev/null
@@ -0,0 +1,48 @@
+<?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/>.
+/**
+ * Log live report ajax renderer.
+ *
+ * @package    report_loglive
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Log live report ajax renderer.
+ *
+ * @since      Moodle 2.7
+ * @package    report_loglive
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_loglive_renderer_ajax extends plugin_renderer_base {
+
+    /**
+     * Render logs for ajax.
+     *
+     * @param report_loglive_renderable $reportloglive object of report_loglive_renderable.
+     *
+     * @return string html to be displayed to user.
+     */
+    public function render_report_loglive_renderable(report_loglive_renderable $reportloglive) {
+        if (empty($reportloglive->selectedlogreader)) {
+            return null;
+        }
+        $table = $reportloglive->get_table(true);
+        return $table->out($reportloglive->perpage, false);
+    }
+}
diff --git a/report/loglive/classes/table_log.php b/report/loglive/classes/table_log.php
new file mode 100644 (file)
index 0000000..489d8fb
--- /dev/null
@@ -0,0 +1,405 @@
+<?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/>.
+
+/**
+ * Table log for displaying logs.
+ *
+ * @package    report_loglive
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+require_once($CFG->libdir . '/tablelib.php');
+
+/**
+ * Table log class for displaying logs.
+ *
+ * @since      Moodle 2.7
+ * @package    report_loglive
+ * @copyright  2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_loglive_table_log extends table_sql {
+
+    /** @var array list of user fullnames shown in report */
+    protected $userfullnames = array();
+
+    /** @var array list of course short names shown in report */
+    protected $courseshortnames = array();
+
+    /** @var array list of context name shown in report */
+    protected $contextname = array();
+
+    /** @var stdClass filters parameters */
+    protected $filterparams;
+
+    /**
+     * Sets up the table_log parameters.
+     *
+     * @param string $uniqueid unique id of form.
+     * @param stdClass $filterparams (optional) filter params.
+     *     - int courseid: id of course
+     *     - int userid: user id
+     *     - int|string modid: Module id or "site_errors" to view site errors
+     *     - int groupid: Group id
+     *     - \core\log\sql_select_reader logreader: reader from which data will be fetched.
+     *     - int edulevel: educational level.
+     *     - string action: view action
+     *     - int date: Date from which logs to be viewed.
+     */
+    public function __construct($uniqueid, $filterparams = null) {
+        parent::__construct($uniqueid);
+
+        $this->set_attribute('class', 'reportloglive generaltable generalbox');
+        $this->set_attribute('aria-live', 'polite');
+        $this->filterparams = $filterparams;
+        // Add course column if logs are displayed for site.
+        $cols = array();
+        $headers = array();
+        if (empty($filterparams->courseid)) {
+            $cols = array('course');
+            $headers = array(get_string('course'));
+        }
+
+        $this->define_columns(array_merge($cols, array('time', 'fullnameuser', 'relatedfullnameuser', 'context', 'component',
+                'eventname', 'description', 'origin', 'ip')));
+        $this->define_headers(array_merge($headers, array(
+                get_string('time'),
+                get_string('fullnameuser'),
+                get_string('eventrelatedfullnameuser', 'report_loglive'),
+                get_string('eventcontext', 'report_loglive'),
+                get_string('eventcomponent', 'report_loglive'),
+                get_string('eventname'),
+                get_string('description'),
+                get_string('eventorigin', 'report_loglive'),
+                get_string('ip_address')
+                )
+            ));
+        $this->collapsible(false);
+        $this->sortable(false);
+        $this->pageable(true);
+        $this->is_downloadable(false);
+    }
+
+    /**
+     * Generate the course column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the course column.
+     */
+    public function col_course($event) {
+        if (empty($event->courseid) || empty($this->courseshortnames[$event->courseid])) {
+            return '-';
+        } else {
+            return $this->courseshortnames[$event->courseid];
+        }
+    }
+
+    /**
+     * Generate the time column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the time column
+     */
+    public function col_time($event) {
+        $recenttimestr = get_string('strftimerecent', 'core_langconfig');
+        return userdate($event->timecreated, $recenttimestr);
+    }
+
+    /**
+     * Generate the username column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the username column
+     */
+    public function col_fullnameuser($event) {
+        // Get extra event data for origin and realuserid.
+        $logextra = $event->get_logextra();
+
+        // Add username who did the action.
+        if (!empty($logextra['realuserid'])) {
+            $a = new stdClass();
+            $params = array('id' => $logextra['realuserid']);
+            if ($event->courseid) {
+                $params['course'] = $event->courseid;
+            }
+            $a->realusername = html_writer::link(new moodle_url("/user/view.php", $params),
+                $this->userfullnames[$logextra['realuserid']]);
+            $params['id'] = $event->userid;
+            $a->asusername = html_writer::link(new moodle_url("/user/view.php", $params),
+                $this->userfullnames[$event->userid]);
+            $username = get_string('eventloggedas', 'report_loglive', $a);
+        } else if (!empty($event->userid) && !empty($this->userfullnames[$event->userid])) {
+            $params = array('id' => $event->userid);
+            if ($event->courseid) {
+                $params['course'] = $event->courseid;
+            }
+            $username = html_writer::link(new moodle_url("/user/view.php", $params), $this->userfullnames[$event->userid]);
+        } else {
+            $username = '-';
+        }
+        return $username;
+    }
+
+    /**
+     * Generate the related username column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the related username column
+     */
+    public function col_relatedfullnameuser($event) {
+        // Add affected user.
+        if (!empty($event->relateduserid) && isset($this->userfullnames[$event->relateduserid])) {
+            $params = array('id' => $event->relateduserid);
+            if ($event->courseid) {
+                $params['course'] = $event->courseid;
+            }
+            return html_writer::link(new moodle_url("/user/view.php", $params), $this->userfullnames[$event->relateduserid]);
+        } else {
+            return '-';
+        }
+    }
+
+    /**
+     * Generate the context column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the context column
+     */
+    public function col_context($event) {
+        // Add context name.
+        if ($event->contextid) {
+            // If context name was fetched before then return, else get one.
+            if (isset($this->contextname[$event->contextid])) {
+                return $this->contextname[$event->contextid];
+            } else {
+                $context = context::instance_by_id($event->contextid, IGNORE_MISSING);
+                if ($context) {
+                    $contextname = $context->get_context_name(true);
+                    if ($url = $context->get_url()) {
+                        $contextname = html_writer::link($url, $contextname);
+                    }
+                } else {
+                    $contextname = get_string('other');
+                }
+            }
+        } else {
+            $contextname = get_string('other');
+        }
+
+        $this->contextname[$event->contextid] = $contextname;
+        return $contextname;
+    }
+
+    /**
+     * Generate the component column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the component column
+     */
+    public function col_component($event) {
+        // Component.
+        $componentname = $event->component;
+        if (($event->component === 'core') || ($event->component === 'legacy')) {
+            return  get_string('coresystem');
+        } else if (get_string_manager()->string_exists('pluginname', $event->component)) {
+            return get_string('pluginname', $event->component);
+        } else {
+            return $componentname;
+        }
+    }
+
+    /**
+     * Generate the event name column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the event name column
+     */
+    public function col_eventname($event) {
+        // Event name.
+        if ($this->filterparams->logreader instanceof logstore_legacy\log\store) {
+            // Hack for support of logstore_legacy.
+            $eventname = $event->eventname;
+        } else {
+            $eventname = $event->get_name();
+        }
+        if ($url = $event->get_url()) {
+            $eventname = $this->action_link($url, $eventname, 'action');
+        }
+        return $eventname;
+    }
+
+    /**
+     * Generate the description column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the description column
+     */
+    public function col_description($event) {
+        // Description.
+        return $event->get_description();
+    }
+
+    /**
+     * Generate the origin column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the origin column
+     */
+    public function col_origin($event) {
+        // Get extra event data for origin and realuserid.
+        $logextra = $event->get_logextra();
+
+        // Add event origin, normally IP/cron.
+        return $logextra['origin'];
+    }
+
+    /**
+     * Generate the ip column.
+     *
+     * @param stdClass $event event data.
+     * @return string HTML for the ip column
+     */
+    public function col_ip($event) {
+        // Get extra event data for origin and realuserid.
+        $logextra = $event->get_logextra();
+
+        $url = new moodle_url("/iplookup/index.php?ip={$logextra['ip']}&user=$event->userid");
+        return $this->action_link($url, $logextra['ip'], 'ip');
+    }
+
+    /**
+     * Method to create a link with popup action.
+     *
+     * @param moodle_url $url The url to open.
+     * @param string $text Anchor text for the link.
+     * @param string $name Name of the popup window.
+     *
+     * @return string html to use.
+     */
+    protected function action_link(moodle_url $url, $text, $name = 'popup') {
+        global $OUTPUT;
+        $link = new action_link($url, $text, new popup_action('click', $url, $name, array('height' => 440, 'width' => 700)));
+        return $OUTPUT->render($link);
+    }
+
+    /**
+     * Query the reader. Store results in the object for use by build_table.
+     *
+     * @param int $pagesize size of page for paginated displayed table.
+     * @param bool $useinitialsbar do you want to use the initials bar.
+     */
+    public function query_db($pagesize, $useinitialsbar = true) {
+
+        $joins = array();
+        $params = array();
+
+        // Set up filtering.
+        if (!empty($this->filterparams->courseid)) {
+            $joins[] = "courseid = :courseid";
+            $params['courseid'] = $this->filterparams->courseid;
+        }
+
+        if (!empty($this->filterparams->date)) {
+            $joins[] = "timecreated > :date";
+            $params['date'] = $this->filterparams->date;
+        }
+
+        if (!empty($this->filterparams->date)) {
+            $joins[] = "anonymous = :anon";
+            $params['anon'] = $this->filterparams->anonymous;
+        }
+
+        $selector = implode(' AND ', $joins);
+
+        $total = $this->filterparams->logreader->get_events_select_count($selector, $params);
+        $this->pagesize($pagesize, $total);
+        $this->rawdata = $this->filterparams->logreader->get_events_select($selector, $params, $this->filterparams->orderby,
+                $this->get_page_start(), $this->get_page_size());
+
+        // Set initial bars.
+        if ($useinitialsbar) {
+            $this->initialbars($total > $pagesize);
+        }
+
+        // Update list of users and courses list which will be displayed on log page.
+        $this->update_users_and_courses_used();
+    }
+
+    /**
+     * Helper function to create list of course shortname and user fullname shown in log report.
+     * This will update $this->userfullnames and $this->courseshortnames array with userfullname and courseshortname (with link),
+     * which will be used to render logs in table.
+     */
+    public function update_users_and_courses_used() {
+        global $SITE, $DB;
+
+        $this->userfullnames =