Merge branch 'MDL9443_Filter_XHTML_files.2014-11-19' of git://github.com/Dave-B/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 24 Nov 2014 17:26:30 +0000 (17:26 +0000)
committerDan Poltawski <dan@moodle.com>
Mon, 24 Nov 2014 17:26:30 +0000 (17:26 +0000)
149 files changed:
admin/settings/plugins.php
admin/settings/security.php
admin/tool/replace/cli/replace.php [new file with mode: 0644]
admin/tool/replace/lang/en/tool_replace.php
backup/moodle2/backup_root_task.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_root_task.class.php
backup/moodle2/restore_stepslib.php
backup/moodle2/tests/moodle2_test.php
backup/restore.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/backup_general_helper.class.php
backup/util/helper/restore_prechecks_helper.class.php
backup/util/ui/renderer.php
backup/util/ui/restore_ui_components.php
badges/criteria/award_criteria.php
badges/criteria/award_criteria_overall.php
badges/renderer.php
blocks/comments/block_comments.php
calendar/lib.php
calendar/tests/ical_test.php
comment/lib.php
composer.json
course/format/topics/lib.php
course/format/weeks/lib.php
course/lib.php
course/view.php
enrol/ldap/settingslib.php
filter/tex/mimetex.exe [changed mode: 0644->0755]
grade/edit/letter/index.php
grade/report/grader/lib.php
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js
grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js
grade/report/grader/yui/src/gradereporttable/js/floatingheaders.js
grade/report/overview/index.php
grade/report/overview/lib.php
grade/report/overview/settings.php
install/lang/el/admin.php
lang/en/admin.php
lang/en/auth.php
lang/en/backup.php
lang/en/grades.php
lib/behat/classes/util.php
lib/bennu/iCalendar_components.php
lib/bennu/iCalendar_parameters.php
lib/bennu/iCalendar_properties.php
lib/bennu/readme_moodle.txt
lib/classes/event/course_viewed.php
lib/classes/plugin_manager.php
lib/classes/session/manager.php
lib/classes/task/file_temp_cleanup_task.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/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/src/editor/js/selection.js
lib/filelib.php
lib/formslib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputlib.php
lib/outputrenderers.php
lib/phpunit/classes/util.php
lib/testing/classes/util.php
lib/tests/behat/behat_hooks.php
lib/tests/events_test.php
lib/tests/fixtures/google_gmail.ics [new file with mode: 0644]
lib/tests/fixtures/ms_outlook_2010.ics [new file with mode: 0644]
lib/tests/fixtures/osx_yosemite.ics [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/scheduled_task_test.php
lib/tests/session_manager_test.php
lib/upgrade.txt
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js
lib/yui/build/moodle-core-dock/moodle-core-dock.js
lib/yui/src/dock/js/dockeditem.js
login/change_password.php
login/confirm.php
login/index.php
login/lib.php
message/lib.php
mod/assign/locallib.php
mod/assign/styles.css
mod/assign/submission/file/lang/en/assignsubmission_file.php
mod/assign/submission/file/locallib.php
mod/assign/submission/file/settings.php
mod/assign/submissionconfirmform.php
mod/assign/tests/locallib_test.php
mod/book/backup/moodle2/backup_book_stepslib.php
mod/book/db/install.xml
mod/book/db/upgrade.php
mod/book/lang/en/book.php
mod/book/lib.php
mod/book/locallib.php
mod/book/mod_form.php
mod/book/settings.php
mod/book/styles.css
mod/book/version.php
mod/book/view.php
mod/data/lib.php
mod/forum/classes/message/inbound/reply_handler.php
mod/forum/lib.php
mod/forum/subscribe.php
mod/forum/tests/mail_test.php
mod/lesson/tests/behat/lesson_progress_bar.feature [new file with mode: 0644]
mod/lesson/view.php
mod/quiz/mod_form.php
mod/quiz/report/statistics/statisticslib.php
mod/quiz/report/statistics/tests/statisticslib_test.php [new file with mode: 0644]
mod/scorm/module.js
mod/wiki/parser/markups/creole.php
mod/wiki/parser/markups/html.php
mod/wiki/parser/markups/wikimarkup.php
mod/wiki/tests/wikiparser_test.php
question/type/calculated/questiontype.php
report/log/classes/renderable.php
report/log/classes/renderer.php
report/log/index.php
report/log/tests/behat/filter_log.feature
report/usersessions/db/access.php [new file with mode: 0644]
report/usersessions/index.php [new file with mode: 0644]
report/usersessions/lang/en/report_usersessions.php [new file with mode: 0644]
report/usersessions/lib.php [new file with mode: 0644]
report/usersessions/locallib.php [new file with mode: 0644]
report/usersessions/tests/behat/usersessions_report.feature [new file with mode: 0644]
report/usersessions/user.php [new file with mode: 0644]
report/usersessions/version.php [new file with mode: 0644]
theme/base/layout/embedded.php
theme/base/style/core.css
theme/base/style/message.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/dock.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/style/moodle.css
theme/canvas/layout/embedded.php
theme/canvas/style/core.css
theme/clean/lib.php
theme/more/lib.php
theme/upgrade.txt
user/editadvanced.php
user/profile/field/menu/field.class.php
user/tests/externallib_test.php
version.php

index bee9753..7a1551a 100644 (file)
@@ -77,6 +77,10 @@ if ($hassiteconfig) {
     $temp->add(new admin_setting_configcheckbox('loginpageautofocus', new lang_string('loginpageautofocus', 'admin'), new lang_string('loginpageautofocus_help', 'admin'), 0));
     $temp->add(new admin_setting_configselect('guestloginbutton', new lang_string('guestloginbutton', 'auth'),
                                               new lang_string('showguestlogin', 'auth'), '1', array('0'=>new lang_string('hide'), '1'=>new lang_string('show'))));
+    $options = array(0 => get_string('no'), 1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5, 10 => 10, 20 => 20, 50 => 50);
+    $temp->add(new admin_setting_configselect('limitconcurrentlogins',
+        new lang_string('limitconcurrentlogins', 'core_auth'),
+        new lang_string('limitconcurrentlogins_desc', 'core_auth'), 0, $options));
     $temp->add(new admin_setting_configtext('alternateloginurl', new lang_string('alternateloginurl', 'auth'),
                                             new lang_string('alternatelogin', 'auth', htmlspecialchars(get_login_url())), ''));
     $temp->add(new admin_setting_configtext('forgottenpasswordurl', new lang_string('forgottenpasswordurl', 'auth'),
index a75b6e1..b455946 100644 (file)
@@ -86,6 +86,9 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
             1800,
             $pwresetoptions);
     $temp->add($adminsetting);
+    $temp->add(new admin_setting_configcheckbox('passwordchangelogout',
+        new lang_string('passwordchangelogout', 'admin'),
+        new lang_string('passwordchangelogout_desc', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('groupenrolmentkeypolicy', new lang_string('groupenrolmentkeypolicy', 'admin'), new lang_string('groupenrolmentkeypolicy_desc', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('disableuserimages', new lang_string('disableuserimages', 'admin'), new lang_string('configdisableuserimages', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('emailchangeconfirmation', new lang_string('emailchangeconfirmation', 'admin'), new lang_string('configemailchangeconfirmation', 'admin'), 1));
diff --git a/admin/tool/replace/cli/replace.php b/admin/tool/replace/cli/replace.php
new file mode 100644 (file)
index 0000000..1f9e74b
--- /dev/null
@@ -0,0 +1,94 @@
+<?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/>.
+
+/**
+ * Search and replace strings throughout all texts in the whole database.
+ *
+ * @package    tool_replace
+ * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__.'/../../../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+require_once($CFG->libdir.'/adminlib.php');
+
+$help =
+    "Search and replace text throughout the whole database.
+
+Options:
+--search=STRING       String to search for.
+--replace=STRING      String to replace with.
+--shorten             Shorten result if necessary.
+--non-interactive     Perform the replacement without confirming.
+-h, --help            Print out this help.
+
+Example:
+\$ sudo -u www-data /usr/bin/php admin/tool/replace/cli/replace.php --search=//oldsitehost --replace=//newsitehost
+";
+
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'search'  => null,
+        'replace' => null,
+        'shorten' => false,
+        'non-interactive' => false,
+        'help'    => false,
+    ),
+    array(
+        'h' => 'help',
+    )
+);
+
+if ($options['help'] || $options['search'] === null || $options['replace'] === null) {
+    echo $help;
+    exit(0);
+}
+
+if (!$DB->replace_all_text_supported()) {
+    cli_error(get_string('notimplemented', 'tool_replace'));
+}
+
+if (empty($options['shorten']) && core_text::strlen($options['search']) < core_text::strlen($options['replace'])) {
+    cli_error(get_string('cannotfit', 'tool_replace'));
+}
+
+try {
+    $search = validate_param($options['search'], PARAM_RAW);
+    $replace = validate_param($options['replace'], PARAM_RAW);
+} catch (invalid_parameter_exception $e) {
+    cli_error(get_string('invalidcharacter', 'tool_replace'));
+}
+
+if (!$options['non-interactive']) {
+    echo get_string('excludedtables', 'tool_replace') . "\n\n";
+    echo get_string('notsupported', 'tool_replace') . "\n\n";
+    $prompt = get_string('cliyesnoprompt', 'admin');
+    $input = cli_input($prompt, '', array(get_string('clianswerno', 'admin'), get_string('cliansweryes', 'admin')));
+    if ($input == get_string('clianswerno', 'admin')) {
+        exit(1);
+    }
+}
+
+if (!db_replace($search, $replace)) {
+    cli_heading(get_string('error'));
+    exit(1);
+}
+
+cli_heading(get_string('success'));
+exit(0);
index 919ed67..481fd06 100644 (file)
@@ -27,11 +27,12 @@ $string['cannotfit'] = 'The replacement is longer than original and shortening i
 $string['disclaimer'] = 'I understand the risks of this operation';
 $string['doit'] = 'Yes, do it!';
 $string['excludedtables'] = 'Several tables are not updated as part of the text replacement. This include configuration, log, events, and session tables.';
-$string['pageheader'] = 'Search and replace text throughout the whole database';
+$string['invalidcharacter'] = 'Invalid characters were found in the search or replacement text.';
 $string['notifyfinished'] = '...finished';
 $string['notifyrebuilding'] = 'Rebuilding course cache...';
 $string['notimplemented'] = 'Sorry, this feature is not implemented in your database driver.';
-$string['notsupported'] ='This script is not supported, always make complete backup before proceeding!<br />This operation can not be reverted!';
+$string['notsupported'] = 'This script should be considered experimental and the changes it makes can not be reverted. Please make a complete backup for running this script!';
+$string['pageheader'] = 'Search and replace text throughout the whole database';
 $string['pluginname'] = 'DB search and replace';
 $string['replacewith'] = 'Replace with this string';
 $string['replacewithhelp'] = 'usually new server URL';
index 01a9f62..0a2df8a 100644 (file)
@@ -129,11 +129,10 @@ class backup_root_task extends backup_task {
         $activities->add_dependency($badges);
         $users->add_dependency($badges);
 
-        // Define calendar events (dependent of users)
+        // Define calendar events.
         $events = new backup_calendarevents_setting('calendarevents', base_setting::IS_BOOLEAN, true);
         $events->set_ui(new backup_setting_ui_checkbox($events, get_string('rootsettingcalendarevents', 'backup')));
         $this->add_setting($events);
-        $users->add_dependency($events);
 
         // Define completion (dependent of users)
         $completion = new backup_userscompletion_setting('userscompletion', base_setting::IS_BOOLEAN, true);
index 651b8d1..de35cc3 100644 (file)
@@ -503,6 +503,14 @@ class backup_course_structure_step extends backup_structure_step {
  */
 class backup_enrolments_structure_step extends backup_structure_step {
 
+    /**
+     * Skip enrolments on the front page.
+     * @return bool
+     */
+    protected function execute_condition() {
+        return ($this->get_courseid() != SITEID);
+    }
+
     protected function define_structure() {
 
         // To know if we are including users
@@ -921,7 +929,12 @@ class backup_gradebook_structure_step extends backup_structure_step {
      * the module gradeitems have been already included in backup
      */
     protected function execute_condition() {
-        return backup_plan_dbops::require_gradebook_backup($this->get_courseid(), $this->get_backupid());
+        $courseid = $this->get_courseid();
+        if ($courseid == SITEID) {
+            return false;
+        }
+
+        return backup_plan_dbops::require_gradebook_backup($courseid, $this->get_backupid());
     }
 
     protected function define_structure() {
@@ -1035,7 +1048,12 @@ class backup_grade_history_structure_step extends backup_structure_step {
      * because we do not want to save the history of items which are not backed up. At least for now.
      */
     protected function execute_condition() {
-        return backup_plan_dbops::require_gradebook_backup($this->get_courseid(), $this->get_backupid());
+        $courseid = $this->get_courseid();
+        if ($courseid == SITEID) {
+            return false;
+        }
+
+        return backup_plan_dbops::require_gradebook_backup($courseid, $this->get_backupid());
     }
 
     protected function define_structure() {
@@ -1088,6 +1106,14 @@ class backup_grade_history_structure_step extends backup_structure_step {
  */
 class backup_userscompletion_structure_step extends backup_structure_step {
 
+    /**
+     * Skip completion on the front page.
+     * @return bool
+     */
+    protected function execute_condition() {
+        return ($this->get_courseid() != SITEID);
+    }
+
     protected function define_structure() {
 
         // Define each element separated
@@ -1617,6 +1643,7 @@ class backup_main_structure_step extends backup_structure_step {
         $info['original_site_identifier_hash'] = md5(get_site_identifier());
         $info['original_course_id'] = $this->get_courseid();
         $originalcourseinfo = backup_controller_dbops::backup_get_original_course_info($this->get_courseid());
+        $info['original_course_format'] = $originalcourseinfo->format;
         $info['original_course_fullname']  = $originalcourseinfo->fullname;
         $info['original_course_shortname'] = $originalcourseinfo->shortname;
         $info['original_course_startdate'] = $originalcourseinfo->startdate;
@@ -1634,7 +1661,7 @@ class backup_main_structure_step extends backup_structure_step {
         $information = new backup_nested_element('information', null, array(
             'name', 'moodle_version', 'moodle_release', 'backup_version',
             'backup_release', 'backup_date', 'mnet_remoteusers', 'include_files', 'include_file_references_to_external_content', 'original_wwwroot',
-            'original_site_identifier_hash', 'original_course_id',
+            'original_site_identifier_hash', 'original_course_id', 'original_course_format',
             'original_course_fullname', 'original_course_shortname', 'original_course_startdate',
             'original_course_contextid', 'original_system_contextid'));
 
@@ -1742,9 +1769,23 @@ class backup_zip_contents extends backup_execution_step implements file_progress
         // Get the zip packer
         $zippacker = get_file_packer('application/vnd.moodle.backup');
 
+        // Track overall progress for the 2 long-running steps (archive to
+        // pathname, get backup information).
+        $reporter = $this->task->get_progress();
+        $reporter->start_progress('backup_zip_contents', 2);
+
         // Zip files
         $result = $zippacker->archive_to_pathname($files, $zipfile, true, $this);
 
+        // If any sub-progress happened, end it.
+        if ($this->startedprogress) {
+            $this->task->get_progress()->end_progress();
+            $this->startedprogress = false;
+        } else {
+            // No progress was reported, manually move it on to the next overall task.
+            $reporter->progress(1);
+        }
+
         // Something went wrong.
         if ($result === false) {
             @unlink($zipfile);
@@ -1752,16 +1793,20 @@ class backup_zip_contents extends backup_execution_step implements file_progress
         }
         // Read to make sure it is a valid backup. Refer MDL-37877 . Delete it, if found not to be valid.
         try {
-            backup_general_helper::get_backup_information_from_mbz($zipfile);
+            backup_general_helper::get_backup_information_from_mbz($zipfile, $this);
         } catch (backup_helper_exception $e) {
             @unlink($zipfile);
             throw new backup_step_exception('error_zip_packing', '', $e->debuginfo);
         }
 
-            // If any progress happened, end it.
+        // If any sub-progress happened, end it.
         if ($this->startedprogress) {
             $this->task->get_progress()->end_progress();
+            $this->startedprogress = false;
+        } else {
+            $reporter->progress(2);
         }
+        $reporter->end_progress();
     }
 
     /**
@@ -1925,13 +1970,22 @@ class backup_annotate_all_question_files extends backup_execution_step {
         $components = backup_qtype_plugin::get_components_and_fileareas();
         // Let's loop
         foreach($rs as $record) {
-            // We don't need to specify filearea nor itemid as far as by
-            // component and context it's enough to annotate the whole bank files
-            // This backups "questiontext", "generalfeedback" and "answerfeedback" fileareas (all them
-            // belonging to the "question" component
-            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', null, null);
-            // Again, it is enough to pick files only by context and component
-            // Do it for qtype specific components
+            // Backup all the file areas the are managed by the core question component.
+            // That is, by the question_type base class. In particular, we don't want
+            // to include files belonging to responses here.
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'questiontext', null);
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'generalfeedback', null);
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answer', null);
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'answerfeedback', null);
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'hint', null);
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'correctfeedback', null);
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'partiallycorrectfeedback', null);
+            backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, 'question', 'incorrectfeedback', null);
+
+            // For files belonging to question types, we make the leap of faith that
+            // all the files belonging to the question type are part of the question definition,
+            // so we can just backup all the files in bulk, without specifying each
+            // file area name separately.
             foreach ($components as $component => $fileareas) {
                 backup_structure_dbops::annotate_files($this->get_backupid(), $record->contextid, $component, null, null);
             }
@@ -2075,6 +2129,12 @@ class backup_activity_grading_structure_step extends backup_structure_step {
      * Include the grading.xml only if the module supports advanced grading
      */
     protected function execute_condition() {
+
+        // No grades on the front page.
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
         return plugin_supports('mod', $this->get_task()->get_modulename(), FEATURE_ADVANCED_GRADING, false);
     }
 
@@ -2148,6 +2208,14 @@ class backup_activity_grading_structure_step extends backup_structure_step {
  */
 class backup_activity_grades_structure_step extends backup_structure_step {
 
+    /**
+     * No grades on the front page.
+     * @return bool
+     */
+    protected function execute_condition() {
+        return ($this->get_courseid() != SITEID);
+    }
+
     protected function define_structure() {
 
         // To know if we are including userinfo
@@ -2232,6 +2300,14 @@ class backup_activity_grades_structure_step extends backup_structure_step {
  */
 class backup_activity_grade_history_structure_step extends backup_structure_step {
 
+    /**
+     * No grades on the front page.
+     * @return bool
+     */
+    protected function execute_condition() {
+        return ($this->get_courseid() != SITEID);
+    }
+
     protected function define_structure() {
 
         // Settings to use.
@@ -2279,6 +2355,12 @@ class backup_activity_grade_history_structure_step extends backup_structure_step
 class backup_course_completion_structure_step extends backup_structure_step {
 
     protected function execute_condition() {
+
+        // No completion on front page.
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
         // Check that all activities have been included
         if ($this->task->is_excluding_activities()) {
             return false;
index 61e2c5c..73384df 100644 (file)
@@ -195,7 +195,7 @@ class restore_root_task extends restore_task {
         $activities->add_dependency($badges);
         $users->add_dependency($badges);
 
-        // Define Calendar events (dependent of users)
+        // Define Calendar events.
         $defaultvalue = false;                      // Safer default
         $changeable = false;
         if (isset($rootsettings['calendarevents']) && $rootsettings['calendarevents']) { // Only enabled when available
@@ -206,7 +206,6 @@ class restore_root_task extends restore_task {
         $events->set_ui(new backup_setting_ui_checkbox($events, get_string('rootsettingcalendarevents', 'backup')));
         $events->get_ui()->set_changeable($changeable);
         $this->add_setting($events);
-        $users->add_dependency($events);
 
         // Define completion (dependent of users)
         $defaultvalue = false;                      // Safer default
index 3035c7e..ee4fa9d 100644 (file)
@@ -89,6 +89,10 @@ class restore_gradebook_structure_step extends restore_structure_step {
      protected function execute_condition() {
         global $CFG, $DB;
 
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
         // No gradebook info found, don't execute
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
@@ -464,6 +468,10 @@ class restore_grade_history_structure_step extends restore_structure_step {
      protected function execute_condition() {
         global $CFG, $DB;
 
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
         // No gradebook info found, don't execute.
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
@@ -1814,9 +1822,15 @@ class restore_ras_and_caps_structure_step extends restore_structure_step {
  * If no instances yet add default enrol methods the same way as when creating new course in UI.
  */
 class restore_default_enrolments_step extends restore_execution_step {
+
     public function define_execution() {
         global $DB;
 
+        // No enrolments in front page.
+        if ($this->get_courseid() == SITEID) {
+            return;
+        }
+
         $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
 
         if ($DB->record_exists('enrol', array('courseid'=>$this->get_courseid(), 'enrol'=>'manual'))) {
@@ -1853,6 +1867,10 @@ class restore_enrolments_structure_step extends restore_structure_step {
      */
     protected function execute_condition() {
 
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
         // Check it is included in the backup
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
@@ -2337,15 +2355,17 @@ class restore_calendarevents_structure_step extends restore_structure_step {
     }
 
     public function process_calendarevents($data) {
-        global $DB, $SITE;
+        global $DB, $SITE, $USER;
 
         $data = (object)$data;
         $oldid = $data->id;
         $restorefiles = true; // We'll restore the files
-        // Find the userid and the groupid associated with the event. Return if not found.
+        // Find the userid and the groupid associated with the event.
         $data->userid = $this->get_mappingid('user', $data->userid);
         if ($data->userid === false) {
-            return;
+            // Blank user ID means that we are dealing with module generated events such as quiz starting times.
+            // Use the current user ID for these events.
+            $data->userid = $USER->id;
         }
         if (!empty($data->groupid)) {
             $data->groupid = $this->get_mappingid('group', $data->groupid);
@@ -2442,6 +2462,11 @@ class restore_course_completion_structure_step extends restore_structure_step {
             return false;
         }
 
+        // No course completion on the front page.
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
         // Check it is included in the backup
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
@@ -2789,6 +2814,10 @@ class restore_activity_grading_structure_step extends restore_structure_step {
      */
      protected function execute_condition() {
 
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
         if (!file_exists($fullpath)) {
@@ -2923,6 +2952,14 @@ class restore_activity_grading_structure_step extends restore_structure_step {
  */
 class restore_activity_grades_structure_step extends restore_structure_step {
 
+    /**
+     * No grades in front page.
+     * @return bool
+     */
+    protected function execute_condition() {
+        return ($this->get_courseid() != SITEID);
+    }
+
     protected function define_structure() {
 
         $paths = array();
@@ -3059,6 +3096,11 @@ class restore_activity_grade_history_structure_step extends restore_structure_st
      * This step is executed only if the grade history file is present.
      */
      protected function execute_condition() {
+
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
         if (!file_exists($fullpath)) {
@@ -3445,6 +3487,11 @@ class restore_userscompletion_structure_step extends restore_structure_step {
              return false;
          }
 
+        // No completion on the front page.
+        if ($this->get_courseid() == SITEID) {
+            return false;
+        }
+
          // No user completion info found, don't execute
         $fullpath = $this->task->get_taskbasepath();
         $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
index 960c9ec..4842d4a 100644 (file)
@@ -391,6 +391,81 @@ class core_backup_moodle2_testcase extends advanced_testcase {
                 'assign', 'allowsubmissionsfromdate', array('id' => $newassign->instance)));
     }
 
+    /**
+     * Test front page backup/restore and duplicate activities
+     * @return void
+     */
+    public function test_restore_frontpage() {
+        global $DB, $CFG, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $generator = $this->getDataGenerator();
+
+        $frontpage = $DB->get_record('course', array('id' => SITEID));
+        $forum = $generator->create_module('forum', array('course' => $frontpage->id));
+
+        // Activities can be duplicated.
+        $this->duplicate($frontpage, $forum->cmid);
+
+        $modinfo = get_fast_modinfo($frontpage);
+        $this->assertEquals(2, count($modinfo->get_instances_of('forum')));
+
+        // Front page backup.
+        $frontpagebc = new backup_controller(backup::TYPE_1COURSE, $frontpage->id,
+                backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
+                $USER->id);
+        $frontpagebackupid = $frontpagebc->get_backupid();
+        $frontpagebc->execute_plan();
+        $frontpagebc->destroy();
+
+        $course = $generator->create_course();
+        $newcourseid = restore_dbops::create_new_course(
+                $course->fullname . ' 2', $course->shortname . '_2', $course->category);
+
+        // Other course backup.
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id,
+                backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT,
+                $USER->id);
+        $otherbackupid = $bc->get_backupid();
+        $bc->execute_plan();
+        $bc->destroy();
+
+        // We can only restore a front page over the front page.
+        $rc = new restore_controller($frontpagebackupid, $course->id,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
+                backup::TARGET_CURRENT_ADDING);
+        $this->assertFalse($rc->execute_precheck());
+        $rc->destroy();
+
+        $rc = new restore_controller($frontpagebackupid, $newcourseid,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
+                backup::TARGET_NEW_COURSE);
+        $this->assertFalse($rc->execute_precheck());
+        $rc->destroy();
+
+        $rc = new restore_controller($frontpagebackupid, $frontpage->id,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
+                backup::TARGET_CURRENT_ADDING);
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+
+        // We can't restore a non-front page course on the front page course.
+        $rc = new restore_controller($otherbackupid, $frontpage->id,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
+                backup::TARGET_CURRENT_ADDING);
+        $this->assertFalse($rc->execute_precheck());
+        $rc->destroy();
+
+        $rc = new restore_controller($otherbackupid, $newcourseid,
+                backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id,
+                backup::TARGET_NEW_COURSE);
+        $this->assertTrue($rc->execute_precheck());
+        $rc->execute_plan();
+        $rc->destroy();
+    }
+
     /**
      * Backs a course up and restores it.
      *
index 3e4cc01..25a216c 100644 (file)
@@ -6,6 +6,7 @@ require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
 
 $contextid   = required_param('contextid', PARAM_INT);
 $stage       = optional_param('stage', restore_ui::STAGE_CONFIRM, PARAM_INT);
+$cancel      = optional_param('cancel', '', PARAM_ALPHA);
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 
@@ -30,7 +31,10 @@ $PAGE->set_title($courseshortname . ': ' . get_string('restore'));
 $PAGE->set_heading($coursefullname);
 
 $renderer = $PAGE->get_renderer('core','backup');
-echo $OUTPUT->header();
+if (empty($cancel)) {
+    // Do not print the header if user cancelled the process, as we are going to redirect the user.
+    echo $OUTPUT->header();
+}
 
 // Prepare a progress bar which can display optionally during long-running
 // operations while setting up the UI.
index 41ff9d3..e7456ce 100644 (file)
@@ -524,7 +524,7 @@ abstract class backup_controller_dbops extends backup_dbops {
      */
     public static function backup_get_original_course_info($courseid) {
         global $DB;
-        return $DB->get_record('course', array('id' => $courseid), 'fullname, shortname, startdate');
+        return $DB->get_record('course', array('id' => $courseid), 'fullname, shortname, startdate, format');
     }
 
     /**
index 434059b..7f76cff 100644 (file)
@@ -556,9 +556,6 @@ abstract class backup_cron_automated_helper {
             return true;
         }
 
-        $backupword = str_replace(' ', '_', core_text::strtolower(get_string('backupfilename')));
-        $backupword = trim(clean_filename($backupword), '_');
-
         if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
             $dir = null;
         }
@@ -573,9 +570,6 @@ abstract class backup_cron_automated_helper {
             $files = array();
             // Store all the matching files into timemodified => stored_file array.
             foreach ($fs->get_area_files($context->id, $component, $filearea, $itemid) as $file) {
-                if (strpos($file->get_filename(), $backupword) !== 0) {
-                    continue;
-                }
                 $files[$file->get_timemodified()] = $file;
             }
             if (count($files) <= $keep) {
@@ -595,8 +589,8 @@ abstract class backup_cron_automated_helper {
         if (!empty($dir) && ($storage == 1 || $storage == 2)) {
             // Calculate backup filename regex, ignoring the date/time/info parts that can be
             // variable, depending of languages, formats and automated backup settings.
-            $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-';
-            $regex = '#^'.preg_quote($filename, '#').'.*\.mbz$#';
+            $filename = backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' . $course->id . '-';
+            $regex = '#' . preg_quote($filename, '#') . '.*\.mbz$#';
 
             // Store all the matching files into filename => timemodified array.
             $files = array();
index e84d2f6..b0425a3 100644 (file)
@@ -155,6 +155,11 @@ abstract class backup_general_helper extends backup_helper {
         } else {
             $info->include_file_references_to_external_content = 0;
         }
+        // Introduced in Moodle 2.9.
+        $info->original_course_format = '';
+        if (!empty($infoarr['original_course_format'])) {
+            $info->original_course_format = $infoarr['original_course_format'];
+        }
         // include_files is a new setting in 2.6.
         if (isset($infoarr['include_files'])) {
             $info->include_files = $infoarr['include_files'];
@@ -230,11 +235,15 @@ abstract class backup_general_helper extends backup_helper {
      * This will only extract the moodle_backup.xml file from an MBZ
      * file and then call {@link self::get_backup_information()}.
      *
+     * This can be a long-running (multi-minute) operation for large backups.
+     * Pass a $progress value to receive progress updates.
+     *
      * @param string $filepath absolute path to the MBZ file.
+     * @param file_progress $progress Progress updates
      * @return stdClass containing information.
      * @since Moodle 2.4
      */
-    public static function get_backup_information_from_mbz($filepath) {
+    public static function get_backup_information_from_mbz($filepath, file_progress $progress = null) {
         global $CFG;
         if (!is_readable($filepath)) {
             throw new backup_helper_exception('missing_moodle_backup_file', $filepath);
@@ -245,7 +254,7 @@ abstract class backup_general_helper extends backup_helper {
         $tmpdir = $CFG->tempdir . '/backup/' . $tmpname;
         $fp = get_file_packer('application/vnd.moodle.backup');
 
-        $extracted = $fp->extract_to_pathname($filepath, $tmpdir, array('moodle_backup.xml'));
+        $extracted = $fp->extract_to_pathname($filepath, $tmpdir, array('moodle_backup.xml'), $progress);
         $moodlefile =  $tmpdir . '/' . 'moodle_backup.xml';
         if (!$extracted || !is_readable($moodlefile)) {
             throw new backup_helper_exception('missing_moodle_backup_xml_file', $moodlefile);
index a80a80b..803cd77 100644 (file)
@@ -105,10 +105,24 @@ abstract class restore_prechecks_helper {
             $warnings[] = get_string('noticenewerbackup','',$message);
         }
 
-        // Error if restoring over frontpage
-        // TODO: Review the whole restore process in order to transform this into one warning (see 1.9)
-        if ($controller->get_courseid() == SITEID) {
-            $errors[] = get_string('errorrestorefrontpage', 'backup');
+        // The original_course_format var was introduced in Moodle 2.9.
+        $originalcourseformat = null;
+        if (!empty($controller->get_info()->original_course_format)) {
+            $originalcourseformat = $controller->get_info()->original_course_format;
+        }
+
+        // We can't restore other course's backups on the front page.
+        if ($controller->get_courseid() == SITEID &&
+                $originalcourseformat != 'site' &&
+                $controller->get_type() == backup::TYPE_1COURSE) {
+            $errors[] = get_string('errorrestorefrontpagebackup', 'backup');
+        }
+
+        // We can't restore front pages over other courses.
+        if ($controller->get_courseid() != SITEID &&
+                $originalcourseformat == 'site' &&
+                $controller->get_type() == backup::TYPE_1COURSE) {
+            $errors[] = get_string('errorrestorefrontpagebackup', 'backup');
         }
 
         // If restoring to different site and restoring users and backup has mnet users warn/error
index e705270..d5d0f11 100644 (file)
@@ -265,7 +265,9 @@ class core_backup_renderer extends plugin_renderer_base {
         $hasrestoreoption = false;
 
         $html  = html_writer::start_tag('div', array('class'=>'backup-course-selector backup-restore'));
-        if ($wholecourse && !empty($categories) && ($categories->get_count() > 0 || $categories->get_search())) {
+        if ($wholecourse && !empty($categories) && ($categories->get_count() > 0 || $categories->get_search()) &&
+                $currentcourse != SITEID) {
+
             // New course
             $hasrestoreoption = true;
             $html .= $form;
@@ -301,7 +303,12 @@ class core_backup_renderer extends plugin_renderer_base {
             $html .= html_writer::end_tag('form');
         }
 
-        if (!empty($courses) && ($courses->get_count() > 0 || $courses->get_search())) {
+        // If we are restoring an activity, then include the current course.
+        if (!$wholecourse) {
+            $courses->invalidate_results(); // Clean list of courses.
+            $courses->set_include_currentcourse();
+        }
+        if (!empty($courses) && ($courses->get_count() > 0 || $courses->get_search()) && $currentcourse != SITEID) {
             // Existing course
             $hasrestoreoption = true;
             $html .= $form;
@@ -311,10 +318,7 @@ class core_backup_renderer extends plugin_renderer_base {
                 $html .= $this->backup_detail_input(get_string('restoretoexistingcourseadding', 'backup'), 'radio', 'target', backup::TARGET_EXISTING_ADDING, array('checked'=>'checked'));
                 $html .= $this->backup_detail_input(get_string('restoretoexistingcoursedeleting', 'backup'), 'radio', 'target', backup::TARGET_EXISTING_DELETING);
             } else {
-                // We only allow restore adding to existing for now. Enforce it here.
                 $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'target', 'value'=>backup::TARGET_EXISTING_ADDING));
-                $courses->invalidate_results(); // Clean list of courses
-                $courses->set_include_currentcourse(); // Show current course in the list
             }
             $selectacoursehtml = $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
             // Display the course selection as required if the form was submitted but this data was not supplied.
index f892b8c..cbd4c70 100644 (file)
@@ -280,13 +280,13 @@ class restore_course_search extends restore_search_base {
         $params = array(
             'contextlevel' => CONTEXT_COURSE,
             'fullnamesearch' => '%'.$this->get_search().'%',
-            'shortnamesearch' => '%'.$this->get_search().'%',
-            'siteid' => SITEID
+            'shortnamesearch' => '%'.$this->get_search().'%'
         );
 
         $select     = " SELECT c.id,c.fullname,c.shortname,c.visible,c.sortorder ";
         $from       = " FROM {course} c ";
-        $where      = " WHERE (".$DB->sql_like('c.fullname', ':fullnamesearch', false)." OR ".$DB->sql_like('c.shortname', ':shortnamesearch', false).") AND c.id <> :siteid";
+        $where      = " WHERE (".$DB->sql_like('c.fullname', ':fullnamesearch', false)." OR ".
+            $DB->sql_like('c.shortname', ':shortnamesearch', false).")";
         $orderby    = " ORDER BY c.sortorder";
 
         if ($this->currentcourseid !== null && !$this->includecurrentcourse) {
index 4287cdd..8cc0a08 100644 (file)
@@ -223,7 +223,7 @@ abstract class award_criteria {
         if (!empty($this->params)) {
             if (count($this->params) > 1) {
                 echo $OUTPUT->box(get_string('criteria_descr_' . $this->criteriatype, 'badges',
-                        strtoupper($agg[$data->get_aggregation_method($this->criteriatype)])), array('clearfix'));
+                        core_text::strtoupper($agg[$data->get_aggregation_method($this->criteriatype)])), array('clearfix'));
             } else {
                 echo $OUTPUT->box(get_string('criteria_descr_single_' . $this->criteriatype , 'badges'), array('clearfix'));
             }
index 32e8711..91a6a9d 100644 (file)
@@ -60,7 +60,7 @@ class award_criteria_overall extends award_criteria {
                 echo html_writer::table($table);
             } else {
                 echo $OUTPUT->box(get_string('criteria_descr_' . $this->criteriatype, 'badges',
-                        strtoupper($agg[$data->get_aggregation_method()])), 'clearfix');
+                        core_text::strtoupper($agg[$data->get_aggregation_method()])), 'clearfix');
             }
             echo $OUTPUT->box_end();
         }
index 52f7205..99c099e 100644 (file)
@@ -362,7 +362,7 @@ class core_badges_renderer extends plugin_renderer_base {
                     $items[] = get_string('criteria_descr_single_' . $type , 'badges') . $c->get_details();
                 } else {
                     $items[] = get_string('criteria_descr_' . $type , 'badges',
-                            strtoupper($agg[$badge->get_aggregation_method($type)])) . $c->get_details();
+                            core_text::strtoupper($agg[$badge->get_aggregation_method($type)])) . $c->get_details();
                 }
             }
         }
@@ -716,7 +716,7 @@ class core_badges_renderer extends plugin_renderer_base {
             }
         } else {
             $output .= get_string('criteria_descr_' . $short . BADGE_CRITERIA_TYPE_OVERALL, 'badges',
-                                    strtoupper($agg[$badge->get_aggregation_method()]));
+                                    core_text::strtoupper($agg[$badge->get_aggregation_method()]));
         }
         $items = array();
         unset($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]);
@@ -725,7 +725,7 @@ class core_badges_renderer extends plugin_renderer_base {
                 $items[] = get_string('criteria_descr_single_' . $short . $type , 'badges') . $c->get_details($short);
             } else {
                 $items[] = get_string('criteria_descr_' . $short . $type , 'badges',
-                        strtoupper($agg[$badge->get_aggregation_method($type)])) . $c->get_details($short);
+                        core_text::strtoupper($agg[$badge->get_aggregation_method($type)])) . $c->get_details($short);
             }
         }
         $output .= html_writer::alist($items, array(), 'ul');
index e654372..b36facc 100644 (file)
@@ -78,6 +78,7 @@ class block_comments extends block_base {
         $args->displaycancel = false;
         $comment = new comment($args);
         $comment->set_view_permission(true);
+        $comment->set_fullwidth();
 
         $this->content = new stdClass();
         $this->content->text = $comment->output(true);
index 9037cbb..96aa0ce 100644 (file)
@@ -1890,6 +1890,51 @@ function calendar_add_event_allowed($event) {
     }
 }
 
+/**
+ * Convert region timezone to php supported timezone
+ *
+ * @param string $tz value from ical file
+ * @return string $tz php supported timezone
+ */
+function calendar_normalize_tz($tz) {
+    switch ($tz) {
+        case('CST'):
+        case('Central Time'):
+        case('Central Standard Time'):
+            $tz = 'America/Chicago';
+            break;
+        case('CET'):
+        case('Central European Time'):
+            $tz = 'Europe/Berlin';
+            break;
+        case('EST'):
+        case('Eastern Time'):
+        case('Eastern Standard Time'):
+            $tz = 'America/New_York';
+            break;
+        case('PST'):
+        case('Pacific Time'):
+        case('Pacific Standard Time'):
+            $tz = 'America/Los_Angeles';
+            break;
+        case('China Time'):
+        case('China Standard Time'):
+            $tz = 'Asia/Beijing';
+            break;
+        case('IST'):
+        case('India Time'):
+        case('India Standard Time'):
+            $tz = 'Asia/New_Delhi';
+            break;
+        case('JST');
+        case('Japan Time'):
+        case('Japan Standard Time'):
+            $tz = 'Asia/Tokyo';
+            break;
+    }
+    return $tz;
+}
+
 /**
  * Manage calendar events
  *
@@ -2919,6 +2964,7 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid, $timez
     $defaulttz = date_default_timezone_get();
     $tz = isset($event->properties['DTSTART'][0]->parameters['TZID']) ? $event->properties['DTSTART'][0]->parameters['TZID'] :
             $timezone;
+    $tz = calendar_normalize_tz($tz);
     $eventrecord->timestart = strtotime($event->properties['DTSTART'][0]->value . ' ' . $tz);
     if (empty($event->properties['DTEND'])) {
         $eventrecord->timeduration = 0; // no duration if no end time specified
@@ -3065,7 +3111,9 @@ function calendar_import_icalendar_events($ical, $courseid, $subscriptionid = nu
     $updatecount = 0;
 
     // Large calendars take a while...
-    core_php_time_limit::raise(300);
+    if (!CLI_SCRIPT) {
+        core_php_time_limit::raise(300);
+    }
 
     // Mark all events in a subscription with a zero timestamp.
     if (!empty($subscriptionid)) {
index 7b4e117..cfc5bb3 100644 (file)
@@ -74,4 +74,63 @@ class core_calendar_ical_testcase extends advanced_testcase {
         $this->setExpectedException('coding_exception');
         calendar_update_subscription($subscription);
     }
+
+    public function test_calendar_add_subscription() {
+        global $DB, $CFG;
+
+        require_once($CFG->dirroot . '/lib/bennu/bennu.inc.php');
+
+        $this->resetAfterTest(true);
+
+        // Test for Microsoft Outlook 2010.
+        $subscription = new stdClass();
+        $subscription->name = 'Microsoft Outlook 2010';
+        $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE;
+        $subscription->eventtype = 'site';
+        $id = calendar_add_subscription($subscription);
+
+        $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/ms_outlook_2010.ics');
+        $ical = new iCalendar();
+        $ical->unserialize($calendar);
+        $this->assertEquals($ical->parser_errors, array());
+
+        $sub = calendar_get_subscription($id);
+        $result = calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
+        $this->assertEquals($count, 1);
+
+        // Test for OSX Yosemite.
+        $subscription = new stdClass();
+        $subscription->name = 'OSX Yosemite';
+        $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE;
+        $subscription->eventtype = 'site';
+        $id = calendar_add_subscription($subscription);
+
+        $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/osx_yosemite.ics');
+        $ical = new iCalendar();
+        $ical->unserialize($calendar);
+        $this->assertEquals($ical->parser_errors, array());
+
+        $sub = calendar_get_subscription($id);
+        $result = calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
+        $this->assertEquals($count, 1);
+
+        // Test for Google Gmail.
+        $subscription = new stdClass();
+        $subscription->name = 'Google Gmail';
+        $subscription->importfrom = CALENDAR_IMPORT_FROM_FILE;
+        $subscription->eventtype = 'site';
+        $id = calendar_add_subscription($subscription);
+
+        $calendar = file_get_contents($CFG->dirroot . '/lib/tests/fixtures/google_gmail.ics');
+        $ical = new iCalendar();
+        $ical->unserialize($calendar);
+        $this->assertEquals($ical->parser_errors, array());
+
+        $sub = calendar_get_subscription($id);
+        $result = calendar_import_icalendar_events($ical, $sub->courseid, $sub->id);
+        $count = $DB->count_records('event', array('subscriptionid' => $sub->id));
+        $this->assertEquals($count, 1);
+    }
 }
index 9df577b..e717258 100644 (file)
@@ -71,6 +71,12 @@ class comment {
     /** @var int The number of comments associated with this comments params */
     protected $totalcommentcount = null;
 
+    /**
+     * Set to true to remove the col attribute from the textarea making it full width.
+     * @var bool
+     */
+    protected $fullwidth = false;
+
     /** @var bool Use non-javascript UI */
     private static $nonjs = false;
     /** @var int comment itemid used in non-javascript UI */
@@ -459,9 +465,20 @@ class comment {
 
             if ($this->can_post()) {
                 // print posting textarea
+                $textareaattrs = array(
+                    'name' => 'content',
+                    'rows' => 2,
+                    'id' => 'dlg-content-'.$this->cid
+                );
+                if (!$this->fullwidth) {
+                    $textareaattrs['cols'] = '20';
+                } else {
+                    $textareaattrs['class'] = 'fullwidth';
+                }
+
                 $html .= html_writer::start_tag('div', array('class' => 'comment-area'));
                 $html .= html_writer::start_tag('div', array('class' => 'db'));
-                $html .= html_writer::tag('textarea', '', array('name' => 'content', 'rows' => 2, 'cols' => 20, 'id' => 'dlg-content-'.$this->cid));
+                $html .= html_writer::tag('textarea', '', $textareaattrs);
                 $html .= html_writer::end_tag('div'); // .db
 
                 $html .= html_writer::start_tag('div', array('class' => 'fd', 'id' => 'comment-action-'.$this->cid));
@@ -945,6 +962,16 @@ class comment {
     public function get_commentarea() {
         return $this->commentarea;
     }
+
+    /**
+     * Make the comments textarea fullwidth.
+     *
+     * @since 2.8.1 + 2.7.4
+     * @param bool $fullwidth
+     */
+    public function set_fullwidth($fullwidth = true) {
+        $this->fullwidth = (bool)$fullwidth;
+    }
 }
 
 /**
index b6f5246..9a918b6 100644 (file)
@@ -8,6 +8,6 @@
     "require-dev": {
         "phpunit/phpunit": "3.7.*",
         "phpunit/dbUnit": "1.2.*",
-        "moodlehq/behat-extension": "1.28.4"
+        "moodlehq/behat-extension": "1.29.0"
     }
 }
index aa04378..f7e91bb 100644 (file)
@@ -143,6 +143,19 @@ class format_topics extends format_base {
 
         // check if there are callbacks to extend course navigation
         parent::extend_course_navigation($navigation, $node);
+
+        // We want to remove the general section if it is empty.
+        $modinfo = get_fast_modinfo($this->get_course());
+        $sections = $modinfo->get_sections();
+        if (!isset($sections[0])) {
+            // The general section is empty to find the navigation node for it we need to get its ID.
+            $section = $modinfo->get_section_info(0);
+            $generalsection = $node->get($section->id, navigation_node::TYPE_SECTION);
+            if ($generalsection) {
+                // We found the node - now remove it.
+                $generalsection->remove();
+            }
+        }
     }
 
     /**
index f554d9c..73d97c2 100644 (file)
@@ -148,6 +148,19 @@ class format_weeks extends format_base {
             }
         }
         parent::extend_course_navigation($navigation, $node);
+
+        // We want to remove the general section if it is empty.
+        $modinfo = get_fast_modinfo($this->get_course());
+        $sections = $modinfo->get_sections();
+        if (!isset($sections[0])) {
+            // The general section is empty to find the navigation node for it we need to get its ID.
+            $section = $modinfo->get_section_info(0);
+            $generalsection = $node->get($section->id, navigation_node::TYPE_SECTION);
+            if ($generalsection) {
+                // We found the node - now remove it.
+                $generalsection->remove();
+            }
+        }
     }
 
     /**
index cd83b66..07cd6ed 100644 (file)
@@ -2054,8 +2054,7 @@ function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
     }
 
     // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
-    // Note that restoring on front page is never allowed.
-    if ($mod->course != SITEID && has_all_capabilities($dupecaps, $coursecontext) &&
+    if (has_all_capabilities($dupecaps, $coursecontext) &&
             plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2)) {
         $actions['duplicate'] = new action_menu_link_secondary(
             new moodle_url($baseurl, array('duplicate' => $mod->id)),
index 6acd0f8..3cc2d0c 100644 (file)
     // anything after that point.
     $eventdata = array('context' => context_course::instance($course->id));
     if (!empty($section) && (int)$section == $section) {
-        $eventdata['other'] = array('coursesectionid' => $section);
+        $eventdata['other'] = array('coursesectionnumber' => $section);
     }
     $event = \core\event\course_viewed::create($eventdata);
     $event->trigger();
index f6616e7..c7549d1 100644 (file)
@@ -202,7 +202,7 @@ class admin_setting_ldap_rolemapping extends admin_setting {
  */
 class enrol_ldap_admin_setting_category extends admin_setting_configselect {
     public function __construct($name, $visiblename, $description) {
-        parent::__construct($name, $visiblename, $description, null, null);
+        parent::__construct($name, $visiblename, $description, 1, null);
     }
 
     public function load_choices() {
@@ -211,7 +211,6 @@ class enrol_ldap_admin_setting_category extends admin_setting_configselect {
         }
 
         $this->choices = make_categories_options();
-        $this->defaultsetting = key($this->choices);
         return true;
     }
 }
old mode 100644 (file)
new mode 100755 (executable)
index 00fb110..89a5912 100644 (file)
@@ -76,6 +76,8 @@ $pagename  = get_string('letters', 'grades');
 $letters = grade_get_letters($context);
 $num = count($letters) + 3;
 
+$override = $DB->record_exists('grade_letters', array('contextid' => $context->id));
+
 //if were viewing the letters
 if (!$edit) {
 
@@ -93,6 +95,10 @@ if (!$edit) {
 
     print_grade_page_head($COURSE->id, 'letter', 'view', get_string('gradeletters', 'grades'));
 
+    if (!empty($override)) {
+        echo $OUTPUT->notification(get_string('gradeletteroverridden', 'grades'), 'notifymessage');
+    }
+
     $stredit = get_string('editgradeletters', 'grades');
     $editlink = html_writer::nonempty_tag('div', html_writer::link($returnurl.$editparam, $stredit), array('class'=>'mdl-align'));
     echo $editlink;
@@ -122,7 +128,7 @@ if (!$edit) {
         $data->$gradeboundaryname = $boundary;
         $i++;
     }
-    $data->override = $DB->record_exists('grade_letters', array('contextid' => $context->id));
+    $data->override = $override;
 
     $mform = new edit_letter_form($returnurl.$editparam, array('num'=>$num, 'admin'=>$admin));
     $mform->set_data($data);
index bdd7727..bec5d0a 100644 (file)
@@ -832,7 +832,8 @@ class grade_report_grader extends grade_report {
                             'id' => $this->course->id,
                             'item' => 'grade',
                             'itemid' => $element['object']->id));
-                        $singleview = $OUTPUT->action_icon($url, new pix_icon('t/editstring', get_string('singleview', 'grades', $element['object']->itemname)));
+                        $singleview = $OUTPUT->action_icon($url, new pix_icon('t/editstring', get_string('singleview', 'grades',
+                                $element['object']->get_name())));
                     }
 
                     $itemcell->colspan = $colspan;
index dca2b93..d89d5dc 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-debug.js differ
index d324e6f..e36c44e 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable-min.js differ
index 212ab0b..4e9d6f6 100644 (file)
Binary files a/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js and b/grade/report/grader/yui/build/moodle-gradereport_grader-gradereporttable/moodle-gradereport_grader-gradereporttable.js differ
index faf7b3b..f1a00d3 100644 (file)
@@ -30,7 +30,8 @@
 var HEIGHT = 'height',
     WIDTH = 'width',
     OFFSETWIDTH = 'offsetWidth',
-    OFFSETHEIGHT = 'offsetHeight';
+    OFFSETHEIGHT = 'offsetHeight',
+    LOGNS = 'moodle-core-grade-report-grader';
 
 CSS.FLOATING = 'floating';
 
@@ -837,43 +838,55 @@ FloatingHeaders.prototype = {
             floatingHeaderStyles[SELECTORS.FOOTERTITLE] = floatingFooterTitleStyles;
         }
 
-        // Apply the styles.
-        this.gradeItemHeadingContainer.setStyles(gradeItemHeadingContainerStyles);
-        this.userColumnHeader.setStyles(userColumnHeaderStyles);
-        this.userColumn.setStyles(userColumnStyles);
-        this.footerRow.setStyles(footerStyles);
+        // Apply the styles and mark elements as floating, or not.
+        if (this.gradeItemHeadingContainer) {
+            this.gradeItemHeadingContainer.setStyles(gradeItemHeadingContainerStyles);
+            if (headerFloats) {
+                this.gradeItemHeadingContainer.addClass(CSS.FLOATING);
+            } else {
+                this.gradeItemHeadingContainer.removeClass(CSS.FLOATING);
+            }
+        }
+        if (this.userColumnHeader) {
+            this.userColumnHeader.setStyles(userColumnHeaderStyles);
+            if (userFloats) {
+                this.userColumnHeader.addClass(CSS.FLOATING);
+            } else {
+                this.userColumnHeader.removeClass(CSS.FLOATING);
+            }
+        }
+        if (this.userColumn) {
+            this.userColumn.setStyles(userColumnStyles);
+            if (userFloats) {
+                this.userColumn.addClass(CSS.FLOATING);
+            } else {
+                this.userColumn.removeClass(CSS.FLOATING);
+            }
+        }
+        if (this.footerRow) {
+            this.footerRow.setStyles(footerStyles);
+            if (footerFloats) {
+                this.footerRow.addClass(CSS.FLOATING);
+            } else {
+                this.footerRow.removeClass(CSS.FLOATING);
+            }
+        }
 
         // And apply the styles to the generic left headers.
         Y.Object.each(floatingHeaderStyles, function(styles, key) {
-            this.floatingHeaderRow[key].setStyles(styles);
+            if (this.floatingHeaderRow[key]) {
+                this.floatingHeaderRow[key].setStyles(styles);
+            }
         }, this);
 
-        // Mark the elements as floating, or not.
-        if (headerFloats) {
-            this.gradeItemHeadingContainer.addClass(CSS.FLOATING);
-        } else {
-            this.gradeItemHeadingContainer.removeClass(CSS.FLOATING);
-        }
-
-        if (userFloats) {
-            this.userColumnHeader.addClass(CSS.FLOATING);
-            this.userColumn.addClass(CSS.FLOATING);
-        } else {
-            this.userColumnHeader.removeClass(CSS.FLOATING);
-            this.userColumn.removeClass(CSS.FLOATING);
-        }
-
-        if (footerFloats) {
-            this.footerRow.addClass(CSS.FLOATING);
-        } else {
-            this.footerRow.removeClass(CSS.FLOATING);
-        }
 
         Y.Object.each(this.floatingHeaderRow, function(value, key) {
-            if (leftTitleFloats) {
-                this.floatingHeaderRow[key].addClass(CSS.FLOATING);
-            } else {
-                this.floatingHeaderRow[key].removeClass(CSS.FLOATING);
+            if (this.floatingHeaderRow[key]) {
+                if (leftTitleFloats) {
+                    this.floatingHeaderRow[key].addClass(CSS.FLOATING);
+                } else {
+                    this.floatingHeaderRow[key].removeClass(CSS.FLOATING);
+                }
             }
         }, this);
 
index af56609..3723f24 100644 (file)
@@ -43,7 +43,7 @@ $systemcontext = context_system::instance();
 require_capability('gradereport/overview:view', $context);
 
 if (empty($userid)) {
-    require_capability('moodle/grade:viewall', $systemcontext);
+    require_capability('moodle/grade:viewall', $context);
 
 } else {
     if (!$DB->get_record('user', array('id'=>$userid, 'deleted'=>0)) or isguestuser($userid)) {
@@ -53,19 +53,19 @@ if (empty($userid)) {
 
 $access = false;
 if (has_capability('moodle/grade:viewall', $systemcontext)) {
-    //ok - can view all course grades
+    // Ok - can view all course grades.
     $access = true;
 
-} else if ($userid == $USER->id and has_capability('moodle/grade:viewall', $context)) {
-    //ok - can view any own grades
+} else if (has_capability('moodle/grade:viewall', $context)) {
+    // Ok - can view any grades in context.
     $access = true;
 
 } else if ($userid == $USER->id and has_capability('moodle/grade:view', $context) and $course->showgrades) {
-    //ok - can view own course grades
+    // Ok - can view own course grades.
     $access = true;
 
 } else if (has_capability('moodle/grade:viewall', context_user::instance($userid)) and $course->showgrades) {
-    // ok - can view grades of this user- parent most probably
+    // Ok - can view grades of this user- parent most probably.
     $access = true;
 }
 
@@ -86,8 +86,8 @@ $USER->grade_last_report[$course->id] = 'overview';
 //first make sure we have proper final grades - this must be done before constructing of the grade tree
 grade_regrade_final_grades($courseid);
 
-if (has_capability('moodle/grade:viewall', $systemcontext)) { //Admins will see all student reports
-    // please note this would be extremely slow if we wanted to implement this properly for all teachers
+if (has_capability('moodle/grade:viewall', $context)) {
+    // Please note this would be extremely slow if we wanted to implement this properly for all teachers.
     $groupmode    = groups_get_course_groupmode($course);   // Groups are being used
     $currentgroup = groups_get_course_group($course, true);
 
index 4bd2e61..c86cf37 100644 (file)
@@ -135,7 +135,7 @@ class grade_report_overview extends grade_report {
     }
 
     public function fill_table() {
-        global $CFG, $DB, $OUTPUT;
+        global $CFG, $DB, $OUTPUT, $USER;
 
         // Only show user's courses instead of all courses.
         if ($this->courses) {
@@ -153,6 +153,11 @@ class grade_report_overview extends grade_report {
                     continue;
                 }
 
+                if ((!has_capability('moodle/grade:view', $coursecontext) || $this->user->id != $USER->id) &&
+                        !has_capability('moodle/grade:viewall', $coursecontext)) {
+                    continue;
+                }
+
                 $courseshortname = format_string($course->shortname, true, array('context' => $coursecontext));
                 $courselink = html_writer::link(new moodle_url('/grade/report/user/index.php', array('id' => $course->id, 'userid' => $this->user->id)), $courseshortname);
                 $canviewhidden = has_capability('moodle/grade:viewhidden', $coursecontext);
@@ -256,7 +261,7 @@ function grade_report_overview_settings_definition(&$mform) {
                       0 => get_string('hide'),
                       1 => get_string('show'));
 
-    if (empty($CFG->grade_overviewreport_showrank)) {
+    if (empty($CFG->grade_report_overview_showrank)) {
         $options[-1] = get_string('defaultprev', 'grades', $options[0]);
     } else {
         $options[-1] = get_string('defaultprev', 'grades', $options[1]);
index c1e43d8..27bbd7a 100644 (file)
@@ -26,7 +26,8 @@ defined('MOODLE_INTERNAL') || die;
 
 if ($ADMIN->fulltree) {
 
-    $settings->add(new admin_setting_configcheckbox('grade_report_overview_showrank', get_string('showrank', 'grades'), get_string('showrank_help', 'grades'), 0, PARAM_INT));
+    $settings->add(new admin_setting_configcheckbox('grade_report_overview_showrank', get_string('showrank', 'grades'),
+                   get_string('showrank_help', 'grades'), 0));
 
     $settings->add(new admin_setting_configselect('grade_report_overview_showtotalsifcontainhidden', get_string('hidetotalifhiddenitems', 'grades'),
                                                       get_string('hidetotalifhiddenitems_help', 'grades'), GRADE_REPORT_HIDE_TOTAL_IF_CONTAINS_HIDDEN,
index 668d5fd..725090d 100644 (file)
@@ -30,5 +30,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['clitypevalue'] = 'δώσε μία τιμή';
 $string['environmentrequireinstall'] = 'απαιτείται να εγκατασταθεί/ ενεργοποιηθεί';
 $string['environmentrequireversion'] = 'απαιτείται η έκδοση {$a->needed} ενώ εσείς έχετε την {$a->current}';
index 8f5ebf8..63e1d81 100644 (file)
@@ -773,6 +773,8 @@ $string['order1'] = 'First';
 $string['order2'] = 'Second';
 $string['order3'] = 'Third';
 $string['order4'] = 'Fourth';
+$string['passwordchangelogout'] = 'Log out after password change';
+$string['passwordchangelogout_desc'] = 'If enabled, when a password is changed, all browser sessions are terminated, apart from the one in which the new password is specified. (This setting does not affect password changes via bulk user upload.)';
 $string['passwordpolicy'] = 'Password policy';
 $string['passwordresettime'] = 'Maximum time to validate password reset request';
 $string['pathtoclam'] = 'clam AV path';
index 8c2fa93..0bfe51b 100644 (file)
@@ -107,6 +107,8 @@ $string['informminpasswordupper'] = 'at least {$a} upper case letter(s)';
 $string['informpasswordpolicy'] = 'The password must have {$a}';
 $string['instructions'] = 'Instructions';
 $string['internal'] = 'Internal';
+$string['limitconcurrentlogins'] = 'Limit concurrent logins';
+$string['limitconcurrentlogins_desc'] = 'If enabled the number of concurrent browser logins for each user is restricted. The oldest session is terminated after reaching the limit, please note that users may lose all unsaved work. This setting is not compatible with single sign-on (SSO) authentication plugins.';
 $string['locked'] = 'Locked';
 $string['authloginviaemail'] = 'Allow login via email';
 $string['authloginviaemail_desc'] = 'Allow users to use both username and email address (if unique) for site login.';
index f2173e3..a449f12 100644 (file)
@@ -114,9 +114,9 @@ $string['error_course_module_not_found'] = 'Orphan course module (id: {$a}) foun
 $string['errorfilenamerequired'] = 'You must enter a valid filename for this backup';
 $string['errorfilenamemustbezip'] = 'The filename you enter must be a ZIP file and have the .mbz extension';
 $string['errorminbackup20version'] = 'This backup file has been created with one development version of Moodle backup ({$a->backup}). Minimum required is {$a->min}. Cannot be restored.';
-$string['errorrestorefrontpage'] = 'Restoring over front page is not allowed.';
 $string['errorinvalidformat'] = 'Unknown backup format';
 $string['errorinvalidformatinfo'] = 'The selected file is not a valid Moodle backup file and can\'t be restored.';
+$string['errorrestorefrontpagebackup'] = 'You can only restore front page backups on the front page';
 $string['executionsuccess'] = 'The backup file was successfully created.';
 $string['filename'] = 'Filename';
 $string['filealiasesrestorefailures'] = 'Aliases restore failures';
index ce64984..1d9513d 100644 (file)
@@ -284,6 +284,7 @@ $string['gradeitemsinc'] = 'Grade items to be included';
 $string['gradeletter'] = 'Grade letter';
 $string['gradeletter_help'] = 'Grade letters are letters, A, B, C, ..., or words, for example Distinction, Merit, Pass, ..., used to represent a range of grades.';
 $string['gradeletternote'] = 'To delete a grade letter just empty any of the<br /> three text areas for that letter and click submit.';
+$string['gradeletteroverridden'] = 'The default grade letters are currently overridden.';
 $string['gradeletters'] = 'Grade letters';
 $string['gradelocked'] = 'Grade is locked';
 $string['gradelong'] = '{$a->grade} / {$a->max}';
index 047ee28..713bb9f 100644 (file)
@@ -290,4 +290,31 @@ class behat_util extends testing_util {
         return behat_command::get_behat_dir() . '/test_environment_enabled.txt';
     }
 
+    /**
+     * Reset contents of all database tables to initial values, reset caches, etc.
+     */
+    public static function reset_all_data() {
+        // Reset all static caches.
+        accesslib_clear_all_caches(true);
+        // Reset the nasty strings list used during the last test.
+        nasty_strings::reset_used_strings();
+
+        filter_manager::reset_caches();
+
+        // Reset course and module caches.
+        if (class_exists('format_base')) {
+            // If file containing class is not loaded, there is no cache there anyway.
+            format_base::reset_course_cache(0);
+        }
+        get_fast_modinfo(0, 0, true);
+
+        // Inform data generator.
+        self::get_data_generator()->reset();
+
+        // Purge dataroot directory.
+        self::reset_dataroot();
+
+        // Reset database.
+        self::reset_database();
+    }
 }
index 1f6bf3d..9b9e313 100644 (file)
@@ -287,8 +287,10 @@ class iCalendar_component {
                 if($parent_component == null) {
                     $parent_component = $this; // If there's no components on the stack, use the iCalendar object
                 }
-                if ($parent_component->add_component($component) === false) {
-                    $this->parser_error("Failed to add component on line $key");
+                if ($component !== null) {
+                    if ($parent_component->add_component($component) === false) {
+                        $this->parser_error("Failed to add component on line $key");
+                    }
                 }
                 if ($parent_component != $this) { // If we're not using the iCalendar
                         array_push($components, $parent_component); // Put the component back on the stack
index d264083..af3f5ad 100644 (file)
@@ -104,9 +104,7 @@ class iCalendar_parameter {
                         'VIDEO'       => array('MPEG', 'QUICKTIME', 'VND.VIVO', 'VND.MOTOROLA.VIDEO', 'VND.MOTOROLA.VIDEOP')
                 );
                 $value = strtoupper($value);
-                if(rfc2445_is_xname($value)) {
-                    return true;
-                }
+                // Mimetype is enumerated above and anything else results in false.
                 @list($type, $subtype) = explode('/', $value);
                 if(empty($type) || empty($subtype)) {
                     return false;
@@ -178,7 +176,7 @@ class iCalendar_parameter {
                 if(empty($value)) {
                     return false;
                 }
-                return (strcspn($value, '";:,') == strlen($value));
+                return (strcspn($value, ';:,') == strlen($value));
             break;
 
             case 'VALUE':
@@ -227,7 +225,7 @@ class iCalendar_parameter {
 
             // Parameters we shouldn't be messing with
             case 'TZID':
-                return $value;
+                return str_replace('"', '', $value);
             break;
         }
     }
index 8a557a4..260c42a 100644 (file)
@@ -1227,6 +1227,8 @@ class iCalendar_property_x extends iCalendar_property {
     function __construct() {
         parent::__construct();
         $this->valid_parameters = array(
+            // X-ALT-DESC (Description) can have FMTTYPE declaration of text/html is a property for HTML content.
+            'FMTTYPE'     => RFC2445_OPTIONAL | RFC2445_ONCE,
             'LANGUAGE'    => RFC2445_OPTIONAL | RFC2445_ONCE,
             RFC2445_XNAME => RFC2445_OPTIONAL
         );
index 783f27e..c27a172 100644 (file)
@@ -5,3 +5,5 @@ modifications:
 2/ replaced mbstring functions with moodle core_text (28 Nov 2011)
 3/ replaced explode in iCalendar_component::unserialize() with preg_split to support various line breaks (20 Nov 2012)
 4/ updated rfc2445_is_valid_value() to accept single part rrule as a valid value (16 Jun 2014)
+5/ updated DTEND;TZID and DTSTAR;TZID values to support quotations (7 Nov 2014)
+6/ added calendar_normalize_tz function to convert region timezone to php supported timezone (7 Nov 2014)
index 1af160a..a63e3ce 100644 (file)
@@ -33,7 +33,7 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about the event.
  *
- *      - int coursesectionid: (optional) The course section ID.
+ *      - int coursesectionnumber: (optional) The course section number.
  * }
  *
  * @package    core
@@ -59,7 +59,17 @@ class course_viewed extends base {
      * @return string
      */
     public function get_description() {
-        return "The user with id '$this->userid' viewed the course with id '$this->courseid'.";
+
+        // We keep compatibility with 2.7 and 2.8 other['coursesectionid'].
+        $sectionstr = '';
+        if (!empty($this->other['coursesectionnumber'])) {
+            $sectionstr = "section number '{$this->other['coursesectionnumber']}' of the ";
+        } else if (!empty($this->other['coursesectionid'])) {
+            $sectionstr = "section number '{$this->other['coursesectionid']}' of the ";
+        }
+        $description = "The user with id '$this->userid' viewed the " . $sectionstr . "course with id '$this->courseid'.";
+
+        return $description;
     }
 
     /**
@@ -78,13 +88,17 @@ class course_viewed extends base {
      */
     public function get_url() {
         global $CFG;
-        $sectionid = null;
-        if (isset($this->other['coursesectionid'])) {
-            $sectionid = $this->other['coursesectionid'];
+
+        // We keep compatibility with 2.7 and 2.8 other['coursesectionid'].
+        $sectionnumber = null;
+        if (isset($this->other['coursesectionnumber'])) {
+            $sectionnumber = $this->other['coursesectionnumber'];
+        } else if (isset($this->other['coursesectionid'])) {
+            $sectionnumber = $this->other['coursesectionid'];
         }
         require_once($CFG->dirroot . '/course/lib.php');
         try {
-            return course_get_url($this->courseid, $sectionid);
+            return course_get_url($this->courseid, $sectionnumber);
         } catch (\Exception $e) {
             return null;
         }
@@ -101,9 +115,15 @@ class course_viewed extends base {
             return null;
         }
 
-        if (isset($this->other['coursesectionid'])) {
-            return array($this->courseid, 'course', 'view section', 'view.php?id=' . $this->courseid . '&amp;sectionid='
-                    . $this->other['coursesectionid'], $this->other['coursesectionid']);
+        // We keep compatibility with 2.7 and 2.8 other['coursesectionid'].
+        if (isset($this->other['coursesectionnumber']) || isset($this->other['coursesectionid'])) {
+            if (isset($this->other['coursesectionnumber'])) {
+                $sectionnumber = $this->other['coursesectionnumber'];
+            } else {
+                $sectionnumber = $this->other['coursesectionid'];
+            }
+            return array($this->courseid, 'course', 'view section', 'view.php?id=' . $this->courseid . '&amp;section='
+                    . $sectionnumber, $sectionnumber);
         }
         return array($this->courseid, 'course', 'view', 'view.php?id=' . $this->courseid, $this->courseid);
     }
index a6c409e..c75a004 100644 (file)
@@ -1112,7 +1112,8 @@ class core_plugin_manager {
 
             'report' => array(
                 'backups', 'completion', 'configlog', 'courseoverview', 'eventlist',
-                'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
+                'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance',
+                'usersessions',
             ),
 
             'repository' => array(
index 84e26ed..0d461c2 100644 (file)
@@ -608,12 +608,72 @@ class manager {
     /**
      * Terminate all sessions of given user unconditionally.
      * @param int $userid
+     * @param string $keepsid keep this sid if present
      */
-    public static function kill_user_sessions($userid) {
+    public static function kill_user_sessions($userid, $keepsid = null) {
         global $DB;
 
         $sessions = $DB->get_records('sessions', array('userid'=>$userid), 'id DESC', 'id, sid');
         foreach ($sessions as $session) {
+            if ($keepsid and $keepsid === $session->sid) {
+                continue;
+            }
+            self::kill_session($session->sid);
+        }
+    }
+
+    /**
+     * Terminate other sessions of current user depending
+     * on $CFG->limitconcurrentlogins restriction.
+     *
+     * This is expected to be called right after complete_user_login().
+     *
+     * NOTE:
+     *  * Do not use from SSO auth plugins, this would not work.
+     *  * Do not use from web services because they do not have sessions.
+     *
+     * @param int $userid
+     * @param string $sid session id to be always keep, usually the current one
+     * @return void
+     */
+    public static function apply_concurrent_login_limit($userid, $sid = null) {
+        global $CFG, $DB;
+
+        // NOTE: the $sid parameter is here mainly to allow testing,
+        //       in most cases it should be current session id.
+
+        if (isguestuser($userid) or empty($userid)) {
+            // This applies to real users only!
+            return;
+        }
+
+        if (empty($CFG->limitconcurrentlogins) or $CFG->limitconcurrentlogins < 0) {
+            return;
+        }
+
+        $count = $DB->count_records('sessions', array('userid' => $userid));
+
+        if ($count <= $CFG->limitconcurrentlogins) {
+            return;
+        }
+
+        $i = 0;
+        $select = "userid = :userid";
+        $params = array('userid' => $userid);
+        if ($sid) {
+            if ($DB->record_exists('sessions', array('sid' => $sid, 'userid' => $userid))) {
+                $select .= " AND sid <> :sid";
+                $params['sid'] = $sid;
+                $i = 1;
+            }
+        }
+
+        $sessions = $DB->get_records_select('sessions', $select, $params, 'timecreated DESC', 'id, sid');
+        foreach ($sessions as $session) {
+            $i++;
+            if ($i <= $CFG->limitconcurrentlogins) {
+                continue;
+            }
             self::kill_session($session->sid);
         }
     }
index 01a7fbf..b5aeea8 100644 (file)
@@ -52,13 +52,28 @@ class file_temp_cleanup_task extends scheduled_task {
         // Show all child nodes prior to their parent.
         $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
 
+        // An array of the full path (key) and date last modified.
+        $modifieddateobject = array();
+
+        // Get the time modified for each directory node. Nodes will be updated
+        // once a file is deleted, so we need a list of the original values.
+        for ($iter->rewind(); $iter->valid(); $iter->next()) {
+            $node = $iter->getRealPath();
+            if (!is_readable($node)) {
+                continue;
+            }
+            $modifieddateobject[$node] = $iter->getMTime();
+        }
+
+        // Now loop through again and remove old files and directories.
         for ($iter->rewind(); $iter->valid(); $iter->next()) {
             $node = $iter->getRealPath();
             if (!is_readable($node)) {
                 continue;
             }
+
             // Check if file or directory is older than the given time.
-            if ($iter->getMTime() < $time) {
+            if ($modifieddateobject[$node] < $time) {
                 if ($iter->isDir() && !$iter->isDot()) {
                     // Don't attempt to delete the directory if it isn't empty.
                     if (!glob($node. DIRECTORY_SEPARATOR . '*')) {
@@ -72,9 +87,11 @@ class file_temp_cleanup_task extends scheduled_task {
                         mtrace("Failed removing file '$node'.");
                     }
                 }
+            } else {
+                // Return the time modified to the original date.
+                touch($node, $modifieddateobject[$node]);
             }
         }
-
     }
 
 }
index 83cdc40..8595032 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 99fc109..e82600d 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 b4429bc..26334a1 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 1124496..3ad02e9 100644 (file)
@@ -421,7 +421,6 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         var textarea = this._content.one(SELECTORS.EQUATION_TEXT),
             equation = textarea.get('value'),
             url,
-            preview,
             currentPos = textarea.get('selectionStart'),
             prefix = '',
             cursorLatex = '\\Downarrow ',
@@ -453,7 +452,6 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         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';
@@ -464,13 +462,30 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
             text: equation
         };
 
-        preview = Y.io(url, {
-            sync: true,
-            data: params
+        Y.io(url, {
+            context: this,
+            data: params,
+            timeout: 500,
+            on: {
+                complete: this._loadPreview
+            }
         });
+    },
+
+    /**
+     * Load returned preview text into preview
+     *
+     * @param {String} id
+     * @param {EventFacade} e
+     * @method _loadPreview
+     * @private
+     */
+    _loadPreview: function(id, preview) {
+        var previewNode = this._content.one(SELECTORS.EQUATION_PREVIEW);
 
         if (preview.status === 200) {
             previewNode.setHTML(preview.responseText);
+
             Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(previewNode))});
         }
     },
@@ -485,6 +500,7 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
      */
     _getDialogueContent: function() {
         var library = this._getLibraryContent(),
+            throttledUpdate = this._throttle(this._updatePreview, 500),
             template = Y.Handlebars.compile(TEMPLATES.FORM);
 
         this._content = Y.Node.create(template({
@@ -507,9 +523,9 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         this._content.delegate('key', this._groupNavigation, 'down:37,39', SELECTORS.LIBRARY_BUTTON, this);
 
         this._content.one(SELECTORS.SUBMIT).on('click', this._setEquation, 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.one(SELECTORS.EQUATION_TEXT).on('valuechange', throttledUpdate, this);
+        this._content.one(SELECTORS.EQUATION_TEXT).on('mouseup', throttledUpdate, this);
+        this._content.one(SELECTORS.EQUATION_TEXT).on('keyup', throttledUpdate, this);
         this._content.delegate('click', this._selectLibraryItem, SELECTORS.LIBRARY_BUTTON, this);
 
         return this._content;
index aba9b1c..c6d37a5 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 51e57e1..613db1e 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 08eff19..d615388 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 bc79e1c..788692f 100644 (file)
@@ -90,15 +90,15 @@ EditorSelection.prototype = {
             this.updateOriginal();
         }, this);
 
-        Y.delegate(['keyup', 'focus', 'mouseleave'], function(e) {
+        this.editor.on(['keyup', 'focus'], function(e) {
                 Y.soon(Y.bind(this._hasSelectionChanged, this, e));
-            }, document.body, '#' + this.editor.get('id'), this);
+            }, this);
 
         // To capture both mouseup and touchend events, we need to track the gesturemoveend event in standAlone mode. Without
         // standAlone, it will only fire if we listened to a gesturemovestart too.
-        Y.delegate('gesturemoveend', function(e) {
+        this.editor.on('gesturemoveend', function(e) {
                 Y.soon(Y.bind(this._hasSelectionChanged, this, e));
-            }, document.body, '#' + this.editor.get('id'), {
+            }, {
                 standAlone: true
             }, this);
 
@@ -121,7 +121,8 @@ EditorSelection.prototype = {
         }
 
         // We can't be active if the editor doesn't have focus at the moment.
-        if (!document.activeElement || Y.one(document.activeElement) !== this.editor) {
+        if (!document.activeElement ||
+                !(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) {
             return false;
         }
 
index 45ea442..7553ff6 100644 (file)
@@ -2288,12 +2288,13 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring
     }
 
     if ($lifetime > 0) {
-        $private = '';
+        $cacheability = ' public,';
         if (isloggedin() and !isguestuser()) {
-            $private = ' private,';
+            // By default, under the conditions above, this file must be cache-able only by browsers.
+            $cacheability = ' private,';
         }
         $nobyteserving = false;
-        header('Cache-Control:'.$private.' max-age='.$lifetime.', no-transform');
+        header('Cache-Control:'.$cacheability.' max-age='.$lifetime.', no-transform');
         header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
         header('Pragma: ');
 
@@ -2366,7 +2367,10 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring
  *  (bool) dontdie - return control to caller afterwards. this is not recommended and only used for cleanup tasks.
  *      if this is passed as true, ignore_user_abort is called.  if you don't want your processing to continue on cancel,
  *      you must detect this case when control is returned using connection_aborted. Please not that session is closed
- *      and should not be reopened.
+ *      and should not be reopened
+ *  (string|null) cacheability - force the cacheability setting of the HTTP response, "private" or "public",
+ *      when $lifetime is greater than 0. Cacheability defaults to "private" when logged in as other than guest; otherwise,
+ *      defaults to "public".
  *
  * @category files
  * @param stored_file $stored_file local file object
@@ -2463,11 +2467,17 @@ function send_stored_file($stored_file, $lifetime=null, $filter=0, $forcedownloa
     }
 
     if ($lifetime > 0) {
-        $private = '';
-        if (isloggedin() and !isguestuser()) {
-            $private = ' private,';
-        }
-        header('Cache-Control:'.$private.' max-age='.$lifetime.', no-transform');
+        $cacheability = ' public,';
+        if (!empty($options['cacheability']) && ($options['cacheability'] === 'public')) {
+            // This file must be cache-able by both browsers and proxies.
+            $cacheability = ' public,';
+        } else if (!empty($options['cacheability']) && ($options['cacheability'] === 'private')) {
+            // This file must be cache-able only by browsers.
+            $cacheability = ' private,';
+        } else if (isloggedin() and !isguestuser()) {
+            $cacheability = ' private,';
+        }
+        header('Cache-Control:'.$cacheability.' max-age='.$lifetime.', no-transform');
         header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
         header('Pragma: ');
 
@@ -4177,7 +4187,13 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
                 send_file($imagefile, basename($imagefile), 60*60*24*14);
             }
 
-            send_stored_file($file, 60*60*24*365, 0, false, array('preview' => $preview)); // enable long caching, there are many images on each page
+            $options = array('preview' => $preview);
+            if (empty($CFG->forcelogin) && empty($CFG->forceloginforprofileimage)) {
+                // Profile images should be cache-able by both browsers and proxies according
+                // to $CFG->forcelogin and $CFG->forceloginforprofileimage.
+                $options['cacheability'] = 'public';
+            }
+            send_stored_file($file, 60*60*24*365, 0, false, $options); // enable long caching, there are many images on each page
 
         } else if ($filearea === 'private' and $context->contextlevel == CONTEXT_USER) {
             require_login();
index acb5db3..1291d31 100644 (file)
@@ -211,7 +211,9 @@ abstract class moodleform {
      * @return string form identifier.
      */
     protected function get_form_identifier() {
-        return get_class($this);
+        $class = get_class($this);
+
+        return preg_replace('/[^a-z0-9_]/i', '_', $class);
     }
 
     /**
index b00833d..5b31e13 100644 (file)
@@ -8650,8 +8650,22 @@ function getremoteaddr($default='0.0.0.0') {
     }
     if (!($variablestoskip & GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR)) {
         if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
-            $hdr = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
-            $address = cleanremoteaddr($hdr[0]);
+            $forwardedaddresses = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']);
+            $address = $forwardedaddresses[0];
+
+            if (substr_count($address, ":") > 1) {
+                // Remove port and brackets from IPv6.
+                if (preg_match("/\[(.*)\]:/", $address, $matches)) {
+                    $address = $matches[1];
+                }
+            } else {
+                // Remove port from IPv4.
+                if (substr_count($address, ":") == 1) {
+                    $address = explode(":", $address)[0];
+                }
+            }
+
+            $address = cleanremoteaddr($address);
             return $address ? $address : $default;
         }
     }
index cd8d996..c0c33c2 100644 (file)
@@ -3088,12 +3088,14 @@ class navbar extends navigation_node {
         } else if ($this->hasitems !== false) {
             return true;
         }
-        $this->page->navigation->initialise($this->page);
-
-        $activenodefound = ($this->page->navigation->contains_active_node() ||
-                            $this->page->settingsnav->contains_active_node());
-
-        $outcome = (count($this->children) > 0 || count($this->prependchildren) || (!$this->ignoreactive && $activenodefound));
+        if (count($this->children) > 0 || count($this->prependchildren) > 0) {
+            // There have been manually added items - there are definitely items.
+            $outcome = true;
+        } else if (!$this->ignoreactive) {
+            // We will need to initialise the navigation structure to check if there are active items.
+            $this->page->navigation->initialise($this->page);
+            $outcome = ($this->page->navigation->contains_active_node() || $this->page->settingsnav->contains_active_node());
+        }
         $this->hasitems = $outcome;
         return $outcome;
     }
@@ -3148,11 +3150,13 @@ class navbar extends navigation_node {
             $items = array_reverse($this->children);
         }
 
-        $navigationactivenode = $this->page->navigation->find_active_node();
-        $settingsactivenode = $this->page->settingsnav->find_active_node();
-
         // Check if navigation contains the active node
         if (!$this->ignoreactive) {
+            // We will need to ensure the navigation has been initialised.
+            $this->page->navigation->initialise($this->page);
+            // Now find the active nodes on both the navigation and settings.
+            $navigationactivenode = $this->page->navigation->find_active_node();
+            $settingsactivenode = $this->page->settingsnav->find_active_node();
 
             if ($navigationactivenode && $settingsactivenode) {
                 // Parse a combined navigation tree
@@ -4384,54 +4388,69 @@ class settings_navigation extends navigation_node {
     protected function load_category_settings() {
         global $CFG;
 
-        $categorynode = $this->add($this->context->get_context_name(), null, null, null, 'categorysettings');
+        // We can land here while being in the context of a block, in which case we
+        // should get the parent context which should be the category one. See self::initialise().
+        if ($this->context->contextlevel == CONTEXT_BLOCK) {
+            $catcontext = $this->context->get_parent_context();
+        } else {
+            $catcontext = $this->context;
+        }
+
+        // Let's make sure that we always have the right context when getting here.
+        if ($catcontext->contextlevel != CONTEXT_COURSECAT) {
+            throw new coding_exception('Unexpected context while loading category settings.');
+        }
+
+        $categorynode = $this->add($catcontext->get_context_name(), null, null, null, 'categorysettings');
         $categorynode->force_open();
 
-        if (can_edit_in_category($this->context->instanceid)) {
-            $url = new moodle_url('/course/management.php', array('categoryid' => $this->context->instanceid));
+        if (can_edit_in_category($catcontext->instanceid)) {
+            $url = new moodle_url('/course/management.php', array('categoryid' => $catcontext->instanceid));
             $editstring = get_string('managecategorythis');
             $categorynode->add($editstring, $url, self::TYPE_SETTING, null, null, new pix_icon('i/edit', ''));
         }
 
-        if (has_capability('moodle/category:manage', $this->context)) {
-            $editurl = new moodle_url('/course/editcategory.php', array('id' => $this->context->instanceid));
+        if (has_capability('moodle/category:manage', $catcontext)) {
+            $editurl = new moodle_url('/course/editcategory.php', array('id' => $catcontext->instanceid));
             $categorynode->add(get_string('editcategorythis'), $editurl, self::TYPE_SETTING, null, 'edit', new pix_icon('i/edit', ''));
 
-            $addsubcaturl = new moodle_url('/course/editcategory.php', array('parent' => $this->context->instanceid));
+            $addsubcaturl = new moodle_url('/course/editcategory.php', array('parent' => $catcontext->instanceid));
             $categorynode->add(get_string('addsubcategory'), $addsubcaturl, self::TYPE_SETTING, null, 'addsubcat', new pix_icon('i/withsubcat', ''));
         }
 
         // Assign local roles
-        if (has_capability('moodle/role:assign', $this->context)) {
-            $assignurl = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid'=>$this->context->id));
+        if (has_capability('moodle/role:assign', $catcontext)) {
+            $assignurl = new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid' => $catcontext->id));
             $categorynode->add(get_string('assignroles', 'role'), $assignurl, self::TYPE_SETTING, null, 'roles', new pix_icon('i/assignroles', ''));
         }
 
         // Override roles
-        if (has_capability('moodle/role:review', $this->context) or count(get_overridable_roles($this->context))>0) {
-            $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid'=>$this->context->id));
+        if (has_capability('moodle/role:review', $catcontext) or count(get_overridable_roles($catcontext)) > 0) {
+            $url = new moodle_url('/'.$CFG->admin.'/roles/permissions.php', array('contextid' => $catcontext->id));
             $categorynode->add(get_string('permissions', 'role'), $url, self::TYPE_SETTING, null, 'permissions', new pix_icon('i/permissions', ''));
         }
         // Check role permissions
-        if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride','moodle/role:override', 'moodle/role:assign'), $this->context)) {
-            $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid'=>$this->context->id));
+        if (has_any_capability(array('moodle/role:assign', 'moodle/role:safeoverride',
+                'moodle/role:override', 'moodle/role:assign'), $catcontext)) {
+            $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid' => $catcontext->id));
             $categorynode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null, 'checkpermissions', new pix_icon('i/checkpermissions', ''));
         }
 
         // Cohorts
-        if (has_any_capability(array('moodle/cohort:view', 'moodle/cohort:manage'), $this->context)) {
-            $categorynode->add(get_string('cohorts', 'cohort'), new moodle_url('/cohort/index.php', array('contextid' => $this->context->id)), self::TYPE_SETTING, null, 'cohort', new pix_icon('i/cohort', ''));
+        if (has_any_capability(array('moodle/cohort:view', 'moodle/cohort:manage'), $catcontext)) {
+            $categorynode->add(get_string('cohorts', 'cohort'), new moodle_url('/cohort/index.php',
+                array('contextid' => $catcontext->id)), self::TYPE_SETTING, null, 'cohort', new pix_icon('i/cohort', ''));
         }
 
         // Manage filters
-        if (has_capability('moodle/filter:manage', $this->context) && count(filter_get_available_in_context($this->context))>0) {
-            $url = new moodle_url('/filter/manage.php', array('contextid'=>$this->context->id));
+        if (has_capability('moodle/filter:manage', $catcontext) && count(filter_get_available_in_context($catcontext)) > 0) {
+            $url = new moodle_url('/filter/manage.php', array('contextid' => $catcontext->id));
             $categorynode->add(get_string('filters', 'admin'), $url, self::TYPE_SETTING, null, 'filters', new pix_icon('i/filter', ''));
         }
 
         // Restore.
-        if (has_capability('moodle/course:create', $this->context)) {
-            $url = new moodle_url('/backup/restorefile.php', array('contextid' => $this->context->id));
+        if (has_capability('moodle/course:create', $catcontext)) {
+            $url = new moodle_url('/backup/restorefile.php', array('contextid' => $catcontext->id));
             $categorynode->add(get_string('restorecourse', 'admin'), $url, self::TYPE_SETTING, null, 'restorecourse', new pix_icon('i/restore', ''));
         }
 
index 6ff4db7..aa20682 100644 (file)
@@ -278,6 +278,15 @@ class theme_config {
      */
     public $larrow = null;
 
+    /**
+     * @var string Accessibility: Up arrow-like character is used in
+     * the book heirarchical navigation.
+     * If the theme does not set characters, appropriate defaults
+     * are set automatically. Please DO NOT
+     * use ^ - this is confusing for blind users.
+     */
+    public $uarrow = null;
+
     /**
      * @var bool Some themes may want to disable ajax course editing.
      */
@@ -452,11 +461,13 @@ class theme_config {
             $baseconfig = $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',
-                              'blockrendermethod');
+        $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', 'uarrow',
+            'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
+            'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod');
 
         foreach ($config as $key=>$value) {
             if (in_array($key, $configurable)) {
@@ -533,7 +544,7 @@ class theme_config {
     }
 
     /**
-     * Checks if arrows $THEME->rarrow, $THEME->larrow have been set (theme/-/config.php).
+     * Checks if arrows $THEME->rarrow, $THEME->larrow, $THEME->uarrow have been set (theme/-/config.php).
      * If not it applies sensible defaults.
      *
      * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,
@@ -546,6 +557,7 @@ class theme_config {
             // Also OK in Win 9x/2K/IE 5.x
             $this->rarrow = '&#x25BA;';
             $this->larrow = '&#x25C4;';
+            $this->uarrow = '&#x25B2;';
             if (empty($_SERVER['HTTP_USER_AGENT'])) {
                 $uagent = '';
             } else {
@@ -564,6 +576,7 @@ class theme_config {
                 // So we use the same ones Konqueror uses.
                 $this->rarrow = '&rarr;';
                 $this->larrow = '&larr;';
+                $this->uarrow = '&uarr;';
             }
             elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])
                 && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {
@@ -571,6 +584,7 @@ class theme_config {
                 // To be safe, non-Unicode browsers!
                 $this->rarrow = '&gt;';
                 $this->larrow = '&lt;';
+                $this->uarrow = '^';
             }
 
             // RTL support - in RTL languages, swap r and l arrows
@@ -1475,6 +1489,10 @@ class theme_config {
             $lifetime = 0;
         } else {
             $lifetime = 60*60*24*60;
+            // By default, theme files must be cache-able by both browsers and proxies.
+            if (!array_key_exists('cacheability', $options)) {
+                $options['cacheability'] = 'public';
+            }
         }
 
         $fs = get_file_storage();
index e69521a..6db714b 100644 (file)
@@ -3196,6 +3196,19 @@ EOD;
         return $this->page->theme->larrow;
     }
 
+    /**
+     * Accessibility: Up arrow-like character is used in
+     * the book heirarchical navigation.
+     * If the theme does not set characters, appropriate defaults
+     * are set automatically. Please DO NOT
+     * use ^ - this is confusing for blind users.
+     *
+     * @return string
+     */
+    public function uarrow() {
+        return $this->page->theme->uarrow;
+    }
+
     /**
      * Returns the custom menu if one has been set
      *
index 8c2727d..c5e408b 100644 (file)
@@ -408,6 +408,9 @@ class phpunit_util extends testing_util {
 
         install_cli_database($options, false);
 
+        // Set the admin email address.
+        $DB->set_field('user', 'email', 'admin@example.com', array('username' => 'admin'));
+
         // Disable all logging for performance and sanity reasons.
         set_config('enabled_stores', '', 'tool_log');
 
@@ -435,7 +438,7 @@ class phpunit_util extends testing_util {
         global $CFG;
 
         $template = '
-        <testsuite name="@component@ test suite">
+        <testsuite name="@component@_testsuite">
             <directory suffix="_test.php">@dir@</directory>
         </testsuite>';
         $data = file_get_contents("$CFG->dirroot/phpunit.xml.dist");
index 520cb77..4fdd3c7 100644 (file)
@@ -715,6 +715,9 @@ abstract class testing_util {
         // Do not delete automatically installed files.
         self::skip_original_data_files($childclassname);
 
+        // Clear file status cache, before checking file_exists.
+        clearstatcache();
+
         // Clean up the dataroot folder.
         $handle = opendir(self::get_dataroot());
         while (false !== ($item = readdir($handle))) {
index 8b5f734..e7f64f0 100644 (file)
@@ -123,6 +123,10 @@ class behat_hooks extends behat_base {
             throw new Exception('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL . '#Running_tests');
         }
 
+        // Reset all data, before checking for is_server_running.
+        // If not done, then it can return apache error, while running tests.
+        behat_util::reset_all_data();
+
         if (!behat_util::is_server_running()) {
             throw new Exception($CFG->behat_wwwroot .
                 ' is not available, ensure you specified correct url and that the server is set up and started.' .
@@ -192,13 +196,7 @@ class behat_hooks extends behat_base {
         // Reset $SESSION.
         \core\session\manager::init_empty_session();
 
-        behat_util::reset_database();
-        behat_util::reset_dataroot();
-
-        accesslib_clear_all_caches(true);
-
-        // Reset the nasty strings list used during the last test.
-        nasty_strings::reset_used_strings();
+        behat_util::reset_all_data();
 
         // Assign valid data to admin user (some generator-related code needs a valid user).
         $user = $DB->get_record('user', array('username' => 'admin'));
@@ -406,7 +404,18 @@ class behat_hooks extends behat_base {
         for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) {
             $pending = '';
             try {
-                $jscode = 'return ' . self::PAGE_READY_JS . ' ? "" : M.util.pending_js.join(":");';
+                $jscode =
+                    'if (typeof M === "undefined") {
+                        if (document.readyState === "complete") {
+                            return "";
+                        } else {
+                            return "incomplete";
+                        }
+                    } else if (' . self::PAGE_READY_JS . ') {
+                        return "";
+                    } else {
+                        return M.util.pending_js.join(":");
+                    }';
                 $pending = $this->getSession()->evaluateScript($jscode);
             } catch (NoSuchWindow $nsw) {
                 // We catch an exception here, in case we just closed the window we were interacting with.
index baaa9e2..23fa7cb 100644 (file)
@@ -238,10 +238,10 @@ class core_events_testcase extends advanced_testcase {
         $this->assertEventContextNotUsed($event);
 
         // Now try with optional parameters.
-        $sectionid = 34;
+        $sectionnumber = 7;
         $eventparams = array();
         $eventparams['context'] = $context;
-        $eventparams['other'] = array('coursesectionid' => $sectionid);
+        $eventparams['other'] = array('coursesectionnumber' => $sectionnumber);
         $event = \core\event\course_viewed::create($eventparams);
 
         // Trigger and capture the event.
@@ -254,8 +254,8 @@ class core_events_testcase extends advanced_testcase {
 
         $this->assertInstanceOf('\core\event\course_viewed', $event);
         $this->assertEquals(context_course::instance($course->id), $event->get_context());
-        $expected = array($course->id, 'course', 'view section', 'view.php?id=' . $course->id . '&amp;sectionid='
-                . $sectionid, $sectionid);
+        $expected = array($course->id, 'course', 'view section', 'view.php?id=' . $course->id . '&amp;section='
+                . $sectionnumber, $sectionnumber);
         $this->assertEventLegacyLogData($expected, $event);
         $this->assertEventContextNotUsed($event);
 
diff --git a/lib/tests/fixtures/google_gmail.ics b/lib/tests/fixtures/google_gmail.ics
new file mode 100644 (file)
index 0000000..47445a2
--- /dev/null
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20141028T213000Z
+DTEND:20141028T223000Z
+DTSTAMP:20141028T211302Z
+ORGANIZER;CN=John Smith:mailto:john.smith@local.host
+UID:hjlv3v1lcerpi629s5gpfuijk0@google.com
+CREATED:20141028T210927Z
+DESCRIPTION:Lorem ipsum dolor sit amet\, consectetur adipisicing elit.
+LAST-MODIFIED:20141028T211302Z
+LOCATION:150 Willis St\, Te Aro\, Wellington 6011\, New Zealand
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:Google calendar
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
diff --git a/lib/tests/fixtures/ms_outlook_2010.ics b/lib/tests/fixtures/ms_outlook_2010.ics
new file mode 100644 (file)
index 0000000..69d85ab
--- /dev/null
@@ -0,0 +1,42 @@
+BEGIN:VCALENDAR
+PRODID:-//Microsoft Corporation//Outlook 14.0 MIMEDIR//EN
+VERSION:2.0
+METHOD:PUBLISH
+X-MS-OLK-FORCEINSPECTOROPEN:TRUE
+BEGIN:VTIMEZONE
+TZID:Central Standard Time
+BEGIN:STANDARD
+DTSTART:16011104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0600
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZOFFSETFROM:-0600
+TZOFFSETTO:-0500
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+CLASS:PUBLIC
+CREATED:20140916T144857Z
+DESCRIPTION:\n
+DTSTART;TZID="Central Standard Time":20140916T090000
+DTSTAMP:20140916T144857Z
+DTEND;TZID="Central Standard Time":20140916T093000
+LAST-MODIFIED:20140916T144857Z
+PRIORITY:5
+SEQUENCE:0
+SUMMARY;LANGUAGE=en-us:test
+TRANSP:OPAQUE
+UID:040000008200E00074C5B7101A82E0080000000000D9F06393D1CF010000000000000000100000007B710E58C9CE8146B1403BF7E84162FB
+X-ALT-DESC;FMTTYPE=text/html:<!DOCTYPE>\n<HTML>\n<HEAD>\n<TITLE></TITLE>\n</HEAD>\n<BODY>\n<!-- Converted from text/rtf format -->\n\n<P DIR=LTR></P>\n\n</BODY>\n</HTML>
+X-MICROSOFT-CDO-BUSYSTATUS:BUSY
+X-MICROSOFT-CDO-IMPORTANCE:1
+X-MICROSOFT-DISALLOW-COUNTER:FALSE
+X-MS-OLK-AUTOFILLLOCATION:TRUE
+X-MS-OLK-AUTOSTARTCHECK:FALSE
+X-MS-OLK-CONFTYPE:0
+END:VEVENT
+END:VCALENDAR
\ No newline at end of file
diff --git a/lib/tests/fixtures/osx_yosemite.ics b/lib/tests/fixtures/osx_yosemite.ics
new file mode 100644 (file)
index 0000000..fe0a2e7
--- /dev/null
@@ -0,0 +1,36 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+VERSION:2.0
+METHOD:PUBLISH
+X-WR-CALNAME:pokus
+X-WR-TIMEZONE:Pacific/Auckland
+X-APPLE-CALENDAR-COLOR:#1BADF8
+BEGIN:VTIMEZONE
+TZID:Pacific/Auckland
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+1200
+RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU
+DTSTART:20070930T020000
+TZNAME:NZDT
+TZOFFSETTO:+1300
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+1300
+RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU
+DTSTART:20080406T030000
+TZNAME:NZST
+TZOFFSETTO:+1200
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20141028T204253Z
+UID:5571BBDC-AC05-4486-9E57-118EBDFFA385
+DTEND;TZID=Pacific/Auckland:20140917T023000
+TRANSP:OPAQUE
+SUMMARY:Test event
+DTSTART;TZID=Pacific/Auckland:20140917T020000
+DTSTAMP:20141028T204253Z
+SEQUENCE:0
+DESCRIPTION:This is a note.
+END:VEVENT
+END:VCALENDAR
index 40953cd..004cc07 100644 (file)
@@ -2821,6 +2821,8 @@ class core_moodlelib_testcase extends advanced_testcase {
      * Tests the getremoteaddr() function.
      */
     public function test_getremoteaddr() {
+        $xforwardedfor = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : null;
+
         $_SERVER['HTTP_X_FORWARDED_FOR'] = '';
         $noip = getremoteaddr('1.1.1.1');
         $this->assertEquals('1.1.1.1', $noip);
@@ -2841,5 +2843,23 @@ class core_moodlelib_testcase extends advanced_testcase {
         $threeip = getremoteaddr();
         $this->assertEquals('127.0.0.1', $threeip);
 
+        $_SERVER['HTTP_X_FORWARDED_FOR'] = '127.0.0.1:65535,127.0.0.2';
+        $portip = getremoteaddr();
+        $this->assertEquals('127.0.0.1', $portip);
+
+        $_SERVER['HTTP_X_FORWARDED_FOR'] = '0:0:0:0:0:0:0:1,127.0.0.2';
+        $portip = getremoteaddr();
+        $this->assertEquals('0:0:0:0:0:0:0:1', $portip);
+
+        $_SERVER['HTTP_X_FORWARDED_FOR'] = '0::1,127.0.0.2';
+        $portip = getremoteaddr();
+        $this->assertEquals('0:0:0:0:0:0:0:1', $portip);
+
+        $_SERVER['HTTP_X_FORWARDED_FOR'] = '[0:0:0:0:0:0:0:1]:65535,127.0.0.2';
+        $portip = getremoteaddr();
+        $this->assertEquals('0:0:0:0:0:0:0:1', $portip);
+
+        $_SERVER['HTTP_X_FORWARDED_FOR'] = $xforwardedfor;
+
     }
 }
index 27b401a..e05edd6 100644 (file)
@@ -312,4 +312,61 @@ class core_scheduled_task_testcase extends advanced_testcase {
         $this->assertGreaterThanOrEqual(0, $hour);
         $this->assertLessThanOrEqual(23, $hour);
     }
+
+    /**
+     * Test that the file_temp_cleanup_task removes directories and
+     * files as expected.
+     */
+    public function test_file_temp_cleanup_task() {
+        global $CFG;
+
+        // Create directories.
+        $dir = $CFG->tempdir . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR . 'courses';
+        mkdir($dir, 0777, true);
+
+        // Create files to be checked and then deleted.
+        $file01 = $dir . DIRECTORY_SEPARATOR . 'sections.xml';
+        file_put_contents($file01, 'test data 001');
+        $file02 = $dir . DIRECTORY_SEPARATOR . 'modules.xml';
+        file_put_contents($file02, 'test data 002');
+        // Change the time modified for the first file, to a time that will be deleted by the task (greater than seven days).
+        touch($file01, time() - (8 * 24 * 3600));
+
+        $task = \core\task\manager::get_scheduled_task('\\core\\task\\file_temp_cleanup_task');
+        $this->assertInstanceOf('\core\task\file_temp_cleanup_task', $task);
+        $task->execute();
+
+        // Scan the directory. Only modules.xml should be left.
+        $filesarray = scandir($dir);
+        $this->assertEquals('modules.xml', $filesarray[2]);
+        $this->assertEquals(3, count($filesarray));
+
+        // Change the time modified on modules.xml.
+        touch($file02, time() - (8 * 24 * 3600));
+        // Change the time modified on the courses directory.
+        touch($CFG->tempdir . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'backup01' . DIRECTORY_SEPARATOR .
+                'courses', time() - (8 * 24 * 3600));
+        // Run the scheduled task to remove the file and directory.
+        $task->execute();
+        $filesarray = scandir($CFG->tempdir . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'backup01');
+        // There should only be two items in the array, '.' and '..'.
+        $this->assertEquals(2, count($filesarray));
+
+        // Change the time modified on all of the files and directories.
+        $dir = new \RecursiveDirectoryIterator($CFG->tempdir);
+        // Show all child nodes prior to their parent.
+        $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST);
+
+        for ($iter->rewind(); $iter->valid(); $iter->next()) {
+            $node = $iter->getRealPath();
+            touch($node, time() - (8 * 24 * 3600));
+        }
+
+        // Run the scheduled task again to remove all of the files and directories.
+        $task->execute();
+        $filesarray = scandir($CFG->tempdir);
+        // All of the files and directories should be deleted.
+        // There should only be two items in the array, '.' and '..'.
+        $this->assertEquals(2, count($filesarray));
+    }
 }
index b9db594..a5a15d9 100644 (file)
@@ -304,7 +304,149 @@ class core_session_manager_testcase extends advanced_testcase {
         \core\session\manager::kill_user_sessions($userid);
 
         $this->assertEquals(1, $DB->count_records('sessions'));
-        $this->assertFalse($DB->record_exists('sessions', array('userid'=>$userid)));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $userid)));
+
+        $record->userid       = $userid;
+        $record->sid          = md5('pokus3');
+        $DB->insert_record('sessions', $record);
+
+        $record->userid       = $userid;
+        $record->sid          = md5('pokus4');
+        $DB->insert_record('sessions', $record);
+
+        $record->userid       = $userid;
+        $record->sid          = md5('pokus5');
+        $DB->insert_record('sessions', $record);
+
+        $this->assertEquals(3, $DB->count_records('sessions', array('userid' => $userid)));
+
+        \core\session\manager::kill_user_sessions($userid, md5('pokus5'));
+
+        $this->assertEquals(1, $DB->count_records('sessions', array('userid' => $userid)));
+        $this->assertEquals(1, $DB->count_records('sessions', array('userid' => $userid, 'sid' => md5('pokus5'))));
+    }
+
+    public function test_apply_concurrent_login_limit() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $guest = guest_user();
+
+        $record = new \stdClass();
+        $record->state        = 0;
+        $record->sessdata     = null;
+        $record->userid       = $user1->id;
+        $record->timemodified = time();
+        $record->firstip      = $record->lastip = '10.0.0.1';
+
+        $record->sid = md5('hokus1');
+        $record->timecreated = 20;
+        $DB->insert_record('sessions', $record);
+        $record->sid = md5('hokus2');
+        $record->timecreated = 10;
+        $DB->insert_record('sessions', $record);
+        $record->sid = md5('hokus3');
+        $record->timecreated = 30;
+        $DB->insert_record('sessions', $record);
+
+        $record->userid = $user2->id;
+        $record->sid = md5('pokus1');
+        $record->timecreated = 20;
+        $DB->insert_record('sessions', $record);
+        $record->sid = md5('pokus2');
+        $record->timecreated = 10;
+        $DB->insert_record('sessions', $record);
+        $record->sid = md5('pokus3');
+        $record->timecreated = 30;
+        $DB->insert_record('sessions', $record);
+
+        $record->timecreated = 10;
+        $record->userid = $guest->id;
+        $record->sid = md5('g1');
+        $DB->insert_record('sessions', $record);
+        $record->sid = md5('g2');
+        $DB->insert_record('sessions', $record);
+        $record->sid = md5('g3');
+        $DB->insert_record('sessions', $record);
+
+        $record->userid = 0;
+        $record->sid = md5('nl1');
+        $DB->insert_record('sessions', $record);
+        $record->sid = md5('nl2');
+        $DB->insert_record('sessions', $record);
+        $record->sid = md5('nl3');
+        $DB->insert_record('sessions', $record);
+
+        set_config('limitconcurrentlogins', 0);
+        $this->assertCount(12, $DB->get_records('sessions'));
+
+        \core\session\manager::apply_concurrent_login_limit($user1->id);
+        \core\session\manager::apply_concurrent_login_limit($user2->id);
+        \core\session\manager::apply_concurrent_login_limit($guest->id);
+        \core\session\manager::apply_concurrent_login_limit(0);
+        $this->assertCount(12, $DB->get_records('sessions'));
+
+        set_config('limitconcurrentlogins', -1);
+
+        \core\session\manager::apply_concurrent_login_limit($user1->id);
+        \core\session\manager::apply_concurrent_login_limit($user2->id);
+        \core\session\manager::apply_concurrent_login_limit($guest->id);
+        \core\session\manager::apply_concurrent_login_limit(0);
+        $this->assertCount(12, $DB->get_records('sessions'));
+
+        set_config('limitconcurrentlogins', 2);
+
+        \core\session\manager::apply_concurrent_login_limit($user1->id);
+        $this->assertCount(11, $DB->get_records('sessions'));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
+
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
+        set_config('limitconcurrentlogins', 2);
+        \core\session\manager::apply_concurrent_login_limit($user2->id, md5('pokus2'));
+        $this->assertCount(10, $DB->get_records('sessions'));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
+
+        \core\session\manager::apply_concurrent_login_limit($guest->id);
+        \core\session\manager::apply_concurrent_login_limit(0);
+        $this->assertCount(10, $DB->get_records('sessions'));
+
+        set_config('limitconcurrentlogins', 1);
+
+        \core\session\manager::apply_concurrent_login_limit($user1->id, md5('grrr'));
+        $this->assertCount(9, $DB->get_records('sessions'));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
+
+        \core\session\manager::apply_concurrent_login_limit($user1->id);
+        $this->assertCount(9, $DB->get_records('sessions'));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
+
+        \core\session\manager::apply_concurrent_login_limit($user2->id, md5('pokus2'));
+        $this->assertCount(8, $DB->get_records('sessions'));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
+
+        \core\session\manager::apply_concurrent_login_limit($user2->id);
+        $this->assertCount(8, $DB->get_records('sessions'));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
+        $this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
+        $this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
+
+        \core\session\manager::apply_concurrent_login_limit($guest->id);
+        \core\session\manager::apply_concurrent_login_limit(0);
+        $this->assertCount(8, $DB->get_records('sessions'));
     }
 
     public function test_kill_all_sessions() {
index c1c2224..79e608d 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in core libraries and APIs,
 information provided here is intended especially for developers.
 
+=== 2.9 ===
+
+* \core\event\course_viewed 'other' argument renamed from coursesectionid to coursesectionnumber as it contains the section number.
+
 === 2.8 ===
 
 * Gradebook grade category option "aggregatesubcats" has been removed completely.
index 6d4e80a..61ff1e0 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js differ
index bac5cb5..bead75c 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js differ
index 5d21f8e..de22f7f 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock.js differ
index dfa7211..a15177a 100644 (file)
@@ -133,6 +133,9 @@ DOCKEDITEM.prototype = {
         Y.log('Showing '+this._getLogDescription(), 'debug', LOGNS);
         panel.setHeader(this.get('titlestring'), this.get('commands'));
         panel.setBody(Y.Node.create('<div class="block_'+this.get('blockclass')+' block_docked"></div>').append(this.get('contents')));
+        if (M.core.actionmenu !== undefined) {
+            M.core.actionmenu.newDOMNode(panel.get('node'));
+        }
         panel.show();
         panel.correctWidth();
 
index cb433d5..eedfddf 100644 (file)
@@ -115,6 +115,10 @@ if ($mform->is_cancelled()) {
         print_error('errorpasswordupdate', 'auth');
     }
 
+    if (!empty($CFG->passwordchangelogout)) {
+        \core\session\manager::kill_user_sessions($USER->id, session_id());
+    }
+
     // Reset login lockout - we want to prevent any accidental confusion here.
     login_unlock_account($USER);
 
index d765361..133aa9b 100644 (file)
@@ -80,6 +80,8 @@ if (!empty($data) || (!empty($p) && !empty($s))) {
 
         complete_user_login($user);
 
+        \core\session\manager::apply_concurrent_login_limit($user->id, session_id());
+
         if ( ! empty($SESSION->wantsurl) ) {   // Send them where they were going
             $goto = $SESSION->wantsurl;
             unset($SESSION->wantsurl);
index 587eb0e..59c55e4 100644 (file)
@@ -93,6 +93,10 @@ foreach($authsequence as $authname) {
 /// Define variables used in page
 $site = get_site();
 
+// Ignore any active pages in the navigation/settings.
+// We do this because there won't be an active page there, and by ignoring the active pages the
+// navigation and settings won't be initialised unless something else needs them.
+$PAGE->navbar->ignore_active();
 $loginsite = get_string("loginsite");
 $PAGE->navbar->add($loginsite);
 
@@ -180,6 +184,8 @@ if ($frm and isset($frm->username)) {                             // Login WITH
     /// Let's get them all set up.
         complete_user_login($user);
 
+        \core\session\manager::apply_concurrent_login_limit($user->id, session_id());
+
         // sets the username cookie
         if (!empty($CFG->nolastloggedin)) {
             // do not store last logged in user in cookie
index dd357f4..ac2a1cf 100644 (file)
@@ -239,6 +239,9 @@ function core_login_process_password_set($token) {
         if (!$userauth->user_update_password($user, $data->password)) {
             print_error('errorpasswordupdate', 'auth');
         }
+        if (!empty($CFG->passwordchangelogout)) {
+            \core\session\manager::kill_user_sessions($user->id, session_id());
+        }
         // Reset login lockout (if present) before a new password is set.
         login_unlock_account($user);
         // Clear any requirement to change passwords.
@@ -251,6 +254,8 @@ function core_login_process_password_set($token) {
         }
         complete_user_login($user); // Triggers the login event.
 
+        \core\session\manager::apply_concurrent_login_limit($user->id, session_id());
+
         $urltogo = core_login_get_return_url();
         unset($SESSION->wantsurl);
         redirect($urltogo, get_string('passwordset'), 1);
index 546e62a..a505624 100644 (file)
@@ -1980,20 +1980,19 @@ function message_get_history($user1, $user2, $limitnum=0, $viewingnewmessages=fa
 function message_print_message_history($user1, $user2 ,$search = '', $messagelimit = 0, $messagehistorylink = false, $viewingnewmessages = false, $showactionlinks = true) {
     global $CFG, $OUTPUT;
 
-    echo $OUTPUT->box_start('center');
-    echo html_writer::start_tag('table', array('cellpadding' => '10', 'class' => 'message_user_pictures'));
-    echo html_writer::start_tag('tr');
-
-    echo html_writer::start_tag('td', array('align' => 'center', 'id' => 'user1'));
+    echo $OUTPUT->box_start('center', 'message_user_pictures');
+    echo $OUTPUT->box_start('user');
+    echo $OUTPUT->box_start('generalbox', 'user1');
     echo $OUTPUT->user_picture($user1, array('size' => 100, 'courseid' => SITEID));
     echo html_writer::tag('div', fullname($user1), array('class' => 'heading'));
-    echo html_writer::end_tag('td');
+    echo $OUTPUT->box_end();
+    echo $OUTPUT->box_end();
 
-    echo html_writer::start_tag('td', array('align' => 'center'));
-    echo html_writer::empty_tag('img', array('src' => $OUTPUT->pix_url('i/twoway'), 'alt' => ''));
-    echo html_writer::end_tag('td');
+    $imgattr = array('src' => $OUTPUT->pix_url('i/twoway'), 'alt' => '', 'width' => 16, 'height' => 16);
+    echo $OUTPUT->box(html_writer::empty_tag('img', $imgattr), 'between');
 
-    echo html_writer::start_tag('td', array('align' => 'center', 'id' => 'user2'));
+    echo $OUTPUT->box_start('user');
+    echo $OUTPUT->box_start('generalbox', 'user2');
     // Show user picture with link is real user else without link.
     if (core_user::is_real_user($user2->id)) {
         echo $OUTPUT->user_picture($user2, array('size' => 100, 'courseid' => SITEID));
@@ -2014,10 +2013,8 @@ function message_print_message_history($user1, $user2 ,$search = '', $messagelim
 
         echo html_writer::tag('div', $useractionlinks, array('class' => 'useractionlinks'));
     }
-
-    echo html_writer::end_tag('td');
-    echo html_writer::end_tag('tr');
-    echo html_writer::end_tag('table');
+    echo $OUTPUT->box_end();
+    echo $OUTPUT->box_end();
     echo $OUTPUT->box_end();
 
     if (!empty($messagehistorylink)) {
index 2e3fec6..7b5067b 100644 (file)
@@ -1501,9 +1501,10 @@ class assign {
     /**
      * Load a count of submissions.
      *
+     * @param bool $includenew When true, also counts the submissions with status 'new'.
      * @return int number of submissions
      */
-    public function count_submissions() {
+    public function count_submissions($includenew = false) {
         global $DB;
 
         if (!$this->has_instance()) {
@@ -1511,6 +1512,12 @@ class assign {
         }
 
         $params = array();
+        $sqlnew = '';
+
+        if (!$includenew) {
+            $sqlnew = ' AND s.status <> :status ';
+            $params['status'] = ASSIGN_SUBMISSION_STATUS_NEW;
+        }
 
         if ($this->get_instance()->teamsubmission) {
             // We cannot join on the enrolment tables for group submissions (no userid).
@@ -1519,14 +1526,16 @@ class assign {
                         WHERE
                             s.assignment = :assignid AND
                             s.timemodified IS NOT NULL AND
-                            s.userid = :groupuserid';
+                            s.userid = :groupuserid' .
+                            $sqlnew;
 
             $params['assignid'] = $this->get_instance()->id;
             $params['groupuserid'] = 0;
         } else {
             $currentgroup = groups_get_activity_group($this->get_course_module(), true);
-            list($esql, $params) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
+            list($esql, $enrolparams) = get_enrolled_sql($this->get_context(), 'mod/assign:submit', $currentgroup, true);
 
+            $params = array_merge($params, $enrolparams);
             $params['assignid'] = $this->get_instance()->id;
 
             $sql = 'SELECT COUNT(DISTINCT s.userid)
@@ -1534,7 +1543,8 @@ class assign {
                        JOIN(' . $esql . ') e ON e.id = s.userid
                        WHERE
                             s.assignment = :assignid AND
-                            s.timemodified IS NOT NULL';
+                            s.timemodified IS NOT NULL ' .
+                            $sqlnew;
 
         }
 
@@ -3739,7 +3749,13 @@ class assign {
 
         $submissionstatement = '';
         if (!empty($adminconfig->submissionstatement)) {
-            $submissionstatement = $adminconfig->submissionstatement;
+            // Format the submission statement before its sent. We turn off para because this is going within
+            // a form element.
+            $options = array(
+                'context' => $this->get_context(),
+                'para' => false
+            );
+            $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
         }
 
         if ($mform == null) {
@@ -4996,7 +5012,13 @@ class assign {
 
         $submissionstatement = '';
         if (!empty($adminconfig->submissionstatement)) {
-            $submissionstatement = $adminconfig->submissionstatement;
+            // Format the submission statement before its sent. We turn off para because this is going within
+            // a form element.
+            $options = array(
+                'context' => $this->get_context(),
+                'para' => false
+            );
+            $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
         }
 
         if ($mform == null) {
@@ -6196,9 +6218,15 @@ class assign {
 
             $submissionstatement = '';
             if (!empty($adminconfig->submissionstatement)) {
-                $submissionstatement = $adminconfig->submissionstatement;
+                // Format the submission statement before its sent. We turn off para because this is going within
+                // a form element.
+                $options = array(
+                    'context' => $this->get_context(),
+                    'para' => false
+                );
+                $submissionstatement = format_text($adminconfig->submissionstatement, FORMAT_MOODLE, $options);
             }
-            $mform->addElement('checkbox', 'submissionstatement', '', '&nbsp;' . $submissionstatement);
+            $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
             $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
         }
 
index 868d48d..2ed075e 100644 (file)
 .path-mod-assign .gradingtable .menu-action img {
     display: none;
 }
+
+.path-mod-assign .editsubmissionform input[name="submissionstatement"] {
+    vertical-align: top;
+}
+.path-mod-assign .editsubmissionform label[for="id_submissionstatement"] {
+    display: inline-block;
+}
\ No newline at end of file
index 70eb6a9..5f1bf0d 100644 (file)
@@ -32,6 +32,8 @@ $string['enabled_help'] = 'If enabled, students are able to upload one or more f
 $string['eventassessableuploaded'] = 'A file has been uploaded.';
 $string['file'] = 'File submissions';
 $string['maxbytes'] = 'Maximum file size';
+$string['maxfiles'] = 'Maximum files per submission';
+$string['maxfiles_help'] = 'If file submissions are enabled, each assignment can be set to accept up to this number of files for their submission.';
 $string['maxfilessubmission'] = 'Maximum number of uploaded files';
 $string['maxfilessubmission_help'] = 'If file submissions are enabled, each student will be able to upload up to this number of files for their submission.';
 $string['maximumsubmissionsize'] = 'Maximum submission size';
index 31c7efc..506eeaa 100644 (file)
@@ -29,7 +29,6 @@ require_once($CFG->libdir.'/eventslib.php');
 defined('MOODLE_INTERNAL') || die();
 
 // File areas for file submission assignment.
-define('ASSIGNSUBMISSION_FILE_MAXFILES', 20);
 define('ASSIGNSUBMISSION_FILE_MAXSUMMARYFILES', 5);
 define('ASSIGNSUBMISSION_FILE_FILEAREA', 'submission_files');
 
@@ -75,7 +74,7 @@ class assign_submission_file extends assign_submission_plugin {
 
         $settings = array();
         $options = array();
-        for ($i = 1; $i <= ASSIGNSUBMISSION_FILE_MAXFILES; $i++) {
+        for ($i = 1; $i <= get_config('assignsubmission_file', 'maxfiles'); $i++) {
             $options[$i] = $i;
         }
 
index 4757819..4f38fee 100644 (file)
@@ -28,6 +28,10 @@ $settings->add(new admin_setting_configcheckbox('assignsubmission_file/default',
                    new lang_string('default', 'assignsubmission_file'),
                    new lang_string('default_help', 'assignsubmission_file'), 1));
 
+$settings->add(new admin_setting_configtext('assignsubmission_file/maxfiles',
+                   new lang_string('maxfiles', 'assignsubmission_file'),
+                   new lang_string('maxfiles_help', 'assignsubmission_file'), 20, PARAM_INT));
+
 if (isset($CFG->maxbytes)) {
 
     $name = new lang_string('maximumsubmissionsize', 'assignsubmission_file');
index 2a7636c..1508026 100644 (file)
@@ -47,7 +47,7 @@ class mod_assign_confirm_submission_form extends moodleform {
              $data) = $this->_customdata;
 
         if ($requiresubmissionstatement) {
-            $mform->addElement('checkbox', 'submissionstatement', '', '&nbsp;' . $submissionstatement);
+            $mform->addElement('checkbox', 'submissionstatement', '', $submissionstatement);
             $mform->addRule('submissionstatement', get_string('required'), 'required', null, 'client');
         }
 
index f1a0c3b..94d10fd 100644 (file)
@@ -322,6 +322,13 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         // Simulate a submission.
         $this->setUser($this->students[0]);
         $submission = $assign->get_user_submission($this->students[0]->id, true);
+
+        // The submission is still new.
+        $this->assertEquals(false, $assign->has_submissions_or_grades());
+
+        // Submit the submission.
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $assign->testable_update_submission($submission, $this->students[0]->id, true, false);
         $data = new stdClass();
         $data->onlinetext_editor = array('itemid'=>file_get_unused_draft_itemid(),
                                          'text'=>'Submission text',
@@ -742,13 +749,97 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id, 0);
         $assign->testable_apply_grade_to_user($data, $this->extrasuspendedstudents[0]->id, 0);
 
+        // Create a new submission with status NEW.
+        $this->setUser($this->extrastudents[4]);
+        $submission = $assign->get_user_submission($this->extrastudents[4]->id, true);
+
         $this->assertEquals(2, $assign->count_grades());
         $this->assertEquals(4, $assign->count_submissions());
+        $this->assertEquals(5, $assign->count_submissions(true));
         $this->assertEquals(2, $assign->count_submissions_need_grading());
         $this->assertEquals(3, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
         $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
     }
 
+    public function test_count_submissions_for_groups() {
+        $this->create_extra_users();
+        $groupid = null;
+        $this->setUser($this->editingteachers[0]);
+        $assign = $this->create_instance(array('assignsubmission_onlinetext_enabled' => 1, 'teamsubmission' => 1));
+
+        // Simulate a submission.
+        $this->setUser($this->extrastudents[0]);
+        $submission = $assign->get_group_submission($this->extrastudents[0]->id, $groupid, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_DRAFT;
+        $assign->testable_update_submission($submission, $this->extrastudents[0]->id, true, false);
+        // Leave this one as DRAFT.
+        $data = new stdClass();
+        $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
+                                         'text' => 'Submission text',
+                                         'format' => FORMAT_MOODLE);
+        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+        $plugin->save($submission, $data);
+
+        // Simulate adding a grade.
+        $this->setUser($this->teachers[0]);
+        $data = new stdClass();
+        $data->grade = '50.0';
+        $assign->testable_apply_grade_to_user($data, $this->extrastudents[0]->id, 0);
+
+        // Simulate a submission.
+        $this->setUser($this->extrastudents[1]);
+        $submission = $assign->get_group_submission($this->extrastudents[1]->id, $groupid, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $assign->testable_update_submission($submission, $this->extrastudents[1]->id, true, false);
+        $data = new stdClass();
+        $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
+                                         'text' => 'Submission text',
+                                         'format' => FORMAT_MOODLE);
+        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+        $plugin->save($submission, $data);
+
+        // Simulate a submission.
+        $this->setUser($this->extrastudents[2]);
+        $submission = $assign->get_group_submission($this->extrastudents[2]->id, $groupid, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $assign->testable_update_submission($submission, $this->extrastudents[2]->id, true, false);
+        $data = new stdClass();
+        $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
+                                         'text' => 'Submission text',
+                                         'format' => FORMAT_MOODLE);
+        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+        $plugin->save($submission, $data);
+
+        // Simulate a submission.
+        $this->setUser($this->extrastudents[3]);
+        $submission = $assign->get_group_submission($this->extrastudents[3]->id, $groupid, true);
+        $submission->status = ASSIGN_SUBMISSION_STATUS_SUBMITTED;
+        $assign->testable_update_submission($submission, $this->extrastudents[3]->id, true, false);
+        $data = new stdClass();
+        $data->onlinetext_editor = array('itemid' => file_get_unused_draft_itemid(),
+                                         'text' => 'Submission text',
+                                         'format' => FORMAT_MOODLE);
+        $plugin = $assign->get_submission_plugin_by_type('onlinetext');
+        $plugin->save($submission, $data);
+
+        // Simulate adding a grade.
+        $this->setUser($this->teachers[0]);
+        $data = new stdClass();
+        $data->grade = '50.0';
+        $assign->testable_apply_grade_to_user($data, $this->extrastudents[3]->id, 0);
+        $assign->testable_apply_grade_to_user($data, $this->extrasuspendedstudents[0]->id, 0);
+
+        // Create a new submission with status NEW.
+        $this->setUser($this->extrastudents[4]);
+        $submission = $assign->get_group_submission($this->extrastudents[4]->id, $groupid, true);
+
+        $this->assertEquals(2, $assign->count_grades());
+        $this->assertEquals(4, $assign->count_submissions());
+        $this->assertEquals(5, $assign->count_submissions(true));
+        $this->assertEquals(3, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_SUBMITTED));
+        $this->assertEquals(1, $assign->count_submissions_with_status(ASSIGN_SUBMISSION_STATUS_DRAFT));
+    }
+
     public function test_get_grading_userid_list() {
         $this->create_extra_users();
         $this->setUser($this->editingteachers[0]);
index 27f1c89..50fbd51 100644 (file)
@@ -31,10 +31,14 @@ class backup_book_activity_structure_step extends backup_activity_structure_step
 
     protected function define_structure() {
 
-        // Define each element separated
-        $book     = new backup_nested_element('book', array('id'), array('name', 'intro', 'introformat', 'numbering', 'customtitles', 'timecreated', 'timemodified'));
+        // Define each element separated.
+        $book = new backup_nested_element('book', array('id'), array(
+            'name', 'intro', 'introformat', 'numbering', 'navstyle',
+            'customtitles', 'timecreated', 'timemodified'));
         $chapters = new backup_nested_element('chapters');
-        $chapter  = new backup_nested_element('chapter', array('id'), array('pagenum', 'subchapter', 'title', 'content', 'contentformat', 'hidden', 'timemcreated', 'timemodified', 'importsrc',));
+        $chapter = new backup_nested_element('chapter', array('id'), array(
+            'pagenum', 'subchapter', 'title', 'content', 'contentformat',
+            'hidden', 'timemcreated', 'timemodified', 'importsrc'));
 
         $book->add_child($chapters);
         $chapters->add_child($chapter);
index 546b1a2..c1b8598 100644 (file)
@@ -12,6 +12,7 @@
         <FIELD NAME="intro" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="numbering" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="navstyle" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
         <FIELD NAME="customtitles" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="revision" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
@@ -40,4 +41,4 @@
       </KEYS>
     </TABLE>
   </TABLES>
-</XMLDB>
\ No newline at end of file
+</XMLDB>
index 3e17901..b71e386 100644 (file)
@@ -207,5 +207,20 @@ function xmldb_book_upgrade($oldversion) {
     // Moodle v2.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2014111800) {
+
+        // Define field navstyle to be added to book.
+        $table = new xmldb_table('book');
+        $field = new xmldb_field('navstyle', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '1', 'numbering');
+
+        // Conditionally launch add field navstyle.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Book savepoint reached.
+        upgrade_mod_savepoint(true, 2014111800, 'book');
+    }
+
     return true;
 }
index 94359f8..d027a5b 100644 (file)
@@ -51,6 +51,14 @@ $string['eventchapterdeleted'] = 'Chapter deleted';
 $string['eventchapterupdated'] = 'Chapter updated';
 $string['eventchapterviewed'] = 'Chapter viewed';
 $string['subchapter'] = 'Subchapter';
+$string['navimages'] = 'Images';
+$string['navoptions'] = 'Available options for navigational links';
+$string['navoptions_desc'] = 'Options for displaying navigation on the book pages';
+$string['navstyle'] = 'Style of navigation';
+$string['navstyle_help'] = '* Images - Icons are used for navigation
+* Text - Chapter titles are used for navigation';
+$string['navtext'] = 'Text';
+$string['navtoc'] = 'TOC Only';
 $string['nocontent'] = 'No content has been added to this book yet.';
 $string['numbering'] = 'Chapter formatting';
 $string['numbering_help'] = '* None - Chapter and subchapter titles have no formatting
index 8f19e2a..6e3fabc 100644 (file)
@@ -41,6 +41,28 @@ function book_get_numbering_types() {
     );
 }
 
+/**
+ * Returns list of available navigation link types.
+ * @return array
+ */
+function book_get_nav_types() {
+    require_once(dirname(__FILE__).'/locallib.php');
+
+    return array (
+        BOOK_LINK_TOCONLY   => get_string('navtoc', 'mod_book'),
+        BOOK_LINK_IMAGE     => get_string('navimages', 'mod_book'),
+        BOOK_LINK_TEXT      => get_string('navtext', 'mod_book'),
+    );
+}
+
+/**
+ * Returns list of available navigation link CSS classes.
+ * @return array
+ */
+function book_get_nav_classes() {
+    return array ('navtoc', 'navimages', 'navtext');
+}
+
 /**
  * Returns all other caps used in module
  * @return array
index 0bc9061..e9c6d5f 100644 (file)
@@ -39,6 +39,16 @@ define('BOOK_NUM_NUMBERS',  '1');
 define('BOOK_NUM_BULLETS',  '2');
 define('BOOK_NUM_INDENTED', '3');
 
+/**
+ * The following defines are used to define the navigation style used within a book.
+ * BOOK_LINK_TOCONLY    Only the table of contents is shown, in a side region.
+ * BOOK_LINK_IMAGE      Arrows link to previous/next/exit pages, in addition to the TOC.
+ * BOOK_LINK_TEXT       Page names and arrows link to previous/next/exit pages, in addition to the TOC.
+ */
+define ('BOOK_LINK_TOCONLY', '0');
+define ('BOOK_LINK_IMAGE', '1');
+define ('BOOK_LINK_TEXT', '2');
+
 /**
  * Preload book chapters and fix toc structure if necessary.
  *
index 40d9f72..e2ac096 100644 (file)
@@ -70,6 +70,25 @@ class mod_book_mod_form extends moodleform_mod {
         $mform->addHelpButton('numbering', 'numbering', 'mod_book');
         $mform->setDefault('numbering', $config->numbering);
 
+        $alloptions = book_get_nav_types();
+        $allowed = explode(',', $config->navoptions);
+        $options = array();
+        foreach ($allowed as $type) {
+            if (isset($alloptions[$type])) {
+                $options[$type] = $alloptions[$type];
+            }
+        }
+        if ($this->current->instance) {
+            if (!isset($options[$this->current->navstyle])) {
+                if (isset($alloptions[$this->current->navstyle])) {
+                    $options[$this->current->navstyle] = $alloptions[$this->current->navstyle];
+                }
+            }
+        }
+        $mform->addElement('select', 'navstyle', get_string('navstyle', 'book'), $options);
+        $mform->addHelpButton('navstyle', 'navstyle', 'mod_book');
+        $mform->setDefault('navstyle', $config->navstyle);
+
         $mform->addElement('checkbox', 'customtitles', get_string('customtitles', 'book'));
         $mform->addHelpButton('customtitles', 'customtitles', 'mod_book');
         $mform->setDefault('customtitles', 0);
index 11420ae..d759e39 100644 (file)
@@ -38,12 +38,20 @@ if ($ADMIN->fulltree) {
         get_string('numberingoptions', 'mod_book'), get_string('numberingoptions_desc', 'mod_book'),
         array_keys($options), $options));
 
+    $navoptions = book_get_nav_types();
+    $settings->add(new admin_setting_configmultiselect('book/navoptions',
+        get_string('navoptions', 'mod_book'), get_string('navoptions_desc', 'mod_book'),
+        array_keys($navoptions), $navoptions));
 
     // Modedit defaults.
 
-    $settings->add(new admin_setting_heading('bookmodeditdefaults', get_string('modeditdefaults', 'admin'), get_string('condifmodeditdefaults', 'admin')));
+    $settings->add(new admin_setting_heading('bookmodeditdefaults',
+        get_string('modeditdefaults', 'admin'), get_string('condifmodeditdefaults', 'admin')));
 
     $settings->add(new admin_setting_configselect('book/numbering',
         get_string('numbering', 'mod_book'), '', BOOK_NUM_NUMBERS, $options));
 
+    $settings->add(new admin_setting_configselect('book/navstyle',
+        get_string('navstyle', 'mod_book'), '', BOOK_LINK_IMAGE, $navoptions));
+
 }
index 4e2daa4..4a60767 100644 (file)
@@ -19,6 +19,9 @@
 .path-mod-book .navtop {
     margin-bottom: 0.5em;
 }
+.path-mod-book .navbottom {
+    margin-top: 0.5em;
+}
 
 /* == Fake toc block == */
 
 .path-mod-book .book_toc_indented li li {
     list-style: none;
 }
+
+/* Text style links */
+.navtop.navtext .chaptername,
+.navbottom.navtext .chaptername {
+    font-weight: bolder;
+}
+.navtop.navtext a,
+.navbottom.navtext a {
+    display: inline-block;
+    max-width: 45%;
+}
+.navtop.navtext a.bookprev,
+.navbottom.navtext a.bookprev {
+    float: left;
+    text-align: left;
+}
+.dir-rtl .navtop.navtext a.bookprev,
+.dir-rtl .navbottom.navtext a.bookprev {
+    float: right;
+    text-align: right;
+}
+
+@media (max-width: 480px) {
+    .path-mod-book .navbottom,
+    .path-mod-book .navtop,
+    .dir-rtl.path-mod-book .navbottom,
+    .dir-rtl.path-mod-book .navtop {
+        text-align: center;
+    }
+    .navtop.navtext a,
+    .navbottom.navtext a {
+        display: block;
+        max-width: 100%;
+        margin: auto;
+    }
+    .navtop.navtext a.bookprev,
+    .navbottom.navtext a.bookprev,
+    .dir-rtl .navtop.navtext a.bookprev,
+    .dir-rtl .navbottom.navtext a.bookprev {
+        float: none;
+    }
+}
index f5915c4..bdfc25c 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die;
 
 $plugin->component = 'mod_book'; // Full name of the plugin (used for diagnostics)
-$plugin->version   = 2014111000; // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2014111800; // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2014110400; // Requires this Moodle version
 $plugin->cron      = 0;          // Period for cron to check this module (secs)
index 3fd9b45..f2a6398 100644 (file)
@@ -127,7 +127,9 @@ book_add_fake_block($chapters, $chapter, $book, $cm, $edit);
 
 // prepare chapter navigation icons
 $previd = null;
+$prevtitle = null;
 $nextid = null;
+$nexttitle = null;
 $last = null;
 foreach ($chapters as $ch) {
     if (!$edit and $ch->hidden) {
@@ -135,37 +137,68 @@ foreach ($chapters as $ch) {
     }
     if ($last == $chapter->id) {
         $nextid = $ch->id;
+        $nexttitle = book_get_chapter_title($ch->id, $chapters, $book, $context);
         break;
     }
     if ($ch->id != $chapter->id) {
         $previd = $ch->id;
+        $prevtitle = book_get_chapter_title($ch->id, $chapters, $book, $context);
     }
     $last = $ch->id;
 }
 
-$navprevicon = right_to_left() ? 'nav_next' : 'nav_prev';
-$navnexticon = right_to_left() ? 'nav_prev' : 'nav_next';
-$navprevdisicon = right_to_left() ? 'nav_next_dis' : 'nav_prev_dis';
 
-$chnavigation = '';
-if ($previd) {
-    $chnavigation .= '<a title="'.get_string('navprev', 'book').'" href="view.php?id='.$cm->id.
-            '&amp;chapterid='.$previd.'"><img src="'.$OUTPUT->pix_url($navprevicon, 'mod_book').'" class="icon" alt="'.get_string('navprev', 'book').'"/></a>';
-} else {
-    $chnavigation .= '<img src="'.$OUTPUT->pix_url($navprevdisicon, 'mod_book').'" class="icon" alt="" />';
-}
-if ($nextid) {
-    $chnavigation .= '<a title="'.get_string('navnext', 'book').'" href="view.php?id='.$cm->id.
-            '&amp;chapterid='.$nextid.'"><img src="'.$OUTPUT->pix_url($navnexticon, 'mod_book').'" class="icon" alt="'.get_string('navnext', 'book').'" /></a>';
-} else {
-    $sec = $DB->get_field('course_sections', 'section', array('id' => $cm->section));
-    $returnurl = course_get_url($course, $sec);
-    $chnavigation .= '<a title="'.get_string('navexit', 'book').'" href="'.$returnurl.'"><img src="'.$OUTPUT->pix_url('nav_exit', 'mod_book').
-            '" class="icon" alt="'.get_string('navexit', 'book').'" /></a>';
-
-    // we are cheating a bit here, viewing the last page means user has viewed the whole book
-    $completion = new completion_info($course);
-    $completion->set_module_viewed($cm);
+if ($book->navstyle) {
+    $navprevicon = right_to_left() ? 'nav_next' : 'nav_prev';
+    $navnexticon = right_to_left() ? 'nav_prev' : 'nav_next';
+    $navprevdisicon = right_to_left() ? 'nav_next_dis' : 'nav_prev_dis';
+
+    $chnavigation = '';
+    if ($previd) {
+        $navprev = get_string('navprev', 'book');
+        if ($book->navstyle == 1) {
+            $chnavigation .= '<a title="' . $navprev . '" class="bookprev" href="view.php?id=' .
+                $cm->id . '&amp;chapterid=' . $previd .  '">' .
+                '<img src="' . $OUTPUT->pix_url($navprevicon, 'mod_book') . '" class="icon" alt="' . $navprev . '"/></a>';
+        } else {
+            $chnavigation .= '<a title="' . $navprev . '" class="bookprev" href="view.php?id=' .
+                $cm->id . '&amp;chapterid=' . $previd . '">' .
+                '<span class="chaptername"><span class="arrow">' . $OUTPUT->larrow() . '&nbsp;</span></span>' .
+                $navprev . ':&nbsp;<span class="chaptername">' . $prevtitle . '</span></a>';
+        }
+    } else {
+        if ($book->navstyle == 1) {
+            $chnavigation .= '<img src="' . $OUTPUT->pix_url($navprevdisicon, 'mod_book') . '" class="icon" alt="" />';
+        }
+    }
+    if ($nextid) {
+        $navnext = get_string('navnext', 'book');
+        if ($book->navstyle == 1) {
+            $chnavigation .= '<a title="' . $navnext . '" class="booknext" href="view.php?id=' .
+                $cm->id . '&amp;chapterid='.$nextid.'">' .
+                '<img src="' . $OUTPUT->pix_url($navnexticon, 'mod_book').'" class="icon" alt="' . $navnext . '" /></a>';
+        } else {
+            $chnavigation .= ' <a title="' . $navnext . '" class="booknext" href="view.php?id=' .
+                $cm->id . '&amp;chapterid='.$nextid.'">' .
+                $navnext . ':<span class="chaptername">&nbsp;' . $nexttitle.
+                '<span class="arrow">&nbsp;' . $OUTPUT->rarrow() . '</span></span></a>';
+        }
+    } else {
+        $navexit = get_string('navexit', 'book');
+        $sec = $DB->get_field('course_sections', 'section', array('id' => $cm->section));
+        $returnurl = course_get_url($course, $sec);
+        if ($book->navstyle == 1) {
+            $chnavigation .= '<a title="' . $navexit . '" class="bookexit"  href="'.$returnurl.'">' .
+                '<img src="' . $OUTPUT->pix_url('nav_exit', 'mod_book') . '" class="icon" alt="' . $navexit . '" /></a>';
+        } else {
+            $chnavigation .= ' <a title="' . $navexit . '" class="bookexit"  href="'.$returnurl.'">' .
+                '<span class="chaptername">' . $navexit . '&nbsp;' . $OUTPUT->uarrow() . '</span></a>';
+        }
+
+        // We cheat a bit here in assuming that viewing the last page means the user viewed the whole book.
+        $completion = new completion_info($course);
+        $completion->set_module_viewed($cm);
+    }
 }
 
 // =====================================================
@@ -175,10 +208,14 @@ if ($nextid) {
 echo $OUTPUT->header();
 echo $OUTPUT->heading($book->name);
 
-// upper nav
-echo '<div class="navtop">'.$chnavigation.'</div>';
+$navclasses = book_get_nav_classes();
+
+if ($book->navstyle) {
+    // Upper navigation.
+    echo '<div class="navtop clearfix ' . $navclasses[$book->navstyle] . '">' . $chnavigation . '</div>';
+}
 
-// chapter itself
+// The chapter itself.
 $hidden = $chapter->hidden ? ' dimmed_text' : null;
 echo $OUTPUT->box_start('generalbox book_content' . $hidden);
 
@@ -198,7 +235,9 @@ echo format_text($chaptertext, $chapter->contentformat, array('noclean'=>true, '
 
 echo $OUTPUT->box_end();
 
-// lower navigation
-echo '<div class="navbottom">'.$chnavigation.'</div>';
+if ($book->navstyle) {
+    // Lower navigation.
+    echo '<div class="navbottom clearfix ' . $navclasses[$book->navstyle] . '">' . $chnavigation . '</div>';
+}
 
 echo $OUTPUT->footer();
index 68eb53d..28f8b10 100644 (file)
@@ -2567,6 +2567,9 @@ function data_reset_userdata($data) {
     $ratingdeloptions->component = 'mod_data';
     $ratingdeloptions->ratingarea = 'entry';
 
+    // Set the file storage - may need it to remove files later.
+    $fs = get_file_storage();
+
     // delete entries if requested
     if (!empty($data->reset_data)) {
         $DB->delete_records_select('comments', "itemid IN ($allrecordssql) AND commentarea='database_entry'", array($data->courseid));
@@ -2575,13 +2578,14 @@ function data_reset_userdata($data) {
 
         if ($datas = $DB->get_records_sql($alldatassql, array($data->courseid))) {
             foreach ($datas as $dataid=>$unused) {
-                fulldelete("$CFG->dataroot/$data->courseid/moddata/data/$dataid");
-
                 if (!$cm = get_coursemodule_from_instance('data', $dataid)) {
                     continue;
                 }
                 $datacontext = context_module::instance($cm->id);
 
+                // Delete any files that may exist.
+                $fs->delete_area_files($datacontext->id, 'mod_data', 'content');
+
                 $ratingdeloptions->contextid = $datacontext->id;
                 $rm->delete_ratings($ratingdeloptions);
             }
@@ -2618,21 +2622,17 @@ function data_reset_userdata($data) {
                 $ratingdeloptions->itemid = $record->id;
                 $rm->delete_ratings($ratingdeloptions);
 
-                $DB->delete_records('comments', array('itemid'=>$record->id, 'commentarea'=>'database_entry'));
-                $DB->delete_records('data_content', array('recordid'=>$record->id));
-                $DB->delete_records('data_records', array('id'=>$record->id));
-                // HACK: this is ugly - the recordid should be before the fieldid!
-                if (!array_key_exists($record->dataid, $fields)) {
-                    if ($fs = $DB->get_records('data_fields', array('dataid'=>$record->dataid))) {
-                        $fields[$record->dataid] = array_keys($fs);
-                    } else {
-                        $fields[$record->dataid] = array();
+                // Delete any files that may exist.
+                if ($contents = $DB->get_records('data_content', array('recordid' => $record->id), '', 'id')) {
+                    foreach ($contents as $content) {
+                        $fs->delete_area_files($datacontext->id, 'mod_data', 'content', $content->id);
                     }
                 }
-                foreach($fields[$record->dataid] as $fieldid) {
-                    fulldelete("$CFG->dataroot/$data->courseid/moddata/data/$record->dataid/$fieldid/$record->id");
-                }
                 $notenrolled[$record->userid] = true;
+
+                $DB->delete_records('comments', array('itemid' => $record->id, 'commentarea' => 'database_entry'));
+                $DB->delete_records('data_content', array('recordid' => $record->id));
+                $DB->delete_records('data_records', array('id' => $record->id));
             }
         }
         $rs->close();
index 67e7b08..90a8045 100644 (file)
@@ -133,12 +133,32 @@ class reply_handler extends \core\message\inbound\handler {
             throw new \core\message\inbound\processing_failed_exception('messageinboundthresholdhit', 'mod_forum', $data);
         }
 
+        $subject = clean_param($messagedata->envelope->subject, PARAM_TEXT);
+        $restring = get_string('re', 'forum');
+        if (strpos($subject, $discussion->name)) {
+            // The discussion name is mentioned in the e-mail subject. This is probably just the standard reply. Use the
+            // standard reply subject instead.
+            $newsubject = $restring . ' ' . $discussion->name;
+            mtrace("--> Note: Post subject matched discussion name. Optimising from {$subject} to {$newsubject}");
+            $subject = $newsubject;
+        } else if (strpos($subject, $post->subject)) {
+            // The replied-to post's subject is mentioned in the e-mail subject.
+            // Use the previous post's subject instead of the e-mail subject.
+            $newsubject = $post->subject;
+            if (!strpos($restring, $post->subject)) {
+                // The previous post did not contain a re string, add it.
+                $newsubject = $restring . ' ' . $subject;
+            }
+            mtrace("--> Note: Post subject matched original post subject. Optimising from {$subject} to {$newsubject}");
+            $subject = $newsubject;
+        }
+
         $addpost = new \stdClass();
         $addpost->course       = $course->id;
         $addpost->forum        = $forum->id;
         $addpost->discussion   = $discussion->id;
         $addpost->modified     = $messagedata->timestamp;
-        $addpost->subject      = clean_param($messagedata->envelope->subject, PARAM_TEXT);
+        $addpost->subject      = $subject;
         $addpost->parent       = $post->id;
         $addpost->itemid       = file_get_unused_draft_itemid();
 
index 36d24e6..fc81e87 100644 (file)
@@ -544,15 +544,15 @@ function forum_cron() {
                 }
             }
 
+            // Save the Inbound Message datakey here to reduce DB queries later.
+            $messageinboundgenerator->set_data($pid);
+            $messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key();
+
             // Caching subscribed users of each forum.
             if (!isset($subscribedusers[$forumid])) {
                 $modcontext = context_module::instance($coursemodules[$forumid]->id);
                 if ($subusers = \mod_forum\subscriptions::fetch_subscribed_users($forums[$forumid], 0, $modcontext, 'u.*', true)) {
 
-                    // Save the Inbound Message datakey here to reduce DB queries later.
-                    $messageinboundgenerator->set_data($pid);
-                    $messageinboundhandlers[$pid] = $messageinboundgenerator->fetch_data_key();
-
                     foreach ($subusers as $postuser) {
                         // this user is subscribed to this forum
                         $subscribedusers[$forumid][$postuser->id] = $postuser->id;
index 45b4a5d..9b1d9ba 100644 (file)
@@ -77,7 +77,7 @@ if (isset($cm->groupmode) && empty($course->groupmodeforce)) {
 $issubscribed = \mod_forum\subscriptions::is_subscribed($user->id, $forum, $discussionid, $cm);
 
 // For a user to subscribe when a groupmode is set, they must have access to at least one group.
-if ($groupmode && !$issubscribed && !has_capability('moodle/site:accessallgroups')) {
+if ($groupmode && !$issubscribed && !has_capability('moodle/site:accessallgroups', $context)) {
     if (!groups_get_all_groups($course->id, $USER->id)) {
         print_error('cannotsubscribe', 'forum');
     }
index 7346107..db6eb94 100644 (file)
@@ -78,6 +78,23 @@ class mod_forum_mail_testcase extends advanced_testcase {
         $this->helper->mailsink->close();
     }
 
+    /**
+     * Perform message inbound setup for the mod_forum reply handler.
+     */
+    protected function helper_spoof_message_inbound_setup() {
+        global $CFG, $DB;
+        // Setup the default Inbound Message mailbox settings.
+        $CFG->messageinbound_domain = 'example.com';
+        $CFG->messageinbound_enabled = true;
+
+        // Must be no longer than 15 characters.
+        $CFG->messageinbound_mailbox = 'moodlemoodle123';
+
+        $record = $DB->get_record('messageinbound_handlers', array('classname' => '\mod_forum\message\inbound\reply_handler'));
+        $record->enabled = true;
+        $record->id = $DB->update_record('messageinbound_handlers', $record);
+    }
+
     /**
      * Helper to create the required number of users in the specified
      * course.
@@ -730,4 +747,67 @@ class mod_forum_mail_testcase extends advanced_testcase {
         // Run cron and check that the expected number of users received the notification.
         $messages = $this->helper_run_cron_check_counts($expectedmessages, $expectedcount);
     }
+
+    public function test_forum_message_inbound_multiple_posts() {
+        $this->resetAfterTest(true);
+
+        // Create a course, with a forum.
+        $course = $this->getDataGenerator()->create_course();
+        $options = array('course' => $course->id, 'forcesubscribe' => FORUM_FORCESUBSCRIBE);
+        $forum = $this->getDataGenerator()->create_module('forum', $options);
+
+        // Create a user enrolled in the course as a student.
+        list($author) = $this->helper_create_users($course, 1);
+
+        $expectedmessages = array();
+
+        // Post a discussion to the forum.
+        list($discussion, $post) = $this->helper_post_to_forum($forum, $author);
+        $this->helper_update_post_time($post, -90);
+
+        $expectedmessages[] = array(
+            'id' => $post->id,
+            'subject' => $post->subject,
+            'count' => 0,
+        );
+
+        // Then post a reply to the first discussion.
+        $reply = $this->helper_post_to_discussion($forum, $discussion, $author);
+        $this->helper_update_post_time($reply, -60);
+
+        $expectedmessages[] = array(
+            'id' => $reply->id,
+            'subject' => $reply->subject,
+            'count' => 1,
+        );
+
+        $expectedcount = 2;
+
+        // Ensure that messageinbound is enabled and configured for the forum handler.
+        $this->helper_spoof_message_inbound_setup();
+
+        $author->emailstop = '0';
+        set_user_preference('message_provider_mod_forum_posts_loggedoff', 'email', $author);
+        set_user_preference('message_provider_mod_forum_posts_loggedin', 'email', $author);
+
+        // Run cron and check that the expected number of users received the notification.
+        // Clear the mailsink, and close the messagesink.
+        $this->helper->mailsink->clear();
+        $this->helper->messagesink->close();
+
+        // Cron daily uses mtrace, turn on buffering to silence output.
+        foreach ($expectedmessages as $post) {
+            $this->expectOutputRegex("/{$post['count']} users were sent post {$post['id']}, '{$post['subject']}'/");
+        }
+
+        forum_cron();
+        $messages = $this->helper->mailsink->get_messages();
+
+        // There should be the expected number of messages.
+        $this->assertEquals($expectedcount, count($messages));
+
+        foreach ($messages as $message) {
+            $this->assertRegExp('/Reply-To: moodlemoodle123\+[^@]*@example.com/', $message->header);
+        }
+    }
 }
diff --git a/mod/lesson/tests/behat/lesson_progress_bar.feature b/mod/lesson/tests/behat/lesson_progress_bar.feature
new file mode 100644 (file)
index 0000000..3236688
--- /dev/null
@@ -0,0 +1,84 @@
+@mod @mod_lesson
+Feature: In a lesson activity, students can see their progress viewing a progress bar.
+  In order to create a lesson with conditional paths
+  As a teacher
+  I need to add pages and questions with links between them
+
+  @javascript
+  Scenario: Student navigation with progress bar
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | student1 | Student | 1 | student1@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 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Lesson" to section "1" and I fill the form with:
+      | Name | Test lesson name |
+      | Description | Test lesson description |
+      | Progress bar | Yes |
+    And I follow "Test lesson name"
+    And I follow "Add a content page"
+    And I set the following fields to these values:
+      | Page title | First page name |
+      | Page contents | First page contents |
+      | id_answer_editor_0 | Next page |
+      | id_jumpto_0 | Next page |
+    And I press "Save page"
+    And I set the field "qtype" to "Add a content page"
+    And I set the following fields to these values:
+      | Page title | Second page name |
+      | Page contents | Second page contents |
+      | id_answer_editor_0 | Previous page |
+      | id_jumpto_0 | Previous page |
+      | id_answer_editor_1 | Next page |
+      | id_jumpto_1 | Next page |
+    And I press "Save page"
+    And I follow "Expanded"
+    And I click on "Add a question page here" "link" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' addlinks ')][3]" "xpath_element"
+    And I set the field "Select a question type" to "Numerical"
+    And I press "Add a question page"
+    And I set the following fields to these values:
+      | Page title | Hardest question ever |
+      | Page contents | 1 + 1? |
+      | id_answer_editor_0 | 2 |
+      | id_response_editor_0 | Correct answer |
+      | id_jumpto_0 | End of lesson |
+      | id_score_0 | 1 |
+      | id_answer_editor_1 | 1 |
+      | id_response_editor_1 | Incorrect answer |
+      | id_jumpto_1 | Second page name |
+      | id_score_1 | 0 |
+    And I press "Save page"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    When I follow "Test lesson name"
+    Then I should see "First page contents"
+    And I should see "You have completed 0% of the lesson"
+    And I press "Next page"
+    And I should see "Second page contents"
+    And I should see "You have completed 33% of the lesson"
+    And I press "Previous page"
+    And I should see "First page contents"
+    And I should see "You have completed 67% of the lesson"
+    And I press "Next page"
+    And I should see "Second page contents"
+    And I should see "You have completed 67% of the lesson"
+    And I press "Next page"
+    And I should see "1 + 1?"
+    And I should see "You have completed 67% of the lesson"
+    And I set the following fields to these values:
+      | Your answer | 2 |
+    And I press "Submit"
+    And I should see "Correct answer"
+    And I press "Continue"
+    And I should see "Congratulations - end of lesson reached"
+    And I should see "You have completed 100% of the lesson"
index 0a008b9..4b6824a 100644 (file)
@@ -429,6 +429,8 @@ if ($pageid != LESSON_EOL) {
         $ntries--;  // need to look at the old attempts :)
     }
     if (!$canmanage) {
+        // Store this now before any modifications to pages viewed.
+        $progressbar = $lessonoutput->progress_bar($lesson);
         // Update the clock / get time information for this user.
         $lesson->stop_timer();
         $gradeinfo = lesson_grade($lesson, $ntries);
@@ -489,15 +491,16 @@ if ($pageid != LESSON_EOL) {
                     if (!$lesson->practice) {
                         $newgradeid = $DB->insert_record("lesson_grades", $grade);
                     }
-                    $lessoncontent .= get_string("eolstudentoutoftimenoanswers", "lesson");
+                    $lessoncontent .= $lessonoutput->paragraph(get_string("eolstudentoutoftimenoanswers", "lesson"));
                 }
             } else {
-                $lessoncontent .= get_string("welldone", "lesson");
+                $lessoncontent .= $lessonoutput->paragraph(get_string("welldone", "lesson"));
             }
         }
 
         // update central gradebook
         lesson_update_grades($lesson, $USER->id);
+        $lessoncontent .= $progressbar;
 
     } else {
         // display for teacher
index a4bee79..694ab98 100644 (file)
@@ -120,7 +120,12 @@ class mod_quiz_mod_form extends moodleform_mod {
         $this->standard_grading_coursemodule_elements();
 
         $mform->removeElement('grade');
-        $mform->addElement('hidden', 'grade', $quizconfig->maximumgrade);
+        if (property_exists($this->current, 'grade')) {
+            $currentgrade = $this->current->grade;
+        } else {
+            $currentgrade = $quizconfig->maximumgrade;
+        }
+        $mform->addElement('hidden', 'grade', $currentgrade);
         $mform->setType('grade', PARAM_FLOAT);
 
         // Number of attempts.
@@ -471,7 +476,7 @@ class mod_quiz_mod_form extends moodleform_mod {
 
                 if ($feedback->mingrade > 0) {
                     $toform['feedbackboundaries['.$key.']'] =
-                            (100.0 * $feedback->mingrade / $toform['grade']) . '%';
+                            round(100.0 * $feedback->mingrade / $toform['grade'], 6) . '%';
                 }
                 $key++;
             }
index b66b83d..2653979 100644 (file)
@@ -46,7 +46,9 @@ function quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts =
     if ($groupstudents) {
         ksort($groupstudents);
         list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents),
-                                                         SQL_PARAMS_NAMED, 'u');
+                SQL_PARAMS_NAMED, 'statsuser');
+        list($grpsql, $grpparams) = quiz_statistics_renumber_placeholders(
+                $grpsql, $grpparams, 'statsuser');
         $whereqa .= " AND quiza.userid $grpsql";
         $qaparams += $grpparams;
     }
@@ -63,6 +65,31 @@ function quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts =
     return array($fromqa, $whereqa, $qaparams);
 }
 
+/**
+ * Re-number all the params beginning with $paramprefix in a fragment of SQL.
+ *
+ * @param string $sql the SQL.
+ * @param array $params the params.
+ * @param string $paramprefix the parameter prefix.
+ * @return array with two elements, the modified SQL, and the modified params.
+ */
+function quiz_statistics_renumber_placeholders($sql, $params, $paramprefix) {
+    $basenumber = null;
+    $newparams = array();
+    $newsql = preg_replace_callback('~:' . preg_quote($paramprefix, '~') . '(\d+)\b~',
+            function($match) use ($paramprefix, $params, &$newparams, &$basenumber) {
+                if ($basenumber === null) {
+                    $basenumber = $match[1] - 1;
+                }
+                $oldname = $paramprefix . $match[1];
+                $newname = $paramprefix . ($match[1] - $basenumber);
+                $newparams[$newname] = $params[$oldname];
+                return ':' . $newname;
+            }, $sql);
+
+    return array($newsql, $newparams);
+}
+
 /**
  * Return a {@link qubaid_condition} from the values returned by {@link quiz_statistics_attempts_sql}.
  *
diff --git a/mod/quiz/report/statistics/tests/statisticslib_test.php b/mod/quiz/report/statistics/tests/statisticslib_test.php
new file mode 100644 (file)
index 0000000..edfb33b
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Unit tests for (some of) statisticslib.php.
+ *
+ * @package   quiz_statistics
+ * @category  test
+ * @copyright 2014 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
+
+/**
+ * Unit tests for (some of) statisticslib.php.
+ *
+ * @copyright 2014 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class quiz_statistics_statisticslib_testcase extends basic_testcase {
+
+    public function test_quiz_statistics_renumber_placeholders_no_op() {
+        list($sql, $params) = quiz_statistics_renumber_placeholders(
+                ' IN (:u1, :u2)', array('u1' => 1, 'u2' => 2), 'u');
+        $this->assertEquals(' IN (:u1, :u2)', $sql);
+        $this->assertEquals(array('u1' => 1, 'u2' => 2), $params);
+    }
+
+    public function test_quiz_statistics_renumber_placeholders_work_to_do() {
+        list($sql, $params) = quiz_statistics_renumber_placeholders(
+                'frog1 IN (:frog100 , :frog101)', array('frog100' => 1, 'frog101' => 2), 'frog');
+        $this->assertEquals('frog1 IN (:frog1 , :frog2)', $sql);
+        $this->assertEquals(array('frog1' => 1, 'frog2' => 2), $params);
+    }
+}
index 201cc9b..9b4cd66 100644 (file)
@@ -802,6 +802,9 @@ M.mod_scorm.connectPrereqCallback = {
             }
             var el_new_tree = document.createElement('div');
             var pagecontent = document.getElementById("page-content");
+            if (!pagecontent) {
+                pagecontent = document.getElementById("content");
+            }
             el_new_tree.setAttribute('id','scormtree123');
             el_new_tree.innerHTML = o.responseText;
             // Make sure it does not show.
index d253fe4..0f642d8 100644 (file)
@@ -91,6 +91,12 @@ class creole_parser extends wiki_markup_parser {
         parent::before_parsing();
     }
 
+    public function get_section($header, $text, $clean = false) {
+        // The requested header is likely to have been passed to htmlspecialchars in
+        // self::before_parsing(), therefore we should decode it when looking for it.
+        return parent::get_section(htmlspecialchars_decode($header), $text, $clean);
+    }
+
     protected function header_block_rule($match) {
         $num = strlen($match[1]);
 
index f2c7067..a2ae932 100644 (file)
@@ -17,10 +17,14 @@ class html_parser extends nwiki_parser {
 
     public function __construct() {
         parent::__construct();
-        $this->tagrules = array('link' => $this->tagrules['link'], 'url' => $this->tagrules['url']);
-
-        // Headers are considered tags here.
-        $this->tagrules['header'] = array('expression' => "/<\s*h([1-$this->maxheaderdepth])\s*>(.+?)<\/h[1-$this->maxheaderdepth]>/is"
+        // The order is important, headers should be parsed before links.
+        $this->tagrules = array(
+            // Headers are considered tags here.
+            'header' => array(
+                'expression' => "/<\s*h([1-$this->maxheaderdepth])\s*>(.+?)<\/h[1-$this->maxheaderdepth]>/is"
+            ),
+            'link' => $this->tagrules['link'],
+            'url' => $this->tagrules['url']
         );
     }
 
@@ -52,7 +56,8 @@ class html_parser extends nwiki_parser {
 
         $h1 = array("<\s*h1\s*>", "<\/h1>");
 
-        preg_match("/(.*?)({$h1[0]}\s*\Q$header\E\s*{$h1[1]}.*?)((?:\n{$h1[0]}.*)|$)/is", $text, $match);
+        $regex = "/(.*?)({$h1[0]}\s*".preg_quote($header, '/')."\s*{$h1[1]}.*?)((?:\n{$h1[0]}.*)|$)/is";
+        preg_match($regex, $text, $match);
 
         if (!empty($match)) {
             return array($match[1], $match[2], $match[3]);
index 1a675d5..4f5135d 100644 (file)
@@ -181,7 +181,9 @@ abstract class wiki_markup_parser extends generic_parser {
         $text = trim($text);
 
         if (!$this->pretty_print && $level == 1) {
-            $text .= parser_utils::h('a', '['.get_string('editsection', 'wiki').']', array('href' => "edit.php?pageid={$this->wiki_page_id}&section=" . urlencode($text), 'class' => 'wiki_edit_section'));
+            $text .= ' ' . parser_utils::h('a', '['.get_string('editsection', 'wiki').']',
+                array('href' => "edit.php?pageid={$this->wiki_page_id}&section=" . urlencode($text),
+                    'class' => 'wiki_edit_section'));
         }
 
         if ($level <= $this->maxheaderdepth) {
@@ -401,7 +403,8 @@ abstract class wiki_markup_parser extends generic_parser {
             $text .= "\n\n";
         }
 
-        preg_match("/(.*?)(=\ *\Q$header\E\ *=*\n.*?)((?:\n=[^=]+.*)|$)/is", $text, $match);
+        $regex = "/(.*?)(=\ *".preg_quote($header, '/')."\ *=*\n.*?)((?:\n=[^=]+.*)|$)/is";
+        preg_match($regex, $text, $match);
 
         if (!empty($match)) {
             return array($match[1], $match[2], $match[3]);
index a7418e0..8f74d12 100644 (file)
@@ -64,7 +64,7 @@ class mod_wiki_wikiparser_test extends basic_testcase {
         $result['parsed_text'] = preg_replace('~[\r\n]~', '', $result['parsed_text']);
         $output                = preg_replace('~[\r\n]~', '', $output);
 
-        $this->assertEquals($result['parsed_text'], $output);
+        $this->assertEquals($output, $result['parsed_text']);
         return true;
     }
 
@@ -74,4 +74,143 @@ class mod_wiki_wikiparser_test extends basic_testcase {
             $i++;
         }
     }
+
+    /**
+     * Check that headings with special characters work as expected with HTML.
+     *
+     * - The heading itself is well displayed,
+     * - The TOC heading is well display,
+     * - The edit link points to the right page,
+     * - The links properly works with get_section.
+     */
+    public function test_special_headings() {
+
+        // First testing HTML markup.
+
+        // Test section name using HTML entities.
+        $input = '<h1>Code &amp; Test</h1>';
+        $output = '<h1><a name="toc-1"></a>Code &amp; Test <a href="edit.php?pageid=&amp;section=Code+%26amp%3B+Test" '.
+            'class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Code &amp; Test <a href="edit.php?pageid=&amp;section=Code+%26amp%3B+'.
+            'Test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'html', 'Code &amp; Test');
+        $actual = wiki_parser_proxy::parse($input, 'html');
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+
+        // Test section name using non-ASCII characters.
+        $input = '<h1>Another áéíóú瀠test</h1>';
+        $output = '<h1><a name="toc-1"></a>Another áéíóú瀠test <a href="edit.php?pageid=&amp;section=Another+%C'.
+            '3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%A7%E2%82%AC+test" class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Another áéíóú瀠test <a href="edit.php?pageid=&amp;section=Another+%C'.
+            '3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%A7%E2%82%AC+test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'html', 'Another áéíóú瀠test');
+        $actual = wiki_parser_proxy::parse($input, 'html');
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+
+        // Test section name with a URL.
+        $input = '<h1>Another http://moodle.org test</h1>';
+        $output = '<h1><a name="toc-1"></a>Another <a href="http://moodle.org">http://moodle.org</a> test <a href="edit.php'.
+            '?pageid=&amp;section=Another+http%3A%2F%2Fmoodle.org+test" class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Another http://moodle.org test <a href="edit.php?pageid=&amp;section='.
+            'Another+http%3A%2F%2Fmoodle.org+test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'html', 'Another http://moodle.org test');
+        $actual = wiki_parser_proxy::parse($input, 'html', array(
+            'link_callback' => '/mod/wiki/locallib.php:wiki_parser_link'
+        ));
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+
+        // Now going to test Creole markup.
+        // Note that Creole uses links to the escaped version of the section.
+
+        // Test section name using HTML entities.
+        $input = '= Code & Test =';
+        $output = '<h1><a name="toc-1"></a>Code &amp; Test <a href="edit.php?pageid=&amp;section=Code+%26amp%3B+Test" '.
+            'class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Code &amp; Test <a href="edit.php?pageid=&amp;section=Code+%26amp%3B+'.
+            'Test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'creole', 'Code &amp; Test');
+        $actual = wiki_parser_proxy::parse($input, 'creole');
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+
+        // Test section name using non-ASCII characters.
+        $input = '= Another áéíóú瀠test =';
+        $output = '<h1><a name="toc-1"></a>Another áéíóú瀠test <a href="edit.php?pageid=&amp;section=Another+%C'.
+            '3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%A7%E2%82%AC+test" class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Another áéíóú瀠test <a href="edit.php?pageid=&amp;section=Another+%C'.
+            '3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%A7%E2%82%AC+test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'creole', 'Another áéíóú瀠test');
+        $actual = wiki_parser_proxy::parse($input, 'creole');
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+
+        // Test section name with a URL, creole does not support linking links in a heading.
+        $input = '= Another http://moodle.org test =';
+        $output = '<h1><a name="toc-1"></a>Another http://moodle.org test <a href="edit.php'.
+            '?pageid=&amp;section=Another+http%3A%2F%2Fmoodle.org+test" class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Another http://moodle.org test <a href="edit.php?pageid=&amp;section='.
+            'Another+http%3A%2F%2Fmoodle.org+test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'creole', 'Another http://moodle.org test');
+        $actual = wiki_parser_proxy::parse($input, 'creole');
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+
+        // Now going to test NWiki markup.
+        // Note that Creole uses links to the escaped version of the section.
+
+        // Test section name using HTML entities.
+        $input = '= Code & Test =';
+        $output = '<h1><a name="toc-1"></a>Code & Test <a href="edit.php?pageid=&amp;section=Code+%26+Test" '.
+            'class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Code & Test <a href="edit.php?pageid=&amp;section=Code+%26+'.
+            'Test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'nwiki', 'Code & Test');
+        $actual = wiki_parser_proxy::parse($input, 'nwiki');
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+
+        // Test section name using non-ASCII characters.
+        $input = '= Another áéíóú瀠test =';
+        $output = '<h1><a name="toc-1"></a>Another áéíóú瀠test <a href="edit.php?pageid=&amp;section=Another+%C'.
+            '3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%A7%E2%82%AC+test" class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Another áéíóú瀠test <a href="edit.php?pageid=&amp;section=Another+%C'.
+            '3%A1%C3%A9%C3%AD%C3%B3%C3%BA%C3%A7%E2%82%AC+test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'nwiki', 'Another áéíóú瀠test');
+        $actual = wiki_parser_proxy::parse($input, 'nwiki');
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+
+        // Test section name with a URL, nwiki does not support linking links in a heading.
+        $input = '= Another http://moodle.org test =';
+        $output = '<h1><a name="toc-1"></a>Another http://moodle.org test <a href="edit.php'.
+            '?pageid=&amp;section=Another+http%3A%2F%2Fmoodle.org+test" class="wiki_edit_section">[edit]</a></h1>' . "\n";
+        $toc = '<div class="wiki-toc"><p class="wiki-toc-title">Table of contents</p><p class="wiki-toc-section-1 '.
+            'wiki-toc-section">1. <a href="#toc-1">Another http://moodle.org test <a href="edit.php?pageid=&amp;section='.
+            'Another+http%3A%2F%2Fmoodle.org+test" class="wiki_edit_section">[edit]</a></a></p></div>';
+        $section = wiki_parser_proxy::get_section($input, 'nwiki', 'Another http://moodle.org test');
+        $actual = wiki_parser_proxy::parse($input, 'nwiki');
+        $this->assertEquals($output, $actual['parsed_text']);
+        $this->assertEquals($toc, $actual['toc']);
+        $this->assertNotEquals(false, $section);
+    }
+
 }
index 7303103..b75057b 100644 (file)
@@ -1082,17 +1082,6 @@ class qtype_calculated extends question_type {
                 $ans = new qtype_numerical_answer(0, $ansvalue, 0, '', 0, $answer->tolerance);
                 $ans->tolerancetype = $answer->tolerancetype;
                 list($answer->min, $answer->max) = $ans->get_tolerance_interval($answer);
-
-                $formattedmin = qtype_calculated_calculate_answer(
-                    $answer->min, $data, $answer->tolerance,
-                    $answer->tolerancetype, $answer->correctanswerlength,
-                    $answer->correctanswerformat, $unit);
-                $answer->min = $formattedmin->answer;
-                $formattedmax = qtype_calculated_calculate_answer(
-                    $answer->max, $data, $answer->tolerance,
-                    $answer->tolerancetype, $answer->correctanswerlength,
-                    $answer->correctanswerformat, $unit);
-                $answer->max = $formattedmax->answer;
             }
             if ($answer->min === '') {
                 // This should mean that something is wrong.
index 25bb947..1a1b95a 100644 (file)
@@ -309,32 +309,22 @@ class report_log_renderable implements renderable {
         $sitecontext = context_system::instance();
         // First check to see if we can override showcourses and showusers.
         $numcourses = $DB->count_records("course");
+        if ($numcourses < COURSE_MAX_COURSES_PER_DROPDOWN && !$this->showcourses) {
+            $this->showcourses = 1;
+        }
+
         // Check if course filter should be shown.
-        if ((has_capability('report/log:view', $sitecontext)) && ($this->showcourses ||
-                (empty($this->showcourses) && ($numcourses < COURSE_MAX_COURSES_PER_DROPDOWN)))) {
-            $courses[0] = get_string('sitelogs');
-            $this->showcourses = true;
+        if (has_capability('report/log:view', $sitecontext) && $this->showcourses) {
             if ($courserecords = $DB->get_records("course", null, "fullname", "id,shortname,fullname,category")) {
                 foreach ($courserecords as $course) {
-                    if ($course->category) {
-                        $courses[$course->id] = format_string(get_course_display_name_for_list($course));
+                    if ($course->id == SITEID) {
+                        $courses[$course->id] = format_string($course->fullname) . ' (' . get_string('site') . ')';
                     } else {
-                        $courses[$course->id] = $SITE->shortname;
+                        $courses[$course->id] = format_string(get_course_display_name_for_list($course));
                     }
                 }
             }
             core_collator::asort($courses);
-        } else {
-            if (!empty($this->course->id)) {
-                $coursecontext = context_course::instance($this->course->id);
-                if (has_capability('report/log:view', $coursecontext)) {
-                    $courses[$this->course->id] = format_string(get_course_display_name_for_list($this->course));
-                } else {
-                    $this->showcourses = false;
-                }
-            } else {
-                $this->showcourses = false;
-            }
         }
         return $courses;
     }
@@ -384,9 +374,12 @@ class report_log_renderable implements renderable {
         $courseusers = get_enrolled_users($context, '', $this->groupid, 'u.id, ' . get_all_user_name_fields(true, 'u'),
                 null, $limitfrom, $limitnum);
 
+        if (count($courseusers) < COURSE_MAX_USERS_PER_DROPDOWN && !$this->showusers) {
+            $this->showusers = 1;
+        }
+
         $users = array();
-        if (($this->showusers) || (count($courseusers) < COURSE_MAX_USERS_PER_DROPDOWN && empty($this->showusers))) {
-            $this->showusers = true;
+        if ($this->showusers) {
             if ($courseusers) {
                 foreach ($courseusers as $courseuser) {
                      $users[$courseuser->id] = fullname($courseuser, has_capability('moodle/site:viewfullnames', $context));
index 607737e..88dcc62 100644 (file)
@@ -94,27 +94,26 @@ class report_log_renderer extends plugin_renderer_base {
         $selectedcourseid = empty($reportlog->course) ? 0 : $reportlog->course->id;
 
         // Add course selector.
+        $sitecontext = context_system::instance();
         $courses = $reportlog->get_course_list();
-        if (!empty($courses)) {
-            if ($reportlog->showcourses) {
-                echo html_writer::label(get_string('selectacourse'), 'menuid', false, array('class' => 'accesshide'));
-                echo html_writer::select($courses, "id", $selectedcourseid, null);
-            } else {
-                $courseoption[$selectedcourseid] = $courses[$selectedcourseid];
-                unset($courses);
-                echo html_writer::label(get_string('selectacourse'), 'menuid', false, array('class' => 'accesshide'));
-                echo html_writer::select($courseoption, "id", $selectedcourseid, null);
-
-                // Check if user is admin and this came because of limitation on number of courses to show in dropdown.
-                $sitecontext = context_system::instance();
-                if (has_capability('report/log:view', $sitecontext)) {
-                    $a = new stdClass();
-                    $a->url = new moodle_url('/report/log/index.php', array('chooselog' => 0,
-                        'group' => $reportlog->get_selected_group(), 'user' => $reportlog->userid,
-                        'id' => $selectedcourseid, 'date' => $reportlog->date, 'modid' => $reportlog->modid,
-                        'showcourses' => 1, 'showusers' => $reportlog->showusers));
-                    print_string('logtoomanycourses', 'moodle', $a);
-                }
+        if (!empty($courses) && $reportlog->showcourses) {
+            echo html_writer::label(get_string('selectacourse'), 'menuid', false, array('class' => 'accesshide'));
+            echo html_writer::select($courses, "id", $selectedcourseid, null);
+        } else {
+            $courses = array();
+            $courses[$selectedcourseid] = get_course_display_name_for_list($reportlog->course) . (($selectedcourseid == SITEID) ?
+                ' (' . get_string('site') . ') ' : '');
+            echo html_writer::label(get_string('selectacourse'), 'menuid', false, array('class' => 'accesshide'));
+            echo html_writer::select($courses, "id", $selectedcourseid, false);
+            // Check if user is admin and this came because of limitation on number of courses to show in dropdown.
+            if (has_capability('report/log:view', $sitecontext)) {
+                $a = new stdClass();
+                $a->url = new moodle_url('/report/log/index.php', array('chooselog' => 0,
+                    'group' => $reportlog->get_selected_group(), 'user' => $reportlog->userid,
+                    'id' => $selectedcourseid, 'date' => $reportlog->date, 'modid' => $reportlog->modid,
+                    'showcourses' => 1, 'showusers' => $reportlog->showusers));
+                $a->url = $a->url->out(false);
+                print_string('logtoomanycourses', 'moodle', $a);
             }
         }
 
@@ -127,26 +126,26 @@ class report_log_renderer extends plugin_renderer_base {
 
         // Add user selector.
         $users = $reportlog->get_user_list();
-        if (!empty($users)) {
-            if ($reportlog->showusers) {
-                echo html_writer::label(get_string('selctauser'), 'menuuser', false, array('class' => 'accesshide'));
-                echo html_writer::select($users, "user", $reportlog->userid, get_string("allparticipants"));
+
+        if ($reportlog->showusers) {
+            echo html_writer::label(get_string('selctauser'), 'menuuser', false, array('class' => 'accesshide'));
+            echo html_writer::select($users, "user", $reportlog->userid, get_string("allparticipants"));
+        } else {
+            $users = array();
+            if (!empty($reportlog->userid)) {
+                $users[$reportlog->userid] = $reportlog->get_selected_user_fullname();
             } else {
-                $users = array();
-                if (!empty($reportlog->userid)) {
-                    $users[$reportlog->userid] = $reportlog->get_selected_user_fullname();
-                } else {
-                    $users[0] = get_string('allparticipants');
-                }
-                echo html_writer::label(get_string('selctauser'), 'menuuser', false, array('class' => 'accesshide'));
-                echo html_writer::select($users, "user", $reportlog->userid, false);
-                $a = new stdClass();
-                $a->url = new moodle_url('/report/log/index.php', array('chooselog' => 0,
-                    'group' => $reportlog->get_selected_group(), 'user' => $reportlog->userid,
-                    'id' => $selectedcourseid, 'date' => $reportlog->date, 'modid' => $reportlog->modid,
-                    'showcourses' => 1, 'showusers' => $reportlog->showusers, 'showcourses' => $reportlog->showcourses));
-                print_string('logtoomanyusers', 'moodle', $a);
+                $users[0] = get_string('allparticipants');
             }
+            echo html_writer::label(get_string('selctauser'), 'menuuser', false, array('class' => 'accesshide'));
+            echo html_writer::select($users, "user", $reportlog->userid, false);
+            $a = new stdClass();
+            $a->url = new moodle_url('/report/log/index.php', array('chooselog' => 0,
+                'group' => $reportlog->get_selected_group(), 'user' => $reportlog->userid,
+                'id' => $selectedcourseid, 'date' => $reportlog->date, 'modid' => $reportlog->modid,
+                'showusers' => 1, 'showcourses' => $reportlog->showcourses));
+            $a->url = $a->url->out(false);
+            print_string('logtoomanyusers', 'moodle', $a);
         }
 
         // Add date selector.
index 14f18b4..d5e00ee 100644 (file)
@@ -44,8 +44,11 @@ $logreader      = optional_param('logreader', '', PARAM_COMPONENT); // Reader wh
 $edulevel    = optional_param('edulevel', -1, PARAM_INT); // Educational level.
 
 $params = array();
-if ($id !== 0) {
+if (!empty($id)) {
     $params['id'] = $id;
+} else {
+    $site = get_site();
+    $id = $site->id;
 }
 if ($group !== 0) {
     $params['group'] = $group;
index 0f5d2c1..a9ad191 100644 (file)
@@ -32,7 +32,7 @@ Feature: In a report, admin can filter log data
     And I log out
     And I log in as "admin"
     When I navigate to "Logs" node in "Site administration > Reports"
-    And I set the field "id" to "Site logs"
+    And I set the field "id" to "Acceptance test site (Site)"
     And I set the field "user" to "All participants"
     And I set the field "logreader" to "Standard log"
     And I press "Get these logs"
@@ -52,7 +52,7 @@ Feature: In a report, admin can filter log data
     And I log out
     And I log in as "admin"
     When I navigate to "Logs" node in "Site administration > Reports"
-    And I set the field "id" to "Site logs"
+    And I set the field "id" to "Acceptance test site (Site)"
     And I set the field "user" to "All participants"
     And I press "Get these logs"
     Then I should see "User logged in as another user"
@@ -76,7 +76,7 @@ Feature: In a report, admin can filter log data
     And I log out
     And I log in as "admin"
     When I navigate to "Logs" node in "Site administration > Reports"
-    And I set the field "id" to "Site logs"
+    And I set the field "id" to "Acceptance test site (Site)"
     And I set the field "user" to "All participants"
     And I press "Get these logs"
     Then I should see "user login"
diff --git a/report/usersessions/db/access.php b/report/usersessions/db/access.php
new file mode 100644 (file)
index 0000000..ac2bd56
--- /dev/null
@@ -0,0 +1,43 @@
+<?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/>.
+
+/**
+ * Capabilities for this report.
+ *
+ * @package   report_usersessions
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+
+    'report/usersessions:manageownsessions' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_USER,
+        'archetypes' => array(
+            'user' => CAP_ALLOW,
+        ),
+
+        // NOTE: shared accounts usually do not allow changing
+        //       of own passwords, this is not very accurate but safer.
+        'clonepermissionsfrom' => 'moodle/user:changeownpassword'
+    ),
+);
+
+
diff --git a/report/usersessions/index.php b/report/usersessions/index.php
new file mode 100644 (file)
index 0000000..ff57fd3
--- /dev/null
@@ -0,0 +1,28 @@
+<?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/>.
+
+/**
+ * Listing of all sessions for current user.
+ *
+ * @package   report_usersessions
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+require(__DIR__ . '/../../config.php');
+
+redirect(new moodle_url('/report/usersessions/user.php'));
diff --git a/report/usersessions/lang/en/report_usersessions.php b/report/usersessions/lang/en/report_usersessions.php
new file mode 100644 (file)
index 0000000..ebbaf93
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Lang strings.
+ *
+ * @package   report_usersessions
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+$string['navigationlink'] = 'Browser sessions';
+$string['mysessions'] = 'My active sessions';
+$string['pluginname'] = 'User sessions report';
+$string['thissession'] = 'Current session';
+$string['usersessions:manageownsessions'] = 'Manage own browser sessions';
diff --git a/report/usersessions/lib.php b/report/usersessions/lib.php
new file mode 100644 (file)
index 0000000..04c2135
--- /dev/null
@@ -0,0 +1,52 @@
+<?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/>.
+
+/**
+ * Lib API functions.
+ *
+ * @package   report_usersessions
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * This function extends the course navigation with the report items
+ *
+ * @param navigation_node $navigation The navigation node to extend
+ * @param stdClass $user
+ * @param stdClass $course The course to object for the report
+ */
+function report_usersessions_extend_navigation_user($navigation, $user, $course) {
+    global $USER;
+
+    if (isguestuser() or !isloggedin()) {
+        return;
+    }
+
+    if (\core\session\manager::is_loggedinas() or $USER->id != $user->id) {
+        // No peeking at somebody else's sessions!
+        return;
+    }
+
+    $context = context_user::instance($USER->id);
+    if (has_capability('report/usersessions:manageownsessions', $context)) {
+        $navigation->add(get_string('navigationlink', 'report_usersessions'),
+            new moodle_url('/report/usersessions/user.php'), $navigation::TYPE_SETTING);
+    }
+}
diff --git a/report/usersessions/locallib.php b/report/usersessions/locallib.php
new file mode 100644 (file)
index 0000000..02f6b5a
--- /dev/null
@@ -0,0 +1,91 @@
+<?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/>.
+
+/**
+ * Lib API functions.
+ *
+ * @package   report_usersessions
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once(__DIR__ . '/lib.php');
+
+/**
+ * Show user friendly duration since last activity.
+ *
+ * @param int $duration in seconds
+ * @return string
+ */
+function report_usersessions_format_duration($duration) {
+
+    // NOTE: The session duration is not accurate thanks to
+    //       $CFG->session_update_timemodified_frequency setting.
+    //       Also there is no point in showing days here because
+    //       the session cleanup should purge all stale sessions
+    //       regularly.
+
+    if ($duration < 60) {
+        return get_string('now');
+    }
+
+    if ($duration < 60 * 60 * 2) {
+        $minutes = (int)($duration / 60);
+        $ago = $minutes . ' ' . get_string('minutes');
+        return get_string('ago', 'core_message', $ago);
+    }
+
+    $hours = (int)($duration / (60 * 60));
+    $ago = $hours . ' ' . get_string('hours');
+    return get_string('ago', 'core_message', $ago);
+}
+
+/**
+ * Show some user friendly IP address info.
+ *
+ * @param string $ip
+ * @return string
+ */
+function report_usersessions_format_ip($ip) {
+    if (strpos($ip, ':') !== false) {
+        // For now ipv6 is not supported yet.
+        return $ip;
+    }
+    $url = new moodle_url('/iplookup/index.php', array('ip' => $ip));
+    return html_writer::link($url, $ip);
+}
+
+/**
+ * Kill user session.
+ *
+ * @param int $id
+ * @return void
+ */
+function report_usersessions_kill_session($id) {
+    global $DB, $USER;
+
+    $session = $DB->get_record('sessions', array('id' => $id, 'userid' => $USER->id), 'id, sid');
+
+    if (!$session or $session->sid === session_id()) {
+        // Do not delete the current session!
+        return;
+    }
+
+    \core\session\manager::kill_session($session->sid);
+}
diff --git a/report/usersessions/tests/behat/usersessions_report.feature b/report/usersessions/tests/behat/usersessions_report.feature
new file mode 100644 (file)
index 0000000..0106772
--- /dev/null
@@ -0,0 +1,10 @@
+@report @report_usersessions
+Feature: In a report, admin can see current sessions
+  In order see usersession data
+  As a admin
+  I need to view usersessions report and see if the current session is listed
+
+  Scenario: Check usersessions report shows current session
+    Given I log in as "admin"
+    When I navigate to "Browser sessions" node in "My profile settings > Activity reports"
+    Then I should see "Current session"
diff --git a/report/usersessions/user.php b/report/usersessions/user.php
new file mode 100644 (file)
index 0000000..7af5d93
--- /dev/null
@@ -0,0 +1,88 @@
+<?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/>.
+
+/**
+ * Listing of all sessions for current user.
+ *
+ * @package   report_usersessions
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+require(__DIR__ . '/../../config.php');
+require_once(__DIR__ . '/locallib.php');
+
+require_login(null, false);
+
+if (isguestuser()) {
+    // No guests here!
+    redirect(new moodle_url('/'));
+    die;
+}
+if (\core\session\manager::is_loggedinas()) {
+    // No login-as users.
+    redirect(new moodle_url('/user/index.php'));
+    die;
+}
+
+$context = context_user::instance($USER->id);
+require_capability('report/usersessions:manageownsessions', $context);
+
+$delete = optional_param('delete', 0, PARAM_INT);
+
+$PAGE->set_url('/report/usersessions/user.php');
+$PAGE->set_context($context);
+$PAGE->set_title(get_string('navigationlink', 'report_usersessions'));
+$PAGE->set_pagelayout('admin');
+
+if ($delete and confirm_sesskey()) {
+    report_usersessions_kill_session($delete);
+    redirect($PAGE->url);
+}
+
+echo $OUTPUT->header();
+echo $OUTPUT->heading(get_string('mysessions', 'report_usersessions'));
+
+$data = array();
+$sql = "SELECT id, timecreated, timemodified, firstip, lastip, sid
+          FROM {sessions}
+         WHERE userid = :userid
+      ORDER BY timemodified DESC";
+$params = array('userid' => $USER->id, 'sid' => session_id());
+
+$sessions = $DB->get_records_sql($sql, $params);
+foreach ($sessions as $session) {
+    if ($session->sid === $params['sid']) {
+        $lastaccess = get_string('thissession', 'report_usersessions');
+        $deletelink = '';
+
+    } else {
+        $lastaccess = report_usersessions_format_duration(time() - $session->timemodified);
+        $url = new moodle_url($PAGE->url, array('delete' => $session->id, 'sesskey' => sesskey()));
+        $deletelink = html_writer::link($url, get_string('logout'));
+    }
+    $data[] = array(userdate($session->timecreated), $lastaccess, report_usersessions_format_ip($session->lastip), $deletelink);
+}
+
+$table = new html_table();
+$table->head  = array(get_string('login'), get_string('lastaccess'), get_string('lastip'), get_string('action'));
+$table->align = array('left', 'left', 'left', 'right');
+$table->data  = $data;
+echo html_writer::table($table);
+
+echo $OUTPUT->footer();
+
diff --git a/report/usersessions/version.php b/report/usersessions/version.php
new file mode 100644 (file)
index 0000000..f2d6dae
--- /dev/null
@@ -0,0 +1,30 @@
+<?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/>.
+
+/**
+ * Version info.
+ *
+ * @package   report_usersessions
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$plugin->version   = 2014111800;       // The current plugin version (Date: YYYYMMDDXX).
+$plugin->requires  = 2014111300;       // Requires this Moodle version.
+$plugin->component = 'report_usersessions'; // Full name of the plugin (used for diagnostics).
index f0f3818..45bc0a4 100644 (file)
@@ -36,7 +36,7 @@ echo $OUTPUT->doctype(); ?>
 
 <!-- END OF HEADER -->
 
-    <div id="content" class="clearfix">
+    <div id="page-content" class="clearfix">
         <?php echo $OUTPUT->main_content() ?>
     </div>
 
index a7a1bf6..8a9f31b 100644 (file)
@@ -680,6 +680,7 @@ table.mod_index {width:100%;}
 .comment-ctrl h5 {margin:0;padding: 5px;}
 .comment-area {max-width: 400px;padding: 5px;}
 .comment-area textarea {width:100%;overflow:auto;}
+.comment-area textarea.fullwidth {-webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box;}
 .comment-area .fd {text-align:right;}
 .comment-meta span {color:gray;}
 .comment-link img { vertical-align: text-bottom; }
@@ -1211,7 +1212,7 @@ x#fitem_id_availabilityconditionsjson input[type=text] {
 audio.mediaplugin_html5audio {width: 300px}
 
 /* TinyMCE moodle media preview frame should not have padding */
-.core_media_preview.pagelayout-embedded #content {padding:0;}
+.core_media_preview.pagelayout-embedded #page-content {padding:0;}
 .core_media_preview.pagelayout-embedded #maincontent {height:0;}
 .core_media_preview.pagelayout-embedded .mediaplugin {margin:0;}
 
index d017ab9..16792cf 100644 (file)
@@ -34,10 +34,41 @@ table.message .searchresults td {padding:5px;}
 .message .messagearea  {padding-left:1%;border-left:1px solid LightGrey;width:74%;float:right;min-height:200px;}
 .message .messagearea .messagehistorytype {clear:both;padding-bottom:20px;}
 
-.message .messagearea .messagehistory .message_user_pictures {margin-left: auto;margin-right: auto;}
-.message .messagearea .messagehistory .message_user_pictures #user1 {vertical-align:top;width:200px;}
-.message .messagearea .messagehistory .message_user_pictures #user2 {vertical-align:top;width:200px;}
-.message .messagearea .messagehistory .message_user_pictures .useractionlinks {font-size:0.9em;}
+
+.message .messagearea .messagehistory .user {
+    vertical-align: top;
+    width: 32%;
+    min-width: 100px;
+    float: left;
+}
+.message .messagearea .messagehistory .user:first-child {
+    margin-left: 13%;
+}
+.message .messagearea .messagehistory .user:last-child {
+    margin-right: 13%;
+}
+.dir-rtl .message .messagearea .messagehistory .user:first-child {
+    margin-right: 13%;
+    margin-left: 0;
+}
+.dir-rtl .message .messagearea .messagehistory .user:last-child {
+    margin-left: 13%;
+    margin-right: 0;
+}
+.message .messagearea .messagehistory .user > div {
+    text-align: center;
+    border: none;
+}
+.message .messagearea .messagehistory .between {
+    float: left;
+    width: 16px;
+    margin: 0 3%;
+    padding-top: 40px;
+}
+.dir-rtl .message .messagearea .messagehistory .between,
+.dir-rtl .message .messagearea .messagehistory .user {
+    float: right;
+}
 
 .message .messagearea .messagehistory .heading {width:100%;clear:both;}
 .message .messagearea .messagehistory .left {padding-bottom:10px;width:50%;float:left;clear:both;}
index f0a3505..6197af8 100644 (file)
@@ -590,6 +590,11 @@ table.mod_index {
 .comment-area textarea {
     width: 100%;
     overflow: auto;
+    &.fullwidth {
+        -webkit-box-sizing: border-box;
+        -moz-box-sizing: border-box;
+        box-sizing: border-box;
+    }
 }
 .comment-area .fd {
     text-align: right;
index ceb28aa..1f5ecd1 100644 (file)
@@ -20,6 +20,7 @@
 body.has_dock {
     #page {
         padding-left: (@dockWidth + (@dockTitleMargin * 3));
+        padding-right: @gridGutterWidth;
     }
     div#dock {
         display: inline;
@@ -126,13 +127,16 @@ body.has_dock {
             .hidepanemicon img {
                 cursor: pointer;
             }
+            img.actionmenu {
+                width: auto;
+            }
         }
     }
 }
 
 .dir-rtl {
     &.has_dock #page {
-        padding-left: 0;
+        padding-left: @gridGutterWidth;
         padding-right: (@dockWidth + (@dockTitleMargin * 3));
     }
     #dock {
index c72b8c5..ae77fc3 100644 (file)
@@ -67,20 +67,65 @@ table.message .searchresults td {
     clear: both;
     padding-bottom: 20px;
 }
-.message .messagearea .messagehistory .message_user_pictures {
-    margin-left: auto;
-    margin-right: auto;
-}
-.message .messagearea .messagehistory .message_user_pictures #user1 {
+.message .messagearea .messagehistory .user {
     vertical-align: top;
-    width: 200px;
+    width: 45%;
+    min-width: 100px;
+    float: left;
 }
-.message .messagearea .messagehistory .message_user_pictures #user2 {
-    vertical-align: top;
-    width: 200px;
+.message .messagearea .messagehistory .user > div {
+    text-align: center;
 }
-.message .messagearea .messagehistory .message_user_pictures .useractionlinks {
-    font-size: 0.9em;
+.message .messagearea .messagehistory .between {
+    float: left;
+    width: 16px;
+    margin: 0 1%;
+    padding-top: 40px;
+}
+@media screen and (min-width: 800px) {
+    .message .messagearea .messagehistory .between {
+        margin: 0 3%;
+    }
+    .message .messagearea .messagehistory .user {
+        width: 32%
+    }
+    .message .messagearea .messagehistory .user:first-child {
+        margin-left: 13%;
+    }
+    .message .messagearea .messagehistory .user:last-child {
+        margin-right: 13%;
+    }
+    .dir-rtl .message .messagearea .messagehistory .user:first-child {
+        margin-right: 13%;
+        margin-left: 0%
+    }
+    .dir-rtl .message .messagearea .messagehistory .user:last-child {
+        margin-left: 13%;
+        margin-right: 0%;
+    }
+}
+@media screen and (min-width: 1200px) {
+    .message .messagearea .messagehistory .user {
+        width: 25%
+    }
+    .message .messagearea .messagehistory .user:first-child {
+        margin-left: 20%;
+    }
+    .message .messagearea .messagehistory .user:last-child {
+        margin-right: 20%;
+    }
+    .dir-rtl .message .messagearea .messagehistory .user:first-child {
+        margin-left: 0;
+        margin-right: 20%;
+    }
+    .dir-rtl .message .messagearea .messagehistory .user:last-child {
+        margin-right: 0%;
+        margin-left: 20%;
+    }
+}
+.dir-rtl .message .messagearea .messagehistory .between,
+.dir-rtl .message .messagearea .messagehistory .user {
+    float: right;
 }
 .message .messagearea .messagehistory .heading {
     width: 100%;
index cb9d7a7..a80620a 100644 (file)
@@ -1,4 +1,4 @@
-.layout-option-noheader #page-header,.layout-option-nonavbar #page-navbar,.layout-option-nofooter #page-footer,.layout-option-nocourseheader .course-content-header,.layout-option-nocoursefooter .course-content-footer{display:none}.empty-region-side-pre #block-region-side-pre,.empty-region-side-post #block-region-side-post,.jsenabled.docked-region-side-post #block-region-side-post,.jsenabled.docked-region-side-pre #block-region-side-pre{display:none}.content-only #region-main.span9,.empty-region-side-post #region-bs-main-and-pre.span9,.empty-region-side-pre #region-bs-main-and-post.span9,.empty-region-side-post #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-post #region-bs-main-and-pre.span9,.jsenabled.docked-region-side-post #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-pre #region-bs-main-and-post.span9{width:100%}.empty-region-side-pre #region-bs-main-and-pre.span9 #region-main,.jsenabled.docked-region-side-pre #region-bs-main-and-pre.span9 #region-main{float:none;width:100%}.empty-region-side-post.used-region-side-pre #region-main.span8,.jsenabled.docked-region-side-post.used-region-side-pre #region-main.span8{width:74.46808510638297%;*width:74.41489361702126%}.empty-region-side-post.used-region-side-pre #block-region-side-pre.span4,.jsenabled.docked-region-side-post.used-region-side-pre #block-region-side-pre.span4{width:23.404255319148934%;*width:23.351063829787233%}.empty-region-side-pre #region-bs-main-and-post.span9 #region-main.span8,.jsenabled.docked-region-side-pre #region-bs-main-and-post.span9 #region-main.span8{float:right}.dir-ltr,.mdl-left,.dir-rtl .mdl-right{text-align:left}.dir-rtl,.mdl-right,.dir-rtl .mdl-left{text-align:right}#add,#remove,.centerpara,.mdl-align{text-align:center}a.dimmed,a.dimmed:link,a.dimmed:visited,a.dimmed_text,a.dimmed_text:link,a.dimmed_text:visited,.dimmed_text,.dimmed_text a,.dimmed_text a:link,.dimmed_text a:visited,.usersuspended,.usersuspended a,.usersuspended a:link,.usersuspended a:visited,.dimmed_category,.dimmed_category a{color:#999}.activity.label .dimmed_text{opacity:.5;filter:alpha(opacity=50)}.unlist,.unlist li,.inline-list,.inline-list li,.block .list,.block .list li,.section li.activity,.section li.movehere,.tabtree li{padding:0;margin:0;list-style:none}.inline,.inline-list li{display:inline}.notifytiny{font-size:10.5px}.notifytiny li,.notifytiny td{font-size:100%}.red,.notifyproblem{color:#b94a48}.green,.notifysuccess{color:#468847}.highlight{background:#d9edf7}.reportlink{text-align:right}a.autolink.glossary:hover{cursor:help}.collapsibleregioncaption{white-space:nowrap}.collapsibleregioncaption img{vertical-align:middle}.jsenabled .hiddenifjs{display:none}.visibleifjs{display:none}.jsenabled .visibleifjs{display:inline}.jsenabled .collapsibleregion{overflow:hidden}.jsenabled .collapsed .collapsibleregioninner{visibility:hidden}.collapsible-actions{display:none;text-align:right}.dir-rtl .collapsible-actions{text-align:left}.jsenabled .collapsible-actions{display:block}.collapsible-actions .collapseexpand{padding-left:20px;background:url([[pix:t/collapsed]]) 2px center no-repeat}.dir-rtl .collapsible-actions .collapseexpand{padding-right:20px;padding-left:0;background:url([[pix:t/collapsed_rtl]]) right center no-repeat}.collapsible-actions .collapse-all,.dir-rtl .collapsible-actions .collapse-all{background-image:url([[pix:t/expanded]])}.yui-overlay .yui-widget-bd{position:relative;top:0;left:0;z-index:1;padding:2px 5px;color:#000;background-color:#ffee69;border:1px solid #a6982b;border-top-color:#d4c237}.clearer{display:block;height:1px;padding:0;margin:0;clear:both;background:transparent;border-width:0}.bold,.warning,.errorbox .title,.pagingbar .title,.pagingbar .thispage{font-weight:bold}img.resize{width:1em;height:1em}.block img.resize,.breadcrumb img.resize{width:.8em;height:.9em}img.icon{width:16px;height:16px;padding-right:6px;vertical-align:text-bottom}.dir-rtl img.icon{padding-right:0;padding-left:6px}img.iconsmall{width:12px;height:12px;margin-right:3px;vertical-align:middle}img.iconhelp,.helplink img{width:16px;height:16px;padding-left:3px;vertical-align:text-bottom}h1 img.iconhelp,h1 img.icon,h2 img.iconhelp,h2 img.icon,h3 img.iconhelp,h3 img.icon,h4 img.iconhelp,h4 img.icon,h5 img.iconhelp,h5 img.icon,h6 img.iconhelp,h6 img.icon{padding:4px;vertical-align:middle}.dir-rtl img.iconhelp,.dir-rtl .helplink img{padding-right:3px;padding-left:0}img.iconlarge{width:24px;height:24px;vertical-align:middle}img.iconsort{padding-left:.3em;margin-bottom:.15em;vertical-align:text-bottom}.dir-rtl img.iconsort{padding-right:.3em;padding-left:0}img.icontoggle{width:50px;height:17px;vertical-align:middle}img.iconkbhelp{width:49px;height:17px}img.icon-pre,.dir-rtl img.icon-post{padding-right:3px;padding-left:0}img.icon-post,.dir-rtl img.icon-pre{padding-right:0;padding-left:3px}.boxaligncenter{margin-right:auto;margin-left:auto}.boxalignright{margin-right:0;margin-left:auto}.boxalignleft{margin-right:auto;margin-left:0}.boxwidthnarrow{width:30%}.boxwidthnormal{width:50%}.boxwidthwide{width:80%}.headermain{font-weight:bold}#maincontent{display:block;height:1px;overflow:hidden}img.uihint{cursor:help}#addmembersform table{margin-right:auto;margin-left:auto}table.flexible .emptyrow{display:none}img.emoticon{width:15px;height:15px;vertical-align:middle}form.popupform,form.popupform div{display:inline}.arrow_button input{overflow:hidden}.action-icon img.smallicon{margin:0 .3em;vertical-align:text-bottom}.no-overflow{padding-bottom:1px;overflow:auto}.pagelayout-report .no-overflow{overflow:visible}.no-overflow>.generaltable{margin-bottom:0}.accesshide{position:absolute;left:-10000px;font-size:1em;font-weight:normal}.dir-rtl .accesshide{top:-30000px;left:auto}span.hide,div.hide{display:none}a.skip-block,a.skip{position:absolute;top:-1000em;font-size:.85em;text-decoration:none}a.skip-block:focus,a.skip-block:active,a.skip:focus,a.skip:active{position:static;display:block}.skip-block-to{display:block;height:1px;overflow:hidden}.addbloglink{text-align:center}.blog_entry .audience{padding-right:4px;text-align:right}.blog_entry .tags{margin-top:15px}.blog_entry .tags .action-icon img.smallicon{width:16px;height:16px}.blog_entry .content{margin-left:43px}#page-group-index #groupeditform{text-align:center}#doc-contents h1{margin:1em 0 0 0}#doc-contents ul{width:90%;padding:0;margin:0}#doc-contents ul li{list-style-type:none}.groupmanagementtable td{vertical-align:top}.groupmanagementtable #existingcell,.groupmanagementtable #potentialcell{width:42%}.groupmanagementtable #buttonscell{width:16%}.groupmanagementtable #buttonscell p.arrow_button input{width:auto;min-width:80%;margin:0 auto}.groupmanagementtable #removeselect_wrapper,.groupmanagementtable #addselect_wrapper{width:100%}.groupmanagementtable #removeselect_wrapper label,.groupmanagementtable #addselect_wrapper label{font-weight:normal}.dir-rtl .groupmanagementtable p{text-align:right}#group-usersummary{width:14em}.groupselector{display:inline-block;margin-top:3px;margin-bottom:3px}.groupselector label{display:inline-block}.loginbox{margin:15px;overflow:visible}.loginbox.twocolumns{margin:15px}.loginbox h2,.loginbox .subcontent{padding:10px;margin:5px;text-align:center}.loginbox .loginpanel .desc{padding:0;margin:0;margin-top:15px;margin-bottom:5px}.loginbox .signuppanel .subcontent{text-align:left}.dir-rtl .loginbox .signuppanel .subcontent{text-align:right}.loginbox .loginsub{margin-right:0;margin-left:0}.loginbox .guestsub,.loginbox .forgotsub,.loginbox .potentialidps{margin:5px 12%}.loginbox .potentialidps .potentialidplist{margin-left:40%}.loginbox .potentialidps .potentialidplist div{text-align:left}.loginbox .loginform{margin-top:1em;text-align:left}.loginbox .loginform .form-label{float:left;width:49%;text-align:right;white-space:nowrap}.loginbox .loginform .form-input{float:right;width:50%}.loginbox .loginform .form-input input{width:6em}.loginbox .signupform{margin-top:1em;text-align:center}.loginbox.twocolumns .loginpanel,.loginbox.twocolumns .signuppanel{display:block;float:left;width:48%;min-height:30px;padding:0;padding-bottom:2000px;margin:0;margin-bottom:-2000px;margin-left:2.76243%;border:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.dir-rtl .loginbox.twocolumns .loginpanel,.dir-rtl .loginbox.twocolumns .signuppanel{float:right}.loginbox .potentialidp .smallicon{margin:0 .3em;vertical-align:text-bottom}.notepost{margin-bottom:1em}.notepost .userpicture{float:left;margin-right:5px}.notepost .content,.notepost .footer{clear:both}.notesgroup{margin-left:20px}.path-my .coursebox .overview{margin:15px 30px 10px 30px}.path-my .coursebox .info{float:none;margin:0}.mod_introbox{padding:10px}table.mod_index{width:100%}.comment-ctrl{display:none;padding:0;margin:0;font-size:12px}.comment-ctrl h5{padding:5px;margin:0}.comment-area{max-width:400px;padding:5px}.comment-area textarea{width:100%;overflow:auto}.comment-area .fd{text-align:right}.comment-meta span{color:gray}.comment-link img{vertical-align:text-bottom}.comment-list{padding:0;margin:0;overflow:auto;font-size:11px;list-style:none}.comment-list li{position:relative;padding:.3em;margin:2px;margin-bottom:5px;clear:both;list-style:none}.comment-list li.first{display:none}.comment-paging{text-align:center}.comment-paging .pageno{padding:2px}.comment-paging .curpage{border:1px solid #CCC}.comment-message .picture{float:left;width:20px}.dir-rtl .comment-message .picture{float:right}.comment-message .text{padding:0;margin:0}.comment-message .text p{padding:0;margin:0 18px 0 0}.comment-delete{position:absolute;top:0;right:0;margin:.3em}.dir-rtl .comment-delete{position:absolute;right:auto;left:0;margin:.3em}.comment-report-selectall{display:none}.comment-link{display:none}.jsenabled .comment-link{display:block}.jsenabled .showcommentsnonjs{display:none}.jsenabled .comment-report-selectall{display:inline}.completion-expired{background:#f2dede}.completion-expected{font-size:10.5px}.completion-sortchoice,.completion-identifyfield{font-size:10.5px;vertical-align:bottom}.completion-progresscell{text-align:right}.completion-expired .completion-expected{font-weight:bold}#page-tag-coursetags_edit .coursetag_edit_centered{position:relative;width:600px;margin:20px auto}#page-tag-coursetags_edit .coursetag_edit_row{clear:both}#page-tag-coursetags_edit .coursetag_edit_row .coursetag_edit_left{float:left;width:50%;text-align:right}#page-tag-coursetags_edit .coursetag_edit_row .coursetag_edit_right{margin-left:50%}#page-tag-coursetags_edit .coursetag_edit_input3{display:none}#page-tag-coursetags_more .coursetag_more_large{font-size:120%}#page-tag-coursetags_more .coursetag_more_small{font-size:80%}#page-tag-coursetags_more .coursetag_more_link{font-size:80%}#tag-description,#tag-blogs{width:100%}#tag-management-box{margin-bottom:10px;line-height:20px}#tag-user-table{width:100%;padding:3px;clear:both}#tag-user-table{*zoom:1}#tag-user-table:before,#tag-user-table:after{display:table;line-height:0;content:""}#tag-user-table:after{clear:both}img.user-image{width:100px;height:100px}#small-tag-cloud-box{width:300px;margin:0 auto}#big-tag-cloud-box{float:none;width:600px;margin:0 auto}ul#tag-cloud-list{padding:5px;margin:0;list-style:none}ul#tag-cloud-list li{display:inline;margin:0;list-style-type:none}#tag-search-box{margin:10px auto;text-align:center}#tag-search-results-container{width:100%;padding:0}#tag-search-results{display:block;float:left;width:60%;padding:0;margin:15px 20% 0 20%}#tag-search-results li{float:left;width:30%;padding-right:1%;padding-left:1%;line-height:20px;text-align:left;list-style:none}span.flagged-tag,span.flagged-tag a{color:#b94a48}table#tag-management-list{width:100%;text-align:left}table#tag-management-list td,table#tag-management-list th{padding:4px;text-align:left;vertical-align:middle}.tag-management-form{text-align:center}#relatedtags-autocomplete-container{width:100%;min-height:4.6em;margin-right:auto;margin-left:auto}#relatedtags-autocomplete{position:relative;display:block;width:60%;margin-right:auto;margin-left:auto}#relatedtags-autocomplete .yui-ac-content{position:absolute;left:20%;z-index:9050;width:420px;overflow:hidden;background:#fff;border:1px solid rgba(0,0,0,0.2)}#relatedtags-autocomplete .ysearchquery{position:absolute;right:10px;z-index:10;color:#808080}#relatedtags-autocomplete .yui-ac-shadow{position:absolute;z-index:9049;width:100%;margin:.3em;background:#a0a0a0}#relatedtags-autocomplete ul{width:100%;padding:0;margin:0;list-style-type:none}#relatedtags-autocomplete li{padding:0 5px;white-space:nowrap;cursor:default}#relatedtags-autocomplete li.yui-ac-highlight{color:#fff;background:#0070a8}h2.tag-heading,div#tag-description,div#tag-blogs,body.tag .managelink{padding:5px}.tag_cloud .s20{font-size:1.5em;font-weight:bold}.tag_cloud .s19{font-size:1.5em}.tag_cloud .s18{font-size:1.4em;font-weight:bold}.tag_cloud .s17{font-size:1.4em}.tag_cloud .s16{font-size:1.3em;font-weight:bold}.tag_cloud .s15{font-size:1.3em}.tag_cloud .s14{font-size:1.2em;font-weight:bold}.tag_cloud .s13{font-size:1.2em}.tag_cloud .s12,.tag_cloud .s11{font-size:1.1em;font-weight:bold}.tag_cloud .s10,.tag_cloud .s9{font-size:1.1em}.tag_cloud .s8,.tag_cloud .s7{font-size:1em;font-weight:bold}.tag_cloud .s6,.tag_cloud .s5{font-size:1em}.tag_cloud .s4,.tag_cloud .s3{font-size:.9em;font-weight:bold}.tag_cloud .s2,.tag_cloud .s1{font-size:.9em}.tag_cloud .s0{font-size:.8em}#webservice-doc-generator td{text-align:left;border:0 solid black}.smartselect{position:absolute}.smartselect .smartselect_mask{background-color:#fff}.smartselect ul{padding:0;margin:0}.smartselect ul li{list-style:none}.smartselect .smartselect_menu{margin-right:5px}.safari .smartselect .smartselect_menu{margin-left:2px}.smartselect .smartselect_menu,.smartselect .smartselect_submenu{display:none;background-color:#FFF;border:1px solid #000}.smartselect .smartselect_menu.visible,.smartselect .smartselect_submenu.visible{display:block}.smartselect .smartselect_menu_content ul li{position:relative;padding:2px 5px}.smartselect .smartselect_menu_content ul li a{color:#333;text-decoration:none}.smartselect .smartselect_menu_content ul li a.selectable{color:inherit}.smartselect .smartselect_submenuitem{background-image:url([[pix:moodle|t/collapsed]]);background-position:100%;background-repeat:no-repeat}.smartselect.spanningmenu .smartselect_submenu{position:absolute;top:-1px;left:100%}.smartselect.spanningmenu .smartselect_submenu a{padding-right:16px;white-space:nowrap}.smartselect.spanningmenu .smartselect_menu_content ul li a.selectable:hover{text-decoration:underline}.smartselect.compactmenu .smartselect_submenu{position:relative;z-index:1010;display:none;margin:2px -3px;margin-left:10px;border-width:0}.smartselect.compactmenu .smartselect_submenu.visible{display:block}.smartselect.compactmenu .smartselect_menu{z-index:1000;overflow:hidden}.smartselect.compactmenu .smartselect_submenu .smartselect_submenu{z-index:1020}.smartselect.compactmenu .smartselect_submenuitem:hover>.smartselect_menuitem_label{font-weight:bold}#page-admin-registration-register .registration_textfield{width:300px}.userenrolment{width:100%;border-collapse:collapse}.userenrolment tr{vertical-align:top}.userenrolment td{height:41px;padding:0}.userenrolment .subfield{margin-right:5px}.userenrolment .col_userdetails .subfield_picture{float:left}.userenrolment .col_lastseen{width:150px}.userenrolment .col_role{width:262px}.userenrolment .col_role .roles,.userenrolment .col_group .groups{margin-right:30px}.userenrolment .col_role .role,.userenrolment .col_group .group{float:left;padding:3px;margin:3px;white-space:nowrap}.userenrolment .col_role .role a,.userenrolment .col_group .group a{margin-left:3px;cursor:pointer}.userenrolment .col_role .addrole,.userenrolment .col_group .addgroup{float:right;padding:3px;margin:3px}.userenrolment .col_role .addrole>*:hover,.userenrolment .col_group .addgroup>*:hover{border-bottom:1px solid #666}.userenrolment .col_role .addrole img,.userenrolment .col_group .addgroup img{vertical-align:baseline}.dir-rtl .userenrolment .col_role .role{float:right}.userenrolment .hasAllRoles .col_role .addrole{display:none}.userenrolment .col_enrol .enrolment{float:left;padding:3px;margin:3px}.userenrolment .col_enrol .enrolment a{float:right;margin-left:3px}#page-enrol-users .enrol_user_buttons{float:right}#page-enrol-users .enrol_user_buttons .enrolusersbutton{display:inline}#page-enrol-users .enrol_user_buttons .enrolusersbutton div,#page-enrol-users .enrol_user_buttons .enrolusersbutton form{display:inline;margin-right:0}#page-enrol-users #filterform{display:inline-block;min-height:20px;padding:19px;padding:9px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-color:#e3e3e3;-webkit-border-radius:4px;-webkit-border-radius:3px;-moz-border-radius:4px;-moz-border-radius:3px;border-radius:4px;border-radius:3px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}#page-enrol-users #filterform blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}#page-enrol-users #filterform .fitem{display:inline-block;margin-right:.3em;line-height:40px;white-space:nowrap}#page-enrol-users #filterform .fitem label{display:inline;padding-right:.3em;line-height:20px}#page-enrol-users #filterform .fitem :before,#page-enrol-users #filterform .fitem:after{display:inline}#page-enrol-users #filterform div,#page-enrol-users #filterform fieldset{display:inline;float:none;width:auto;margin:0;clear:none}#page-enrol-users #filterform select,#page-enrol-users #filterform .ftext input{width:7em}#page-enrol-users #filterform input,#page-enrol-users #filterform select{margin-bottom:0}#page-enrol-users .user-enroller-panel .uep-search-results .user .details{width:237px}#page-enrol-users .user-enroller-panel .uep-search-results .cohort .details{width:237px}.dir-rtl#page-enrol-users .col_userdetails .subfield_picture{float:right}.dir-rtl#page-enrol-users .enrol_user_buttons{float:left}.dir-rtl#page-enrol-users .enrol_user_buttons .enrolusersbutton{margin-right:1em;margin-left:0}.dir-rtl#page-enrol-users .enrol_user_buttons .enrolusersbutton div{margin-left:0}.dir-rtl#page-enrol-users #filterform .fitem{margin-right:0;margin-left:.3em}.dir-rtl#page-enrol-users #filterform .fitem label{padding-right:0;padding-left:.3em}.dir-rtl .headermain{float:right}.dir-rtl .headermenu{float:left}.dir-rtl .loginbox .loginform .form-label{float:right;text-align:left}.dir-rtl .loginbox .loginform .form-input{margin-right:1%;text-align:right}.dir-rtl .yui3-menu-hidden{left:0}#page-admin-roles-define.dir-rtl #rolesform .felement{margin-right:180px}#page-message-edit.dir-rtl table.generaltable th.c0{text-align:right}.corelightbox{position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;background-color:#CCC}.corelightbox img{position:fixed;top:50%;left:50%}.mod-indent-outer{display:table}.mod-indent{display:table-cell}.label .mod-indent{float:left;padding-top:20px}.mod-indent-1{width:30px}.mod-indent-2{width:60px}.mod-indent-3{width:90px}.mod-indent-4{width:120px}.mod-indent-5{width:150px}.mod-indent-6{width:180px}.mod-indent-7{width:210px}.mod-indent-8{width:240px}.mod-indent-9{width:270px}.mod-indent-10{width:300px}.mod-indent-11{width:330px}.mod-indent-12{width:360px}.mod-indent-13{width:390px}.mod-indent-14{width:420px}.mod-indent-15{width:450px}.mod-indent-16{width:480px}.mod-indent-huge{width:480px}.resourcecontent .mediaplugin_mp3 object{width:600px;height:25px}.resourcecontent audio.mediaplugin_html5audio{width:600px}.resourceimage{max-width:100%}.mediaplugin_mp3 object{width:300px;height:15px}audio.mediaplugin_html5audio{width:300px}.core_media_preview.pagelayout-embedded #content{padding:0}.core_media_preview.pagelayout-embedded #maincontent{height:0}body#page-lib-editor-tinymce-plugins-moodlemedia-preview{min-width:0;padding:0;margin:0;background:0}.dir-rtl .ygtvtn,.dir-rtl .ygtvtm,.dir-rtl .ygtvtmh,.dir-rtl .ygtvtmhh,.dir-rtl .ygtvtp,.dir-rtl .ygtvtph,.dir-rtl .ygtvtphh,.dir-rtl .ygtvln,.dir-rtl .ygtvlm,.dir-rtl .ygtvlmh,.dir-rtl .ygtvlmhh,.dir-rtl .ygtvlp,.dir-rtl .ygtvlph,.dir-rtl .ygtvlphh,.dir-rtl .ygtvdepthcell,.dir-rtl .ygtvok,.dir-rtl .ygtvok:hover,.dir-rtl .ygtvcancel,.dir-rtl .ygtvcancel:hover{width:18px;height:22px;cursor:pointer;background-image:url([[pix:theme|yui2-treeview-sprite-rtl]]);background-repeat:no-repeat}.dir-rtl .ygtvtn{background-position:0 -5600px}.dir-rtl .ygtvtm{background-position:0 -4000px}.dir-rtl .ygtvtmh,.dir-rtl .ygtvtmhh{background-position:0 -4800px}.dir-rtl .ygtvtp{background-position:0 -6400px}.dir-rtl .ygtvtph,.dir-rtl .ygtvtphh{background-position:0 -7200px}.dir-rtl .ygtvln{background-position:0 -1600px}.dir-rtl .ygtvlm{background-position:0 0}.dir-rtl .ygtvlmh,.dir-rtl .ygtvlmhh{background-position:0 -800px}.dir-rtl .ygtvlp{background-position:0 -2400px}.dir-rtl .ygtvlph,.dir-rtl .ygtvlphh{background-position:0 -3200px}.dir-rtl .ygtvdepthcell{background-position:0 -8000px}.dir-rtl .ygtvok{background-position:0 -8800px}.dir-rtl .ygtvok:hover{background-position:0 -8844px}.dir-rtl .ygtvcancel{background-position:0 -8822px}.dir-rtl .ygtvcancel:hover{background-position:0 -8866px}.dir-rtl.yui-skin-sam .yui-panel .hd{text-align:right}.dir-rtl .yui-skin-sam .yui-layout .yui-layout-unit div.yui-layout-bd{text-align:right}.dir-rtl .clearlooks2.ie9 .mceAlert .mceMiddle span,.dir-rtl .clearlooks2 .mceConfirm .mceMiddle span{top:44px}.dir-rtl .o2k7Skin table,.dir-rtl .o2k7Skin tbody,.dir-rtl .o2k7Skin a,.dir-rtl .o2k7Skin img,.dir-rtl .o2k7Skin tr,.dir-rtl .o2k7Skin div,.dir-rtl .o2k7Skin td,.dir-rtl .o2k7Skin iframe,.dir-rtl .o2k7Skin span,.dir-rtl .o2k7Skin *,.dir-rtl .o2k7Skin .mceText,.dir-rtl .o2k7Skin .mceListBox .mceText{text-align:right}.path-rating .ratingtable{width:100%;margin-bottom:1em}.path-rating .ratingtable th.rating{width:100%}.path-rating .ratingtable td.rating,.path-rating .ratingtable td.time{text-align:center;white-space:nowrap}.initialbar a,.initialbar strong{padding-right:3px;padding-left:3px}.moodle-dialogue-base .moodle-dialogue-lightbox{background-color:#AAA}.moodle-dialogue-base .hidden,.moodle-dialogue-base .moodle-dialogue-hidden{display:none}.no-scrolling{overflow:hidden}.moodle-dialogue-base .moodle-dialogue-fullscreen{position:fixed;top:0;right:0;bottom:-50px;left:0}.moodle-dialogue-base .moodle-dialogue-fullscreen .moodle-dialogue-content{overflow:au