Merge branch 'MDL-43804-master' of git://github.com/ankitagarwal/moodle
authorSam Hemelryk <sam@moodle.com>
Sun, 9 Feb 2014 19:50:23 +0000 (08:50 +1300)
committerSam Hemelryk <sam@moodle.com>
Sun, 9 Feb 2014 19:50:23 +0000 (08:50 +1300)
Conflicts:
theme/bootstrapbase/style/moodle.css

196 files changed:
admin/cli/backup.php [new file with mode: 0644]
admin/webservice/service_user_settings.php
auth/db/tests/db_test.php
auth/ldap/tests/plugin_test.php
backup/backup.php
backup/controller/backup_controller.class.php
backup/controller/base_controller.class.php
backup/controller/restore_controller.class.php
backup/import.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
backup/restore.php
backup/util/dbops/backup_controller_dbops.class.php
backup/util/dbops/backup_structure_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/helper/backup_helper.class.php
backup/util/includes/backup_includes.php
backup/util/includes/restore_includes.php
backup/util/plan/backup_plan.class.php
backup/util/plan/base_plan.class.php
backup/util/plan/base_task.class.php
backup/util/plan/restore_plan.class.php
backup/util/plan/restore_structure_step.class.php
backup/util/structure/backup_structure_processor.class.php
backup/util/ui/restore_ui.class.php
backup/util/ui/restore_ui_stage.class.php
backup/util/xml/parser/progressive_parser.class.php
badges/tests/badgeslib_test.php
blocks/comments/tests/events_test.php
config-dist.php
course/edit.php
course/format/lib.php
course/format/renderer.php
enrol/database/tests/sync_test.php
files/tests/externallib_test.php
grade/grading/form/guide/backup/moodle2/backup_gradingform_guide_plugin.class.php
grade/grading/form/guide/backup/moodle2/restore_gradingform_guide_plugin.class.php
grade/tests/edittreelib_test.php
grade/tests/externallib_test.php
group/clientlib.js
install/lang/zh_tw/error.php
lang/en/auth.php
lang/en/dbtransfer.php
lib/behat/form_field/behat_form_select.php
lib/classes/event/user_login_failed.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/classes/progress/base.php [moved from backup/util/progress/core_backup_progress.class.php with 78% similarity]
lib/classes/progress/display.php [moved from backup/util/progress/core_backup_display_progress.class.php with 86% similarity]
lib/classes/progress/display_if_slow.php [moved from backup/util/progress/core_backup_display_progress_if_slow.class.php with 82% similarity]
lib/classes/progress/null.php [moved from backup/util/progress/core_backup_null_progress.class.php with 88% similarity]
lib/classes/update/deployer.php
lib/db/install.xml
lib/db/upgrade.php
lib/dml/mssql_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlite3_pdo_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dtl/database_exporter.php
lib/javascript-static.js
lib/moodlelib.php
lib/outputactions.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/pdflib.php
lib/setup.php
lib/tablelib.php
lib/tests/accesslib_test.php
lib/tests/authlib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/code_test.php [deleted file]
lib/tests/completionlib_test.php
lib/tests/coursecatlib_test.php
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/tests/progress_test.php [moved from backup/util/progress/tests/progress_test.php with 81% similarity]
lib/upgrade.txt
lib/upgradelib.php
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/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/dock/js/dock.js
lib/yui/src/dock/js/tabheightmanager.js
lib/yui/src/notification/js/alert.js
lib/yui/src/notification/js/confirm.js
lib/yui/src/notification/js/dialogue.js
lib/yui/src/notification/js/exception.js
lib/yuilib/readme_moodle.txt
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/feedback/comments/locallib.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/pdf.php
mod/assign/gradingtable.php
mod/assign/locallib.php
mod/assign/submission/comments/tests/events_test.php
mod/assign/tests/behat/quickgrading.feature [new file with mode: 0644]
mod/assign/tests/externallib_test.php
mod/assign/upgrade.txt
mod/assign/version.php
mod/book/tests/events_test.php
mod/chat/view.php
mod/choice/tests/events_test.php
mod/choice/view.php
mod/data/edit.php
mod/data/tests/lib_test.php
mod/feedback/analysis.php
mod/feedback/analysis_course.php
mod/feedback/complete.php
mod/feedback/complete_guest.php
mod/feedback/delete_completed.php
mod/feedback/delete_item.php
mod/feedback/delete_template.php
mod/feedback/edit.php
mod/feedback/edit_item.php
mod/feedback/import.php
mod/feedback/index.php
mod/feedback/mapcourse.php
mod/feedback/print.php
mod/feedback/show_entries.php
mod/feedback/show_entries_anonym.php
mod/feedback/show_nonrespondents.php
mod/feedback/tests/events_test.php
mod/feedback/use_templ.php
mod/feedback/view.php
mod/forum/classes/post_form.php
mod/forum/lib.php
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/unsubscribeall.php
mod/forum/view.php
mod/glossary/deleteentry.php
mod/glossary/edit.php
mod/glossary/editcategories.php
mod/glossary/export.php
mod/glossary/exportentry.php
mod/glossary/import.php
mod/glossary/tests/events_test.php
mod/glossary/view.php
mod/lesson/import.php
mod/lti/grade.php
mod/page/view.php
mod/quiz/attempt.php
mod/quiz/locallib.php
mod/quiz/report/default.php
mod/quiz/report/statistics/classes/calculated.php
mod/quiz/report/statistics/classes/calculator.php
mod/quiz/report/statistics/lang/en/quiz_statistics.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_form.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/tests/fixtures/mdl_question_states.csv
mod/quiz/report/statistics/tests/statistics_test.php
mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php
mod/quiz/review.php
mod/quiz/startattempt.php
mod/quiz/summary.php
mod/scorm/tests/event_test.php
mod/survey/view.php
mod/wiki/pagelib.php
mod/wiki/tests/events_test.php
question/classes/statistics/questions/calculated.php
question/classes/statistics/questions/calculated_for_subquestion.php
question/classes/statistics/questions/calculator.php
question/preview.php
question/upgrade.txt
question/yui/build/moodle-question-preview/moodle-question-preview-debug.js [new file with mode: 0644]
question/yui/build/moodle-question-preview/moodle-question-preview-min.js [new file with mode: 0644]
question/yui/build/moodle-question-preview/moodle-question-preview.js [new file with mode: 0644]
question/yui/src/preview/build.json [new file with mode: 0644]
question/yui/src/preview/js/preview.js [moved from question/preview.js with 63% similarity]
question/yui/src/preview/meta/preview.json [new file with mode: 0644]
repository/filepicker.js
repository/tests/repositorylib_test.php
theme/bootstrapbase/config.php
theme/bootstrapbase/javascript/dock.js [new file with mode: 0644]
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/dock.less [new file with mode: 0644]
theme/bootstrapbase/style/moodle.css
theme/clean/config.php
theme/yui_combo.php
theme/yui_image.php
version.php

diff --git a/admin/cli/backup.php b/admin/cli/backup.php
new file mode 100644 (file)
index 0000000..2770c03
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This script allows to do backup.
+ *
+ * @package    core
+ * @subpackage cli
+ * @copyright  2013 Lancaster University
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', 1);
+
+require(dirname(dirname(dirname(__FILE__))).'/config.php');
+require_once($CFG->libdir.'/clilib.php');
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(array(
+    'courseid' => false,
+    'courseshortname' => '',
+    'destination' => '',
+    'help' => false,
+    ), array('h' => 'help'));
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help'] || !($options['courseid'] || $options['courseshortname'])) {
+    $help = <<<EOL
+Perform backup of the given course.
+
+Options:
+--courseid=INTEGER          Course ID for backup.
+--courseshortname=STRING    Course shortname for backup.
+--destination=STRING        Path where to store backup file. If not set the backup
+                            will be stored within the course backup file area.
+-h, --help                  Print out this help.
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/cli/backup.php --courseid=2 --destination=/moodle/backup/\n
+EOL;
+
+    echo $help;
+    die;
+}
+
+$admin = get_admin();
+if (!$admin) {
+    mtrace("Error: No admin account was found");
+    die;
+}
+
+// Do we need to store backup somewhere else?
+$dir = rtrim($options['destination'], '/');
+if (!empty($dir)) {
+    if (!file_exists($dir) || !is_dir($dir) || !is_writable($dir)) {
+        mtrace("Destination directory does not exists or not writable.");
+        die;
+    }
+}
+
+// Check that the course exists.
+if ($options['courseid']) {
+    $course = $DB->get_record('course', array('id' => $options['courseid']), '*', MUST_EXIST);
+} else if ($options['courseshortname']) {
+    $course = $DB->get_record('course', array('shortname' => $options['courseshortname']), '*', MUST_EXIST);
+}
+
+cli_heading('Performing backup...');
+$bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
+                            backup::INTERACTIVE_YES, backup::MODE_GENERAL, $admin->id);
+// Set the default filename.
+$format = $bc->get_format();
+$type = $bc->get_type();
+$id = $bc->get_id();
+$users = $bc->get_plan()->get_setting('users')->get_value();
+$anonymised = $bc->get_plan()->get_setting('anonymize')->get_value();
+$filename = backup_plan_dbops::get_default_backup_filename($format, $type, $id, $users, $anonymised);
+$bc->get_plan()->get_setting('filename')->set_value($filename);
+
+// Execution.
+$bc->finish_ui();
+$bc->execute_plan();
+$results = $bc->get_results();
+$file = $results['backup_destination']; // May be empty if file already moved to target location.
+
+// Do we need to store backup somewhere else?
+if (!empty($dir)) {
+    if ($file) {
+        mtrace("Writing " . $dir.'/'.$filename);
+        if ($file->copy_content_to($dir.'/'.$filename)) {
+            $file->delete();
+            mtrace("Backup completed.");
+        } else {
+            mtrace("Destination directory does not exist or is not writable. Leaving the backup in the course backup file area.");
+        }
+    }
+} else {
+    mtrace("Backup completed, the new file is listed in the backup area of the given course");
+}
+$bc->destroy();
+exit(0);
\ No newline at end of file
index 18d0ef5..8a87fdb 100644 (file)
@@ -68,7 +68,7 @@ if ($usersettingsform->is_cancelled()) {
     //TODO: assign capability
 
     //display successful notification
-    $notification = $OUTPUT->notification(get_string('usersettingssaved', 'webservice'), 'success');
+    $notification = $OUTPUT->notification(get_string('usersettingssaved', 'webservice'), 'notifysuccess');
 }
 
 echo $OUTPUT->header();
index 97a1eca..9fccdd2 100644 (file)
@@ -28,11 +28,17 @@ defined('MOODLE_INTERNAL') || die();
 
 
 class auth_db_testcase extends advanced_testcase {
+    /** @var string Original error log */
+    protected $oldlog;
 
     protected function init_auth_database() {
         global $DB, $CFG;
         require_once("$CFG->dirroot/auth/db/auth.php");
 
+        // Discard error logs from AdoDB.
+        $this->oldlog = ini_get('error_log');
+        ini_set('error_log', "$CFG->dataroot/testlog.log");
+
         $dbman = $DB->get_manager();
 
         set_config('extencoding', 'utf-8', 'auth/db');
@@ -133,6 +139,8 @@ class auth_db_testcase extends advanced_testcase {
         $dbman = $DB->get_manager();
         $table = new xmldb_table('auth_db_users');
         $dbman->drop_table($table);
+
+        ini_set('error_log', $this->oldlog);
     }
 
     public function test_plugin() {
index 955b81e..13cdcc0 100644 (file)
@@ -268,10 +268,8 @@ class auth_ldap_plugin_testcase extends advanced_testcase {
         $sink->close();
 
         // Check that the event is valid.
-        $this->assertCount(2, $events);
-        $event = $events[0];
-        $this->assertInstanceOf('\core\event\user_updated', $event);
-        $event = $events[1];
+        $this->assertCount(1, $events);
+        $event = reset($events);
         $this->assertInstanceOf('\core\event\user_loggedin', $event);
         $this->assertEquals('user', $event->objecttable);
         $this->assertEquals('2', $event->objectid);
index 0f3bf7a..688726d 100644 (file)
@@ -97,7 +97,7 @@ echo $OUTPUT->header();
 
 // Prepare a progress bar which can display optionally during long-running
 // operations while setting up the UI.
-$slowprogress = new core_backup_display_progress_if_slow(get_string('preparingui', 'backup'));
+$slowprogress = new \core\progress\display_if_slow(get_string('preparingui', 'backup'));
 
 $previous = optional_param('previous', false, PARAM_BOOL);
 if ($backup->get_stage() == backup_ui::STAGE_SCHEMA && !$previous) {
@@ -121,7 +121,7 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
     // Display an extra backup step bar so that we can show the 'processing' step first.
     echo html_writer::start_div('', array('id' => 'executionprogress'));
     echo $renderer->progress_bar($backup->get_progress_bar());
-    $backup->get_controller()->set_progress(new core_backup_display_progress());
+    $backup->get_controller()->set_progress(new \core\progress\display());
 
     // Prepare logger and add to end of chain.
     $logger = new core_backup_html_logger($CFG->debugdeveloper ? backup::LOG_DEBUG : backup::LOG_INFO);
index 345831c..cd124e5 100644 (file)
@@ -110,7 +110,7 @@ class backup_controller extends base_controller {
 
         // By default there is no progress reporter. Interfaces that wish to
         // display progress must set it.
-        $this->progress = new core_backup_null_progress();
+        $this->progress = new \core\progress\null();
 
         // Instantiate the output_controller singleton and active it if interactive and inmediate
         $oc = output_controller::get_instance();
index 4b017b7..0998796 100644 (file)
@@ -24,7 +24,7 @@
  */
 abstract class base_controller extends backup implements loggable {
     /**
-     * @var core_backup_progress Progress reporting object.
+     * @var \core\progress\base Progress reporting object.
      */
     protected $progress;
 
@@ -37,7 +37,7 @@ abstract class base_controller extends backup implements loggable {
      * Gets the progress reporter, which can be used to report progress within
      * the backup or restore process.
      *
-     * @return core_backup_progress Progress reporting object
+     * @return \core\progress\base Progress reporting object
      */
     public function get_progress() {
         return $this->progress;
@@ -46,9 +46,9 @@ abstract class base_controller extends backup implements loggable {
     /**
      * Sets the progress reporter.
      *
-     * @param core_backup_progress $progress Progress reporting object
+     * @param \core\progress\base $progress Progress reporting object
      */
-    public function set_progress(core_backup_progress $progress) {
+    public function set_progress(\core\progress\base $progress) {
         $this->progress = $progress;
     }
 
@@ -82,4 +82,4 @@ abstract class base_controller extends backup implements loggable {
     public function log($message, $level, $a = null, $depth = null, $display = false) {
         backup_helper::log($message, $level, $a, $depth, $display, $this->logger);
     }
-}
\ No newline at end of file
+}
index f4d3958..822992c 100644 (file)
@@ -70,10 +70,10 @@ class restore_controller extends base_controller {
      * @param int $mode backup::MODE_[ GENERAL | HUB | IMPORT | SAMESITE ]
      * @param int $userid
      * @param int $target backup::TARGET_[ NEW_COURSE | CURRENT_ADDING | CURRENT_DELETING | EXISTING_ADDING | EXISTING_DELETING ]
-     * @param core_backup_progress $progress Optional progress monitor
+     * @param \core\progress\base $progress Optional progress monitor
      */
     public function __construct($tempdir, $courseid, $interactive, $mode, $userid, $target,
-            core_backup_progress $progress = null) {
+            \core\progress\base $progress = null) {
         $this->tempdir = $tempdir;
         $this->courseid = $courseid;
         $this->interactive = $interactive;
@@ -111,7 +111,7 @@ class restore_controller extends base_controller {
         if ($progress) {
             $this->progress = $progress;
         } else {
-            $this->progress = new core_backup_null_progress();
+            $this->progress = new \core\progress\null();
         }
         $this->progress->start_progress('Constructing restore_controller');
 
index 7a32057..b2f34ab 100644 (file)
@@ -95,7 +95,7 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
     echo $renderer->progress_bar($backup->get_progress_bar());
 
     // Start the progress display - we split into 2 chunks for backup and restore.
-    $progress = new core_backup_display_progress();
+    $progress = new \core\progress\display();
     $progress->start_progress('', 2);
     $backup->get_controller()->set_progress($progress);
 
index 46862a7..4e79cc5 100644 (file)
@@ -1748,13 +1748,13 @@ class backup_zip_contents extends backup_execution_step implements file_progress
         // Start tracking progress if necessary.
         if (!$this->startedprogress) {
             $reporter->start_progress('extract_file_to_dir', ($max == file_progress::INDETERMINATE)
-                    ? core_backup_progress::INDETERMINATE : $max);
+                    ? \core\progress\base::INDETERMINATE : $max);
             $this->startedprogress = true;
         }
 
         // Pass progress through to whatever handles it.
         $reporter->progress(($progress == file_progress::INDETERMINATE)
-                ? core_backup_progress::INDETERMINATE : $progress);
+                ? \core\progress\base::INDETERMINATE : $progress);
      }
 }
 
index cba5771..1045d94 100644 (file)
@@ -3564,7 +3564,7 @@ class restore_create_question_files extends restore_execution_step {
 
         // Track progress, as this task can take a long time.
         $progress = $this->task->get_progress();
-        $progress->start_progress($this->get_name(), core_backup_progress::INDETERMINATE);
+        $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
 
         // Let's process only created questions
         $questionsrs = $DB->get_recordset_sql("SELECT bi.itemid, bi.newitemid, bi.parentitemid, q.qtype
index 9612163..3ac6f8d 100644 (file)
@@ -34,7 +34,7 @@ echo $OUTPUT->header();
 
 // Prepare a progress bar which can display optionally during long-running
 // operations while setting up the UI.
-$slowprogress = new core_backup_display_progress_if_slow(get_string('preparingui', 'backup'));
+$slowprogress = new \core\progress\display_if_slow(get_string('preparingui', 'backup'));
 
 // Overall, allow 10 units of progress.
 $slowprogress->start_progress('', 10);
@@ -90,7 +90,7 @@ $slowprogress->end_progress();
 
 if (!$restore->is_independent()) {
     // Use a temporary (disappearing) progress bar to show the precheck progress if any.
-    $precheckprogress = new core_backup_display_progress_if_slow(get_string('preparingdata', 'backup'));
+    $precheckprogress = new \core\progress\display_if_slow(get_string('preparingdata', 'backup'));
     $restore->get_controller()->set_progress($precheckprogress);
     if ($restore->get_stage() == restore_ui::STAGE_PROCESS && !$restore->requires_substage()) {
         try {
@@ -99,7 +99,7 @@ if (!$restore->is_independent()) {
             // Show the current restore state (header with bolded item).
             echo $renderer->progress_bar($restore->get_progress_bar());
             // Start displaying the actual progress bar percentage.
-            $restore->get_controller()->set_progress(new core_backup_display_progress());
+            $restore->get_controller()->set_progress(new \core\progress\display());
             // Prepare logger.
             $logger = new core_backup_html_logger($CFG->debugdeveloper ? backup::LOG_DEBUG : backup::LOG_INFO);
             $restore->get_controller()->add_logger($logger);
index d9715d1..a6cfb55 100644 (file)
@@ -350,10 +350,10 @@ abstract class backup_controller_dbops extends backup_dbops {
      * to track progress in processing (in case this task takes a long time).
      *
      * @param string $backupid Backup ID
-     * @param core_backup_progress $progress Optional progress monitor
+     * @param \core\progress\base $progress Optional progress monitor
      */
     public static function get_moodle_backup_information($backupid,
-            core_backup_progress $progress = null) {
+            \core\progress\base $progress = null) {
 
         // Start tracking progress if required (for load_controller).
         if ($progress) {
index 61a903a..6202906 100644 (file)
@@ -133,9 +133,9 @@ abstract class backup_structure_dbops extends backup_dbops {
      *
      * @param string $backupid Backup ID
      * @param string $itemname Item name
-     * @param core_backup_progress $progress Progress tracker
+     * @param \core\progress\base $progress Progress tracker
      */
-    public static function move_annotations_to_final($backupid, $itemname, core_backup_progress $progress) {
+    public static function move_annotations_to_final($backupid, $itemname, \core\progress\base $progress) {
         global $DB;
         $progress->start_progress('move_annotations_to_final');
         $rs = $DB->get_recordset('backup_ids_temp', array('backupid' => $backupid, 'itemname' => $itemname));
index 00fb8f3..079b63c 100644 (file)
@@ -112,10 +112,10 @@ abstract class restore_dbops {
      *
      * @param string $restoreid Restore id
      * @param string $inforeffile File path
-     * @param core_backup_progress $progress Progress tracker
+     * @param \core\progress\base $progress Progress tracker
      */
     public static function load_inforef_to_tempids($restoreid, $inforeffile,
-            core_backup_progress $progress = null) {
+            \core\progress\base $progress = null) {
 
         if (!file_exists($inforeffile)) { // Shouldn't happen ever, but...
             throw new backup_helper_exception('missing_inforef_xml_file', $inforeffile);
@@ -123,7 +123,7 @@ abstract class restore_dbops {
 
         // Set up progress tracking (indeterminate).
         if (!$progress) {
-            $progress = new core_backup_null_progress();
+            $progress = new \core\progress\null();
         }
         $progress->start_progress('Loading inforef.xml file');
 
@@ -419,10 +419,10 @@ abstract class restore_dbops {
      *
      * @param string $restoreid Restore id
      * @param string $usersfile File path
-     * @param core_backup_progress $progress Progress tracker
+     * @param \core\progress\base $progress Progress tracker
      */
     public static function load_users_to_tempids($restoreid, $usersfile,
-            core_backup_progress $progress = null) {
+            \core\progress\base $progress = null) {
 
         if (!file_exists($usersfile)) { // Shouldn't happen ever, but...
             throw new backup_helper_exception('missing_users_xml_file', $usersfile);
@@ -430,7 +430,7 @@ abstract class restore_dbops {
 
         // Set up progress tracking (indeterminate).
         if (!$progress) {
-            $progress = new core_backup_null_progress();
+            $progress = new \core\progress\null();
         }
         $progress->start_progress('Loading users into temporary table');
 
@@ -861,13 +861,13 @@ abstract class restore_dbops {
      * @param int|null $olditemid
      * @param int|null $forcenewcontextid explicit value for the new contextid (skip mapping)
      * @param bool $skipparentitemidctxmatch
-     * @param core_backup_progress $progress Optional progress reporter
+     * @param \core\progress\base $progress Optional progress reporter
      * @return array of result object
      */
     public static function send_files_to_pool($basepath, $restoreid, $component, $filearea,
             $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null,
             $forcenewcontextid = null, $skipparentitemidctxmatch = false,
-            core_backup_progress $progress = null) {
+            \core\progress\base $progress = null) {
         global $DB, $CFG;
 
         $backupinfo = backup_general_helper::get_backup_information(basename($basepath));
@@ -1084,10 +1084,10 @@ abstract class restore_dbops {
      * @param string $basepath Base path of unzipped backup
      * @param string $restoreid Restore ID
      * @param int $userid Default userid for files
-     * @param core_backup_progress $progress Object used for progress tracking
+     * @param \core\progress\base $progress Object used for progress tracking
      */
     public static function create_included_users($basepath, $restoreid, $userid,
-            core_backup_progress $progress) {
+            \core\progress\base $progress) {
         global $CFG, $DB;
         $progress->start_progress('Creating included users');
 
@@ -1481,10 +1481,10 @@ abstract class restore_dbops {
      * @param int $courseid Course id
      * @param int $userid User id
      * @param bool $samesite True if restore is to same site
-     * @param core_backup_progress $progress Progress reporter
+     * @param \core\progress\base $progress Progress reporter
      */
     public static function precheck_included_users($restoreid, $courseid, $userid, $samesite,
-            core_backup_progress $progress) {
+            \core\progress\base $progress) {
         global $CFG, $DB;
 
         // To return any problem found
@@ -1570,10 +1570,10 @@ abstract class restore_dbops {
      * @param int $courseid Course id
      * @param int $userid User id
      * @param bool $samesite True if restore is to same site
-     * @param core_backup_progress $progress Optional progress tracker
+     * @param \core\progress\base $progress Optional progress tracker
      */
     public static function process_included_users($restoreid, $courseid, $userid, $samesite,
-            core_backup_progress $progress = null) {
+            \core\progress\base $progress = null) {
         global $DB;
 
         // Just let precheck_included_users() to do all the hard work
index c2d59aa..963e471 100644 (file)
@@ -46,9 +46,9 @@ abstract class backup_helper {
      * progress reports.
      *
      * @param string $backupid Backup id
-     * @param core_backup_progress $progress Optional progress reporting object
+     * @param \core\progress\base $progress Optional progress reporting object
      */
-    static public function clear_backup_dir($backupid, core_backup_progress $progress = null) {
+    static public function clear_backup_dir($backupid, \core\progress\base $progress = null) {
         global $CFG;
         if (!self::delete_dir_contents($CFG->tempdir . '/backup/' . $backupid, '', $progress)) {
             throw new backup_helper_exception('cannot_empty_backup_temp_dir');
@@ -63,9 +63,9 @@ abstract class backup_helper {
      * progress reports.
      *
      * @param string $backupid Backup id
-     * @param core_backup_progress $progress Optional progress reporting object
+     * @param \core\progress\base $progress Optional progress reporting object
      */
-     static public function delete_backup_dir($backupid, core_backup_progress $progress = null) {
+     static public function delete_backup_dir($backupid, \core\progress\base $progress = null) {
          global $CFG;
          self::clear_backup_dir($backupid, $progress);
          return rmdir($CFG->tempdir . '/backup/' . $backupid);
@@ -81,9 +81,9 @@ abstract class backup_helper {
      *
      * @param string $dir Directory to delete
      * @param string $excludedir Exclude this directory
-     * @param core_backup_progress $progress Optional progress reporting object
+     * @param \core\progress\base $progress Optional progress reporting object
      */
-    static public function delete_dir_contents($dir, $excludeddir='', core_backup_progress $progress = null) {
+    static public function delete_dir_contents($dir, $excludeddir='', \core\progress\base $progress = null) {
         global $CFG;
 
         if ($progress) {
@@ -154,9 +154,9 @@ abstract class backup_helper {
      * progress reports.
      *
      * @param int $deletefrom Time to delete from
-     * @param core_backup_progress $progress Optional progress reporting object
+     * @param \core\progress\base $progress Optional progress reporting object
      */
-    static public function delete_old_backup_dirs($deletefrom, core_backup_progress $progress = null) {
+    static public function delete_old_backup_dirs($deletefrom, \core\progress\base $progress = null) {
         global $CFG;
 
         $status = true;
@@ -213,12 +213,12 @@ abstract class backup_helper {
      *
      * @param int $backupid
      * @param string $filepath zip file containing the backup
-     * @param core_backup_progress $progress Optional progress monitor
+     * @param \core\progress\base $progress Optional progress monitor
      * @return stored_file if created, null otherwise
      *
      * @throws moodle_exception in case of any problems
      */
-    static public function store_backup_file($backupid, $filepath, core_backup_progress $progress = null) {
+    static public function store_backup_file($backupid, $filepath, \core\progress\base $progress = null) {
         global $CFG;
 
         // First of all, get some information from the backup_controller to help us decide
index 2a4f222..87d237d 100644 (file)
@@ -72,10 +72,6 @@ require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/core_backup_html_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php');
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php');
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_null_progress.class.php');
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_display_progress.class.php');
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_display_progress_if_slow.class.php');
 require_once($CFG->dirroot . '/backup/util/settings/setting_dependency.class.php');
 require_once($CFG->dirroot . '/backup/util/settings/base_setting.class.php');
 require_once($CFG->dirroot . '/backup/util/settings/backup_setting.class.php');
index fd223c5..ec0f8eb 100644 (file)
@@ -61,10 +61,6 @@ require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/core_backup_html_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php');
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php');
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_null_progress.class.php');
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_display_progress.class.php');
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_display_progress_if_slow.class.php');
 require_once($CFG->dirroot . '/backup/util/factories/backup_factory.class.php');
 require_once($CFG->dirroot . '/backup/util/factories/restore_factory.class.php');
 require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php');
index 3d537d2..2fcb469 100644 (file)
@@ -91,7 +91,7 @@ class backup_plan extends base_plan implements loggable {
      * Gets the progress reporter, which can be used to report progress within
      * the backup or restore process.
      *
-     * @return core_backup_progress Progress reporting object
+     * @return \core\progress\base Progress reporting object
      */
     public function get_progress() {
         return $this->controller->get_progress();
index c465b2e..07f3805 100644 (file)
@@ -185,7 +185,7 @@ abstract class base_plan implements checksumable, executable {
      * Gets the progress reporter, which can be used to report progress within
      * the backup or restore process.
      *
-     * @return core_backup_progress Progress reporting object
+     * @return \core\progress\base Progress reporting object
      */
     public abstract function get_progress();
 
index e167b89..19cb072 100644 (file)
@@ -126,7 +126,7 @@ abstract class base_task implements checksumable, executable, loggable {
      * Gets the progress reporter, which can be used to report progress within
      * the backup or restore process.
      *
-     * @return core_backup_progress Progress reporting object
+     * @return \core\progress\base Progress reporting object
      */
     public function get_progress() {
         return $this->plan->get_progress();
index e173e0e..6f5b5b5 100644 (file)
@@ -98,7 +98,7 @@ class restore_plan extends base_plan implements loggable {
      * Gets the progress reporter, which can be used to report progress within
      * the backup or restore process.
      *
-     * @return core_backup_progress Progress reporting object
+     * @return \core\progress\base Progress reporting object
      */
     public function get_progress() {
         return $this->controller->get_progress();
index 300f92d..e4685b0 100644 (file)
@@ -103,7 +103,7 @@ abstract class restore_structure_step extends restore_step {
 
         // Set up progress tracking.
         $progress = $this->get_task()->get_progress();
-        $progress->start_progress($this->get_name(), core_backup_progress::INDETERMINATE);
+        $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
         $xmlparser->set_progress($progress);
 
         // And process it, dispatch to target methods in step will start automatically
@@ -229,7 +229,7 @@ abstract class restore_structure_step extends restore_step {
         // the execute() method here, which does set up progress like this.)
         $progress = $this->get_task()->get_progress();
         if (!$progress->is_in_progress_section() ||
-                $progress->get_current_max() !== core_backup_progress::INDETERMINATE) {
+                $progress->get_current_max() !== \core\progress\base::INDETERMINATE) {
             $progress = null;
         }
 
index 88cfeca..2cc41dc 100644 (file)
@@ -38,7 +38,7 @@ class backup_structure_processor extends base_processor {
     protected $vars;   // array of backup::VAR_XXX => helper value pairs to be used by source specifications
 
     /**
-     * @var core_backup_progress Progress tracker (null if none)
+     * @var \core\progress\base Progress tracker (null if none)
      */
     protected $progress;
 
@@ -46,9 +46,9 @@ class backup_structure_processor extends base_processor {
      * Constructor.
      *
      * @param xml_writer $writer XML writer to save data
-     * @param core_backup_progress $progress Progress tracker (optional)
+     * @param c\core\progress\base$progress Progress tracker (optional)
      */
-    public function __construct(xml_writer $writer, core_backup_progress $progress = null) {
+    public function __construct(xml_writer $writer, \core\progress\base $progress = null) {
         $this->writer = $writer;
         $this->progress = $progress;
         $this->vars = array();
index 251cbc1..cd74972 100644 (file)
@@ -51,7 +51,7 @@ class restore_ui extends base_ui {
     protected $stage = null;
 
     /**
-     * @var core_backup_progress Progress indicator (where there is no controller)
+     * @var \core\progress\base Progress indicator (where there is no controller)
      */
     protected $progressreporter = null;
 
@@ -146,11 +146,11 @@ class restore_ui extends base_ui {
      * there are long-running tasks even though there is no restore controller
      * in use.
      *
-     * @return core_backup_null_progress
+     * @return \core\progress\null
      */
     public function get_progress_reporter() {
         if (!$this->progressreporter) {
-            $this->progressreporter = new core_backup_null_progress();
+            $this->progressreporter = new \core\progress\null();
         }
         return $this->progressreporter;
     }
@@ -158,9 +158,9 @@ class restore_ui extends base_ui {
     /**
      * Sets the progress reporter that will be returned by get_progress_reporter.
      *
-     * @param core_backup_progress $progressreporter Progress reporter
+     * @param c\core\progress\base$progressreporter Progress reporter
      */
-    public function set_progress_reporter(core_backup_progress $progressreporter) {
+    public function set_progress_reporter(\core\progress\base $progressreporter) {
         $this->progressreporter = $progressreporter;
     }
 
index ef559fd..63dc00e 100644 (file)
@@ -94,7 +94,7 @@ abstract class restore_ui_stage extends base_ui_stage {
  */
 abstract class restore_ui_independent_stage {
     /**
-     * @var core_backup_progress Optional progress reporter
+     * @var \core\progress\base Optional progress reporter
      */
     private $progressreporter;
 
@@ -117,11 +117,11 @@ abstract class restore_ui_independent_stage {
      * in use. There is a similar function in restore_ui. but that class is not
      * used on some stages.
      *
-     * @return core_backup_null_progress
+     * @return \core\progress\null
      */
     public function get_progress_reporter() {
         if (!$this->progressreporter) {
-            $this->progressreporter = new core_backup_null_progress();
+            $this->progressreporter = new \core\progress\null();
         }
         return $this->progressreporter;
     }
@@ -129,9 +129,9 @@ abstract class restore_ui_independent_stage {
     /**
      * Sets the progress reporter that will be returned by get_progress_reporter.
      *
-     * @param core_backup_progress $progressreporter Progress reporter
+     * @param \core\progress\base $progressreporter Progress reporter
      */
-    public function set_progress_reporter(core_backup_progress $progressreporter) {
+    public function set_progress_reporter(\core\progress\base $progressreporter) {
         $this->progressreporter = $progressreporter;
     }
 
@@ -272,13 +272,13 @@ class restore_ui_stage_confirm extends restore_ui_independent_stage implements f
         // Start tracking progress if necessary.
         if (!$this->startedprogress) {
             $reporter->start_progress('extract_file_to_dir',
-                    ($max == file_progress::INDETERMINATE) ? core_backup_progress::INDETERMINATE : $max);
+                    ($max == file_progress::INDETERMINATE) ? \core\progress\base::INDETERMINATE : $max);
             $this->startedprogress = true;
         }
 
         // Pass progress through to whatever handles it.
         $reporter->progress(
-                ($progress == file_progress::INDETERMINATE) ? core_backup_progress::INDETERMINATE : $progress);
+                ($progress == file_progress::INDETERMINATE) ? \core\progress\base::INDETERMINATE : $progress);
     }
 
     /**
index 882852a..a5ebc4d 100644 (file)
@@ -29,7 +29,7 @@
  * attributes and case folding and works only with UTF-8 content. It's one
  * progressive push parser because, intead of loading big crunchs of information
  * in memory, it "publishes" (pushes) small information in a "propietary array format" througt
- * the corresponding @progressive_parser_procesor, that will be the responsibe for
+ * the corresponding @progressive_parser_processor, that will be the responsibe for
  * returning information into handy formats to higher levels.
  *
  * Note that, while this progressive parser is able to process any XML file, it is
@@ -37,7 +37,7 @@
  * the expected behaviour) so information belonging to the same path can be returned in
  * different chunks if there are inner levels/paths in the middle. Be warned!
  *
- * The "propietary array format" that the parser publishes to the @progressive_parser_procesor
+ * The "propietary array format" that the parser publishes to the @progressive_parser_processor
  * is this:
  *    array (
  *        'path' => path where the tags belong to,
@@ -56,7 +56,11 @@ class progressive_parser {
     protected $xml_parser; // PHP's low level XML SAX parser
     protected $file;       // full path to file being progressively parsed | => mutually exclusive
     protected $contents;   // contents being progressively parsed          |
-    protected $procesor;   // progressive_parser_procesor to be used to publish processed information
+
+    /**
+     * @var progressive_parser_processor to be used to publish processed information
+     */
+    protected $processor;
 
     protected $level;      // level of the current tag
     protected $path;       // path of the current tag
@@ -68,7 +72,7 @@ class progressive_parser {
     protected $currtag;    // name/value/attributes of the tag being processed
 
     /**
-     * @var core_backup_progress Progress tracker called for each action
+     * @var \core\progress\base Progress tracker called for each action
      */
     protected $progress;
 
@@ -129,9 +133,9 @@ class progressive_parser {
      *
      * The caller should have already called start_progress on the progress tracker.
      *
-     * @param core_backup_progress $progress Progress tracker
+     * @param \core\progress\base $progress Progress tracker
      */
-    public function set_progress(core_backup_progress $progress) {
+    public function set_progress(\core\progress\base $progress) {
         $this->progress = $progress;
     }
 
index 97b3271..aa074b4 100644 (file)
@@ -233,7 +233,7 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
         $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
         $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id));
-        $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->id => $this->module->id));
+        $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY, 'module_'.$this->module->cmid => $this->module->cmid));
 
         // Set completion for forum activity.
         $c = new completion_info($this->course);
index cb4a1c5..1e7aae4 100644 (file)
@@ -88,7 +88,7 @@ class block_comments_events_testcase extends advanced_testcase {
         $this->assertEquals($url, $event->get_url());
 
         // Comments when block is on module (wiki) page.
-        $context = context_module::instance($this->wiki->id);
+        $context = context_module::instance($this->wiki->cmid);
         $args = new stdClass;
         $args->context   = $context;
         $args->course    = $this->course;
@@ -111,7 +111,7 @@ class block_comments_events_testcase extends advanced_testcase {
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\block_comments\event\comment_created', $event);
         $this->assertEquals($context, $event->get_context());
-        $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->id));
+        $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->cmid));
         $this->assertEquals($url, $event->get_url());
         $this->assertEventContextNotUsed($event);
     }
@@ -153,7 +153,7 @@ class block_comments_events_testcase extends advanced_testcase {
         $this->assertEquals($url, $event->get_url());
 
         // Comments when block is on module (wiki) page.
-        $context = context_module::instance($this->wiki->id);
+        $context = context_module::instance($this->wiki->cmid);
         $args = new stdClass;
         $args->context   = $context;
         $args->course    = $this->course;
@@ -177,7 +177,7 @@ class block_comments_events_testcase extends advanced_testcase {
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\block_comments\event\comment_deleted', $event);
         $this->assertEquals($context, $event->get_context());
-        $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->id));
+        $url = new moodle_url('/mod/wiki/view.php', array('id' => $this->wiki->cmid));
         $this->assertEquals($url, $event->get_url());
         $this->assertEventContextNotUsed($event);
     }
index b125d6a..1f59f8e 100644 (file)
@@ -681,9 +681,11 @@ $CFG->admin = 'admin';
 // Example:
 //   $CFG->behat_additionalfeatures = array('/home/developer/code/wipfeatures');
 //
-// You can make behat save a screenshot when a scenario fails.
+// You can make behat save several dumps when a scenario fails. The dumps currently saved are:
+// * a dump of the DOM in it's state at the time of failure; and
+// * a screenshot (JavaScript is required for the screenshot functionality, so not all browsers support this option)
 // Example:
-//   $CFG->behat_screenshots_path = '/my/path/to/save/screenshots';
+//   $CFG->behat_faildump_path = '/my/path/to/save/failure/dumps';
 //
 //=========================================================================
 // 12. DEVELOPER DATA GENERATOR
index f257f91..b88ab77 100644 (file)
@@ -45,8 +45,11 @@ if ($id) {
         print_error('cannoteditsiteform');
     }
 
-    $course = course_get_format($id)->get_course();
+    // Login to the course and retrieve also all fields defined by course format.
+    $course = get_course($id);
     require_login($course);
+    $course = course_get_format($course)->get_course();
+
     $category = $DB->get_record('course_categories', array('id'=>$course->category), '*', MUST_EXIST);
     $coursecontext = context_course::instance($course->id);
     require_capability('moodle/course:update', $coursecontext);
index 38c4a69..a68ce5a 100644 (file)
@@ -237,14 +237,21 @@ abstract class format_base {
         if ($this->course === false) {
             $this->course = get_course($this->courseid);
             $options = $this->get_format_options();
+            $dbcoursecolumns = null;
             foreach ($options as $optionname => $optionvalue) {
-                if (!isset($this->course->$optionname)) {
-                    $this->course->$optionname = $optionvalue;
-                } else {
-                    debugging('The option name '.$optionname.' in course format '.$this->format.
-                        ' is invalid because the field with the same name exists in {course} table',
-                        DEBUG_DEVELOPER);
+                if (isset($this->course->$optionname)) {
+                    // Course format options must not have the same names as existing columns in db table "course".
+                    if (!isset($dbcoursecolumns)) {
+                        $dbcoursecolumns = $DB->get_columns('course');
+                    }
+                    if (isset($dbcoursecolumns[$optionname])) {
+                        debugging('The option name '.$optionname.' in course format '.$this->format.
+                            ' is invalid because the field with the same name exists in {course} table',
+                            DEBUG_DEVELOPER);
+                        continue;
+                    }
                 }
+                $this->course->$optionname = $optionvalue;
             }
         }
         return $this->course;
index b719a13..edc7d87 100644 (file)
@@ -373,7 +373,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 if ($cancomplete && $completioninfo->is_enabled($thismod) != COMPLETION_TRACKING_NONE) {
                     $total++;
                     $completiondata = $completioninfo->get_data($thismod, true);
-                    if ($completiondata->completionstate == COMPLETION_COMPLETE) {
+                    if ($completiondata->completionstate == COMPLETION_COMPLETE ||
+                            $completiondata->completionstate == COMPLETION_COMPLETE_PASS) {
                         $complete++;
                     }
                 }
index e46d1b1..9e73cc9 100644 (file)
@@ -31,9 +31,16 @@ class enrol_database_testcase extends advanced_testcase {
     protected static $users = array();
     protected static $roles = array();
 
+    /** @var string Original error log */
+    protected $oldlog;
+
     protected function init_enrol_database() {
         global $DB, $CFG;
 
+        // Discard error logs from AdoDB.
+        $this->oldlog = ini_get('error_log');
+        ini_set('error_log', "$CFG->dataroot/testlog.log");
+
         $dbman = $DB->get_manager();
 
         set_config('dbencoding', 'utf-8', 'enrol_database');
@@ -160,6 +167,8 @@ class enrol_database_testcase extends advanced_testcase {
         self::$courses = null;
         self::$users = null;
         self::$roles = null;
+
+        ini_set('error_log', $this->oldlog);
     }
 
     protected function reset_enrol_database() {
index 36383fb..3922c64 100644 (file)
@@ -223,7 +223,7 @@ class core_files_externallib_testcase extends advanced_testcase {
         // Insert the information about the file.
         $contentid = $DB->insert_record('data_content', $datacontent);
         // Required information for uploading a file.
-        $context = context_module::instance($module->id);
+        $context = context_module::instance($module->cmid);
         $usercontext = context_user::instance($USER->id);
         $component = 'mod_data';
         $filearea = 'content';
@@ -251,6 +251,7 @@ class core_files_externallib_testcase extends advanced_testcase {
         $testfilelisting = core_files_external::get_files($context->id, $component, $filearea, $itemid, '/', $filename);
 
         // With the information that we have provided we should get an object exactly like the one below.
+        $coursecontext = context_course::instance($course->id);
         $testdata = array();
         $testdata['parents'] = array();
         $testdata['parents']['0'] = array('contextid' => 1,
@@ -265,43 +266,43 @@ class core_files_externallib_testcase extends advanced_testcase {
                                           'itemid' => null,
                                           'filepath' => null,
                                           'filename' => 'Miscellaneous');
-        $testdata['parents']['2'] = array('contextid' => 15,
+        $testdata['parents']['2'] = array('contextid' => $coursecontext->id,
                                           'component' => null,
                                           'filearea' => null,
                                           'itemid' => null,
                                           'filepath' => null,
                                           'filename' => 'Test course 1');
-        $testdata['parents']['3'] = array('contextid' => 20,
+        $testdata['parents']['3'] = array('contextid' => $context->id,
                                           'component' => null,
                                           'filearea' => null,
                                           'itemid' => null,
                                           'filepath' => null,
                                           'filename' => 'Mod data upload test (Database)');
-        $testdata['parents']['4'] = array('contextid' => 20,
+        $testdata['parents']['4'] = array('contextid' => $context->id,
                                           'component' => 'mod_data',
                                           'filearea' => 'content',
                                           'itemid' => null,
                                           'filepath' => null,
                                           'filename' => 'Fields');
         $testdata['files'] = array();
-        $testdata['files']['0'] = array('contextid' => 20,
+        $testdata['files']['0'] = array('contextid' => $context->id,
                                         'component' => 'mod_data',
                                         'filearea' => 'content',
-                                        'itemid' => '1',
+                                        'itemid' => $itemid,
                                         'filepath' => '/',
                                         'filename' => 'Simple4.txt',
-                                        'url' => 'http://www.example.com/moodle/pluginfile.php/20/mod_data/content/1/Simple4.txt',
+                                        'url' => 'http://www.example.com/moodle/pluginfile.php/'.$context->id.'/mod_data/content/'.$itemid.'/Simple4.txt',
                                         'isdir' => false,
                                         'timemodified' => $timemodified);
         // Make sure that they are the same.
-        $this->assertEquals($testfilelisting, $testdata);
+        $this->assertEquals($testdata, $testfilelisting);
 
         // Try again but without the context. Minus one signals the function to use other variables to obtain the context.
         $nocontext = -1;
         $modified = 0;
         // Context level and instance ID are used to determine what the context is.
         $contextlevel = 'module';
-        $instanceid = $module->id;
+        $instanceid = $module->cmid;
         $testfilelisting = core_files_external::get_files($nocontext, $component, $filearea, $itemid, '/', $filename, $modified, $contextlevel, $instanceid);
         $this->assertEquals($testfilelisting, $testdata);
     }
index 75af83a..6ca09c9 100644 (file)
@@ -65,7 +65,7 @@ class backup_gradingform_guide_plugin extends backup_gradingform_plugin {
 
         $pluginwrapper->add_child($criteria);
         $criteria->add_child($criterion);
-        $criteria->add_child($comments);
+        $pluginwrapper->add_child($comments);
         $comments->add_child($comment);
 
         // Set sources to populate the data.
index dd99814..f833404 100644 (file)
@@ -48,6 +48,11 @@ class restore_gradingform_guide_plugin extends restore_gradingform_plugin {
         $paths[] = new restore_path_element('gradingform_guide_comment',
             $this->get_pathfor('/guidecomments/guidecomment'));
 
+        // MDL-37714: Correctly locate frequent used comments in both the
+        // current and incorrect old format.
+        $paths[] = new restore_path_element('gradingform_guide_comment_legacy',
+            $this->get_pathfor('/guidecriteria/guidecomments/guidecomment'));
+
         return $paths;
     }
 
@@ -99,6 +104,20 @@ class restore_gradingform_guide_plugin extends restore_gradingform_plugin {
         $DB->insert_record('gradingform_guide_comments', $data);
     }
 
+    /**
+     * Processes comments element data
+     *
+     * @param array|stdClass $data The data to insert as a comment
+     */
+    public function process_gradingform_guide_comment_legacy($data) {
+        global $DB;
+
+        $data = (object)$data;
+        $data->definitionid = $this->get_new_parentid('grading_definition');
+
+        $DB->insert_record('gradingform_guide_comments', $data);
+    }
+
     /**
      * Processes filling element data
      *
index f3d768c..051c155 100644 (file)
@@ -52,7 +52,7 @@ class core_grade_edittreelib_testcase extends advanced_testcase {
         $scale = $this->getDataGenerator()->create_scale();
         $course = $this->getDataGenerator()->create_course();
         $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
-        $modulecontext = context_module::instance($assign->id);
+        $modulecontext = context_module::instance($assign->cmid);
         // The generator returns a dummy object, lets get the real assign object.
         $assign = new assign($modulecontext, false, false);
         $cm = $assign->get_course_module();
index 3230236..e341297 100644 (file)
@@ -66,7 +66,7 @@ class core_grading_externallib_testcase extends externallib_advanced_testcase {
         // Create a teacher and give them capabilities.
         $coursecontext = context_course::instance($course->id);
         $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $coursecontext->id, 3);
-        $modulecontext = context_module::instance($cm->id);
+        $modulecontext = context_module::instance($cm->cmid);
         $this->assignUserCapability('mod/assign:grade', $modulecontext->id, $roleid);
 
         // Create the teacher's enrolment record.
@@ -146,7 +146,7 @@ class core_grading_externallib_testcase extends externallib_advanced_testcase {
         $DB->insert_record('gradingform_rubric_levels', $rubriclevel2);
 
         // Call the external function.
-        $cmids = array ($cm->id);
+        $cmids = array ($cm->cmid);
         $areaname = 'submissions';
         $result = core_grading_external::get_definitions($cmids, $areaname);
 
@@ -209,7 +209,7 @@ class core_grading_externallib_testcase extends externallib_advanced_testcase {
         // Create a teacher and give them capabilities.
         $coursecontext = context_course::instance($course->id);
         $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $coursecontext->id, 3);
-        $modulecontext = context_module::instance($assign->id);
+        $modulecontext = context_module::instance($assign->cmid);
         $this->assignUserCapability('mod/assign:grade', $modulecontext->id, $roleid);
 
         // Create the teacher's enrolment record.
index fef76b1..ca22495 100644 (file)
@@ -64,22 +64,6 @@ function UpdatableGroupsCombo(wwwRoot, courseId) {
         }
 
     };
-
-    // Add onchange event to groups list box.
-    // Okay, this is not working in IE. The onchange is never fired...
-    // I'm hard coding the onchange in ../index.php. Not ideal, but it works
-    // then. vyshane AT moodle DOT com.
-    /*
-    groupsComboEl = document.getElementById("groups");
-    if (groupsComboEl) {
-        groupsComboEl.setAttribute("onchange", "membersCombo.refreshMembers(this.options[this.selectedIndex].value);");
-    }
-    */
-
-    // Hide the updategroups input since AJAX will take care of this.
-    YUI().use('yui2-dom', function (Y) {
-        Y.YUI2.util.Dom.setStyle("updategroups", "display", "none");
-    });
 }
 
 
@@ -131,9 +115,10 @@ function UpdatableMembersCombo(wwwRoot, courseId) {
     };
 
     // Hide the updatemembers input since AJAX will take care of this.
-    YUI().use('yui2-dom', function (Y) {
-        Y.YUI2.util.Dom.setStyle("updatemembers", "display", "none");
-    });
+    var updatemembers = Y.one('#updatemembers');
+    if (updatemembers) {
+        updatemembers.hide();
+    }
 }
 
 /**
index d98781c..06391c3 100644 (file)
@@ -44,7 +44,7 @@ $string['cannotunzipfile'] = '無法解壓縮檔案。';
 $string['componentisuptodate'] = '元件已經是最新的了。';
 $string['dmlexceptiononinstall'] = '<p>資料庫有誤 [{$a->錯誤碼}].<br />{$a->排除故障資訊}</p>';
 $string['downloadedfilecheckfailed'] = '下載檔案檢查錯誤。';
-$string['invalidmd5'] = '無效的 md5';
+$string['invalidmd5'] = '這檢查變項是錯的,再試一次';
 $string['missingrequiredfield'] = '缺少部份必填欄位';
 $string['remotedownloaderror'] = '下載元件至伺服器失敗,檢查代理伺服器的設定、高度建議安裝PHP cURL,您必須手動下載<a href="{$a->url}">{$a->url}</a>,並且複製到伺服器"{$a->dest}" 解壓縮';
 $string['wrongdestpath'] = '錯誤的目的路徑。';
index 6e8a439..1d73f7e 100644 (file)
@@ -83,6 +83,7 @@ $string['errorminpasswordupper'] = 'Passwords must have at least {$a} upper case
 $string['errorpasswordupdate'] = 'Error updating password, password not changed';
 $string['event_user_loggedin'] = 'User has logged in';
 $string['eventuserloggedinas'] = 'User logged in as another user';
+$string['eventuserloginfailed'] = 'User login failed';
 $string['forcechangepassword'] = 'Force change password';
 $string['forcechangepasswordfirst_help'] = 'Force users to change password on their first login to Moodle.';
 $string['forcechangepassword_help'] = 'Force users to change password on their next login to Moodle.';
index ca9a20f..0bd37d5 100644 (file)
@@ -35,4 +35,5 @@ $string['checkingsourcetables'] = 'Checking source table structure';
 $string['importschemaexception'] = 'Current database structure does not match all install.xml files. <br /> {$a}';
 $string['importversionmismatchexception'] = 'Current version {$a->currentver} does not match exported version {$a->schemaver}.';
 $string['malformedxmlexception'] = 'Malformed XML found, can not continue.';
+$string['tablex'] = 'Table {$a}:';
 $string['unknowntableexception'] = 'Unknown table {$a} found in export file.';
index dce4028..208b95a 100644 (file)
@@ -94,8 +94,23 @@ class behat_form_select extends behat_form_field {
             return;
         }
 
+        // Wrapped in try & catch as the element may disappear if an AJAX request was submitted.
+        try {
+            $multiple = $this->field->hasAttribute('multiple');
+        } catch (Exception $e) {
+            // We do not specify any specific Exception type as there are
+            // different exceptions that can be thrown by the driver and
+            // we can not control them all, also depending on the selenium
+            // version the exception type can change.
+            return;
+        }
+
+        // Wait for all the possible AJAX requests that have been
+        // already triggered by selectOption() to be finished.
+        $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
+
         // Single select sometimes needs an extra click in the option.
-        if (!$this->field->hasAttribute('multiple')) {
+        if (!$multiple) {
 
             // Using the driver direcly because Element methods are messy when dealing
             // with elements inside containers.
@@ -104,11 +119,6 @@ class behat_form_select extends behat_form_field {
                 // Wrapped in a try & catch as we can fall into race conditions
                 // and the element may not be there.
                 try {
-
-                    // Wait for all the possible AJAX requests that have been
-                    // already triggered by selectOption() to be finished.
-                    $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
-
                     current($optionnodes)->click();
                 } catch (Exception $e) {
                     // We continue and return as this means that the element is not there or it is not the same.
@@ -118,10 +128,6 @@ class behat_form_select extends behat_form_field {
 
         } else {
 
-            // Wait for all the possible AJAX requests that have been
-            // already triggered by selectOption() to be finished.
-            $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
-
             // Wrapped in a try & catch as we can fall into race conditions
             // and the element may not be there.
             try {
diff --git a/lib/classes/event/user_login_failed.php b/lib/classes/event/user_login_failed.php
new file mode 100644 (file)
index 0000000..6533a10
--- /dev/null
@@ -0,0 +1,109 @@
+<?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/>.
+
+/**
+ * User login failed event.
+ *
+ * @package    core
+ * @copyright  2014 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User login failed event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      @type string username name of user.
+ *      @type int reason failure reason.
+ * }
+ *
+ * @package    core
+ * @copyright  2014 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_login_failed extends \core\event\base {
+    /**
+     * Init method.
+     *
+     * @return void
+     */
+    protected function init() {
+        $this->context = \context_system::instance();
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Return localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserloginfailed', 'auth');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Login failed for user "' . $this->other['username'] . '" for reason id: ' . $this->other['reason'];
+    }
+
+    /**
+     * Get URL related to the action.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        if (isset($this->data['userid'])) {
+            return new \moodle_url('/user/profile.php', array('id' => $this->data['userid']));
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'login', 'error', 'index.php', $this->other['username']);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        if (!isset($this->other['reason'])) {
+            throw new \coding_exception("other['reason'] has to be specified.");
+        } else if (!isset($this->other['username'])) {
+            throw new \coding_exception("other['username'] has to be specified.");
+        }
+    }
+
+}
index f0d7455..7ae4380 100644 (file)
@@ -662,6 +662,10 @@ class core_plugin_manager {
             return 'svn';
         }
 
+        if (is_dir($pluginroot.'/.hg')) {
+            return 'mercurial';
+        }
+
         return false;
     }
 
similarity index 78%
rename from backup/util/progress/core_backup_progress.class.php
rename to lib/classes/progress/base.php
index 7204e58..3944682 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+namespace core\progress;
+
+defined('MOODLE_INTERNAL') || die();
+
 /**
- * Base class for handling progress information during a backup and restore.
+ * Base class for handling progress information.
  *
- * Subclasses should generally override the current_progress function which
+ * Subclasses should generally override the {@link current_progress} function which
  * summarises all progress information.
  *
- * @package core_backup
+ * @package core_progress
  * @copyright 2013 The Open University
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-abstract class core_backup_progress {
+abstract class base {
     /**
      * @var int Constant indicating that the number of progress calls is unknown.
      */
@@ -36,7 +40,7 @@ abstract class core_backup_progress {
      * frontendservertimeout config option to a lower value, such as 180
      * seconds (default for some commercial products).
      *
-     * @var int The number of seconds that can pass without progress() calls.
+     * @var int The number of seconds that can pass without {@link progress()} calls.
      */
     const TIME_LIMIT_WITHOUT_PROGRESS = 3600;
 
@@ -66,7 +70,7 @@ abstract class core_backup_progress {
     protected $currents = array();
 
     /**
-     * @var int Array of counts within parent progress entry (ignored for first)
+     * @var int[] Array of counts within parent progress entry (ignored for first)
      */
     protected $parentcounts = array();
 
@@ -76,28 +80,28 @@ abstract class core_backup_progress {
      * This can be called multiple times for nested progress sections. It must
      * be paired with calls to end_progress.
      *
-     * The progress maximum may be INDETERMINATE if the current operation has
+     * The progress maximum may be {@link self::INDETERMINATE} if the current operation has
      * an unknown number of steps. (This is default.)
      *
      * Calling this function will always result in a new display, so this
      * should not be called exceedingly frequently.
      *
-     * When it is complete by calling end_progress, each start_progress section
+     * When it is complete by calling {@link end_progress()}, each {@link start_progress} section
      * automatically adds progress to its parent, as defined by $parentcount.
      *
      * @param string $description Description to display
      * @param int $max Maximum value of progress for this section
      * @param int $parentcount How many progress points this section counts for
-     * @throws coding_exception If max is invalid
+     * @throws \coding_exception If max is invalid
      */
     public function start_progress($description, $max = self::INDETERMINATE,
             $parentcount = 1) {
         if ($max != self::INDETERMINATE && $max < 0) {
-            throw new coding_exception(
+            throw new \coding_exception(
                     'start_progress() max value cannot be negative');
         }
         if ($parentcount < 1) {
-            throw new coding_exception(
+            throw new \coding_exception(
                     'start_progress() parent progress count must be at least 1');
         }
         if (!empty($this->descriptions)) {
@@ -105,13 +109,13 @@ abstract class core_backup_progress {
             if ($prevmax !== self::INDETERMINATE) {
                 $prevcurrent = end($this->currents);
                 if ($prevcurrent + $parentcount > $prevmax) {
-                    throw new coding_exception(
+                    throw new \coding_exception(
                             'start_progress() parent progress would exceed max');
                 }
             }
         } else {
             if ($parentcount != 1) {
-                throw new coding_exception(
+                throw new \coding_exception(
                         'start_progress() progress count must be 1 when no parent');
             }
         }
@@ -125,16 +129,16 @@ abstract class core_backup_progress {
     /**
      * Marks the end of an operation that will display progress.
      *
-     * This must be paired with each start_progress call.
+     * This must be paired with each {@link start_progress} call.
      *
      * If there is a parent progress section, its progress will be increased
      * automatically to reflect the end of the child section.
      *
-     * @throws coding_exception If progress hasn't been started
+     * @throws \coding_exception If progress hasn't been started
      */
     public function end_progress() {
         if (!count($this->descriptions)) {
-            throw new coding_exception('end_progress() without start_progress()');
+            throw new \coding_exception('end_progress() without start_progress()');
         }
         array_pop($this->descriptions);
         array_pop($this->maxes);
@@ -154,29 +158,23 @@ abstract class core_backup_progress {
      * Indicates that progress has occurred.
      *
      * The progress value should indicate the total progress so far, from 0
-     * to the value supplied for $max (inclusive) in start_progress.
+     * to the value supplied for $max (inclusive) in {@link start_progress}.
      *
      * You do not need to call this function for every value. It is OK to skip
      * values. It is also OK to call this function as often as desired; it
-     * doesn't do anything if called more than once per second.
+     * doesn't update the display if called more than once per second.
      *
-     * It must be INDETERMINATE if start_progress was called with $max set to
+     * It must be INDETERMINATE if {@link start_progress} was called with $max set to
      * INDETERMINATE. Otherwise it must not be indeterminate.
      *
      * @param int $progress Progress so far
-     * @throws coding_exception If progress value is invalid
+     * @throws \coding_exception If progress value is invalid
      */
     public function progress($progress = self::INDETERMINATE) {
-        // Ignore too-frequent progress calls (more than once per second).
-        $now = $this->get_time();
-        if ($now === $this->lastprogresstime) {
-            return;
-        }
-
         // Check we are inside a progress section.
         $max = end($this->maxes);
         if ($max === false) {
-            throw new coding_exception(
+            throw new \coding_exception(
                     'progress() without start_progress');
         }
 
@@ -184,34 +182,55 @@ abstract class core_backup_progress {
         if ($progress === self::INDETERMINATE) {
             // Indeterminate progress.
             if ($max !== self::INDETERMINATE) {
-                throw new coding_exception(
+                throw new \coding_exception(
                         'progress() INDETERMINATE, expecting value');
             }
         } else {
             // Determinate progress.
             $current = end($this->currents);
             if ($max === self::INDETERMINATE) {
-                throw new coding_exception(
+                throw new \coding_exception(
                         'progress() with value, expecting INDETERMINATE');
             } else if ($progress < 0 || $progress > $max) {
-                throw new coding_exception(
+                throw new \coding_exception(
                         'progress() value out of range');
             } else if ($progress < $current) {
-                throw new coding_Exception(
+                throw new \coding_exception(
                         'progress() value may not go backwards');
             }
             $this->currents[key($this->currents)] = $progress;
         }
 
+        // Don't update progress bar too frequently (more than once per second).
+        $now = $this->get_time();
+        if ($now === $this->lastprogresstime) {
+            return;
+        }
+
         // Update progress.
         $this->count++;
         $this->lastprogresstime = $now;
 
         // Update time limit before next progress display.
-        core_php_time_limit::raise(self::TIME_LIMIT_WITHOUT_PROGRESS);
+        \core_php_time_limit::raise(self::TIME_LIMIT_WITHOUT_PROGRESS);
         $this->update_progress();
     }
 
+    /**
+     * An alternative to calling progress. This keeps track of the number of items done internally. Call this method
+     * with no parameters to increment the internal counter by one or you can use the $incby parameter to specify a positive
+     * change in progress. The internal progress counter should not exceed $max as passed to {@link start_progress} for this
+     * section.
+     *
+     * If you called {@link start_progress} with parameter INDETERMINATE then you cannot call this method.
+     *
+     * @var int $incby The positive change to apply to the internal progress counter. Defaults to 1.
+     */
+    public function increment_progress($incby = 1) {
+        $current = end($this->currents);
+        $this->progress($current + $incby);
+    }
+
     /**
      * Gets time (this is provided so that unit tests can override it).
      *
@@ -236,24 +255,25 @@ abstract class core_backup_progress {
     /**
      * Checks max value of current progress section.
      *
-     * @return int Current max value (may be core_backup_progress::INDETERMINATE)
-     * @throws coding_exception If not in a progress section
+     * @return int Current max value - may be {@link \core\progress\base::INDETERMINATE}.
+     * @throws \coding_exception If not in a progress section
      */
     public function get_current_max() {
         $max = end($this->maxes);
         if ($max === false) {
-            throw new coding_exception('Not inside progress section');
+            throw new \coding_exception('Not inside progress section');
         }
         return $max;
     }
 
     /**
+     * @throws \coding_exception
      * @return string Current progress section description
      */
     public function get_current_description() {
         $description = end($this->descriptions);
         if ($description === false) {
-            throw new coding_exception('Not inside progress section');
+            throw new \coding_exception('Not inside progress section');
         }
         return $description;
     }
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+namespace core\progress;
+
+defined('MOODLE_INTERNAL') || die();
+
 /**
  * Progress handler that uses a standard Moodle progress bar to display
  * progress. The Moodle progress bar cannot show indeterminate progress,
  * so we do extra output in addition to the bar.
  *
- * @package core_backup
+ * @package core_progress
  * @copyright 2013 The Open University
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_backup_display_progress extends core_backup_progress {
+class display extends base {
     /**
      * @var int Number of wibble states (state0...stateN-1 classes in CSS)
      */
     const WIBBLE_STATES = 13;
 
     /**
-     * @var progress_bar Current progress bar.
+     * @var \progress_bar Current progress bar.
      */
     private $bar;
 
@@ -54,8 +58,8 @@ class core_backup_display_progress extends core_backup_progress {
     }
 
     /**
-     * By default, the progress section names do not display because (in backup)
-     * these are usually untranslated and incomprehensible. To make them
+     * By default, the progress section names do not display because
+     * these will probably be untranslated and incomprehensible. To make them
      * display, call this method.
      *
      * @param bool $displaynames True to display names
@@ -69,15 +73,15 @@ class core_backup_display_progress extends core_backup_progress {
      *
      * Called in constructor and in update_progress if required.
      *
-     * @throws coding_exception If already started
+     * @throws \coding_exception If already started
      */
     public function start_html() {
         if ($this->bar) {
-            throw new coding_exception('Already started');
+            throw new \coding_exception('Already started');
         }
-        $this->bar = new progress_bar();
+        $this->bar = new \progress_bar();
         $this->bar->create();
-        echo html_writer::start_div('wibbler');
+        echo \html_writer::start_div('wibbler');
     }
 
     /**
@@ -92,13 +96,13 @@ class core_backup_display_progress extends core_backup_progress {
         $this->bar = null;
 
         // End wibbler div.
-        echo html_writer::end_div();
+        echo \html_writer::end_div();
     }
 
     /**
      * When progress is updated, updates the bar.
      *
-     * @see core_backup_progress::update_progress()
+     * @see \core\progress\base::update_progress()
      */
     public function update_progress() {
         // If finished...
@@ -114,7 +118,7 @@ class core_backup_display_progress extends core_backup_progress {
             // (up to once per second).
             if (time() != $this->lastwibble) {
                 $this->lastwibble = time();
-                echo html_writer::div('', 'wibble state' . $this->currentstate);
+                echo \html_writer::div('', 'wibble state' . $this->currentstate);
 
                 // Go on to next colour.
                 $this->currentstate += $this->direction;
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+namespace core\progress;
+
+defined('MOODLE_INTERNAL') || die();
+
 /**
  * Progress handler that uses a standard Moodle progress bar to display
- * progress. Same as core_backup_display_progress, but the bar does not
+ * progress. Same as \core\progress\display, but the bar does not
  * appear until a certain time has elapsed, and disappears automatically
  * after it finishes.
  *
  * The bar can be re-used, i.e. if you end all sections it will disappear,
  * but if you start all sections, a new bar will be output.
  *
- * @package core_backup
+ * @package core_progress
  * @copyright 2013 The Open University
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_backup_display_progress_if_slow extends core_backup_display_progress {
+class display_if_slow extends display {
     /**
      * @var int Waits this many seconds before displaying progress bar
      */
@@ -57,10 +61,10 @@ class core_backup_display_progress_if_slow extends core_backup_display_progress
      * Constructs the progress reporter. This will not output HTML just yet,
      * until the required delay time expires.
      *
-     * @param string $heading Text to display above bar (if it appears); '' for none
+     * @param string $heading Text to display above bar (if it appears); '' for none (default)
      * @param int $delay Delay time (default 5 seconds)
      */
-    public function __construct($heading, $delay = self::DEFAULT_DISPLAY_DELAY) {
+    public function __construct($heading = '', $delay = self::DEFAULT_DISPLAY_DELAY) {
         // Set start time based on delay.
         $this->starttime = time() + $delay;
         $this->heading = $heading;
@@ -71,16 +75,16 @@ class core_backup_display_progress_if_slow extends core_backup_display_progress
      * Starts displaying the progress bar, with optional heading and a special
      * div so it can be hidden later.
      *
-     * @see core_backup_display_progress::start_html()
+     * @see \core\progress\display::start_html()
      */
     public function start_html() {
         global $OUTPUT;
-        $this->id = 'core_backup_display_progress_if_slow' . self::$nextid;
+        $this->id = 'core_progress_display_if_slow' . self::$nextid;
         self::$nextid++;
 
         // Containing div includes a CSS class so that it can be themed if required,
         // and an id so it can be automatically hidden at end.
-        echo html_writer::start_div('core_backup_display_progress_if_slow',
+        echo \html_writer::start_div('core_progress_display_if_slow',
                 array('id' => $this->id));
 
         // Display optional heading.
@@ -96,7 +100,7 @@ class core_backup_display_progress_if_slow extends core_backup_display_progress
      * When progress is updated, after a certain time, starts actually displaying
      * the progress bar.
      *
-     * @see core_backup_progress::update_progress()
+     * @see \core\progress\base::update_progress()
      */
     public function update_progress() {
         // If we haven't started yet, consider starting.
@@ -116,12 +120,12 @@ class core_backup_display_progress_if_slow extends core_backup_display_progress
     /**
      * Finishes parent display then closes div and hides it.
      *
-     * @see core_backup_display_progress::end_html()
+     * @see \core\progress\display::end_html()
      */
     public function end_html() {
         parent::end_html();
-        echo html_writer::end_div();
-        echo html_writer::script('document.getElementById("' . $this->id .
+        echo \html_writer::end_div();
+        echo \html_writer::script('document.getElementById("' . $this->id .
                 '").style.display = "none"');
     }
 }
similarity index 88%
rename from backup/util/progress/core_backup_null_progress.class.php
rename to lib/classes/progress/null.php
index 3e02369..f332a30 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+namespace core\progress;
+
+defined('MOODLE_INTERNAL') || die();
+
 /**
  * Progress handler that ignores progress entirely.
  *
- * @package core_backup
+ * @package core_progress
  * @copyright 2013 The Open University
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_backup_null_progress extends core_backup_progress {
+class null extends base {
     public function update_progress() {
         // Do nothing.
     }
index 0eccb8e..d255609 100644 (file)
@@ -189,6 +189,10 @@ class deployer {
             return 'svn';
         }
 
+        if (is_dir($pluginroot.'/.hg')) {
+            return 'mercurial';
+        }
+
         return false;
     }
 
index 4dd6833..f266669 100644 (file)
         <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
         <FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The position in the quiz where this question appears"/>
         <FIELD NAME="subquestion" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="variant" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
         <FIELD NAME="s" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="effectiveweight" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
         <FIELD NAME="negcovar" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
index 0fe0e43..3f7b51c 100644 (file)
@@ -85,7 +85,7 @@ defined('MOODLE_INTERNAL') || die();
  * @return bool always true
  */
 function xmldb_main_upgrade($oldversion) {
-    global $CFG, $USER, $DB, $OUTPUT, $SITE;
+    global $CFG, $USER, $DB, $OUTPUT, $SITE, $COURSE;
 
     require_once($CFG->libdir.'/db/upgradelib.php'); // Core Upgrade-related functions
 
@@ -316,6 +316,10 @@ function xmldb_main_upgrade($oldversion) {
             $dbman->drop_field($table, $field);
         }
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         upgrade_main_savepoint(true, 2012031500.02);
     }
 
@@ -447,6 +451,10 @@ function xmldb_main_upgrade($oldversion) {
             $dbman->add_field($table, $field);
         }
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // Main savepoint reached
         upgrade_main_savepoint(true, 2012050300.03);
     }
@@ -563,6 +571,10 @@ function xmldb_main_upgrade($oldversion) {
             $dbman->add_field($table, $field);
         }
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // Add course_sections_availability to add completion & grade availability conditions
         $table = new xmldb_table('course_sections_availability');
 
@@ -1334,6 +1346,10 @@ function xmldb_main_upgrade($oldversion) {
         // Launch change of type for field format
         $dbman->change_field_type($table, $field);
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // Main savepoint reached
         upgrade_main_savepoint(true, 2012110200.00);
     }
@@ -1386,6 +1402,10 @@ function xmldb_main_upgrade($oldversion) {
             }
         }
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // Main savepoint reached
         upgrade_main_savepoint(true, 2012110201.00);
     }
@@ -1501,6 +1521,7 @@ function xmldb_main_upgrade($oldversion) {
         if ($SITE->format !== 'site') {
             $DB->set_field('course', 'format', 'site', array('id' => $SITE->id));
             $SITE->format = 'site';
+            $COURSE->format = 'site';
         }
 
         // Main savepoint reached
@@ -1989,6 +2010,10 @@ function xmldb_main_upgrade($oldversion) {
             $dbman->drop_field($table, $field);
         }
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // Main savepoint reached.
         upgrade_main_savepoint(true, 2013040300.01);
     }
@@ -2376,6 +2401,10 @@ function xmldb_main_upgrade($oldversion) {
             $dbman->add_field($table, $field);
         }
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // Define field calendartype to be added to user.
         $table = new xmldb_table('user');
         $field = new xmldb_field('calendartype', XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, 'gregorian');
@@ -2400,6 +2429,10 @@ function xmldb_main_upgrade($oldversion) {
             $dbman->add_field($table, $field);
         }
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // Main savepoint reached.
         upgrade_main_savepoint(true, 2013091000.02);
     }
@@ -2423,6 +2456,10 @@ function xmldb_main_upgrade($oldversion) {
             $dbman->drop_field($table, $field);
         }
 
+        // Since structure of 'course' table has changed we need to re-read $SITE from DB.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // Main savepoint reached.
         upgrade_main_savepoint(true, 2013091000.03);
     }
@@ -2968,5 +3005,19 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014012400.00);
     }
 
+    if ($oldversion < 2014020500.00) {
+        // Define field variant to be added to question_statistics.
+        $table = new xmldb_table('question_statistics');
+        $field = new xmldb_field('variant', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'subquestion');
+
+        // Conditionally launch add field variant.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014020500.00);
+    }
+
     return true;
 }
index 9b8e98f..1360898 100644 (file)
@@ -327,7 +327,7 @@ class mssql_native_moodle_database extends moodle_database {
         if ($result) {
             while ($row = mssql_fetch_row($result)) {
                 $tablename = reset($row);
-                if ($this->prefix !== '') {
+                if ($this->prefix !== false && $this->prefix !== '') {
                     if (strpos($tablename, $this->prefix) !== 0) {
                         continue;
                     }
index 8e99da4..fed5c85 100644 (file)
@@ -406,7 +406,7 @@ class oci_native_moodle_database extends moodle_database {
         oci_free_statement($stmt);
         $records = array_map('strtolower', $records['TABLE_NAME']);
         foreach ($records as $tablename) {
-            if ($this->prefix !== '') {
+            if ($this->prefix !== false && $this->prefix !== '') {
                 if (strpos($tablename, $this->prefix) !== 0) {
                     continue;
                 }
index 14b2cc3..717e632 100644 (file)
@@ -321,7 +321,7 @@ class pgsql_native_moodle_database extends moodle_database {
         if ($result) {
             while ($row = pg_fetch_row($result)) {
                 $tablename = reset($row);
-                if ($this->prefix !== '') {
+                if ($this->prefix !== false && $this->prefix !== '') {
                     if (strpos($tablename, $this->prefix) !== 0) {
                         continue;
                     }
index 7828eb7..fcfe446 100644 (file)
@@ -150,7 +150,7 @@ class sqlite3_pdo_moodle_database extends pdo_moodle_database {
         foreach ($rstables as $table) {
             $table = $table['name'];
             $table = strtolower($table);
-            if ($this->prefix !== '') {
+            if ($this->prefix !== false && $this->prefix !== '') {
                 if (strpos($table, $this->prefix) !== 0) {
                     continue;
                 }
index 5110a32..eb4569f 100644 (file)
@@ -380,7 +380,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
         if ($result) {
             while ($row = sqlsrv_fetch_array($result)) {
                 $tablename = reset($row);
-                if ($this->prefix !== '') {
+                if ($this->prefix !== false && $this->prefix !== '') {
                     if (strpos($tablename, $this->prefix) !== 0) {
                         continue;
                     }
index 4d9b7bb..5cdc3f2 100644 (file)
@@ -132,7 +132,7 @@ abstract class database_exporter {
         if ($this->check_schema and $errors = $this->manager->check_database_schema($this->schema)) {
             $details = '';
             foreach ($errors as $table=>$items) {
-                $details .= '<div>'.get_string('table').' '.$table.':';
+                $details .= '<div>'.get_string('tablex', 'dbtransfer', $table);
                 $details .= '<ul>';
                 foreach ($items as $item) {
                     $details .= '<li>'.$item.'</li>';
index 066b625..c00a0ef 100644 (file)
@@ -223,10 +223,16 @@ M.util.set_user_preference = function(name, value) {
 
 /**
  * Prints a confirmation dialog in the style of DOM.confirm().
- * @param object event A YUI DOM event or null if launched manually
- * @param string message The message to show in the dialog
- * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
- * @param function fn A JS function to run if YES is clicked.
+ *
+ * @method show_confirm_dialog
+ * @param {EventFacade} e
+ * @param {Object} args
+ * @param {String} args.message The question to ask the user
+ * @param {Function} [args.callback] A callback to apply on confirmation.
+ * @param {Object} [args.scope] The scope to use when calling the callback.
+ * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
+ * @param {String} [args.cancellabel] The label to use on the cancel button.
+ * @param {String} [args.continuelabel] The label to use on the continue button.
  */
 M.util.show_confirm_dialog = function(e, args) {
     var target = e.target;
@@ -234,47 +240,35 @@ M.util.show_confirm_dialog = function(e, args) {
         e.preventDefault();
     }
 
-    YUI().use('yui2-container', 'yui2-event', function(Y) {
-        var simpledialog = new Y.YUI2.widget.SimpleDialog('confirmdialog',
-            {width: '300px',
-              fixedcenter: true,
-              modal: true,
-              visible: false,
-              draggable: false
-            }
-        );
-
-        simpledialog.setHeader(M.str.admin.confirmation);
-        simpledialog.setBody(args.message);
-        simpledialog.cfg.setProperty('icon', Y.YUI2.widget.SimpleDialog.ICON_WARN);
-
-        var handle_cancel = function() {
-            simpledialog.hide();
-        };
-
-        var handle_yes = function() {
-            simpledialog.hide();
+    YUI().use('moodle-core-notification-confirm', function(Y) {
+        var confirmationDialogue = new M.core.confirm({
+            width: '300px',
+            center: true,
+            modal: true,
+            visible: false,
+            draggable: false,
+            title: M.util.get_string('confirmation', 'admin'),
+            noLabel: M.util.get_string('cancel', 'moodle'),
+            question: args.message
+        });
 
+        // The dialogue was submitted with a positive value indication.
+        confirmationDialogue.on('complete-yes', function(e) {
+            // Handle any callbacks.
             if (args.callback) {
-                // args comes from PHP, so callback will be a string, needs to be evaluated by JS
-                var callback = null;
-                if (Y.Lang.isFunction(args.callback)) {
-                    callback = args.callback;
-                } else {
-                    callback = eval('('+args.callback+')');
+                if (!Y.Lang.isFunction(args.callback)) {
+                    Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
+                            'warn', 'M.util.show_confirm_dialog');
+                    return;
                 }
 
+                var scope = e.target;
                 if (Y.Lang.isObject(args.scope)) {
-                    var sc = args.scope;
-                } else {
-                    var sc = e.target;
+                    scope = args.scope;
                 }
 
-                if (args.callbackargs) {
-                    callback.apply(sc, args.callbackargs);
-                } else {
-                    callback.apply(sc);
-                }
+                var callbackargs = args.callbackargs || [];
+                args.callback.apply(scope, callbackargs);
                 return;
             }
 
@@ -288,9 +282,7 @@ M.util.show_confirm_dialog = function(e, args) {
                 window.location = targetancestor.get('href');
 
             } else if (target.test('input')) {
-                targetform = target.ancestor(function(node) { return node.get('tagName').toLowerCase() == 'form'; });
-                // We cannot use target.ancestor('form') on the previous line
-                // because of http://yuilibrary.com/projects/yui3/ticket/2531561
+                targetform = target.ancestor('form', true);
                 if (!targetform) {
                     return;
                 }
@@ -300,32 +292,26 @@ M.util.show_confirm_dialog = function(e, args) {
                 }
                 targetform.submit();
 
-            } else if (target.get('tagName').toLowerCase() == 'form') {
-                // We cannot use target.test('form') on the previous line because of
-                // http://yuilibrary.com/projects/yui3/ticket/2531561
+            } else if (target.test('form')) {
                 target.submit();
 
-            } else if (M.cfg.developerdebug) {
-                alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM");
+            } else {
+                Y.log("Element of type " + target.get('tagName') +
+                        " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM",
+                        'warn', 'javascript-static');
             }
-        };
+        }, this);
 
-        if (!args.cancellabel) {
-            args.cancellabel = M.str.moodle.cancel;
-        }
-        if (!args.continuelabel) {
-            args.continuelabel = M.str.moodle.yes;
+        if (args.cancellabel) {
+            confirmationDialogue.set('noLabel', args.cancellabel);
         }
 
-        var buttons = [
-            {text: args.cancellabel,   handler: handle_cancel, isDefault: true},
-            {text: args.continuelabel, handler: handle_yes}
-        ];
-
-        simpledialog.cfg.queueProperty('buttons', buttons);
+        if (args.continuelabel) {
+            confirmationDialogue.set('yesLabel', args.continuelabel);
+        }
 
-        simpledialog.render(document.body);
-        simpledialog.show();
+        confirmationDialogue.render()
+                .show();
     });
 };
 
index 05c7e16..39cfd10 100644 (file)
@@ -3397,9 +3397,7 @@ function get_user_key($script, $userid, $instance=null, $iprestriction=null, $va
  * @return bool Always returns true
  */
 function update_user_login_times() {
-    global $USER, $DB, $CFG;
-
-    require_once($CFG->dirroot.'/user/lib.php');
+    global $USER, $DB;
 
     if (isguestuser()) {
         // Do not update guest access times/ips for performance.
@@ -3425,7 +3423,9 @@ function update_user_login_times() {
     $USER->lastaccess = $user->lastaccess = $now;
     $USER->lastip = $user->lastip = getremoteaddr();
 
-    user_update_user($user, false);
+    // Note: do not call user_update_user() here because this is part of the login process,
+    //       the login event means that these fields were updated.
+    $DB->update_record('user', $user);
     return true;
 }
 
@@ -4338,16 +4338,24 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
         // Use manual if auth not set.
         $auth = empty($user->auth) ? 'manual' : $user->auth;
         if (!empty($user->suspended)) {
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_SUSPENDED;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                    'other' => array('username' => $username, 'reason' => $failurereason)));
+            $event->trigger();
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
         if ($auth=='nologin' or !is_enabled_auth($auth)) {
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             // Legacy way to suspend user.
             $failurereason = AUTH_LOGIN_SUSPENDED;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                    'other' => array('username' => $username, 'reason' => $failurereason)));
+            $event->trigger();
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Disabled Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
         $auths = array($auth);
@@ -4355,16 +4363,27 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
     } else {
         // Check if there's a deleted record (cheaply), this should not happen because we mangle usernames in delete_user().
         if ($DB->get_field('user', 'id', array('username' => $username, 'mnethostid' => $CFG->mnet_localhost_id,  'deleted' => 1))) {
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_NOUSER;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
+                    'reason' => $failurereason)));
+            $event->trigger();
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Deleted Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
 
         // Do not try to authenticate non-existent accounts when user creation is not disabled.
         if (!empty($CFG->authpreventaccountcreation)) {
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_NOUSER;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
+                    'reason' => $failurereason)));
+            $event->trigger();
+
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Unknown user, can not create new accounts:  $username  ".
+                    $_SERVER['HTTP_USER_AGENT']);
             return false;
         }
 
@@ -4380,9 +4399,14 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
     } else if ($user->id) {
         // Verify login lockout after other ways that may prevent user login.
         if (login_is_lockedout($user)) {
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_LOCKOUT;
+
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                    'other' => array('username' => $username, 'reason' => $failurereason)));
+            $event->trigger();
+
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Login lockout:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
     } else {
@@ -4428,14 +4452,21 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
 
         if (empty($user->id)) {
             $failurereason = AUTH_LOGIN_NOUSER;
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
+                    'reason' => $failurereason)));
+            $event->trigger();
             return false;
         }
 
         if (!empty($user->suspended)) {
             // Just in case some auth plugin suspended account.
-            add_to_log(SITEID, 'login', 'error', 'index.php', $username);
-            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             $failurereason = AUTH_LOGIN_SUSPENDED;
+            // Trigger login failed event.
+            $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                    'other' => array('username' => $username, 'reason' => $failurereason)));
+            $event->trigger();
+            error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Suspended Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
             return false;
         }
 
@@ -4445,7 +4476,6 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
     }
 
     // Failed if all the plugins have failed.
-    add_to_log(SITEID, 'login', 'error', 'index.php', $username);
     if (debugging('', DEBUG_ALL)) {
         error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Failed Login:  $username  ".$_SERVER['HTTP_USER_AGENT']);
     }
@@ -4453,8 +4483,16 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
     if ($user->id) {
         login_attempt_failed($user);
         $failurereason = AUTH_LOGIN_FAILED;
+        // Trigger login failed event.
+        $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                'other' => array('username' => $username, 'reason' => $failurereason)));
+        $event->trigger();
     } else {
         $failurereason = AUTH_LOGIN_NOUSER;
+        // Trigger login failed event.
+        $event = \core\event\user_login_failed::create(array('other' => array('username' => $username,
+                'reason' => $failurereason)));
+        $event->trigger();
     }
 
     return false;
index 12c10c3..38c6fdb 100644 (file)
@@ -93,13 +93,19 @@ class confirm_action extends component_action {
      *
      * @param string $message The message to display to the user when they are shown
      *    the confirm dialogue.
-     * @param string $callback The method to call when the user confirms the action.
+     * @param string $callback Deprecated since 2.7
      * @param string $continuelabel The string to use for he continue button
      * @param string $cancellabel The string to use for the cancel button
      */
     public function __construct($message, $callback = null, $continuelabel = null, $cancellabel = null) {
+        if ($callback !== null) {
+            debugging('The callback argument to new confirm_action() has been deprecated.' .
+                    ' If you need to use a callback, please write Javascript to use moodle-core-notification-confirmation ' .
+                    'and attach to the provided events.',
+                    DEBUG_DEVELOPER);
+        }
         parent::__construct('click', 'M.util.show_confirm_dialog', array(
-                'message' => $message, 'callback' => $callback,
+                'message' => $message,
                 'continuelabel' => $continuelabel, 'cancellabel' => $cancellabel));
     }
 }
@@ -197,4 +203,4 @@ class popup_action extends component_action {
 
         return $jsoptions;
     }
-}
\ No newline at end of file
+}
index 1f39798..904708c 100644 (file)
@@ -3267,6 +3267,9 @@ EOD;
             } else {
                 $additionalclasses[] = 'empty-region-'.$region;
             }
+            if ($this->page->blocks->region_completely_docked($region, $this)) {
+                $additionalclasses[] = 'docked-region-'.$region;
+            }
         }
         foreach ($this->page->layout_options as $option => $value) {
             if ($value) {
index 0e55357..18aea45 100644 (file)
@@ -195,6 +195,15 @@ class page_requirements_manager {
         $this->YUI_config->comboBase    = $this->yui3loader->comboBase;
         $this->YUI_config->combine      = $this->yui3loader->combine;
 
+        // If we've had to patch any YUI modules between releases, we must override the YUI configuration to include them.
+        // For important information on patching YUI modules, please see http://docs.moodle.org/dev/YUI/Patching.
+        if (!empty($CFG->yuipatchedmodules) && !empty($CFG->yuipatchlevel)) {
+            $this->YUI_config->define_patched_core_modules($this->yui3loader->local_comboBase,
+                    $CFG->yui3version,
+                    $CFG->yuipatchlevel,
+                    $CFG->yuipatchedmodules);
+        }
+
         $configname = $this->YUI_config->set_config_source('lib/yui/config/yui2.js');
         $this->YUI_config->add_group('yui2', array(
             // Loader configuration for our 2in3, for now ignores $CFG->useexternalyui.
@@ -1773,6 +1782,59 @@ class YUI_config {
         }
         return $modules;
     }
+
+    /**
+     * Define YUI modules which we have been required to patch between releases.
+     *
+     * We must do this because we aggressively cache content on the browser, and we must also override use of the
+     * external CDN which will serve the true authoritative copy of the code without our patches.
+     *
+     * @param String combobase The local combobase
+     * @param String yuiversion The current YUI version
+     * @param Int patchlevel The patch level we're working to for YUI
+     * @param Array patchedmodules An array containing the names of the patched modules
+     * @return void
+     */
+    public function define_patched_core_modules($combobase, $yuiversion, $patchlevel, $patchedmodules) {
+        // The version we use is suffixed with a patchlevel so that we can get additional revisions between YUI releases.
+        $subversion = $yuiversion . '_' . $patchlevel;
+
+        if ($this->comboBase == $combobase) {
+            // If we are using the local combobase in the loader, we can add a group and still make use of the combo
+            // loader. We just need to specify a different root which includes a slightly different YUI version number
+            // to include our patchlevel.
+            $patterns = array();
+            $modules = array();
+            foreach ($patchedmodules as $modulename) {
+                // We must define the pattern and module here so that the loader uses our group configuration instead of
+                // the standard module definition. We may lose some metadata provided by upstream but this will be
+                // loaded when the module is loaded anyway.
+                $patterns[$modulename] = array(
+                    'group' => 'yui-patched',
+                );
+                $modules[$modulename] = array();
+            }
+
+            // Actually add the patch group here.
+            $this->add_group('yui-patched', array(
+                'combine' => true,
+                'root' => $subversion . '/',
+                'patterns' => $patterns,
+                'modules' => $modules,
+            ));
+
+        } else {
+            // The CDN is in use - we need to instead use the local combobase for this module and override the modules
+            // definition. We cannot use the local base - we must use the combobase because we cannot invalidate the
+            // local base in browser caches.
+            $fullpathbase = $combobase . $subversion . '/';
+            foreach ($patchedmodules as $modulename) {
+                $this->modules[$modulename] = array(
+                    'fullpath' => $fullpathbase . $modulename . '/' . $modulename . '-min.js'
+                );
+            }
+        }
+    }
 }
 
 /**
index 3b72e6e..309698e 100644 (file)
@@ -81,6 +81,9 @@ define('K_CELL_HEIGHT_RATIO', 1.25);
 /** reduction factor for small font */
 define('K_SMALL_RATIO', 2/3);
 
+/** Throw exceptions from errors so they can be caught and recovered from. */
+define('K_TCPDF_THROW_EXCEPTION_ERROR', true);
+
 require_once(dirname(__FILE__).'/tcpdf/tcpdf.php');
 
 /**
index 94115d4..1618676 100644 (file)
@@ -349,6 +349,15 @@ if (!defined('AJAX_SCRIPT')) {
 $CFG->yui2version = '2.9.0';
 $CFG->yui3version = '3.13.0';
 
+// Patching the upstream YUI release.
+// For important information on patching YUI modules, please see http://docs.moodle.org/dev/YUI/Patching.
+// If we need to patch a YUI modules between official YUI releases, the yuipatchlevel will need to be manually
+// incremented here. The module will also need to be listed in the yuipatchedmodules.
+// When upgrading to a subsequent version of YUI, these should be reset back to 0 and an empty array.
+$CFG->yuipatchlevel = 0;
+$CFG->yuipatchedmodules = array(
+);
+
 // Store settings from config.php in array in $CFG - we can use it later to detect problems and overrides.
 if (!isset($CFG->config_php_settings)) {
     $CFG->config_php_settings = (array)$CFG;
index 97b5def..8e3296d 100644 (file)
@@ -797,7 +797,7 @@ class flexible_table {
             }
             return format_text($text, $format, $options);
         } else {
-            $eci =& $this->export_class_instance();
+            $eci = $this->export_class_instance();
             return $eci->format_text($text, $format, $options, $courseid);
         }
     }
@@ -1530,9 +1530,9 @@ class table_spreadsheet_export_format_parent extends table_default_export_format
         $filename = $filename.'.'.$this->fileextension;
         $this->define_workbook();
         // format types
-        $this->formatnormal =& $this->workbook->add_format();
+        $this->formatnormal = $this->workbook->add_format();
         $this->formatnormal->set_bold(0);
-        $this->formatheaders =& $this->workbook->add_format();
+        $this->formatheaders = $this->workbook->add_format();
         $this->formatheaders->set_bold(1);
         $this->formatheaders->set_align('center');
         // Sending HTTP headers
index 6a9f8d6..ac7cc06 100644 (file)
@@ -60,7 +60,7 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertNotEmpty($ACCESSLIB_PRIVATE->rolepermissions);
         $this->assertNotEmpty($ACCESSLIB_PRIVATE->rolepermissions);
         $this->assertNotEmpty($ACCESSLIB_PRIVATE->accessdatabyuser);
-        accesslib_clear_all_caches(true);
+        accesslib_clear_all_caches_for_unit_testing();
         $this->assertEmpty($ACCESSLIB_PRIVATE->rolepermissions);
         $this->assertEmpty($ACCESSLIB_PRIVATE->rolepermissions);
         $this->assertEmpty($ACCESSLIB_PRIVATE->dirtycontexts);
@@ -2095,7 +2095,7 @@ class core_accesslib_testcase extends advanced_testcase {
         unassign_capability('moodle/site:accessallgroups', $allroles['teacher'], $frontpagecontext->id, true);
         unset($rc);
 
-        accesslib_clear_all_caches(false); // Must be done after assign_capability().
+        accesslib_clear_all_caches_for_unit_testing(); // Must be done after assign_capability().
 
 
         // Test role_assign(), role_unassign(), role_unassign_all() functions.
@@ -2112,7 +2112,7 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertEquals(0, $DB->count_records('role_assignments', array('contextid'=>$context->id)));
         unset($context);
 
-        accesslib_clear_all_caches(false); // Just in case.
+        accesslib_clear_all_caches_for_unit_testing(); // Just in case.
 
 
         // Test has_capability(), get_users_by_capability(), role_switch(), reload_all_capabilities() and friends functions.
@@ -2173,7 +2173,7 @@ class core_accesslib_testcase extends advanced_testcase {
 
         assign_capability('mod/page:view', CAP_PREVENT, $allroles['guest'], $systemcontext, true);
 
-        accesslib_clear_all_caches(false); // Must be done after assign_capability().
+        accesslib_clear_all_caches_for_unit_testing(); /// Must be done after assign_capability().
 
         // Extra tests for guests and not-logged-in users because they can not be verified by cross checking
         // with get_users_by_capability() where they are ignored.
@@ -2296,7 +2296,7 @@ class core_accesslib_testcase extends advanced_testcase {
         unset($permissions);
         unset($roles);
 
-        accesslib_clear_all_caches(false); // Must be done after assign_capability().
+        accesslib_clear_all_caches_for_unit_testing(); // must be done after assign_capability().
 
         // Test time - let's set up some real user, just in case the logic for USER affects the others...
         $USER = $DB->get_record('user', array('id'=>$testusers[3]));
index be7666f..2a1a05e 100644 (file)
@@ -133,35 +133,104 @@ class core_authlib_testcase extends advanced_testcase {
         $user1 = $this->getDataGenerator()->create_user(array('username'=>'username1', 'password'=>'password1'));
         $user2 = $this->getDataGenerator()->create_user(array('username'=>'username2', 'password'=>'password2', 'suspended'=>1));
         $user3 = $this->getDataGenerator()->create_user(array('username'=>'username3', 'password'=>'password3', 'auth'=>'nologin'));
-
+        // Capture events.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username1', 'password1');
+        $events = $sink->get_events();
+        $sink->close();
+
+        // No event is triggred.
+        $this->assertEmpty($events);
         $this->assertInstanceOf('stdClass', $result);
         $this->assertEquals($user1->id, $result->id);
 
         $reason = null;
+        // Capture event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username1', 'password1', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+
+        // No event is triggred.
+        $this->assertEmpty($events);
         $this->assertInstanceOf('stdClass', $result);
         $this->assertEquals(AUTH_LOGIN_OK, $reason);
 
         $reason = null;
+        // Capture failed login event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username1', 'nopass', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
         $this->assertFalse($result);
         $this->assertEquals(AUTH_LOGIN_FAILED, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username1');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username1');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED);
+        $this->assertEventContextNotUsed($event);
 
         $reason = null;
+        // Capture failed login event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username2', 'password2', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
         $this->assertFalse($result);
         $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username2');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username2');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED);
+        $this->assertEventContextNotUsed($event);
 
         $reason = null;
+        // Capture failed login event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username3', 'password3', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
         $this->assertFalse($result);
         $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username3');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username3');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED);
+        $this->assertEventContextNotUsed($event);
 
         $reason = null;
+        // Capture failed login event.
+        $sink = $this->redirectEvents();
         $result = authenticate_user_login('username4', 'password3', false, $reason);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
         $this->assertFalse($result);
         $this->assertEquals(AUTH_LOGIN_NOUSER, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username4');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username4');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_NOUSER);
+        $this->assertEventContextNotUsed($event);
 
         set_config('lockoutthreshold', 3);
 
index d89c9c7..6ac31a6 100644 (file)
@@ -78,11 +78,11 @@ class behat_hooks extends behat_base {
     protected static $currentstepexception = null;
 
     /**
-     * If we are saving screenshots on failures we should use the same parent dir during a run.
+     * If we are saving any kind of dump on failure we should use the same parent dir during a run.
      *
      * @var The parent dir name
      */
-    protected static $screenshotsdirname = false;
+    protected static $faildumpdirname = false;
 
     /**
      * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
@@ -142,8 +142,8 @@ class behat_hooks extends behat_base {
             self::$lastbrowsersessionstart = time();
         }
 
-        if (!empty($CFG->behat_screenshots_path) && !is_writable($CFG->behat_screenshots_path)) {
-            throw new Exception('You set $CFG->behat_screenshots_path to a non-writable directory');
+        if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
+            throw new Exception('You set $CFG->behat_faildump_path to a non-writable directory');
         }
     }
 
@@ -272,7 +272,7 @@ class behat_hooks extends behat_base {
         global $CFG;
 
         // Save a screenshot if the step failed.
-        if (!empty($CFG->behat_screenshots_path) &&
+        if (!empty($CFG->behat_faildump_path) &&
                 $event->getResult() === StepEvent::FAILED) {
             $this->take_screenshot($event);
         }
@@ -297,12 +297,29 @@ class behat_hooks extends behat_base {
     }
 
     /**
-     * Getter for self::$screenshotsdirname
+     * Execute any steps required after the step has finished.
+     *
+     * This includes creating an HTML dump of the content if there was a failure.
+     *
+     * @AfterStep
+     */
+    public function after_step($event) {
+        global $CFG;
+
+        // Save the page content if the step failed.
+        if (!empty($CFG->behat_faildump_path) &&
+                $event->getResult() === StepEvent::FAILED) {
+            $this->take_contentdump($event);
+        }
+    }
+
+    /**
+     * Getter for self::$faildumpdirname
      *
      * @return string
      */
-    protected function get_run_screenshots_dir() {
-        return self::$screenshotsdirname;
+    protected function get_run_faildump_dir() {
+        return self::$faildumpdirname;
     }
 
     /**
@@ -312,34 +329,61 @@ class behat_hooks extends behat_base {
      * @param StepEvent $event
      */
     protected function take_screenshot(StepEvent $event) {
-        global $CFG;
-
         // Goutte can't save screenshots.
         if (!$this->running_javascript()) {
             return false;
         }
 
-        // All the run screenshots in the same parent dir.
-        if (!$screenshotsdirname = self::get_run_screenshots_dir()) {
-            $screenshotsdirname = self::$screenshotsdirname = date('Ymd_His');
+        list ($dir, $filename) = $this->get_faildump_filename($event, 'png');
+        $this->saveScreenshot($filename, $dir);
+    }
+
+    /**
+     * Take a dump of the page content when a step fails.
+     *
+     * @throws Exception
+     * @param StepEvent $event
+     */
+    protected function take_contentdump(StepEvent $event) {
+        list ($dir, $filename) = $this->get_faildump_filename($event, 'html');
+
+        $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w');
+        fwrite($fh, $this->getSession()->getPage()->getContent());
+        fclose($fh);
+    }
+
+    /**
+     * Determine the full pathname to store a failure-related dump.
+     *
+     * This is used for content such as the DOM, and screenshots.
+     *
+     * @param StepEvent $event
+     * @param String $filetype The file suffix to use.
+     */
+    protected function get_faildump_filename(StepEvent $event, $filetype) {
+        global $CFG;
+
+        // All the contentdumps should be in the same parent dir.
+        if (!$faildumpdir = self::get_run_faildump_dir()) {
+            $faildumpdir = self::$faildumpdirname = date('Ymd_His');
 
-            $dir = $CFG->behat_screenshots_path . DIRECTORY_SEPARATOR . $screenshotsdirname;
+            $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
 
-            if (!mkdir($dir, $CFG->directorypermissions, true)) {
+            if (!is_dir($dir) && !mkdir($dir, $CFG->directorypermissions, true)) {
                 // It shouldn't, we already checked that the directory is writable.
-                throw new Exception('No directories can be created inside $CFG->behat_screenshots_path, check the directory permissions.');
+                throw new Exception('No directories can be created inside $CFG->behat_faildump_path, check the directory permissions.');
             }
         } else {
             // We will always need to know the full path.
-            $dir = $CFG->behat_screenshots_path . DIRECTORY_SEPARATOR . $screenshotsdirname;
+            $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
         }
 
         // The scenario title + the failed step text.
-        // We want a i-am-the-scenario-title_i-am-the-failed-step.png format.
+        // We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format.
         $filename = $event->getStep()->getParent()->getTitle() . '_' . $event->getStep()->getText();
-        $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename) . '.png';
+        $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename) . '.' . $filetype;
 
-        $this->saveScreenshot($filename, $dir);
+        return array($dir, $filename);
     }
 
     /**
@@ -513,3 +557,4 @@ class behat_hooks extends behat_base {
     }
 
 }
+
diff --git a/lib/tests/code_test.php b/lib/tests/code_test.php
deleted file mode 100644 (file)
index b206567..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<?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/>.
-
-/**
- * Code quality unit tests that are fast enough to run each time.
- *
- * @package    core
- * @category   phpunit
- * @copyright  &copy; 2006 The Open University
- * @author     T.J.Hunt@open.ac.uk
- * @license    http://www.gnu.org/copyleft/gpl.html GNU Public License
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-class core_code_testcase extends advanced_testcase {
-    protected $badstrings;
-    protected $extensions_to_ignore = array('exe', 'gif', 'ico', 'jpg', 'png', 'ttf', 'log');
-    protected $ignore_folders = array();
-
-    public function test_dnc() {
-        global $CFG;
-
-        if ($CFG->ostype === 'UNIX') {
-            // Try it the faster way.
-            $oldcwd = getcwd();
-            chdir($CFG->dirroot);
-            $output = null;
-            $exclude = array();
-            foreach ($this->extensions_to_ignore as $ext) {
-                $exclude[] = '--exclude="*.'.$ext.'"';
-            }
-            $exclude = implode(' ', $exclude);
-            exec('grep -r '.$exclude.' DONOT'.'COMMIT .', $output, $code);
-            chdir($oldcwd);
-            // Return code 0 means found, return code 1 means NOT found, 127 is grep not found.
-            if ($code == 1) {
-                // Executed only if no file failed the test.
-                $this->assertTrue(true);
-                return;
-            }
-        }
-
-        $regexp = '/\.(' . implode('|', $this->extensions_to_ignore) . ')$/';
-        $this->badstrings = array();
-        $this->badstrings['DONOT' . 'COMMIT'] = 'DONOT' . 'COMMIT'; // If we put the literal string here, it fails the test!
-        $this->badstrings['trailing whitespace'] = "[\t ][\r\n]";
-        foreach ($this->badstrings as $description => $ignored) {
-            $this->allok[$description] = true;
-        }
-        $this->recurseFolders($CFG->dirroot, 'search_file_for_dnc', $regexp, true);
-        $this->assertTrue(true); // Executed only if no file failed the test.
-    }
-
-    protected function search_file_for_dnc($filepath) {
-        $content = file_get_contents($filepath);
-        foreach ($this->badstrings as $description => $badstring) {
-            if (stripos($content, $badstring) !== false) {
-                $this->fail("File $filepath contains $description.");
-            }
-        }
-    }
-}
index 63d67d6..23d7770 100644 (file)
@@ -812,7 +812,7 @@ class core_completionlib_testcase extends advanced_testcase {
         $this->assertInstanceOf('\core\event\course_module_completion_updated', $event);
         $this->assertEquals($forum->cmid, $event->get_record_snapshot('course_modules_completion', $event->objectid)->coursemoduleid);
         $this->assertEquals($current, $event->get_record_snapshot('course_modules_completion', $event->objectid));
-        $this->assertEquals(context_module::instance($forum->id), $event->get_context());
+        $this->assertEquals(context_module::instance($forum->cmid), $event->get_context());
         $this->assertEquals($USER->id, $event->userid);
         $this->assertEquals($this->user->id, $event->other['relateduserid']);
         $this->assertInstanceOf('moodle_url', $event->get_url());
index 61b3b65..b4b8180 100644 (file)
@@ -370,16 +370,18 @@ class core_coursecatlib_testcase extends advanced_testcase {
      * Test the countall function
      */
     public function test_count_all() {
-        // There should be just the default category.
-        $this->assertEquals(1, coursecat::count_all());
+        global $DB;
+        // Dont assume there is just one. An add-on might create a category as part of the install.
+        $numcategories = $DB->count_records('course_categories');
+        $this->assertEquals($numcategories, coursecat::count_all());
         $category1 = coursecat::create(array('name' => 'Cat1'));
         $category2 = coursecat::create(array('name' => 'Cat2', 'parent' => $category1->id));
         $category3 = coursecat::create(array('name' => 'Cat3', 'parent' => $category2->id, 'visible' => 0));
-        // Now we've got four.
-        $this->assertEquals(4, coursecat::count_all());
+        // Now we've got three more.
+        $this->assertEquals($numcategories + 3, coursecat::count_all());
         cache_helper::purge_by_event('changesincoursecat');
         // We should still have 4.
-        $this->assertEquals(4, coursecat::count_all());
+        $this->assertEquals($numcategories + 3, coursecat::count_all());
     }
 
     /**
index 46c710b..c279e28 100644 (file)
@@ -134,7 +134,7 @@ class core_messagelib_testcase extends advanced_testcase {
         // however mod_quiz doesn't have a data generator.
         // Instead we're going to use backup notifications and give and take away the capability at various levels.
         $assign = $this->getDataGenerator()->create_module('assign', array('course'=>$course->id));
-        $modulecontext = context_module::instance($assign->id);
+        $modulecontext = context_module::instance($assign->cmid);
 
         // Create and enrol a teacher.
         $teacherrole = $DB->get_record('role', array('shortname'=>'editingteacher'), '*', MUST_EXIST);
@@ -162,7 +162,7 @@ class core_messagelib_testcase extends advanced_testcase {
         // They should now be able to see the backup message.
         assign_capability('moodle/site:config', CAP_ALLOW, $teacherrole->id, $modulecontext->id, true);
         accesslib_clear_all_caches_for_unit_testing();
-        $modulecontext = context_module::instance($assign->id);
+        $modulecontext = context_module::instance($assign->cmid);
         $this->assertTrue(has_capability('moodle/site:config', $modulecontext));
 
         $providers = message_get_providers_for_user($teacher->id);
@@ -173,7 +173,7 @@ class core_messagelib_testcase extends advanced_testcase {
         // They should not be able to see the backup message.
         assign_capability('moodle/site:config', CAP_PROHIBIT, $teacherrole->id, $coursecontext->id, true);
         accesslib_clear_all_caches_for_unit_testing();
-        $modulecontext = context_module::instance($assign->id);
+        $modulecontext = context_module::instance($assign->cmid);
         $this->assertFalse(has_capability('moodle/site:config', $modulecontext));
 
         $providers = message_get_providers_for_user($teacher->id);
index dbb4b80..2e73ea2 100644 (file)
@@ -2452,10 +2452,8 @@ class core_moodlelib_testcase extends advanced_testcase {
         $events = $sink->get_events();
         $sink->close();
 
-        $this->assertCount(2, $events);
-        $event = $events[0];
-        $this->assertInstanceOf('\core\event\user_updated', $event);
-        $event = $events[1];
+        $this->assertCount(1, $events);
+        $event = reset($events);
         $this->assertInstanceOf('\core\event\user_loggedin', $event);
         $this->assertEquals('user', $event->objecttable);
         $this->assertEquals($user->id, $event->objectid);
@@ -2470,7 +2468,6 @@ class core_moodlelib_testcase extends advanced_testcase {
 
         $this->assertTimeCurrent($USER->firstaccess);
         $this->assertTimeCurrent($USER->lastaccess);
-        $this->assertTimeCurrent($USER->timemodified);
         $this->assertTimeCurrent($USER->currentlogin);
         $this->assertSame(sesskey(), $USER->sesskey);
         $this->assertTimeCurrent($USER->preference['_lastloaded']);
similarity index 81%
rename from backup/util/progress/tests/progress_test.php
rename to lib/tests/progress_test.php
index 1e4f8c8..5181f4f 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for the progress classes.
  *
- * @package core_backup
+ * @package core_progress
  * @category phpunit
  * @copyright 2013 The Open University
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 
 defined('MOODLE_INTERNAL') || die();
 
-// Include all the needed stuff.
-global $CFG;
-require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php');
-
 /**
  * Progress tests.
  */
-class backup_progress_testcase extends basic_testcase {
+class core_progress_testcase extends basic_testcase {
 
     /**
      * Tests for basic use with simple numeric progress.
      */
     public function test_basic() {
-        $progress = new core_backup_mock_progress();
+        $progress = new core_mock_progress();
 
         // Check values of empty progress things.
         $this->assertFalse($progress->is_in_progress_section());
@@ -59,7 +55,7 @@ class backup_progress_testcase extends basic_testcase {
         core_php_time_limit::get_and_clear_unit_test_data();
         $progress->progress(2);
         $this->assertTrue($progress->was_update_called());
-        $this->assertEquals(array(core_backup_progress::TIME_LIMIT_WITHOUT_PROGRESS),
+        $this->assertEquals(array(\core\progress\base::TIME_LIMIT_WITHOUT_PROGRESS),
                 core_php_time_limit::get_and_clear_unit_test_data());
 
         // Check the new value.
@@ -68,7 +64,7 @@ class backup_progress_testcase extends basic_testcase {
         // Do another progress run at same time, it should be ignored.
         $progress->progress(3);
         $this->assertFalse($progress->was_update_called());
-        $this->assert_min_max(0.2, 0.2, $progress);
+        $this->assert_min_max(0.3, 0.3, $progress);
 
         // End the section. This should cause an update.
         $progress->end_progress();
@@ -86,7 +82,7 @@ class backup_progress_testcase extends basic_testcase {
      */
     public function test_nested() {
         // Outer progress goes from 0 to 10.
-        $progress = new core_backup_mock_progress();
+        $progress = new core_mock_progress();
         $progress->start_progress('hello', 10);
 
         // Get up to 4, check position.
@@ -163,7 +159,7 @@ class backup_progress_testcase extends basic_testcase {
      * Tests the feature for 'weighting' nested progress.
      */
     public function test_nested_weighted() {
-        $progress = new core_backup_mock_progress();
+        $progress = new core_mock_progress();
         $progress->start_progress('', 10);
 
         // First nested child has 2 units of its own and is worth 1 unit.
@@ -183,7 +179,7 @@ class backup_progress_testcase extends basic_testcase {
         $this->assert_min_max(0.4, 0.4, $progress);
 
         // Next indeterminate child is worth 6 units.
-        $progress->start_progress('', core_backup_progress::INDETERMINATE, 6);
+        $progress->start_progress('', \core\progress\base::INDETERMINATE, 6);
         $progress->step_time();
         $progress->progress();
         $this->assert_min_max(0.4, 1.0, $progress);
@@ -196,7 +192,7 @@ class backup_progress_testcase extends basic_testcase {
      * to be similar.
      */
     public function test_realistic() {
-        $progress = new core_backup_mock_progress();
+        $progress = new core_mock_progress();
         $progress->start_progress('parent', 100);
         $progress->start_progress('child', 1);
         $progress->progress(1);
@@ -210,7 +206,7 @@ class backup_progress_testcase extends basic_testcase {
      * zero entries.
      */
     public function test_zero() {
-        $progress = new core_backup_mock_progress();
+        $progress = new core_mock_progress();
         $progress->start_progress('parent', 100);
         $progress->progress(1);
         $this->assert_min_max(0.01, 0.01, $progress);
@@ -229,7 +225,7 @@ class backup_progress_testcase extends basic_testcase {
      * Tests for any exceptions due to invalid calls.
      */
     public function test_exceptions() {
-        $progress = new core_backup_mock_progress();
+        $progress = new core_mock_progress();
 
         // Check errors when empty.
         try {
@@ -268,7 +264,7 @@ class backup_progress_testcase extends basic_testcase {
         // Indeterminate when value expected.
         $progress->start_progress('hello', 10);
         try {
-            $progress->progress(core_backup_progress::INDETERMINATE);
+            $progress->progress(\core\progress\base::INDETERMINATE);
             $this->fail();
         } catch (coding_exception $e) {
             $this->assertEquals(1, preg_match('~expecting value~', $e->getMessage()));
@@ -321,14 +317,73 @@ class backup_progress_testcase extends basic_testcase {
         }
     }
 
+    public function test_progress_change() {
+
+        $progress = new core_mock_progress();
+
+        $progress->start_progress('hello', 50);
+
+
+        for ($n = 1; $n <= 10; $n++) {
+            $progress->increment_progress();
+        }
+
+        // Check numeric position and indeterminate count.
+        $this->assert_min_max(0.2, 0.2, $progress);
+        $this->assertEquals(1, $progress->get_progress_count());
+
+        // Make some progress and check that the time limit gets added.
+        $progress->step_time();
+
+        for ($n = 1; $n <= 20; $n++) {
+            $progress->increment_progress();
+        }
+
+        $this->assertTrue($progress->was_update_called());
+
+        // Check the new value.
+        $this->assert_min_max(0.6, 0.6, $progress);
+        $this->assertEquals(2, $progress->get_progress_count());
+
+        for ($n = 1; $n <= 10; $n++) {
+            $progress->increment_progress();
+        }
+        $this->assertFalse($progress->was_update_called());
+        $this->assert_min_max(0.8, 0.8, $progress);
+        $this->assertEquals(2, $progress->get_progress_count());
+
+        // Do another progress run at same time, it should be ignored.
+        $progress->increment_progress(5);
+        $this->assertFalse($progress->was_update_called());
+        $this->assert_min_max(0.9, 0.9, $progress);
+        $this->assertEquals(2, $progress->get_progress_count());
+
+        for ($n = 1; $n <= 3; $n++) {
+            $progress->step_time();
+            $progress->increment_progress(1);
+        }
+        $this->assertTrue($progress->was_update_called());
+        $this->assert_min_max(0.96, 0.96, $progress);
+        $this->assertEquals(5, $progress->get_progress_count());
+
+
+        // End the section. This should cause an update.
+        $progress->end_progress();
+        $this->assertTrue($progress->was_update_called());
+        $this->assertEquals(5, $progress->get_progress_count());
+
+        // Because there are no sections left open, it thinks we finished.
+        $this->assert_min_max(1.0, 1.0, $progress);
+    }
+
     /**
      * Checks the current progress values are as expected.
      *
      * @param number $min Expected min progress
      * @param number $max Expected max progress
-     * @param core_backup_mock_progress $progress
+     * @param core_mock_progress $progress
      */
-    private function assert_min_max($min, $max, core_backup_mock_progress $progress) {
+    private function assert_min_max($min, $max, core_mock_progress $progress) {
         $this->assertEquals(array($min, $max),
                 $progress->get_progress_proportion_range());
     }
@@ -338,7 +393,7 @@ class backup_progress_testcase extends basic_testcase {
  * Helper class that records when update_progress is called and allows time
  * stepping.
  */
-class core_backup_mock_progress extends core_backup_progress {
+class core_mock_progress extends \core\progress\base {
     private $updatecalled = false;
     private $time = 1;
 
index 508fa8d..780ee74 100644 (file)
@@ -23,10 +23,15 @@ YUI:
     deprecated and replaced by the modal attribute. This was actually
     changed in Moodle 2.2, but has only been marked as deprecated now. It
     will be removed in Moodle 2.9.
+  * When destroying any type of dialogue based on moodle-core-notification, the relevant content is also removed from
+    the DOM. Previously it was left orphaned.
 
 JavaSript:
     * The findChildNodes global function has been deprecated. Y.all should
       be used instead.
+    * The callback argument to confirm_action and M.util.show_confirm_dialog has been deprecated. If you need to write a
+      confirmation which includes a callback, please use moodle-core-notification-confirmation and attach callbacks to the
+      events provided.
 
 * New locking api and admin settings to configure the system locking type.
 
index 338a5cc..816d574 100644 (file)
@@ -1503,7 +1503,7 @@ function install_core($version, $verbose) {
  * @return void, may throw exception
  */
 function upgrade_core($version, $verbose) {
-    global $CFG;
+    global $CFG, $SITE, $DB, $COURSE;
 
     raise_memory_limit(MEMORY_EXTRA);
 
@@ -1534,6 +1534,10 @@ function upgrade_core($version, $verbose) {
             upgrade_main_savepoint($result, $version, false);
         }
 
+        // In case structure of 'course' table has been changed and we forgot to update $SITE, re-read it from db.
+        $SITE = $DB->get_record('course', array('id' => $SITE->id));
+        $COURSE = clone($SITE);
+
         // perform all other component upgrade routines
         update_capabilities('moodle');
         log_update_descriptions('moodle');
@@ -2065,7 +2069,7 @@ function upgrade_grade_item_fix_sortorder() {
 
     $transaction = $DB->start_delegated_transaction();
 
-    $sql = "SELECT g1.id, g1.courseid, g1.sortorder
+    $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
               FROM {grade_items} g1
               JOIN {grade_items} g2 ON g1.courseid = g2.courseid
              WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id
index 1362dc3..a6d6a55 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 a1ab1de..9689bb0 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 f2c325a..57588b7 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 2d90867..8b3d536 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js and b/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js differ
index 18a0282..c831e54 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js and b/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js differ
index 2d90867..8b3d536 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js and b/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js differ
index cf8103c..c34e867 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js differ
index cdef098..0a7c636 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js differ
index cf8103c..c34e867 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js differ
index 2e691ac..2e4bc5d 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js differ
index 138747b..313aa1c 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js differ
index cf96f86..27a544a 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js differ
index 065a4e2..c4687f0 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js differ
index 731bf9f..b1177b1 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js differ
index 065a4e2..c4687f0 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js differ
index f73ddea..1b78536 100644 (file)
@@ -191,7 +191,7 @@ M.core.dock.fixTitleOrientation = function(title, text) {
     }
 
     // We need to fix a font-size - sorry theme designers.
-    test = Y.Node.create('<h2><span class="transform-test-node" style="font-size:'+fontsize+';">'+text+'</span></h2>');
+    test = Y.Node.create('<h2 class="transform-test-heading"><span class="transform-test-node" style="font-size:'+fontsize+';">'+text+'</span></h2>');
     BODY.insert(test, 0);
     width = test.one('span').get('offsetWidth') * 1.2;
     height = test.one('span').get('offsetHeight');
@@ -202,12 +202,10 @@ M.core.dock.fixTitleOrientation = function(title, text) {
 
     // Move the title into position
     title.setStyles({
-        'margin' : '0',
-        'padding' : '0',
         'position' : 'relative',
         'fontSize' : fontsize,
         'width' : width,
-        'top' : width/2
+        'top' : (width - height)/2
     });
 
     // Positioning is different when in RTL mode.
@@ -758,14 +756,16 @@ DOCK.prototype = {
             showregions = false,
             i;
         // First look for understood regions.
-        Y.all(SELECTOR.blockregion).each(function(){
+        Y.all(SELECTOR.blockregion).each(function(region){
             var regionname = region.getData('blockregion');
             if (region.all('.block').size() > 0) {
                 populatedblockregions++;
                 BODY.addClass('used-region-'+regionname);
                 BODY.removeClass('empty-region-'+regionname);
+                BODY.removeClass('docked-region-'+regionname);
             } else {
                 BODY.addClass('empty-region-'+regionname);
+                BODY.addClass('docked-region-'+regionname);
                 BODY.removeClass('used-region-'+regionname);
             }
         });
index 4fc5895..4fd5899 100644 (file)
@@ -37,7 +37,11 @@ TABHEIGHTMANAGER.prototype = {
         var dock = this.get('dock'),
             node = dock.get('dockNode'),
             items = dock.dockeditems,
-            possibleheight = node.get('offsetHeight') - node.one('.controls').get('offsetHeight') - (dock.get('bufferPanel')*3) - (items.length*2),
+            containermargin = parseInt(node.one('.dockeditem_container').getStyle('marginTop').replace('/[^0-9]+$/', ''), 10),
+            dockheight = node.get('offsetHeight') - containermargin,
+            controlheight = node.one('.controls').get('offsetHeight'),
+            buffer = (dock.get('bufferPanel') * 3),
+            possibleheight = dockheight - controlheight - buffer - (items.length*2),
             totalheight = 0,
             id, dockedtitle;
         if (items.length > 0) {
index f51c20b..b7a0edc 100644 (file)
@@ -21,7 +21,7 @@ ALERT = function(config) {
     ALERT.superclass.constructor.apply(this, [config]);
 };
 Y.extend(ALERT, M.core.dialogue, {
-    _enterKeypress : null,
+    closeEvents: [],
     initializer : function() {
         this.publish('complete');
         var yes = Y.Node.create('<input type="button" id="id_yuialertconfirm-' + this.get('COUNT') + '" value="'+this.get(CONFIRMYES)+'" />'),
@@ -33,12 +33,22 @@ Y.extend(ALERT, M.core.dialogue, {
         this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
         this.setStdModContent(Y.WidgetStdMod.HEADER,
                 '<h1 id="moodle-dialogue-'+this.get('COUNT')+'-header-text">' + this.get(TITLE) + '</h1>', Y.WidgetStdMod.REPLACE);
-        this.after('destroyedChange', function(){this.get(BASE).remove();}, this);
-        this._enterKeypress = Y.on('key', this.submit, window, 'down:13', this);
-        yes.on('click', this.submit, this);
+
+        this.closeEvents.push(
+            Y.on('key', this.submit, window, 'down:13', this),
+            yes.on('click', this.submit, this)
+        );
+
+        var closeButton = this.get('boundingBox').one('.closebutton');
+        if (closeButton) {
+            // The close button should act exactly like the 'No' button.
+            this.closeEvents.push(
+                closeButton.on('click', this.submit, this)
+            );
+        }
     },
     submit : function() {
-        this._enterKeypress.detach();
+        new Y.EventHandle(this.closeEvents).detach();
         this.fire('complete');
         this.hide();
         this.destroy();
index fe80edc..6c4bfaa 100644 (file)
@@ -20,8 +20,7 @@ CONFIRM = function(config) {
     CONFIRM.superclass.constructor.apply(this, [config]);
 };
 Y.extend(CONFIRM, M.core.dialogue, {
-    _enterKeypress : null,
-    _escKeypress : null,
+    closeEvents: [],
     initializer : function() {
         this.publish('complete');
         this.publish('complete-yes');
@@ -37,15 +36,24 @@ Y.extend(CONFIRM, M.core.dialogue, {
         this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
         this.setStdModContent(Y.WidgetStdMod.HEADER,
                 '<h1 id="moodle-dialogue-'+this.get('COUNT')+'-header-text">' + this.get(TITLE) + '</h1>', Y.WidgetStdMod.REPLACE);
-        this.after('destroyedChange', function(){this.get(BASE).remove();}, this);
-        this._enterKeypress = Y.on('key', this.submit, window, 'down:13', this, true);
-        this._escKeypress = Y.on('key', this.submit, window, 'down:27', this, false);
-        yes.on('click', this.submit, this, true);
-        no.on('click', this.submit, this, false);
+
+        this.closeEvents.push(
+            Y.on('key', this.submit, window, 'down:13', this, true),
+            Y.on('key', this.submit, window, 'down:27', this, false),
+            yes.on('click', this.submit, this, true),
+            no.on('click', this.submit, this, false)
+        );
+
+        var closeButton = this.get('boundingBox').one('.closebutton');
+        if (closeButton) {
+            // The close button should act exactly like the 'No' button.
+            this.closeEvents.push(
+                closeButton.on('click', this.submit, this)
+            );
+        }
     },
     submit : function(e, outcome) {
-        this._enterKeypress.detach();
-        this._escKeypress.detach();
+        new Y.EventHandle(this.closeEvents).detach();
         this.fire('complete', outcome);
         if (outcome) {
             this.fire('complete-yes');
index 25a0c83..2f3424a 100644 (file)
@@ -111,6 +111,11 @@ Y.extend(DIALOGUE, Y.Panel, {
             this.show();
             this.keyDelegation();
         }
+
+        // Remove the dialogue from the DOM when it is destroyed.
+        this.after('destroyedChange', function(){
+            this.get(BASE).remove(true);
+        }, this);
     },
 
     /**
index ed4324c..ebd83f6 100644 (file)
@@ -49,7 +49,6 @@ Y.extend(EXCEPTION, M.core.dialogue, {
             this._hideTimeout = setTimeout(function(){self.hide();}, delay);
         }
         this.after('visibleChange', this.visibilityChanged, this);
-        this.after('destroyedChange', function(){this.get(BASE).remove();}, this);
         this._keypress = Y.on('key', this.hide, window, 'down:13,27', this);
         this.centerDialogue();
     },
index 7c7a058..64da716 100644 (file)
@@ -12,6 +12,9 @@ Description of import of various YUI libraries into Moodle:
 * update lib/thrirdpartylibs.xml
 * verify our simpleyui rollup contents in /theme/yui_combo.php
 
+If you need to patch the YUI library between its official releases, you *must* read
+http://docs.moodle.org/dev/YUI/Patching.
+
 3/ YUI3 Gallery version gallery-2013.10.02-20-26:
 * selective copy of the "build" directory for the checked out tag of yui3-gallery.
   Unit test code coverage files (*-coverage.js) are removed but no other changes are made.
index cc7056f..3061e78 100644 (file)
@@ -122,6 +122,14 @@ $functions = array(
                 'type' => 'write'
         ),
 
+        'mod_assign_save_grades' => array(
+                'classname' => 'mod_assign_external',
+                'methodname' => 'save_grades',
+                'classpath' => 'mod/assign/externallib.php',
+                'description' => 'Save multiple grade updates for an assignment.',
+                'type' => 'write'
+        ),
+
         'mod_assign_save_user_extensions' => array(
                 'classname' => 'mod_assign_external',
                 'methodname' => 'save_user_extensions',
index dc6e06a..2330f90 100644 (file)
@@ -1665,6 +1665,7 @@ class mod_assign_external extends external_api {
     public static function save_grade_parameters() {
         global $CFG;
         require_once("$CFG->dirroot/mod/assign/locallib.php");
+        require_once("$CFG->dirroot/grade/grading/lib.php");
         $instance = new assign(null, null, null);
         $pluginfeedbackparams = array();
 
@@ -1675,20 +1676,41 @@ class mod_assign_external extends external_api {
             }
         }
 
+        $advancedgradingdata = array();
+        $methods = array_keys(grading_manager::available_methods(false));
+        foreach ($methods as $method) {
+            require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
+            $details  = call_user_func('gradingform_'.$method.'_controller::get_external_instance_filling_details');
+            if (!empty($details)) {
+                $items = array();
+                foreach ($details as $key => $value) {
+                    $value->required = VALUE_OPTIONAL;
+                    unset($value->content->keys['id']);
+                    $items[$key] = new external_multiple_structure (new external_single_structure(
+                        array(
+                            'criterionid' => new external_value(PARAM_INT, 'criterion id'),
+                            'fillings' => $value
+                        )
+                    ));
+                }
+                $advancedgradingdata[$method] = new external_single_structure($items, 'items', VALUE_OPTIONAL);
+            }
+        }
+
         return new external_function_parameters(
             array(
                 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'),
                 'userid' => new external_value(PARAM_INT, 'The student id to operate on'),
-                'grade' => new external_value(PARAM_FLOAT, 'The new grade for this user'),
+                'grade' => new external_value(PARAM_FLOAT, 'The new grade for this user. Ignored if advanced grading used'),
                 'attemptnumber' => new external_value(PARAM_INT, 'The attempt number (-1 means latest attempt)'),
                 'addattempt' => new external_value(PARAM_BOOL, 'Allow another attempt if the attempt reopen method is manual'),
                 'workflowstate' => new external_value(PARAM_ALPHA, 'The next marking workflow state'),
                 'applytoall' => new external_value(PARAM_BOOL, 'If true, this grade will be applied ' .
                                                                'to all members ' .
                                                                'of the group (for group assignments).'),
-                'plugindata' => new external_single_structure(
-                    $pluginfeedbackparams
-                )
+                'plugindata' => new external_single_structure($pluginfeedbackparams, 'plugin data', VALUE_DEFAULT, array()),
+                'advancedgradingdata' => new external_single_structure($advancedgradingdata, 'advanced grading data',
+                                                                       VALUE_DEFAULT, array())
             )
         );
     }
@@ -1698,12 +1720,13 @@ class mod_assign_external extends external_api {
      *
      * @param int $assignmentid The id of the assignment
      * @param int $userid The id of the user
-     * @param float $grade The grade
+     * @param float $grade The grade (ignored if the assignment uses advanced grading)
      * @param int $attemptnumber The attempt number
      * @param bool $addattempt Allow another attempt
      * @param string $workflowstate New workflow state
      * @param bool $applytoall Apply the grade to all members of the group
      * @param array $plugindata Custom data used by plugins
+     * @param array $advancedgradingdata Advanced grading data
      * @return null
      * @since Moodle 2.6
      */
@@ -1714,7 +1737,8 @@ class mod_assign_external extends external_api {
                                       $addattempt,
                                       $workflowstate,
                                       $applytoall,
-                                      $plugindata) {
+                                      $plugindata = array(),
+                                      $advancedgradingdata = array()) {
         global $CFG, $USER;
         require_once("$CFG->dirroot/mod/assign/locallib.php");
 
@@ -1726,22 +1750,38 @@ class mod_assign_external extends external_api {
                                                   'workflowstate' => $workflowstate,
                                                   'addattempt' => $addattempt,
                                                   'applytoall' => $applytoall,
-                                                  'plugindata' => $plugindata));
+                                                  'plugindata' => $plugindata,
+                                                  'advancedgradingdata' => $advancedgradingdata));
 
-        $cm = get_coursemodule_from_instance('assign', $assignmentid, 0, false, MUST_EXIST);
+        $cm = get_coursemodule_from_instance('assign', $params['assignmentid'], 0, false, MUST_EXIST);
         $context = context_module::instance($cm->id);
-
+        self::validate_context($context);
         $assignment = new assign($context, $cm, null);
 
-        $gradedata = (object)$plugindata;
-
-        $gradedata->addattempt = $addattempt;
-        $gradedata->attemptnumber = $attemptnumber;
-        $gradedata->workflowstate = $workflowstate;
-        $gradedata->applytoall = $applytoall;
-        $gradedata->grade = $grade;
+        $gradedata = (object)$params['plugindata'];
+
+        $gradedata->addattempt = $params['addattempt'];
+        $gradedata->attemptnumber = $params['attemptnumber'];
+        $gradedata->workflowstate = $params['workflowstate'];
+        $gradedata->applytoall = $params['applytoall'];
+        $gradedata->grade = $params['grade'];
+
+        if (!empty($params['advancedgradingdata'])) {
+            $advancedgrading = array();
+            $criteria = reset($params['advancedgradingdata']);
+            foreach ($criteria as $key => $criterion) {
+                $details = array();
+                foreach ($criterion as $value) {
+                    foreach ($value['fillings'] as $filling) {
+                        $details[$value['criterionid']] = $filling;
+                    }
+                }
+                $advancedgrading[$key] = $details;
+            }
+            $gradedata->advancedgrading = $advancedgrading;
+        }
 
-        $assignment->save_grade($userid, $gradedata);
+        $assignment->save_grade($params['userid'], $gradedata);
 
         return null;
     }
@@ -1756,6 +1796,157 @@ class mod_assign_external extends external_api {
         return null;
     }
 
+    /**
+     * Describes the parameters for save_grades
+     * @return external_external_function_parameters
+     * @since  Moodle 2.7
+     */
+    public static function save_grades_parameters() {
+        global $CFG;
+        require_once("$CFG->dirroot/mod/assign/locallib.php");
+        require_once("$CFG->dirroot/grade/grading/lib.php");
+        $instance = new assign(null, null, null);
+        $pluginfeedbackparams = array();
+
+        foreach ($instance->get_feedback_plugins() as $plugin) {
+            $pluginparams = $plugin->get_external_parameters();
+            if (!empty($pluginparams)) {
+                $pluginfeedbackparams = array_merge($pluginfeedbackparams, $pluginparams);
+            }
+        }
+
+        $advancedgradingdata = array();
+        $methods = array_keys(grading_manager::available_methods(false));
+        foreach ($methods as $method) {
+            require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
+            $details  = call_user_func('gradingform_'.$method.'_controller::get_external_instance_filling_details');
+            if (!empty($details)) {
+                $items = array();
+                foreach ($details as $key => $value) {
+                    $value->required = VALUE_OPTIONAL;
+                    unset($value->content->keys['id']);
+                    $items[$key] = new external_multiple_structure (new external_single_structure(
+                        array(
+                            'criterionid' => new external_value(PARAM_INT, 'criterion id'),
+                            'fillings' => $value
+                        )
+                    ));
+                }
+                $advancedgradingdata[$method] = new external_single_structure($items, 'items', VALUE_OPTIONAL);
+            }
+        }
+
+        return new external_function_parameters(
+            array(
+                'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'),
+                'applytoall' => new external_value(PARAM_BOOL, 'If true, this grade will be applied ' .
+                                                               'to all members ' .
+                                                               'of the group (for group assignments).'),
+                'grades' => new external_multiple_structure(
+                    new external_single_structure(
+                        array (
+                            'userid' => new external_value(PARAM_INT, 'The student id to operate on'),
+                            'grade' => new external_value(PARAM_FLOAT, 'The new grade for this user. '.
+                                                                       'Ignored if advanced grading used'),
+                            'attemptnumber' => new external_value(PARAM_INT, 'The attempt number (-1 means latest attempt)'),
+                            'addattempt' => new external_value(PARAM_BOOL, 'Allow another attempt if manual attempt reopen method'),
+                            'workflowstate' => new external_value(PARAM_ALPHA, 'The next marking workflow state'),
+                            'plugindata' => new external_single_structure($pluginfeedbackparams, 'plugin data',
+                                                                          VALUE_DEFAULT, array()),
+                            'advancedgradingdata' => new external_single_structure($advancedgradingdata, 'advanced grading data',
+                                                                                   VALUE_DEFAULT, array())
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Save multiple student grades for a single assignment.
+     *
+     * @param int $assignmentid The id of the assignment
+     * @param boolean $applytoall If set to true and this is a team assignment,
+     * apply the grade to all members of the group
+     * @param array $grades grade data for one or more students that includes
+     *                  userid - The id of the student being graded
+     *                  grade - The grade (ignored if the assignment uses advanced grading)
+     *                  attemptnumber - The attempt number
+     *                  addattempt - Allow another attempt
+     *                  workflowstate - New workflow state
+     *                  plugindata - Custom data used by plugins
+     *                  advancedgradingdata - Optional Advanced grading data
+     * @throws invalid_parameter_exception if multiple grades are supplied for
+     * a team assignment that has $applytoall set to true
+     * @return null
+     * @since Moodle 2.7
+     */
+    public static function save_grades($assignmentid, $applytoall = false, $grades) {
+        global $CFG, $USER;
+        require_once("$CFG->dirroot/mod/assign/locallib.php");
+
+        $params = self::validate_parameters(self::save_grades_parameters(),
+                                            array('assignmentid' => $assignmentid,
+                                                  'applytoall' => $applytoall,
+                                                  'grades' => $grades));
+
+        $cm = get_coursemodule_from_instance('assign', $params['assignmentid'], 0, false, MUST_EXIST);
+        $context = context_module::instance($cm->id);
+        self::validate_context($context);
+        $assignment = new assign($context, $cm, null);
+
+        if ($assignment->get_instance()->teamsubmission && $params['applytoall']) {
+            // Check that only 1 user per submission group is provided.
+            $groupids = array();
+            foreach ($params['grades'] as $gradeinfo) {
+                $group = $assignment->get_submission_group($gradeinfo['userid']);
+                if (in_array($group->id, $groupids)) {
+                    throw new invalid_parameter_exception('Multiple grades for the same team have been supplied '
+                                                          .' this is not permitted when the applytoall flag is set');
+                } else {
+                    $groupids[] = $group->id;
+                }
+            }
+        }
+
+        foreach ($params['grades'] as $gradeinfo) {
+            $gradedata = (object)$gradeinfo['plugindata'];
+            $gradedata->addattempt = $gradeinfo['addattempt'];
+            $gradedata->attemptnumber = $gradeinfo['attemptnumber'];
+            $gradedata->workflowstate = $gradeinfo['workflowstate'];
+            $gradedata->applytoall = $params['applytoall'];
+            $gradedata->grade = $gradeinfo['grade'];
+
+            if (!empty($gradeinfo['advancedgradingdata'])) {
+                $advancedgrading = array();
+                $criteria = reset($gradeinfo['advancedgradingdata']);
+                foreach ($criteria as $key => $criterion) {
+                    $details = array();
+                    foreach ($criterion as $value) {
+                        foreach ($value['fillings'] as $filling) {
+                            $details[$value['criterionid']] = $filling;
+                        }
+                    }
+                    $advancedgrading[$key] = $details;
+                }
+                $gradedata->advancedgrading = $advancedgrading;
+            }
+            $assignment->save_grade($gradeinfo['userid'], $gradedata);
+        }
+
+        return null;
+    }
+
+    /**
+     * Describes the return value for save_grades
+     *
+     * @return external_single_structure
+     * @since Moodle 2.7
+     */
+    public static function save_grades_returns() {
+        return null;
+    }
+
     /**
      * Describes the parameters for copy_previous_attempt
      * @return external_external_function_parameters
index 96dde23..e0ea32e 100644 (file)
@@ -94,7 +94,10 @@ class assign_feedback_comments extends assign_feedback_plugin {
                 $commenttext = $feedbackcomments->commenttext;
             }
         }
-        return optional_param('quickgrade_comments_' . $userid, '', PARAM_TEXT) != $commenttext;
+        // Note that this handles the difference between empty and not in the quickgrading
+        // form at all (hidden column).
+        $newvalue = optional_param('quickgrade_comments_' . $userid, false, PARAM_TEXT);
+        return ($newvalue !== false) && ($newvalue != $commenttext);
     }
 
 
@@ -173,6 +176,11 @@ class assign_feedback_comments extends assign_feedback_plugin {
     public function save_quickgrading_changes($userid, $grade) {
         global $DB;
         $feedbackcomment = $this->get_feedback_comments($grade->id);
+        $feedbackpresent = optional_param('quickgrade_comments_' . $userid, false, PARAM_TEXT) !== false;
+        if (!$feedbackpresent) {
+            // Nothing to save (e.g. hidden column).
+            return true;
+        }
         if ($feedbackcomment) {
             $feedbackcomment->commenttext = optional_param('quickgrade_comments_' . $userid, '', PARAM_TEXT);
             return $DB->update_record('assignfeedback_comments', $feedbackcomment);
index 3117df0..468b103 100644 (file)
@@ -207,9 +207,17 @@ class document_services {
             $tmpfile = $tmpdir . '/' . self::COMBINED_PDF_FILENAME;
 
             @unlink($tmpfile);
-            $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
+            try {
+                $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
+            } catch (\Exception $e) {
+                debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER);
+                // TCPDF does not recover from errors so we need to re-initialise the class.
+                $pagecount = 0;
+            }
             if ($pagecount == 0) {
                 // We at least want a single blank page.
+                debugging('TCPDF did not produce a valid pdf:' . $tmpfile . '. Replacing with a blank pdf.', DEBUG_DEVELOPER);
+                $pdf = new pdf();
                 $pdf->AddPage();
                 @unlink($tmpfile);
                 $files = false;
@@ -229,14 +237,27 @@ class document_services {
 
         $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);
 
+        // Detect corrupt generated pdfs and replace with a blank one.
+        if ($files) {
+            $pagecount = $pdf->load_pdf($tmpfile);
+            if ($pagecount <= 0) {
+                $files = false;
+            }
+        }
+
         if (!$files) {
             // This was a blank pdf.
+            unset($pdf);
+            $pdf = new pdf();
             $content = $pdf->Output(self::COMBINED_PDF_FILENAME, 'S');
             $file = $fs->create_file_from_string($record, $content);
         } else {
             // This was a combined pdf.
             $file = $fs->create_file_from_pathname($record, $tmpfile);
             @unlink($tmpfile);
+
+            // Test the generated file for correctness.
+            $compatiblepdf = pdf::ensure_pdf_compatible($file);
         }
 
         return $file;
index d11f077..c3c5f00 100644 (file)
@@ -78,6 +78,7 @@ class pdf extends \FPDI {
     public function combine_pdfs($pdflist, $outfilename) {
 
         raise_memory_limit(MEMORY_EXTRA);
+        $olddebug = error_reporting(0);
 
         $this->setPageUnit('pt');
         $this->setPrintHeader(false);
@@ -97,6 +98,7 @@ class pdf extends \FPDI {
         }
 
         $this->save_pdf($outfilename);
+        error_reporting($olddebug);
 
         return $totalpagecount;
     }
@@ -125,6 +127,7 @@ class pdf extends \FPDI {
      */
     public function load_pdf($filename) {
         raise_memory_limit(MEMORY_EXTRA);
+        $olddebug = error_reporting(0);
 
         $this->setPageUnit('pt');
         $this->scale = 72.0 / 100.0;
@@ -138,6 +141,7 @@ class pdf extends \FPDI {
         $this->pagecount = $this->setSourceFile($filename);
         $this->filename = $filename;
 
+        error_reporting($olddebug);
         return $this->pagecount;
     }
 
@@ -389,7 +393,9 @@ class pdf extends \FPDI {
      * @param string $filename the filename for the PDF (including the full path)
      */
     public function save_pdf($filename) {
+        $olddebug = error_reporting(0);
         $this->Output($filename, 'F');
+        error_reporting($olddebug);
     }
 
     /**
@@ -461,33 +467,26 @@ class pdf extends \FPDI {
      * @return string path to copy or converted pdf (false == fail)
      */
     public static function ensure_pdf_compatible(\stored_file $file) {
-        global $CFG;
-
-        $fp = $file->get_content_file_handle();
-        $ident = fread($fp, 10);
-        if (substr_compare('%PDF-', $ident, 0, 5) !== 0) {
-            return false; // This is not a PDF file at all.
-        }
-        $ident = substr($ident, 5); // Remove the '%PDF-' part.
-        $ident = explode('\x0A', $ident); // Truncate to first '0a' character.
-        list($major, $minor) = explode('.', $ident[0]); // Split the major / minor version.
-        $major = intval($major);
-        $minor = intval($minor);
-        if ($major == 0 || $minor == 0) {
-            return false; // Not a valid PDF version number.
-        }
         $temparea = \make_temp_directory('assignfeedback_editpdf');
         $hash = $file->get_contenthash(); // Use the contenthash to make sure the temp files have unique names.
         $tempsrc = $temparea . "/src-$hash.pdf";
         $tempdst = $temparea . "/dst-$hash.pdf";
+        $file->copy_content_to($tempsrc); // Copy the file.
 
-        if ($major = 1 && $minor<=4) {
-            // PDF is valid version - just create a copy we can use.
-            $file->copy_content_to($tempdst); // Copy the file.
-            return $tempdst;
+        $pdf = new pdf();
+        $pagecount = 0;
+        try {
+            $pagecount = $pdf->load_pdf($tempsrc);
+        } catch (\Exception $e) {
+            // PDF was not valid - try running it through ghostscript to clean it up.
+            $pagecount = 0;
+        }
+
+        if ($pagecount > 0) {
+            // Page is valid and can be read by tcpdf.
+            return $tempsrc;
         }
 
-        $file->copy_content_to($tempsrc); // Copy the file.
 
         $gsexec = \escapeshellarg(\get_config('assignfeedback_editpdf', 'gspath'));
         $tempdstarg = \escapeshellarg($tempdst);
@@ -500,6 +499,20 @@ class pdf extends \FPDI {
             return false;
         }
 
+        $pdf = new pdf();
+        $pagecount = 0;
+        try {
+            $pagecount = $pdf->load_pdf($tempdst);
+        } catch (\Exception $e) {
+            // PDF was not valid - try running it through ghostscript to clean it up.
+            $pagecount = 0;
+        }
+        if ($pagecount <= 0) {
+            @unlink($tempdst);
+            // Could not parse the converted pdf.
+            return false;
+        }
+
         return $tempdst;
     }
 
index fe5f1e6..c424251 100644 (file)
@@ -759,6 +759,9 @@ class assign_grading_table extends table_sql implements renderable {
                               id="selectuser_' . $row->userid . '"
                               name="selectedusers"
                               value="' . $row->userid . '"/>';
+        $selectcol .= '<input type="hidden"
+                              name="grademodified_' . $row->userid . '"
+                              value="' . $row->timemarked . '"/>';
         return $selectcol;
     }
 
index fc62b56..0ab485f 100644 (file)
@@ -1251,12 +1251,8 @@ class assign {
                               maxlength="10"
                               class="quickgrade"/>';
                 $o .= '&nbsp;/&nbsp;' . format_float($this->get_instance()->grade, 2);
-                $o .= '<input type="hidden"
-                              name="grademodified_' . $userid . '"
-                              value="' . $modified . '"/>';
                 return $o;
             } else {
-                $o .= '<input type="hidden" name="grademodified_' . $userid . '" value="' . $modified . '"/>';
                 if ($grade == -1 || $grade === null) {
                     $o .= '-';
                 } else {
@@ -1295,9 +1291,6 @@ class assign {
                     $o .= '<option value="' . $optionid . '" ' . $selected . '>' . $option . '</option>';
                 }
                 $o .= '</select>';
-                $o .= '<input type="hidden" ' .
-                             'name="grademodified_' . $userid . '" ' .
-                             'value="' . $modified . '"/>';
                 return $o;
             } else {
                 $scaleid = (int)$grade;
@@ -4945,8 +4938,8 @@ class assign {
             $record->userid = $userid;
             if ($modified >= 0) {
                 $record->grade = unformat_float(optional_param('quickgrade_' . $record->userid, -1, PARAM_TEXT));
-                $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', '', PARAM_TEXT);
-                $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', '', PARAM_INT);
+                $record->workflowstate = optional_param('quickgrade_' . $record->userid.'_workflowstate', false, PARAM_TEXT);
+                $record->allocatedmarker = optional_param('quickgrade_' . $record->userid.'_allocatedmarker', false, PARAM_INT);
             } else {
                 // This user was not in the grading table.
                 continue;
@@ -4984,6 +4977,8 @@ class assign {
         foreach ($currentgrades as $current) {
             $modified = $users[(int)$current->userid];
             $grade = $this->get_user_grade($modified->userid, false);
+            // Check to see if the grade column was even visible.
+            $gradecolpresent = optional_param('quickgrade_' . $modified->userid, false, PARAM_INT) !== false;
 
             // Check to see if the outcomes were modified.
             if ($CFG->enableoutcomes) {
@@ -4991,7 +4986,9 @@ class assign {
                     $oldoutcome = $outcome->grades[$modified->userid]->grade;
                     $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
                     $newoutcome = optional_param($paramname, -1, PARAM_FLOAT);
-                    if ($oldoutcome != $newoutcome) {
+                    // Check to see if the outcome column was even visible.
+                    $outcomecolpresent = optional_param($paramname, false, PARAM_FLOAT) !== false;
+                    if ($outcomecolpresent && ($oldoutcome != $newoutcome)) {
                         // Can't check modified time for outcomes because it is not reported.
                         $modifiedusers[$modified->userid] = $modified;
                         continue;
@@ -5002,6 +4999,8 @@ class assign {
             // Let plugins participate.
             foreach ($this->feedbackplugins as $plugin) {
                 if ($plugin->is_visible() && $plugin->is_enabled() && $plugin->supports_quickgrading()) {
+                    // The plugins must handle is_quickgrading_modified correctly - ie
+                    // handle hidden columns.
                     if ($plugin->is_quickgrading_modified($modified->userid, $grade)) {
                         if ((int)$current->lastmodified > (int)$modified->lastmodified) {
                             return get_string('errorrecordmodified', 'assign');
@@ -5022,10 +5021,14 @@ class assign {
             if ($current->grade !== null) {
                 $current->grade = floatval($current->grade);
             }
-            if ($current->grade !== $modified->grade ||
-                 ($this->get_instance()->markingallocation && $current->allocatedmarker != $modified->allocatedmarker ) ||
-                 ($this->get_instance()->markingworkflow && $current->workflowstate !== $modified->workflowstate )) {
-
+            $gradechanged = $gradecolpresent && $current->grade !== $modified->grade;
+            $markingallocationchanged = $this->get_instance()->markingallocation &&
+                                            ($modified->allocatedmarker !== false) &&
+                                            ($current->allocatedmarker != $modified->allocatedmarker);
+            $workflowstatechanged = $this->get_instance()->markingworkflow &&
+                                            ($modified->workflowstate !== false) &&
+                                            ($current->workflowstate != $modified->workflowstate);
+            if ($gradechanged || $markingallocationchanged || $workflowstatechanged) {
                 // Grade changed.
                 if ($this->grading_disabled($modified->userid)) {
                     continue;
@@ -5050,6 +5053,7 @@ class assign {
             $flags = $this->get_user_flags($userid, true);
             $grade->grade= grade_floatval(unformat_float($modified->grade));
             $grade->grader= $USER->id;
+            $gradecolpresent = optional_param('quickgrade_' . $userid, false, PARAM_INT) !== false;
 
             // Save plugins data.
             foreach ($this->feedbackplugins as $plugin) {
@@ -5063,11 +5067,21 @@ class assign {
                 }
             }
 
-            if ($flags->workflowstate != $modified->workflowstate ||
-                $flags->allocatedmarker != $modified->allocatedmarker) {
+            // These will be set to false if they are not present in the quickgrading
+            // form (e.g. column hidden).
+            $workflowstatemodified = ($modified->workflowstate !== false) &&
+                                        ($flags->workflowstate != $modified->workflowstate);
+
+            $allocatedmarkermodified = ($modified->allocatedmarker !== false) &&
+                                        ($flags->allocatedmarker != $modified->allocatedmarker);
 
+            if ($workflowstatemodified) {
                 $flags->workflowstate = $modified->workflowstate;
+            }
+            if ($allocatedmarkermodified) {
                 $flags->allocatedmarker = $modified->allocatedmarker;
+            }
+            if ($workflowstatemodified || $allocatedmarkermodified) {
                 $this->update_user_flags($flags);
             }
             $this->update_grade($grade);
@@ -5082,8 +5096,10 @@ class assign {
                 foreach ($modified->gradinginfo->outcomes as $outcomeid => $outcome) {
                     $oldoutcome = $outcome->grades[$modified->userid]->grade;
                     $paramname = 'outcome_' . $outcomeid . '_' . $modified->userid;
-                    $newoutcome = optional_param($paramname, -1, PARAM_INT);
-                    if ($oldoutcome != $newoutcome) {
+                    // This will be false if the input was not in the quickgrading
+                    // form (e.g. column hidden).
+                    $newoutcome = optional_param($paramname, false, PARAM_INT);
+                    if ($newoutcome !== false && ($oldoutcome != $newoutcome)) {
                         $data[$outcomeid] = $newoutcome;
                     }
                 }
index 76b2fa3..eacc25c 100644 (file)
@@ -73,7 +73,7 @@ class assignsubmission_comments_events_testcase extends mod_assign_base_testcase
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\assignsubmission_comments\event\comment_created', $event);
         $this->assertEquals($context, $event->get_context());
-        $url = new moodle_url('/mod/assign/view.php', array('id' => $submission->id));
+        $url = new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id));
         $this->assertEquals($url, $event->get_url());
         $this->assertEventContextNotUsed($event);
     }
@@ -111,7 +111,7 @@ class assignsubmission_comments_events_testcase extends mod_assign_base_testcase
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\assignsubmission_comments\event\comment_deleted', $event);
         $this->assertEquals($context, $event->get_context());
-        $url = new moodle_url('/mod/assign/view.php', array('id' => $submission->id));
+        $url = new moodle_url('/mod/assign/view.php', array('id' => $assign->get_course_module()->id));
         $this->assertEquals($url, $event->get_url());
         $this->assertEventContextNotUsed($event);
     }
diff --git a/mod/assign/tests/behat/quickgrading.feature b/mod/assign/tests/behat/quickgrading.feature
new file mode 100644 (file)
index 0000000..4190767
--- /dev/null
@@ -0,0 +1,142 @@
+@mod @mod_assign
+Feature: In an assignment, teachers grade multiple students on one page
+  In order to quickly give students grades and feedback
+  As a teacher
+  I need to grade multiple students on one page
+
+  @javascript
+  Scenario: Grade multiple students on one page
+    Given the following "courses" exists:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exists:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@asd.com |
+      | student1 | Student | 1 | student1@asd.com |
+      | student2 | Student | 2 | student2@asd.com |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    When I log in as "admin"
+    And I set the following administration settings values:
+      | Enable outcomes | 1 |
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Outcomes"
+    And I follow "Edit outcomes"
+    And I press "Add a new outcome"
+    And I press "Continue"
+    And I fill the moodle form with:
+      | Name | 1337dom scale |
+      | Scale | Noob, Nub, 1337, HaXor |
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I follow "Outcomes"
+    And I follow "Edit outcomes"
+    And I press "Add a new outcome"
+    And I fill the moodle form with:
+      | Full name | M8d skillZ! |
+      | Short name | skillZ! |
+      | Scale | 1337dom scale |
+    And I press "Save changes"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Assignment" to section "1" and I fill the form with:
+      | Assignment name | Test assignment name |
+      | Description | Submit your online text |
+      | assignsubmission_onlinetext_enabled | 1 |
+      | assignsubmission_file_enabled | 0 |
+      | M8d skillZ! | 1 |
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I press "Add submission"
+    And I fill the moodle form with:
+      | Online text | I'm the student1 submission |
+    And I press "Save changes"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    When I press "Add submission"
+    And I fill the moodle form with:
+      | Online text | I'm the student2 submission |
+    And I press "Save changes"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I follow "View/grade all submissions"
+    And I click on "Grade Student 1" "link" in the "Student 1" "table_row"
+    And I fill the moodle form with:
+      | Grade out of 100 | 50.0 |
+      | M8d skillZ! | 1337 |
+      | Feedback comments | I'm the teacher first feedback |
+    And I press "Save changes"
+    And I press "Continue"
+    Then I click on "Quick grading" "checkbox"
+    And I fill in "User grade" with "60.0"
+    And I press "Save all quick grading changes"
+    And I should see "The grade changes were saved"
+    And I press "Continue"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I should see "I'm the teacher first feedback"
+    And I should see "60.0"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I should see "1337"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I should not see "I'm the teacher first feedback"
+    And I should not see "60.0"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I should not see "1337"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I follow "View/grade all submissions"
+    And I click on "Hide User picture" "link"
+    And I click on "Hide Full name" "link"
+    And I click on "Hide Email address" "link"
+    And I click on "Hide Status" "link"
+    And I click on "Hide Grade" "link"
+    And I click on "Hide Edit" "link"
+    And I click on "Hide Last modified (submission)" "link"
+    And I click on "Hide Online text" "link"
+    And I click on "Hide Submission comments" "link"
+    And I click on "Hide Last modified (grade)" "link"
+    And I click on "Hide Feedback comments" "link"
+    And I click on "Hide Final grade" "link"
+    And I click on "Hide Outcomes" "link"
+    And I press "Save all quick grading changes"
+    And I should see "The grade changes were saved"
+    And I press "Continue"
+    And I log out
+    And I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I should see "I'm the teacher first feedback"
+    And I should see "60.0"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I should see "1337"
+    And I log out
+    And I log in as "student2"
+    And I follow "Course 1"
+    And I follow "Test assignment name"
+    And I should not see "I'm the teacher first feedback"
+    And I should not see "60.0"
+    And I follow "Course 1"
+    And I follow "Grades"
+    And I should not see "1337"
index 5ab2faa..f764d23 100644 (file)
@@ -66,7 +66,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         // Create a teacher and give them capabilities.
         $context = context_course::instance($course->id);
         $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $context->id, 3);
-        $context = context_module::instance($assign->id);
+        $context = context_module::instance($assign->cmid);
         $this->assignUserCapability('mod/assign:grade', $context->id, $roleid);
 
         // Create the teacher's enrolment record.
@@ -163,7 +163,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         // Create the user and give them capabilities.
         $context = context_course::instance($course1->id);
         $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
-        $context = context_module::instance($assign1->id);
+        $context = context_module::instance($assign1->cmid);
         $this->assignUserCapability('mod/assign:view', $context->id, $roleid);
 
         // Create the user enrolment record.
@@ -332,7 +332,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         // Create a teacher and give them capabilities.
         $context = context_course::instance($course->id);
         $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $context->id, 3);
-        $context = context_module::instance($assign->id);
+        $context = context_module::instance($assign->cmid);
         $this->assignUserCapability('mod/assign:grade', $context->id, $roleid);
 
         // Create the teacher's enrolment record.
@@ -403,7 +403,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         // Create a teacher and give them capabilities.
         $context = context_course::instance($course->id);
         $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $context->id, 3);
-        $context = context_module::instance($assign->id);
+        $context = context_module::instance($assign->cmid);
         $this->assignUserCapability('mod/assign:revealidentities', $context->id, $roleid);
 
         // Create the teacher's enrolment record.
@@ -916,7 +916,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
 
         $student1 = self::getDataGenerator()->create_user();
         $student2 = self::getDataGenerator()->create_user();
-        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
         $this->getDataGenerator()->enrol_user($student1->id,
                                               $course->id,
                                               $studentrole->id);
@@ -945,8 +945,8 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         // Now try a grade.
         $feedbackpluginparams = array();
         $feedbackpluginparams['files_filemanager'] = $draftidfile;
-        $feedbackeditorparams = array('text'=>'Yeeha!',
-                                        'format'=>1);
+        $feedbackeditorparams = array('text' => 'Yeeha!',
+                                        'format' => 1);
         $feedbackpluginparams['assignfeedbackcomments_editor'] = $feedbackeditorparams;
         $result = mod_assign_external::save_grade($instance->id,
                                                   $student1->id,
@@ -965,6 +965,296 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         $this->assertEquals($result['assignments'][0]['grades'][0]['grade'], '50.0');
     }
 
+    /**
+     * Test save grades with advanced grading data
+     */
+    public function test_save_grades_with_advanced_grading() {
+        global $DB, $USER;
+
+        $this->resetAfterTest(true);
+        // Create a course and assignment and users.
+        $course = self::getDataGenerator()->create_course();
+
+        $teacher = self::getDataGenerator()->create_user();
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $this->getDataGenerator()->enrol_user($teacher->id,
+                                              $course->id,
+                                              $teacherrole->id);
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $params['course'] = $course->id;
+        $params['assignfeedback_file_enabled'] = 0;
+        $params['assignfeedback_comments_enabled'] = 0;
+        $instance = $generator->create_instance($params);
+        $cm = get_coursemodule_from_instance('assign', $instance->id);
+        $context = context_module::instance($cm->id);
+
+        $assign = new assign($context, $cm, $course);
+
+        $student1 = self::getDataGenerator()->create_user();
+        $student2 = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student1->id,
+                                              $course->id,
+                                              $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student2->id,
+                                              $course->id,
+                                              $studentrole->id);
+
+        $this->setUser($teacher);
+
+        $feedbackpluginparams = array();
+        $feedbackpluginparams['files_filemanager'] = 0;
+        $feedbackeditorparams = array('text' => '', 'format' => 1);
+        $feedbackpluginparams['assignfeedbackcomments_editor'] = $feedbackeditorparams;
+
+        // Create advanced grading data.
+        // Create grading area.
+        $gradingarea = array(
+            'contextid' => $context->id,
+            'component' => 'mod_assign',
+            'areaname' => 'submissions',
+            'activemethod' => 'rubric'
+        );
+        $areaid = $DB->insert_record('grading_areas', $gradingarea);
+
+        // Create a rubric grading definition.
+        $rubricdefinition = array (
+            'areaid' => $areaid,
+            'method' => 'rubric',
+            'name' => 'test',
+            'status' => 20,
+            'copiedfromid' => 1,
+            'timecreated' => 1,
+            'usercreated' => $teacher->id,
+            'timemodified' => 1,
+            'usermodified' => $teacher->id,
+            'timecopied' => 0
+        );
+        $definitionid = $DB->insert_record('grading_definitions', $rubricdefinition);
+
+        // Create a criterion with a level.
+        $rubriccriteria = array (
+             'definitionid' => $definitionid,
+             'sortorder' => 1,
+             'description' => 'Demonstrate an understanding of disease control',
+             'descriptionformat' => 0
+        );
+        $criterionid = $DB->insert_record('gradingform_rubric_criteria', $rubriccriteria);
+        $rubriclevel1 = array (
+            'criterionid' => $criterionid,
+            'score' => 50,
+            'definition' => 'pass',
+            'definitionformat' => 0
+        );
+        $rubriclevel2 = array (
+            'criterionid' => $criterionid,
+            'score' => 100,
+            'definition' => 'excellent',
+            'definitionformat' => 0
+        );
+        $rubriclevel3 = array (
+            'criterionid' => $criterionid,
+            'score' => 0,
+            'definition' => 'fail',
+            'definitionformat' => 0
+        );
+        $levelid1 = $DB->insert_record('gradingform_rubric_levels', $rubriclevel1);
+        $levelid2 = $DB->insert_record('gradingform_rubric_levels', $rubriclevel2);
+        $levelid3 = $DB->insert_record('gradingform_rubric_levels', $rubriclevel3);
+
+        // Create the filling.
+        $student1filling = array (
+            'criterionid' => $criterionid,
+            'levelid' => $levelid1,
+            'remark' => 'well done you passed',
+            'remarkformat' => 0
+        );
+
+        $student2filling = array (
+            'criterionid' => $criterionid,
+            'levelid' => $levelid2,
+            'remark' => 'Excellent work',
+            'remarkformat' => 0
+        );
+
+        $student1criteria = array(array('criterionid' => $criterionid, 'fillings' => array($student1filling)));
+        $student1advancedgradingdata = array('rubric' => array('criteria' => $student1criteria));
+
+        $student2criteria = array(array('criterionid' => $criterionid, 'fillings' => array($student2filling)));
+        $student2advancedgradingdata = array('rubric' => array('criteria' => $student2criteria));
+
+        $grades = array();
+        $student1gradeinfo = array();
+        $student1gradeinfo['userid'] = $student1->id;
+        $student1gradeinfo['grade'] = 0; // Ignored since advanced grading is being used.
+        $student1gradeinfo['attemptnumber'] = -1;
+        $student1gradeinfo['addattempt'] = true;
+        $student1gradeinfo['workflowstate'] = 'released';
+        $student1gradeinfo['plugindata'] = $feedbackpluginparams;
+        $student1gradeinfo['advancedgradingdata'] = $student1advancedgradingdata;
+        $grades[] = $student1gradeinfo;
+
+        $student2gradeinfo = array();
+        $student2gradeinfo['userid'] = $student2->id;
+        $student2gradeinfo['grade'] = 0; // Ignored since advanced grading is being used.
+        $student2gradeinfo['attemptnumber'] = -1;
+        $student2gradeinfo['addattempt'] = true;
+        $student2gradeinfo['workflowstate'] = 'released';
+        $student2gradeinfo['plugindata'] = $feedbackpluginparams;
+        $student2gradeinfo['advancedgradingdata'] = $student2advancedgradingdata;
+        $grades[] = $student2gradeinfo;
+
+        $result = mod_assign_external::save_grades($instance->id, false, $grades);
+        // No warnings.
+        $this->assertEquals(0, count($result));
+
+        $student1grade = $DB->get_record('assign_grades',
+                                         array('userid' => $student1->id, 'assignment' => $instance->id),
+                                         '*',
+                                         MUST_EXIST);
+        $this->assertEquals($student1grade->grade, '50.0');
+
+        $student2grade = $DB->get_record('assign_grades',
+                                         array('userid' => $student2->id, 'assignment' => $instance->id),
+                                         '*',
+                                         MUST_EXIST);
+        $this->assertEquals($student2grade->grade, '100.0');
+    }
+
+    /**
+     * Test save grades for a team submission
+     */
+    public function test_save_grades_with_group_submission() {
+        global $DB, $USER, $CFG;
+        require_once($CFG->dirroot . '/group/lib.php');
+
+        $this->resetAfterTest(true);
+        // Create a course and assignment and users.
+        $course = self::getDataGenerator()->create_course();
+
+        $teacher = self::getDataGenerator()->create_user();
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+        $this->getDataGenerator()->enrol_user($teacher->id,
+                                              $course->id,
+                                              $teacherrole->id);
+
+        $groupingdata = array();
+        $groupingdata['courseid'] = $course->id;
+        $groupingdata['name'] = 'Group assignment grouping';
+
+        $grouping = self::getDataGenerator()->create_grouping($groupingdata);
+
+        $group1data = array();
+        $group1data['courseid'] = $course->id;
+        $group1data['name'] = 'Team 1';
+        $group2data = array();
+        $group2data['courseid'] = $course->id;
+        $group2data['name'] = 'Team 2';
+
+        $group1 = self::getDataGenerator()->create_group($group1data);
+        $group2 = self::getDataGenerator()->create_group($group2data);
+
+        groups_assign_grouping($grouping->id, $group1->id);
+        groups_assign_grouping($grouping->id, $group2->id);
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $params['course'] = $course->id;
+        $params['teamsubmission'] = 1;
+        $params['teamsubmissiongroupingid'] = $grouping->id;
+        $instance = $generator->create_instance($params);
+        $cm = get_coursemodule_from_instance('assign', $instance->id);
+        $context = context_module::instance($cm->id);
+
+        $assign = new assign($context, $cm, $course);
+
+        $student1 = self::getDataGenerator()->create_user();
+        $student2 = self::getDataGenerator()->create_user();
+        $student3 = self::getDataGenerator()->create_user();
+        $student4 = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student1->id,
+                                              $course->id,
+                                              $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student2->id,
+                                              $course->id,
+                                              $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student3->id,
+                                              $course->id,
+                                              $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student4->id,
+                                              $course->id,
+                                              $studentrole->id);
+
+        groups_add_member($group1->id, $student1->id);
+        groups_add_member($group1->id, $student2->id);
+        groups_add_member($group1->id, $student3->id);
+        groups_add_member($group2->id, $student4->id);
+        $this->setUser($teacher);
+
+        $feedbackpluginparams = array();
+        $feedbackpluginparams['files_filemanager'] = 0;
+        $feedbackeditorparams = array('text' => '', 'format' => 1);
+        $feedbackpluginparams['assignfeedbackcomments_editor'] = $feedbackeditorparams;
+
+        $grades1 = array();
+        $student1gradeinfo = array();
+        $student1gradeinfo['userid'] = $student1->id;
+        $student1gradeinfo['grade'] = 50;
+        $student1gradeinfo['attemptnumber'] = -1;
+        $student1gradeinfo['addattempt'] = true;
+        $student1gradeinfo['workflowstate'] = 'released';
+        $student1gradeinfo['plugindata'] = $feedbackpluginparams;
+        $grades1[] = $student1gradeinfo;
+
+        $student2gradeinfo = array();
+        $student2gradeinfo['userid'] = $student2->id;
+        $student2gradeinfo['grade'] = 75;
+        $student2gradeinfo['attemptnumber'] = -1;
+        $student2gradeinfo['addattempt'] = true;
+        $student2gradeinfo['workflowstate'] = 'released';
+        $student2gradeinfo['plugindata'] = $feedbackpluginparams;
+        $grades1[] = $student2gradeinfo;
+
+        $this->setExpectedException('invalid_parameter_exception');
+        // Expect an exception since 2 grades have been submitted for the same team.
+        $result = mod_assign_external::save_grades($instance->id, true, $grades1);
+
+        $grades2 = array();
+        $student3gradeinfo = array();
+        $student3gradeinfo['userid'] = $student3->id;
+        $student3gradeinfo['grade'] = 50;
+        $student3gradeinfo['attemptnumber'] = -1;
+        $student3gradeinfo['addattempt'] = true;
+        $student3gradeinfo['workflowstate'] = 'released';
+        $student3gradeinfo['plugindata'] = $feedbackpluginparams;
+        $grades2[] = $student3gradeinfo;
+
+        $student4gradeinfo = array();
+        $student4gradeinfo['userid'] = $student4->id;
+        $student4gradeinfo['grade'] = 75;
+        $student4gradeinfo['attemptnumber'] = -1;
+        $student4gradeinfo['addattempt'] = true;
+        $student4gradeinfo['workflowstate'] = 'released';
+        $student4gradeinfo['plugindata'] = $feedbackpluginparams;
+        $grades2[] = $student4gradeinfo;
+        $result = mod_assign_external::save_grades($instance->id, true, $grades2);
+        // There should be no warnings.
+        $this->assertEquals(0, count($result));
+
+        $student3grade = $DB->get_record('assign_grades',
+                                         array('userid' => $student3->id, 'assignment' => $instance->id),
+                                         '*',
+                                         MUST_EXIST);
+        $this->assertEquals($student3grade->grade, '50.0');
+
+        $student4grade = $DB->get_record('assign_grades',
+                                         array('userid' => $student4->id, 'assignment' => $instance->id),
+                                         '*',
+                                         MUST_EXIST);
+        $this->assertEquals($student4grade->grade, '75.0');
+    }
+
     /**
      * Test copy_previous_attempt
      */
@@ -1072,7 +1362,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase {
         // Create a teacher and give them capabilities.
         $context = context_course::instance($course->id);
         $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $context->id, 3);
-        $context = context_module::instance($assign->id);
+        $context = context_module::instance($assign->cmid);
         $this->assignUserCapability('mod/assign:grade', $context->id, $roleid);
 
         // Create the teacher's enrolment record.
index ec94096..998a2ff 100644 (file)
@@ -3,6 +3,12 @@ This files describes API changes in the assign code.
 * Added setting sendstudentnotifications to assign DB table with admin defaults. This sets the default value for the
   "Notify students" option on the grading forms. This setting can be retrieved via webservices.
 
+=== 2.7 ===
+
+* Web service function mod_assign_save_grade has an additional optional parameter $advancedgradingdata which allows
+  advanced grading data to be used.
+* A new web service function mod_assign_save_grades has been added which allows multiple grades to be processed.
+
 === 2.6.1 ===
 
 * format_text() is no longer used for formating assignment content to be used in events (assign_submission_onlinetext::save()) or
index 0c741d7..e6e6d2e 100644 (file)
@@ -25,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version  = 2014011500;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version  = 2014020500;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires = 2013110500;    // Requires this Moodle version.
 $plugin->cron     = 60;
 
index 3f4dfc3..2992884 100644 (file)
@@ -65,7 +65,7 @@ class mod_book_events_testcase extends advanced_testcase {
 
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\mod_book\event\chapter_created', $event);
-        $this->assertEquals(context_module::instance($book->id), $event->get_context());
+        $this->assertEquals(context_module::instance($book->cmid), $event->get_context());
         $this->assertEquals($chapter->id, $event->objectid);
         $expected = array($course->id, 'book', 'add chapter', 'view.php?id='.$book->cmid.'&chapterid='.$chapter->id,
             $chapter->id, $book->cmid);
@@ -98,7 +98,7 @@ class mod_book_events_testcase extends advanced_testcase {
 
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\mod_book\event\chapter_updated', $event);
-        $this->assertEquals(context_module::instance($book->id), $event->get_context());
+        $this->assertEquals(context_module::instance($book->cmid), $event->get_context());
         $this->assertEquals($chapter->id, $event->objectid);
         $expected = array($course->id, 'book', 'update chapter', 'view.php?id='.$book->cmid.'&chapterid='.$chapter->id,
             $chapter->id, $book->cmid);
@@ -133,7 +133,7 @@ class mod_book_events_testcase extends advanced_testcase {
 
         // Checking that the event contains the expected values.
         $this->assertInstanceOf('\mod_book\event\chapter_deleted', $event);
-        $this->assertEquals(context_module::instance($book->id), $event->get_context());
+        $this->assertEquals(context_module::instance($book->cmid), $event->get_context());
         $this->assertEquals($chapter->id, $event->objectid);
         $this->assertEquals($chapter, $event->get_record_snapshot('book_chapters', $chapter->id));
         $this->assertEventLegacyLogData(array('1', 2, false), $event);
index f029df5..ae53c6f 100644 (file)
@@ -61,7 +61,7 @@ $PAGE->set_context($context);
 
 // show some info for guests
 if (isguestuser()) {
-    $PAGE->set_title(format_string($chat->name));
+    $PAGE->set_title($chat->name);
     echo $OUTPUT->header();
     echo $OUTPUT->confirm('<p>'.get_string('noguests', 'chat').'</p>'.get_string('liketologin'),
             get_login_url(), $CFG->wwwroot.'/course/view.php?id='.$course->id);
index 4b85263..dcc9030 100644 (file)
@@ -77,7 +77,7 @@ class mod_choice_events_testcase extends advanced_testcase {
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\mod_choice\event\answer_submitted', $events[0]);
         $this->assertEquals($user->id, $events[0]->userid);
-        $this->assertEquals(context_module::instance($this->choice->id), $events[0]->get_context());
+        $this->assertEquals(context_module::instance($this->choice->cmid), $events[0]->get_context());
         $this->assertEquals(1, $events[0]->other['choiceid']);
         $this->assertEquals(3, $events[0]->other['optionid']);
         $expected = array($this->course->id, "choice", "choose", 'view.php?id=' . $this->cm->id, $this->choice->id, $this->cm->id);
@@ -128,7 +128,7 @@ class mod_choice_events_testcase extends advanced_testcase {
         $this->assertCount(1, $events);
         $this->assertInstanceOf('\mod_choice\event\answer_updated', $events[0]);
         $this->assertEquals($user->id, $events[0]->userid);
-        $this->assertEquals(context_module::instance($this->choice->id), $events[0]->get_context());
+        $this->assertEquals(context_module::instance($this->choice->cmid), $events[0]->get_context());
         $this->assertEquals(1, $events[0]->other['choiceid']);
         $this->assertEquals(3, $events[0]->other['optionid']);
         $expected = array($this->course->id, "choice", "choose again", 'view.php?id=' . $this->cm->id,
@@ -189,7 +189,7 @@ class mod_choice_events_testcase extends advanced_testcase {
         $this->assertCou