Merge branch 'master_MDL-37714' of git://github.com/greg-or/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 3 Feb 2014 16:58:30 +0000 (17:58 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Mon, 3 Feb 2014 16:58:30 +0000 (17:58 +0100)
252 files changed:
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/renderer.php
admin/tool/behat/version.php
admin/webservice/service_user_settings.php
auth/ldap/tests/plugin_test.php
badges/tests/badgeslib_test.php
blocks/comments/tests/events_test.php
blog/tests/bloglib_test.php
cache/classes/loaders.php
cohort/tests/cohortlib_test.php
config-dist.php
course/dndupload.js
course/edit_form.php
course/editsection_form.php
course/moodleform_mod.php
course/tests/courselib_test.php
enrol/imsenterprise/lib.php
enrol/imsenterprise/tests/imsenterprise_test.php
enrol/manual/manage.php
enrol/manual/unenrolself.php
enrol/meta/tests/plugin_test.php
enrol/paypal/unenrolself.php
enrol/self/lib.php
enrol/self/unenrolself.php
enrol/tests/enrollib_test.php
files/tests/externallib_test.php
grade/tests/edittreelib_test.php
grade/tests/externallib_test.php
group/groupings.php
group/overview.php
install/lang/fr/error.php
lib/behat/classes/behat_command.php
lib/behat/form_field/behat_form_editor.php
lib/behat/form_field/behat_form_field.php
lib/behat/form_field/behat_form_select.php
lib/classes/event/assessable_submitted.php
lib/classes/event/assessable_uploaded.php
lib/classes/event/base.php
lib/classes/event/blog_association_created.php
lib/classes/event/comment_created.php
lib/classes/event/comment_deleted.php
lib/classes/event/comments_viewed.php
lib/classes/event/course_module_instance_list_viewed.php
lib/classes/event/course_module_viewed.php
lib/classes/event/user_enrolment_created.php
lib/classes/event/user_enrolment_deleted.php
lib/classes/lock/db_record_lock_factory.php [new file with mode: 0644]
lib/classes/lock/file_lock_factory.php [new file with mode: 0644]
lib/classes/lock/lock.php [new file with mode: 0644]
lib/classes/lock/lock_config.php [new file with mode: 0644]
lib/classes/lock/lock_factory.php [new file with mode: 0644]
lib/classes/lock/postgres_lock_factory.php [new file with mode: 0644]
lib/classes/plugin_manager.php
lib/db/install.xml
lib/db/upgrade.php
lib/ddl/database_manager.php
lib/ddl/mysql_sql_generator.php
lib/dml/moodle_database.php
lib/dml/mssql_native_moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/oci_native_moodle_database.php
lib/dml/pdo_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/filestorage/tgz_extractor.php
lib/moodlelib.php
lib/outputrenderers.php
lib/pdflib.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/event_mock.php
lib/setuplib.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_hooks.php
lib/tests/completionlib_test.php
lib/tests/coursecatlib_test.php
lib/tests/event_content_viewed_test.php
lib/tests/event_course_module_instance_list_viewed.php
lib/tests/event_course_module_viewed.php
lib/tests/event_test.php
lib/tests/events_test.php
lib/tests/fixtures/event_fixtures.php
lib/tests/lock_config_test.php [new file with mode: 0644]
lib/tests/lock_test.php [new file with mode: 0644]
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js
lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll.js [new file with mode: 0644]
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/src/chooserdialogue/js/chooserdialogue.js
lib/yui/src/lockscroll/build.json [new file with mode: 0644]
lib/yui/src/lockscroll/js/lockscroll.js [new file with mode: 0644]
lib/yui/src/lockscroll/meta/lockscroll.json [new file with mode: 0644]
lib/yui/src/notification/js/dialogue.js
lib/yui/src/notification/meta/notification.json
message/tests/behat/message_participants.feature [new file with mode: 0644]
mod/assign/classes/event/all_submissions_downloaded.php
mod/assign/classes/event/assessable_submitted.php
mod/assign/classes/event/extension_granted.php
mod/assign/classes/event/identities_revealed.php
mod/assign/classes/event/marker_updated.php
mod/assign/classes/event/statement_accepted.php
mod/assign/classes/event/submission_duplicated.php
mod/assign/classes/event/submission_graded.php
mod/assign/classes/event/submission_locked.php
mod/assign/classes/event/submission_status_updated.php
mod/assign/classes/event/submission_unlocked.php
mod/assign/classes/event/submission_updated.php
mod/assign/classes/event/workflow_state_updated.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/submission/file/classes/event/assessable_uploaded.php
mod/assign/submission/file/tests/events_test.php
mod/assign/submission/onlinetext/classes/event/assessable_uploaded.php
mod/assign/submission/onlinetext/tests/events_test.php
mod/assign/tests/behat/quickgrading.feature [new file with mode: 0644]
mod/assign/tests/externallib_test.php
mod/assign/version.php
mod/assignment/version.php
mod/book/classes/event/chapter_created.php
mod/book/classes/event/chapter_deleted.php
mod/book/classes/event/chapter_updated.php
mod/book/classes/event/chapter_viewed.php
mod/book/tests/events_test.php
mod/book/tool/exportimscp/classes/event/book_exported.php
mod/book/tool/exportimscp/tests/events_test.php
mod/book/tool/importhtml/tests/locallib_test.php
mod/book/tool/print/classes/event/book_printed.php
mod/book/tool/print/classes/event/chapter_printed.php
mod/book/tool/print/tests/events_test.php
mod/book/version.php
mod/chat/classes/event/message_sent.php
mod/chat/classes/event/sessions_viewed.php
mod/chat/lib.php
mod/chat/tests/events_test.php
mod/chat/version.php
mod/choice/classes/event/answer_submitted.php
mod/choice/classes/event/answer_updated.php
mod/choice/classes/event/report_viewed.php
mod/choice/report.php
mod/choice/tests/events_test.php
mod/choice/version.php
mod/data/tests/lib_test.php
mod/data/version.php
mod/feedback/classes/event/response_deleted.php
mod/feedback/classes/event/response_submitted.php
mod/feedback/tests/events_test.php
mod/feedback/version.php
mod/folder/classes/event/folder_updated.php
mod/folder/tests/events_test.php
mod/folder/version.php
mod/forum/classes/event/assessable_uploaded.php
mod/forum/tests/externallib_test.php
mod/forum/tests/lib_test.php
mod/forum/version.php
mod/glossary/tests/events_test.php
mod/glossary/version.php
mod/imscp/version.php
mod/label/version.php
mod/lesson/classes/event/essay_assessed.php
mod/lesson/classes/event/essay_attempt_viewed.php
mod/lesson/classes/event/highscore_added.php
mod/lesson/classes/event/highscores_viewed.php
mod/lesson/classes/event/lesson_ended.php
mod/lesson/classes/event/lesson_started.php
mod/lesson/tests/events_test.php
mod/lesson/version.php
mod/lti/version.php
mod/page/version.php
mod/quiz/attemptlib.php
mod/quiz/classes/event/attempt_abandoned.php
mod/quiz/classes/event/attempt_becameoverdue.php
mod/quiz/classes/event/attempt_started.php
mod/quiz/classes/event/attempt_submitted.php
mod/quiz/locallib.php
mod/quiz/tests/events_test.php
mod/quiz/tests/quizdisplayoptions_test.php
mod/quiz/version.php
mod/resource/tests/events_test.php
mod/resource/version.php
mod/scorm/classes/event/attempt_deleted.php
mod/scorm/classes/event/course_module_viewed.php
mod/scorm/classes/event/interactions_viewed.php
mod/scorm/classes/event/report_viewed.php
mod/scorm/classes/event/sco_launched.php
mod/scorm/classes/event/tracks_viewed.php
mod/scorm/classes/event/user_report_viewed.php
mod/scorm/tests/event_test.php
mod/scorm/version.php
mod/survey/version.php
mod/url/version.php
mod/wiki/classes/event/comments_viewed.php
mod/wiki/classes/event/page_created.php
mod/wiki/classes/event/page_deleted.php
mod/wiki/classes/event/page_diff_viewed.php
mod/wiki/classes/event/page_history_viewed.php
mod/wiki/classes/event/page_locks_deleted.php
mod/wiki/classes/event/page_map_viewed.php
mod/wiki/classes/event/page_updated.php
mod/wiki/classes/event/page_version_deleted.php
mod/wiki/classes/event/page_version_restored.php
mod/wiki/classes/event/page_version_viewed.php
mod/wiki/classes/event/page_viewed.php
mod/wiki/tests/events_test.php
mod/wiki/version.php
mod/workshop/classes/event/assessable_uploaded.php
mod/workshop/classes/event/course_module_viewed.php
mod/workshop/version.php
notes/tests/events_test.php
question/behaviour/manualgraded/tests/walkthrough_test.php
question/editlib.php
question/engine/datalib.php
question/engine/tests/helpers.php
question/engine/tests/qubaid_condition_test.php [moved from question/engine/tests/datalib_test.php with 98% similarity]
question/engine/tests/question_engine_data_mapper_test.php [new file with mode: 0644]
question/format/xml/format.php
question/format/xml/tests/xmlformat_test.php
question/question.php
question/type/edit_question_form.php
question/type/essay/tests/helper.php
question/type/multianswer/tests/helper.php
question/type/questiontypebase.php
report/loglive/index.php
report/participation/index.php
repository/tests/behat/cancel_add_file.feature
repository/tests/behat/create_folders.feature
repository/tests/behat/create_shortcut.feature
repository/tests/behat/delete_files.feature
repository/tests/behat/overwrite_file.feature
repository/tests/behat/zip_and_unzip.feature
repository/tests/repositorylib_test.php
theme/base/style/core.css
theme/base/style/course.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
version.php
webservice/tests/events_test.php

index d8b2b51..b07e92f 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically.';
+$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
 $string['allavailablesteps'] = 'All the available steps definitions';
+$string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
+$string['errorcomposer'] = 'Composer dependencies are not installed.';
+$string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
+$string['errorsetconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot need to be set in config.php.';
+$string['erroruniqueconfig'] = '$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot values need to be different than $CFG->dataroot, $CFG->prefix, $CFG->wwwroot, $CFG->phpunit_dataroot and $CFG->phpunit_prefix values.';
 $string['giveninfo'] = 'Given. Processes to set up the environment';
 $string['infoheading'] = 'Info';
 $string['installinfo'] = 'Read {$a} for installation and tests execution info';
-$string['moreinfoin'] = 'More info in {$a}';
 $string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
 $string['newtestsinfo'] = 'Read {$a} for info about how to write new tests';
 $string['nostepsdefinitions'] = 'There aren\'t steps definitions matching this filters';
 $string['pluginname'] = 'Acceptance testing';
-$string['runclitool'] = 'To list the steps definitions you need to run the Behat CLI tool to create the $CFG->behat_dataroot directory. Go to your moodle dirroot and run "{$a}"';
 $string['stepsdefinitionscomponent'] = 'Area';
 $string['stepsdefinitionscontains'] = 'Contains';
 $string['stepsdefinitionsfilters'] = 'Steps definitions';
@@ -41,6 +44,7 @@ $string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected o
 $string['unknownexceptioninfo'] = 'There was a problem with Selenium or your browser. Please ensure you are using the latest version of Selenium. Error:';
 $string['viewsteps'] = 'Filter';
 $string['wheninfo'] = 'When. Actions that provokes an event';
-$string['wrongbehatsetup'] = 'Something is wrong with behat setup, ensure:<ul>
-<li>You ran "php admin/tool/behat/cli/init.php" from your moodle root directory</li>
-<li>vendor/bin/behat file has execution permissions</li></ul>';
+$string['wrongbehatsetup'] = 'Something is wrong with the behat setup and so step definitions cannot be listed: <b>{$a->errormsg}</b><br/><br/>Please check:<ul>
+<li>$CFG->behat_dataroot, $CFG->behat_prefix and $CFG->behat_wwwroot are set in config.php with different values from $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.</li>
+<li>You ran "{$a->behatinit}" from your Moodle root directory.</li>
+<li>Dependencies are installed in vendor/ and {$a->behatcommand} file has execution permissions.</li></ul>';
index 324241b..cc5b4ab 100644 (file)
@@ -45,37 +45,7 @@ class tool_behat_renderer extends plugin_renderer_base {
      */
     public function render_stepsdefinitions($stepsdefinitions, $form) {
 
-        $title = get_string('pluginname', 'tool_behat');
-
-        // Header.
-        $html = $this->output->header();
-        $html .= $this->output->heading($title);
-
-        // Info.
-        $installurl = behat_command::DOCS_URL . '#Installation';
-        $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
-        $writetestsurl = behat_command::DOCS_URL . '#Writting_features';
-        $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
-        $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
-        $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
-        $infos = array(
-            get_string('installinfo', 'tool_behat', $installlink),
-            get_string('newtestsinfo', 'tool_behat', $writetestslink),
-            get_string('newstepsinfo', 'tool_behat', $writestepslink)
-        );
-
-        // List of steps.
-        $html .= $this->output->box_start();
-        $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
-        $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
-        $html .= html_writer::empty_tag('div');
-        $html .= html_writer::empty_tag('ul');
-        $html .= html_writer::empty_tag('li');
-        $html .= implode(html_writer::end_tag('li') . html_writer::empty_tag('li'), $infos);
-        $html .= html_writer::end_tag('li');
-        $html .= html_writer::end_tag('ul');
-        $html .= html_writer::end_tag('div');
-        $html .= $this->output->box_end();
+        $html = $this->generic_info();
 
         // Form.
         ob_start();
@@ -123,4 +93,75 @@ class tool_behat_renderer extends plugin_renderer_base {
 
         return $html;
     }
+
+    /**
+     * Renders an error message adding the generic info about the tool purpose and setup.
+     *
+     * @param string $msg The error message
+     * @return string HTML
+     */
+    public function render_error($msg) {
+
+        $html = $this->generic_info();
+
+        $a = new stdClass();
+        $a->errormsg = $msg;
+        $a->behatcommand = behat_command::get_behat_command();
+        $a->behatinit = 'php admin' . DIRECTORY_SEPARATOR . 'tool' . DIRECTORY_SEPARATOR .
+            'behat' . DIRECTORY_SEPARATOR . 'cli' . DIRECTORY_SEPARATOR . 'init.php';
+
+        $msg = get_string('wrongbehatsetup', 'tool_behat', $a);
+
+        // Error box including generic error string + specific error msg.
+        $html .= $this->output->box_start('box errorbox');
+        $html .= html_writer::tag('div', $msg);
+        $html .= $this->output->box_end();
+
+        $html .= $this->output->footer();
+
+        return $html;
+    }
+
+    /**
+     * Generic info about the tool.
+     *
+     * @return string
+     */
+    protected function generic_info() {
+
+        $title = get_string('pluginname', 'tool_behat');
+
+        // Header.
+        $html = $this->output->header();
+        $html .= $this->output->heading($title);
+
+        // Info.
+        $installurl = behat_command::DOCS_URL . '#Installation';
+        $installlink = html_writer::tag('a', $installurl, array('href' => $installurl, 'target' => '_blank'));
+        $writetestsurl = behat_command::DOCS_URL . '#Writting_features';
+        $writetestslink = html_writer::tag('a', $writetestsurl, array('href' => $writetestsurl, 'target' => '_blank'));
+        $writestepsurl = behat_command::DOCS_URL . '#Adding_steps_definitions';
+        $writestepslink = html_writer::tag('a', $writestepsurl, array('href' => $writestepsurl, 'target' => '_blank'));
+        $infos = array(
+            get_string('installinfo', 'tool_behat', $installlink),
+            get_string('newtestsinfo', 'tool_behat', $writetestslink),
+            get_string('newstepsinfo', 'tool_behat', $writestepslink)
+        );
+
+        // List of steps.
+        $html .= $this->output->box_start();
+        $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
+        $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
+        $html .= html_writer::empty_tag('div');
+        $html .= html_writer::empty_tag('ul');
+        $html .= html_writer::empty_tag('li');
+        $html .= implode(html_writer::end_tag('li') . html_writer::empty_tag('li'), $infos);
+        $html .= html_writer::end_tag('li');
+        $html .= html_writer::end_tag('ul');
+        $html .= html_writer::end_tag('div');
+        $html .= $this->output->box_end();
+
+        return $html;
+    }
+
 }
index 12b96cd..223011a 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2013110501;
+$plugin->version   = 2014012900;
 $plugin->requires  = 2013110500; // Requires Moodle 2.5.
 $plugin->component = 'tool_behat';
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 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 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 7cb8e88..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,8 +111,9 @@ 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);
     }
 
     /**
@@ -152,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;
@@ -176,7 +177,8 @@ 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 0109fd4..b4af764 100644 (file)
@@ -183,6 +183,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($arr, $event);
         $this->assertEquals("blog_entry_added", $event->get_legacy_eventname());
         $this->assertEventLegacyData($blog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -215,6 +216,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($blog, $event);
         $arr = array (SITEID, 'blog', 'update', 'index.php?userid=' . $this->userid . '&entryid=' . $blog->id, $blog->subject);
         $this->assertEventLegacyLogData($arr, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -249,6 +251,7 @@ class core_bloglib_testcase extends advanced_testcase {
                 $blog->id);
         $this->assertEventLegacyLogData($arr, $event);
         $this->assertEventLegacyData($blog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
 
@@ -301,6 +304,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $arr = array(SITEID, 'blog', 'add association', 'index.php?userid=' . $this->userid . '&entryid=' . $blog->id,
                      $blog->subject, $this->cmid, $this->userid);
         $this->assertEventLegacyLogData($arr, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -381,6 +385,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEquals($url, $event->get_url());
         $arr = array(SITEID, 'blog', 'view', $url2->out(), 'view blog entry');
         $this->assertEventLegacyLogData($arr, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -416,6 +421,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEquals($this->postid, $event->other['itemid']);
         $url = new moodle_url('/blog/index.php', array('entryid' => $this->postid));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -452,6 +458,7 @@ class core_bloglib_testcase extends advanced_testcase {
         $this->assertEquals($this->postid, $event->other['itemid']);
         $url = new moodle_url('/blog/index.php', array('entryid' => $this->postid));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
     }
 }
 
index 994aef2..c59af74 100644 (file)
@@ -1742,7 +1742,7 @@ class cache_session extends cache {
      * Purges the session cache of all data belonging to the current user.
      */
     public function purge_current_user() {
-        $keys = $this->get_store()->find_all($this->get_key_prefix());
+        $keys = $this->get_store()->find_by_prefix($this->get_key_prefix());
         $this->get_store()->delete_many($keys);
     }
 
index 64f40c7..6392b84 100644 (file)
@@ -106,6 +106,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($cohort->contextid, $event->contextid);
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
         $this->assertEventLegacyData($cohort, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_update_cohort() {
@@ -176,6 +177,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($updatedcohort->contextid, $event->contextid);
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $id));
         $this->assertEventLegacyData($cohort, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_delete_cohort() {
@@ -213,6 +215,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($cohort->id, $event->objectid);
         $this->assertEquals($cohort, $event->get_record_snapshot('cohort', $cohort->id));
         $this->assertEventLegacyData($cohort, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_delete_category() {
@@ -270,6 +273,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($user->id, $event->relateduserid);
         $this->assertEquals($USER->id, $event->userid);
         $this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_remove_member() {
@@ -313,6 +317,7 @@ class core_cohort_cohortlib_testcase extends advanced_testcase {
         $this->assertEquals($user->id, $event->relateduserid);
         $this->assertEquals($USER->id, $event->userid);
         $this->assertEventLegacyData((object) array('cohortid' => $cohort->id, 'userid' => $user->id), $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_cohort_is_member() {
index 6733547..b125d6a 100644 (file)
@@ -470,6 +470,33 @@ $CFG->admin = 'admin';
 // will be sent to supportemail.
 //      $CFG->supportuserid = -20;
 //
+// Moodle 2.7 introduces a locking api for critical tasks (e.g. cron).
+// The default locking system to use is DB locking for MySQL and Postgres, and File
+// locking for Oracle and SQLServer. If $CFG->preventfilelocking is set, then the default
+// will always be DB locking. It can be manually set to one of the lock
+// factory classes listed below, or one of your own custom classes implementing the
+// \core\lock\lock_factory interface.
+//
+//      $CFG->lock_factory = "auto";
+//
+// The list of available lock factories is:
+//
+// "\\core\\lock\\file_lock_factory" - File locking
+//      Uses lock files stored by default in the dataroot. Whether this
+//      works on clusters depends on the file system used for the dataroot.
+//
+// "\\core\\lock\\db_row_lock_factory" - DB locking based on table rows.
+//
+// "\\core\\lock\\postgres_lock_factory" - DB locking based on postgres advisory locks.
+//
+// "\\core\\lock\\mysql_lock_factory" - DB locking based on mysql lock functions.
+//
+// Settings used by the lock factories
+//
+// Location for lock files used by the File locking factory. This must exist
+// on a shared file system that supports locking.
+//      $CFG->lock_file_root = $CFG->dataroot . '/lock';
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index 00b19d4..5207f10 100644 (file)
@@ -102,31 +102,32 @@ M.course_dndupload = {
      * is available (or to explain why it is not available)
      */
     add_status_div: function() {
-        var coursecontents = document.getElementById(this.pagecontentid);
+        var Y = this.Y,
+            coursecontents = Y.one('#' + this.pagecontentid),
+            div,
+            handlefile = (this.handlers.filehandlers.length > 0),
+            handletext = false,
+            handlelink = false,
+            i = 0,
+            styletop,
+            styletopunit;
+
         if (!coursecontents) {
             return;
         }
 
-        var div = document.createElement('div');
-        div.id = 'dndupload-status';
-        div.style.opacity = 0.0;
-        coursecontents.insertBefore(div, coursecontents.firstChild);
+        div = Y.Node.create('<div id="dndupload-status"></div>').setStyle('opacity', '0.0');
+        coursecontents.insert(div, 0);
 
-        var Y = this.Y;
-        div = Y.one(div);
-        var handlefile = (this.handlers.filehandlers.length > 0);
-        var handletext = false;
-        var handlelink = false;
-        var i;
-        for (i=0; i<this.handlers.types.length; i++) {
+        for (i = 0; i < this.handlers.types.length; i++) {
             switch (this.handlers.types[i].identifier) {
-            case 'text':
-            case 'text/html':
-                handletext = true;
-                break;
-            case 'url':
-                handlelink = true;
-                break;
+                case 'text':
+                case 'text/html':
+                    handletext = true;
+                    break;
+                case 'url':
+                    handlelink = true;
+                    break;
             }
         }
         $msgident = 'dndworking';
@@ -141,16 +142,20 @@ M.course_dndupload = {
         }
         div.setContent(M.util.get_string($msgident, 'moodle'));
 
+        styletop = div.getStyle('top') || '0px';
+        styletopunit = styletop.replace(/^\d+/, '');
+        styletop = parseInt(styletop.replace(/\D*$/, ''), 10);
+
         var fadeanim = new Y.Anim({
             node: '#dndupload-status',
             from: {
                 opacity: 0.0,
-                top: '-30px'
+                top: (styletop - 30).toString() + styletopunit
             },
 
             to: {
                 opacity: 1.0,
-                top: '0px'
+                top: styletop.toString() + styletopunit
             },
             duration: 0.5
         });
index 2dcbce2..1de01c9 100644 (file)
@@ -321,6 +321,7 @@ class course_edit_form extends moodleform {
                     $options[$grouping->id] = format_string($grouping->name);
                 }
             }
+            core_collator::asort($options);
             $gr_el =& $mform->getElement('defaultgroupingid');
             $gr_el->load($options);
         }
index 7663e93..7161284 100644 (file)
@@ -67,13 +67,14 @@ class editsection_form extends moodleform {
             // Grouping conditions - only if grouping is enabled at site level
             if (!empty($CFG->enablegroupmembersonly)) {
                 $options = array();
-                $options[0] = get_string('none');
                 if ($groupings = $DB->get_records('groupings', array('courseid' => $course->id))) {
                     foreach ($groupings as $grouping) {
                         $options[$grouping->id] = format_string(
                                 $grouping->name, true, array('context' => $context));
                     }
                 }
+                core_collator::asort($options);
+                $options = array(0 => get_string('none')) + $options;
                 $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options);
                 $mform->addHelpButton('groupingid', 'groupingsection', 'group');
             }
index b398be4..9b54833 100644 (file)
@@ -504,12 +504,13 @@ abstract class moodleform_mod extends moodleform {
         if ($this->_features->groupings or $this->_features->groupmembersonly) {
             //groupings selector - used for normal grouping mode or also when restricting access with groupmembersonly
             $options = array();
-            $options[0] = get_string('none');
             if ($groupings = $DB->get_records('groupings', array('courseid'=>$COURSE->id))) {
                 foreach ($groupings as $grouping) {
                     $options[$grouping->id] = format_string($grouping->name);
                 }
             }
+            core_collator::asort($options);
+            $options = array(0 => get_string('none')) + $options;
             $mform->addElement('select', 'groupingid', get_string('grouping', 'group'), $options);
             $mform->addHelpButton('groupingid', 'grouping', 'group');
         }
index 3f8d5b8..f2f1a69 100644 (file)
@@ -26,8 +26,9 @@
 defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
-require_once($CFG->dirroot.'/course/lib.php');
-require_once($CFG->dirroot.'/course/tests/fixtures/course_capability_assignment.php');
+require_once($CFG->dirroot . '/course/lib.php');
+require_once($CFG->dirroot . '/course/tests/fixtures/course_capability_assignment.php');
+require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php');
 
 class core_course_courselib_testcase extends advanced_testcase {
 
@@ -1400,8 +1401,10 @@ class core_course_courselib_testcase extends advanced_testcase {
         // Catch the events.
         $sink = $this->redirectEvents();
 
-        // Create the course.
-        $course = $this->getDataGenerator()->create_course();
+        // Create the course with an id number which is used later when generating a course via the imsenterprise plugin.
+        $data = new stdClass();
+        $data->idnumber = 'idnumber';
+        $course = $this->getDataGenerator()->create_course($data);
         // Get course from DB for comparison.
         $course = $DB->get_record('course', array('id' => $course->id));
 
@@ -1420,6 +1423,33 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($course, $event);
         $expectedlog = array(SITEID, 'course', 'new', 'view.php?id=' . $course->id, $course->fullname . ' (ID ' . $course->id . ')');
         $this->assertEventLegacyLogData($expectedlog, $event);
+
+        // Now we want to trigger creating a course via the imsenterprise.
+        // Delete the course we created earlier, as we want the imsenterprise plugin to create this.
+        // We do not want print out any of the text this function generates while doing this, which is why
+        // we are using ob_start() and ob_end_clean().
+        ob_start();
+        delete_course($course);
+        ob_end_clean();
+
+        // Create the XML file we want to use.
+        $imstestcase = new enrol_imsenterprise_testcase();
+        $imstestcase->imsplugin = enrol_get_plugin('imsenterprise');
+        $imstestcase->set_test_config();
+        $imstestcase->set_xml_file(false, array($course));
+
+        // Capture the event.
+        $sink = $this->redirectEvents();
+        $imstestcase->imsplugin->cron();
+        $events = $sink->get_events();
+        $sink->close();
+        $event = $events[0];
+
+        // Validate the event triggered is \core\event\course_created. There is no need to validate the other values
+        // as they have already been validated in the previous steps. Here we only want to make sure that when the
+        // imsenterprise plugin creates a course an event is triggered.
+        $this->assertInstanceOf('\core\event\course_created', $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1498,6 +1528,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($movedcoursehidden, $event);
         $expectedlog = array($movedcoursehidden->id, 'course', 'move', 'edit.php?id=' . $movedcoursehidden->id, $movedcoursehidden->id);
         $this->assertEventLegacyLogData($expectedlog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1543,6 +1574,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($course, $event);
         $expectedlog = array(SITEID, 'course', 'delete', 'view.php?id=' . $course->id, $course->fullname . '(ID ' . $course->id . ')');
         $this->assertEventLegacyLogData($expectedlog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1584,6 +1616,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $course->context = $coursecontext;
         $course->options = array();
         $this->assertEventLegacyData($course, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1646,6 +1679,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyData($category2, $event);
         $expectedlog = array(SITEID, 'category', 'delete', 'index.php', $category2->name . '(ID ' . $category2->id . ')');
         $this->assertEventLegacyLogData($expectedlog, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -1711,6 +1745,7 @@ class core_course_courselib_testcase extends advanced_testcase {
             'samesite' => $rc->is_samesite()
         );
         $this->assertEventLegacyData($legacydata, $event);
+        $this->assertEventContextNotUsed($event);
 
         // Destroy the resource controller since we are done using it.
         $rc->destroy();
@@ -1765,6 +1800,7 @@ class core_course_courselib_testcase extends advanced_testcase {
         $sectionnum = $section->section;
         $expectedlegacydata = array($course->id, "course", "editsection", 'editsection.php?id=' . $id, $sectionnum);
         $this->assertEventLegacyLogData($expectedlegacydata, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_course_integrity_check() {
@@ -1924,6 +1960,7 @@ class core_course_courselib_testcase extends advanced_testcase {
 
         $arr = array($cm->course, "course", "add mod", "../mod/assign/view.php?id=$mod->id", "assign $cm->instance");
         $this->assertEventLegacyLogData($arr, $event);
+        $this->assertEventContextNotUsed($event);
 
     }
 
@@ -2029,7 +2066,7 @@ class core_course_courselib_testcase extends advanced_testcase {
 
         $arr = array($cm->course, "course", "update mod", "../mod/forum/view.php?id=$mod->id", "forum $cm->instance");
         $this->assertEventLegacyLogData($arr, $event);
-
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
index 34a0442..6db0851 100644 (file)
@@ -377,24 +377,11 @@ class enrol_imsenterprise_plugin extends enrol_plugin {
                         } else {
                             $course->category = $this->get_default_category_id();
                         }
-                        $course->timecreated = time();
                         $course->startdate = time();
                         // Choose a sort order that puts us at the start of the list!
                         $course->sortorder = 0;
-                        $courseid = $DB->insert_record('course', $course);
 
-                        // Setup default enrolment plugins.
-                        $course->id = $courseid;
-                        enrol_course_updated(true, $course, null);
-
-                        // Setup the blocks.
-                        $course = $DB->get_record('course', array('id' => $courseid));
-                        blocks_add_default_course_blocks($course);
-
-                        // Create default 0-section.
-                        course_create_sections_if_missing($course, 0);
-
-                        add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
+                        $course = create_course($course);
 
                         $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
                     }
index 390f276..a86006e 100644 (file)
@@ -42,7 +42,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
     /**
      * @var $imsplugin enrol_imsenterprise_plugin IMS plugin instance.
      */
-    protected $imsplugin;
+    public $imsplugin;
 
     /**
      * Setup required for all tests.
@@ -254,7 +254,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
     /**
      * Sets the plugin configuration for testing
      */
-    protected function set_test_config() {
+    public function set_test_config() {
         $this->imsplugin->set_config('mailadmins', false);
         $this->imsplugin->set_config('prev_path', '');
         $this->imsplugin->set_config('createnewusers', true);
@@ -268,7 +268,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
      * @param bool|array $users false or array of users StdClass
      * @param bool|array $courses false or of courses StdClass
      */
-    protected function set_xml_file($users = false, $courses = false) {
+    public function set_xml_file($users = false, $courses = false) {
 
         $xmlcontent = '<enterprise>';
 
index 7d91e4b..6c86e6f 100644 (file)
@@ -123,7 +123,6 @@ if ($canenrol && optional_param('add', false, PARAM_BOOL) && confirm_sesskey())
                 $timeend = $timestart + $extendperiod;
             }
             $enrol_manual->enrol_user($instance, $adduser->id, $roleid, $timestart, $timeend);
-            add_to_log($course->id, 'course', 'enrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
         }
 
         $potentialuserselector->invalidate_selected_users();
@@ -139,7 +138,6 @@ if ($canunenrol && optional_param('remove', false, PARAM_BOOL) && confirm_sesske
     if (!empty($userstounassign)) {
         foreach($userstounassign as $removeuser) {
             $enrol_manual->unenrol_user($instance, $removeuser->id);
-            add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
         }
 
         $potentialuserselector->invalidate_selected_users();
index c7304fb..9dd3bc5 100644 (file)
@@ -49,7 +49,7 @@ $PAGE->set_title($plugin->get_instance_name($instance));
 
 if ($confirm and confirm_sesskey()) {
     $plugin->unenrol_user($instance, $USER->id);
-    add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //TODO: there should be userid somewhere!
+
     redirect(new moodle_url('/index.php'));
 }
 
index 05cb522..a23c148 100644 (file)
@@ -474,6 +474,7 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         $expectedlegacyeventdata->enrol = 'meta';
         $expectedlegacyeventdata->courseid = $course2->id;
         $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -507,6 +508,7 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         $this->assertEquals(0, $DB->count_records('user_enrolments'));
         $this->assertInstanceOf('\core\event\user_enrolment_deleted', $event);
         $this->assertEquals('user_unenrolled', $event->get_legacy_eventname());
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -546,5 +548,6 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         $expectedlegacyeventdata->enrol = 'meta';
         $expectedlegacyeventdata->courseid = $course2->id;
         $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+        $this->assertEventContextNotUsed($event);
     }
 }
index 8fd16d2..61155c8 100644 (file)
@@ -49,7 +49,7 @@ $PAGE->set_title($plugin->get_instance_name($instance));
 
 if ($confirm and confirm_sesskey()) {
     $plugin->unenrol_user($instance, $USER->id);
-    add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //there should be userid somewhere!
+
     redirect(new moodle_url('/index.php'));
 }
 
index d64281a..edc114f 100644 (file)
@@ -195,7 +195,6 @@ class enrol_self_plugin extends enrol_plugin {
         }
 
         $this->enrol_user($instance, $USER->id, $instance->roleid, $timestart, $timeend);
-        add_to_log($instance->courseid, 'course', 'enrol', '../enrol/users.php?id='.$instance->courseid, $instance->courseid); //TODO: There should be userid somewhere!
 
         if ($instance->password and $instance->customint1 and $data->enrolpassword !== $instance->password) {
             // It must be a group enrolment, let's assign group too.
index 7e7d269..2da62ee 100644 (file)
@@ -49,7 +49,7 @@ $PAGE->set_title($plugin->get_instance_name($instance));
 
 if ($confirm and confirm_sesskey()) {
     $plugin->unenrol_user($instance, $USER->id);
-    add_to_log($course->id, 'course', 'unenrol', '../enrol/users.php?id='.$course->id, $course->id); //TODO: there should be userid somewhere!
+
     redirect(new moodle_url('/index.php'));
 }
 
index fcf9cad..61a4392 100644 (file)
@@ -310,10 +310,56 @@ class core_enrollib_testcase extends advanced_testcase {
         $dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $admin->id));
         $this->assertInstanceOf('\core\event\user_enrolment_created', $event);
         $this->assertEquals($dbuserenrolled->id, $event->objectid);
+        $this->assertEquals(context_course::instance($course1->id), $event->get_context());
         $this->assertEquals('user_enrolled', $event->get_legacy_eventname());
         $expectedlegacyeventdata = $dbuserenrolled;
         $expectedlegacyeventdata->enrol = $manual->get_name();
         $expectedlegacyeventdata->courseid = $course1->id;
         $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+        $expected = array($course1->id, 'course', 'enrol', '../enrol/users.php?id=' . $course1->id, $course1->id);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
+    }
+
+    /**
+     * Test user_enrolment_deleted event.
+     */
+    public function test_user_enrolment_deleted_event() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $manualplugin = enrol_get_plugin('manual');
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $student = $DB->get_record('role', array('shortname' => 'student'));
+
+        $enrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'), '*', MUST_EXIST);
+
+        // Enrol user.
+        $manualplugin->enrol_user($enrol, $user->id, $student->id);
+
+        // Get the user enrolment information, used to validate legacy event data.
+        $dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $user->id));
+
+        // Unenrol user and capture event.
+        $sink = $this->redirectEvents();
+        $manualplugin->unenrol_user($enrol, $user->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
+        // Validate the event.
+        $this->assertInstanceOf('\core\event\user_enrolment_deleted', $event);
+        $this->assertEquals(context_course::instance($course->id), $event->get_context());
+        $this->assertEquals('user_unenrolled', $event->get_legacy_eventname());
+        $expectedlegacyeventdata = $dbuserenrolled;
+        $expectedlegacyeventdata->enrol = $manualplugin->get_name();
+        $expectedlegacyeventdata->courseid = $course->id;
+        $expectedlegacyeventdata->lastenrol = true;
+        $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+        $expected = array($course->id, 'course', 'unenrol', '../enrol/users.php?id=' . $course->id, $course->id);
+        $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 }
index 36383fb..bf34e14 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';
@@ -301,7 +301,7 @@ class core_files_externallib_testcase extends advanced_testcase {
         $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 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 ec8b094..063397e 100644 (file)
@@ -68,9 +68,13 @@ echo $OUTPUT->heading($strgroupings);
 $data = array();
 if ($groupings = $DB->get_records('groupings', array('courseid'=>$course->id), 'name')) {
     $canchangeidnumber = has_capability('moodle/course:changeidnumber', $context);
+    foreach ($groupings as $gid => $grouping) {
+        $groupings[$gid]->formattedname = format_string($grouping->name, true, array('context' => $context));
+    }
+    core_collator::asort_objects_by_property($groupings, 'formattedname');
     foreach($groupings as $grouping) {
         $line = array();
-        $line[0] = format_string($grouping->name);
+        $line[0] = $grouping->formattedname;
 
         if ($groups = groups_get_all_groups($courseid, 0, $grouping->id)) {
             $groupnames = array();
index b3ae333..967f2c1 100644 (file)
@@ -62,8 +62,12 @@ $strfiltergroups     = get_string('filtergroups', 'group');
 $strnogroups         = get_string('nogroups', 'group');
 $strdescription      = get_string('description');
 
-// Get all groupings
+// Get all groupings and sort them by formatted name.
 $groupings = $DB->get_records('groupings', array('courseid'=>$courseid), 'name');
+foreach ($groupings as $gid => $grouping) {
+    $groupings[$gid]->formattedname = format_string($grouping->name, true, array('context' => $context));
+}
+core_collator::asort_objects_by_property($groupings, 'formattedname');
 $members = array();
 foreach ($groupings as $grouping) {
     $members[$grouping->id] = array();
@@ -136,7 +140,7 @@ echo $strfiltergroups;
 $options = array();
 $options[0] = get_string('all');
 foreach ($groupings as $grouping) {
-    $options[$grouping->id] = strip_tags(format_string($grouping->name));
+    $options[$grouping->id] = strip_tags($grouping->formattedname);
 }
 $popupurl = new moodle_url($rooturl.'&group='.$groupid);
 $select = new single_select($popupurl, 'grouping', $options, $groupingid, array());
@@ -199,7 +203,7 @@ foreach ($members as $gpgid=>$groupdata) {
     if ($gpgid < 0) {
         echo $OUTPUT->heading($strnotingrouping, 3);
     } else {
-        echo $OUTPUT->heading(format_string($groupings[$gpgid]->name), 3);
+        echo $OUTPUT->heading($groupings[$gpgid]->formattedname, 3);
         $description = file_rewrite_pluginfile_urls($groupings[$gpgid]->description, 'pluginfile.php', $context->id, 'grouping', 'description', $gpgid);
         $options = new stdClass;
         $options->noclean = true;
index 0388de7..fed5364 100644 (file)
@@ -46,7 +46,8 @@ $string['dmlexceptiononinstall'] = '<p>Une erreur de base de données est surven
 $string['downloadedfilecheckfailed'] = 'La vérification du fichier téléchargé à échoué';
 $string['invalidmd5'] = 'Le code de contrôle md5 n\'est pas valide';
 $string['missingrequiredfield'] = 'Un champ obligatoire n\'est pas renseigné';
-$string['remotedownloaderror'] = 'Le téléchargement de composants sur votre serveur a échoué. Veuillez vérifier les réglages de proxy. L\'extension cURL de PHP est vivement recommandée.<br /><br />Vous devez télécharger manuellement le fichier <a href="{$a->url}">{$a->url}</a>, le copier sur votre serveur à l\'emplacement « {$a->dest} » et le décompresser à cet endroit';
+$string['remotedownloaderror'] = '<p>Le téléchargement du composant sur votre serveur a échoué. Veuillez vérifier les réglages de proxy. L\'extension cURL de PHP est vivement recommandée.</p>
+<p>Vous devez télécharger manuellement le fichier <a href="{$a->url}">{$a->url}</a>, le copier sur votre serveur à l\'emplacement « {$a->dest} » et le décompresser à cet endroit.</p>';
 $string['wrongdestpath'] = 'Chemin de destination incorrect';
 $string['wrongsourcebase'] = 'Adresse URL de base de la source incorrecte';
 $string['wrongzipfilename'] = 'Nom de fichier ZIP incorrect';
index 76a4d25..a7060bd 100644 (file)
@@ -114,8 +114,7 @@ class behat_command {
     /**
      * Checks if behat is set up and working
      *
-     * Uses notice() instead of behat_error() because is
-     * also called from web interface
+     * Notifies failures both from CLI and web interface.
      *
      * It checks behat dependencies have been installed and runs
      * the behat help command to ensure it works as expected
@@ -125,24 +124,11 @@ class behat_command {
     public static function behat_setup_problem() {
         global $CFG;
 
-        $clibehaterrorstr = "Behat dependencies not installed. Ensure you ran the composer installer. " . self::DOCS_URL . "#Installation\n";
-
         // Moodle setting.
         if (!self::are_behat_dependencies_installed()) {
 
-
-            // With HTML.
-            if (!CLI_SCRIPT) {
-
-                $msg = get_string('wrongbehatsetup', 'tool_behat');
-                $docslink = self::DOCS_URL . '#Installation';
-                $docslink = html_writer::tag('a', $docslink, array('href' => $docslink, 'target' => '_blank'));
-                $msg .= get_string('moreinfoin', 'tool_behat', $docslink);
-            } else {
-                $msg = $clibehaterrorstr;
-            }
-
-            self::output_msg($msg);
+            // Returning composer error code to avoid conflicts with behat and moodle error codes.
+            self::output_msg(get_string('errorcomposer', 'tool_behat'));
             return BEHAT_EXITCODE_COMPOSER;
         }
 
@@ -150,22 +136,40 @@ class behat_command {
         list($output, $code) = self::run(' --help');
 
         if ($code != 0) {
+
             // Returning composer error code to avoid conflicts with behat and moodle error codes.
-            if (!CLI_SCRIPT) {
-                $msg = get_string('wrongbehatsetup', 'tool_behat');
-            } else {
-                $msg = $clibehaterrorstr;
-            }
-            self::output_msg($msg);
+            self::output_msg(get_string('errorbehatcommand', 'tool_behat', self::get_behat_command()));
             return BEHAT_EXITCODE_COMPOSER;
         }
 
+        // No empty values.
+        if (empty($CFG->behat_dataroot) || empty($CFG->behat_prefix) || empty($CFG->behat_wwwroot)) {
+            self::output_msg(get_string('errorsetconfig', 'tool_behat'));
+            return BEHAT_EXITCODE_CONFIG;
+
+        }
+
+        // Not repeated values.
+        // We only need to check this when the behat site is not running as
+        // at this point, when it is running, all $CFG->behat_* vars have
+        // already been copied to $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
+        if (!defined('BEHAT_SITE_RUNNING') &&
+                ($CFG->behat_prefix == $CFG->prefix ||
+                $CFG->behat_dataroot == $CFG->dataroot ||
+                $CFG->behat_wwwroot == $CFG->wwwroot ||
+                (!empty($CFG->phpunit_prefix) && $CFG->phpunit_prefix == $CFG->behat_prefix) ||
+                (!empty($CFG->phpunit_dataroot) && $CFG->phpunit_dataroot == $CFG->behat_dataroot)
+                )) {
+            self::output_msg(get_string('erroruniqueconfig', 'tool_behat'));
+            return BEHAT_EXITCODE_CONFIG;
+        }
+
         // Checking behat dataroot existence otherwise echo about admin/tool/behat/cli/init.php.
         if (!empty($CFG->behat_dataroot)) {
             $CFG->behat_dataroot = realpath($CFG->behat_dataroot);
         }
         if (empty($CFG->behat_dataroot) || !is_dir($CFG->behat_dataroot) || !is_writable($CFG->behat_dataroot)) {
-            self::output_msg(get_string('runclitool', 'tool_behat', 'php admin/tool/behat/cli/init.php'));
+            self::output_msg(get_string('errordataroot', 'tool_behat'));
             return BEHAT_EXITCODE_CONFIG;
         }
 
@@ -193,13 +197,25 @@ class behat_command {
      * @return void
      */
     protected static function output_msg($msg) {
+        global $CFG, $PAGE;
 
+        // If we are using the web interface we want pretty messages.
         if (!CLI_SCRIPT) {
-            // General info about the tool purpose.
-            $msg = get_string('aim', 'tool_behat') . '<br /><br />' . $msg;
-            notice($msg);
+
+            $renderer = $PAGE->get_renderer('tool_behat');
+            echo $renderer->render_error($msg);
+
+            // Stopping execution.
+            exit(1);
+
         } else {
-            echo $msg;
+
+            // We continue execution after this.
+            $clibehaterrorstr = "Ensure you set \$CFG->behat_* vars in config.php " .
+                "and you ran admin/tool/behat/cli/init.php.\n" .
+                "More info in " . self::DOCS_URL . "#Installation\n\n";
+
+            echo 'Error: ' . $msg . "\n\n" . $clibehaterrorstr;
         }
     }
 
index 5773d12..4f100ac 100644 (file)
@@ -58,6 +58,7 @@ class behat_form_editor extends behat_form_field {
                 if ($editorid = $this->get_editor_id()) {
 
                     // Set the value to the iframe and save it to the textarea.
+                    $value = str_replace('"', '\"', $value);
                     $this->session->executeScript('
                         tinyMCE.get("'.$editorid.'").setContent("' . $value . '");
                         tinyMCE.get("'.$editorid.'").save();
index 8db4579..284befd 100644 (file)
@@ -114,10 +114,22 @@ class behat_form_field {
         // Textareas are considered text based elements.
         $tagname = strtolower($this->field->getTagName());
         if ($tagname == 'textarea') {
-            return false;
-        }
 
-        if ($tagname == 'input') {
+            if (!$this->running_javascript()) {
+                return false;
+            }
+
+            // If there is an iframe with $id + _ifr there a TinyMCE editor loaded.
+            $xpath = '//iframe[@id="' . $this->field->getAttribute('id') . '_ifr"]';
+            if (!$this->session->getPage()->find('xpath', $xpath)) {
+
+                // Generic one if it is a normal textarea.
+                return false;
+            }
+
+            $classname = 'behat_form_editor';
+
+        } else if ($tagname == 'input') {
             $type = $this->field->getAttribute('type');
             switch ($type) {
                 case 'text':
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 {
index 8f49ae1..66dc0b9 100644 (file)
@@ -59,7 +59,7 @@ abstract class assessable_submitted extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
-        if (!$this->context->contextlevel === CONTEXT_MODULE) {
+        if (!$this->contextlevel === CONTEXT_MODULE) {
             throw new \coding_exception('Content level must be CONTEXT_MODULE.');
         }
     }
index efdc7ec..83a3361 100644 (file)
@@ -66,7 +66,7 @@ abstract class assessable_uploaded extends \core\event\base {
      * @return void
      */
     protected function validate_data() {
-        if (!$this->context->contextlevel === CONTEXT_MODULE) {
+        if (!$this->contextlevel === CONTEXT_MODULE) {
             throw new \coding_exception('Content level must be CONTEXT_MODULE.');
         } else if (!isset($this->other['pathnamehashes']) || !is_array($this->other['pathnamehashes'])) {
             throw new \coding_exception('pathnamehashes must be set in $other and must be an array.');
index d1cbe32..38a999e 100644 (file)
@@ -355,7 +355,7 @@ abstract class base implements \IteratorAggregate {
         if (isset($this->context)) {
             return $this->context;
         }
-        $this->context = \context::instance_by_id($this->data['contextid'], false);
+        $this->context = \context::instance_by_id($this->data['contextid'], IGNORE_MISSING);
         return $this->context;
     }
 
index f09d4d7..0fa689b 100644 (file)
@@ -69,8 +69,8 @@ class blog_association_created extends \core\event\base {
      * @return string
      */
     public function get_description() {
-        return "Blog association added between entry id $this->other['blogid'] and $this->other['associatetype'] with id
-                $this->other['associateid']";
+        return "Blog association added between entry id {$this->other['blogid']} and {$this->other['associatetype']} with id
+                {$this->other['associateid']}";
     }
 
     /**
index 09935b8..420c4b8 100644 (file)
@@ -79,7 +79,12 @@ abstract class comment_created extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return $this->context->get_url();
+        $context = $this->get_context();
+        if ($context) {
+            return $context->get_url();
+        } else {
+            return null;
+        }
     }
 
     /**
index 2c20348..57084fb 100644 (file)
@@ -79,7 +79,12 @@ abstract class comment_deleted extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return $this->context->get_url();
+        $context = $this->get_context();
+        if ($context) {
+            return $context->get_url();
+        } else {
+            return null;
+        }
     }
 
     /**
index a048d01..fbea09d 100644 (file)
@@ -72,6 +72,11 @@ abstract class comments_viewed extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return $this->context->get_url();
+        $context = $this->get_context();
+        if ($context) {
+            return $context->get_url();
+        } else {
+            return null;
+        }
     }
 }
index cf94035..f83b5d7 100644 (file)
@@ -105,7 +105,7 @@ abstract class course_module_instance_list_viewed extends base{
      * @return void
      */
     protected function validate_data() {
-        if ($this->context->contextlevel !== CONTEXT_COURSE) {
+        if ($this->contextlevel !== CONTEXT_COURSE) {
             throw new \coding_exception('The context must be a course level context.');
         }
     }
index 43e1504..b455b34 100644 (file)
@@ -72,7 +72,7 @@ abstract class course_module_viewed extends base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url("/mod/$this->objecttable/view.php", array('id' => $this->context->instanceid));
+        return new \moodle_url("/mod/$this->objecttable/view.php", array('id' => $this->contextinstanceid));
     }
 
     /**
@@ -81,8 +81,8 @@ abstract class course_module_viewed extends base {
      * @return array|null
      */
     protected function get_legacy_logdata() {
-        return array($this->courseid, $this->objecttable, 'view', 'view.php?id=' . $this->context->instanceid, $this->objectid,
-                     $this->context->instanceid);
+        return array($this->courseid, $this->objecttable, 'view', 'view.php?id=' . $this->contextinstanceid, $this->objectid,
+                     $this->contextinstanceid);
     }
 
     /**
index 23539ca..825eb53 100644 (file)
@@ -97,6 +97,15 @@ class user_enrolment_created extends base {
         return $legacyeventdata;
     }
 
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'course', 'enrol', '../enrol/users.php?id=' . $this->courseid, $this->courseid);
+    }
+
     /**
      * Custom validation.
      *
index e178aab..4c7a905 100644 (file)
@@ -95,6 +95,14 @@ class user_enrolment_deleted extends base {
         return (object)$this->other['userenrolment'];
     }
 
+    /**
+     * Return legacy data for add_to_log().
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array($this->courseid, 'course', 'unenrol', '../enrol/users.php?id=' . $this->courseid, $this->courseid);
+    }
 
     /**
      * Custom validation.
diff --git a/lib/classes/lock/db_record_lock_factory.php b/lib/classes/lock/db_record_lock_factory.php
new file mode 100644 (file)
index 0000000..70cbdb2
--- /dev/null
@@ -0,0 +1,254 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This is a db record locking factory.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * This is a db record locking factory.
+ *
+ * This lock factory uses record locks relying on sql of the form "SET XXX where YYY" and checking if the
+ * value was set. It supports timeouts, autorelease and can work on any DB. The downside - is this
+ * will always be slower than some shared memory type locking function.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class db_record_lock_factory implements lock_factory {
+
+    /** @var \moodle_database $db Hold a reference to the global $DB */
+    protected $db;
+
+    /** @var string $type Used to prefix lock keys */
+    protected $type;
+
+    /** @var array $openlocks - List of held locks - used by auto-release */
+    protected $openlocks = array();
+
+    /**
+     * Is available.
+     * @return boolean - True if this lock type is available in this environment.
+     */
+    public function is_available() {
+        return true;
+    }
+
+    /**
+     * Almighty constructor.
+     * @param string $type - Used to prefix lock keys.
+     */
+    public function __construct($type) {
+        global $DB;
+
+        $this->type = $type;
+        // Save a reference to the global $DB so it will not be released while we still have open locks.
+        $this->db = $DB;
+
+        \core_shutdown_manager::register_function(array($this, 'auto_release'));
+    }
+
+    /**
+     * Return information about the blocking behaviour of the lock type on this platform.
+     * @return boolean - True
+     */
+    public function supports_timeout() {
+        return true;
+    }
+
+    /**
+     * Will this lock type will be automatically released when a process ends.
+     *
+     * @return boolean - True (shutdown handler)
+     */
+    public function supports_auto_release() {
+        return true;
+    }
+
+    /**
+     * Multiple locks for the same resource can be held by a single process.
+     * @return boolean - False - not process specific.
+     */
+    public function supports_recursion() {
+        return false;
+    }
+
+    /**
+     * This function generates a unique token for the lock to use.
+     * It is important that this token is not solely based on time as this could lead
+     * to duplicates in a clustered environment (especially on VMs due to poor time precision).
+     */
+    protected function generate_unique_token() {
+        $uuid = '';
+
+        if (function_exists("uuid_create")) {
+            $context = null;
+            uuid_create($context);
+
+            uuid_make($context, UUID_MAKE_V4);
+            uuid_export($context, UUID_FMT_STR, $uuid);
+        } else {
+            // Fallback uuid generation based on:
+            // "http://www.php.net/manual/en/function.uniqid.php#94959".
+            $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+
+                // 32 bits for "time_low".
+                mt_rand(0, 0xffff), mt_rand(0, 0xffff),
+
+                // 16 bits for "time_mid".
+                mt_rand(0, 0xffff),
+
+                // 16 bits for "time_hi_and_version",
+                // four most significant bits holds version number 4.
+                mt_rand(0, 0x0fff) | 0x4000,
+
+                // 16 bits, 8 bits for "clk_seq_hi_res",
+                // 8 bits for "clk_seq_low",
+                // two most significant bits holds zero and one for variant DCE1.1.
+                mt_rand(0, 0x3fff) | 0x8000,
+
+                // 48 bits for "node".
+                mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff));
+        }
+        return trim($uuid);
+    }
+
+
+    /**
+     * Create and get a lock
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     * @param int $maxlifetime - Unused by this lock type.
+     * @return boolean - true if a lock was obtained.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+
+        $token = $this->generate_unique_token();
+        $now = time();
+        $giveuptime = $now + $timeout;
+        $expires = $now + $maxlifetime;
+
+        if (!$this->db->record_exists('lock_db', array('resourcekey' => $resource))) {
+            $record = new \stdClass();
+            $record->resourcekey = $resource;
+            $result = $this->db->insert_record('lock_db', $record);
+        }
+
+        $params = array('expires' => $expires,
+                        'token' => $token,
+                        'resourcekey' => $resource,
+                        'now' => $now);
+        $sql = 'UPDATE {lock_db}
+                   SET
+                       expires = :expires,
+                       owner = :token
+                 WHERE
+                       resourcekey = :resourcekey AND
+                       (owner IS NULL OR expires < :now)';
+
+        do {
+            $now = time();
+            $params['now'] = $now;
+            $this->db->execute($sql, $params);
+
+            $countparams = array('owner' => $token, 'resourcekey' => $resource);
+            $result = $this->db->count_records('lock_db', $countparams);
+            $locked = $result === 1;
+            if (!$locked) {
+                usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+            }
+            // Try until the giveup time.
+        } while (!$locked && $now < $giveuptime);
+
+        if ($locked) {
+            $this->openlocks[$token] = 1;
+            return new lock($token, $this);
+        }
+
+        return false;
+    }
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @return boolean - true if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock) {
+        $params = array('noexpires' => null,
+                        'token' => $lock->get_key(),
+                        'noowner' => null);
+
+        $sql = 'UPDATE {lock_db}
+                    SET
+                        expires = :noexpires,
+                        owner = :noowner
+                    WHERE
+                        owner = :token';
+        $result = $this->db->execute($sql, $params);
+        if ($result) {
+            unset($this->openlocks[$lock->get_key()]);
+        }
+        return $result;
+    }
+
+    /**
+     * Extend a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+     * @return boolean - true if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        $now = time();
+        $expires = $now + $maxlifetime;
+        $params = array('expires' => $expires,
+                        'token' => $lock->get_key());
+
+        $sql = 'UPDATE {lock_db}
+                    SET
+                        expires = :expires,
+                    WHERE
+                        owner = :token';
+
+        $this->db->execute($sql, $params);
+        $countparams = array('owner' => $lock->get_key());
+        $result = $this->count_records('lock_db', $countparams);
+
+        return $result === 0;
+    }
+
+    /**
+     * Auto release any open locks on shutdown.
+     * This is required, because we may be using persistent DB connections.
+     */
+    public function auto_release() {
+        // Called from the shutdown handler. Must release all open locks.
+        foreach ($this->openlocks as $key => $unused) {
+            $lock = new lock($key, $this);
+            $this->release_lock($lock);
+        }
+    }
+}
diff --git a/lib/classes/lock/file_lock_factory.php b/lib/classes/lock/file_lock_factory.php
new file mode 100644 (file)
index 0000000..5062de8
--- /dev/null
@@ -0,0 +1,200 @@
+<?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/>.
+
+/**
+ * Flock based file locking factory.
+ *
+ * The file lock factory returns file locks locked with the flock function. Works OK, except on some
+ * NFS, exotic shared storage and exotic server OSes (like windows). On windows, a second attempt to get a
+ * lock will block indefinitely instead of timing out.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Flock based file locking factory.
+ *
+ * The file lock factory returns file locks locked with the flock function. Works OK, except on some
+ * NFS, exotic shared storage and exotic server OSes (like windows). On windows, a second attempt to get a
+ * lock will block indefinitely instead of timing out.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class file_lock_factory implements lock_factory {
+
+    /** @var string $type - The type of lock, e.g. cache, cron, session. */
+    protected $type;
+
+    /** @var string $lockdirectory - Full system path to the directory used to store file locks. */
+    protected $lockdirectory;
+
+    /** @var boolean $verbose - If true, debugging info about the owner of the lock will be written to the lock file. */
+    protected $verbose;
+
+    /**
+     * Create this lock factory.
+     *
+     * @param string $type - The type, e.g. cron, cache, session
+     */
+    public function __construct($type) {
+        global $CFG;
+
+        $this->type = $type;
+        if (!isset($CFG->file_lock_root)) {
+            $this->lockdirectory = $CFG->dataroot . '/lock';
+        } else {
+            $this->lockdirectory = $CFG->file_lock_root;
+        }
+        $this->verbose = false;
+        if ($CFG->debugdeveloper) {
+            $this->verbose = true;
+        }
+    }
+
+    /**
+     * Return information about the blocking behaviour of the lock type on this platform.
+     * @return boolean - False if attempting to get a lock will block indefinitely.
+     */
+    public function supports_timeout() {
+        global $CFG;
+
+        return $CFG->ostype !== 'WINDOWS';
+    }
+
+    /**
+     * This lock type will be automatically released when a process ends.
+     * @return boolean - True
+     */
+    public function supports_auto_release() {
+        return true;
+    }
+
+    /**
+     * Is available.
+     * @return boolean - True if preventfilelocking is not set - or the file_lock_root is not in dataroot.
+     */
+    public function is_available() {
+        global $CFG;
+        $preventfilelocking = !empty($CFG->preventfilelocking);
+        $lockdirisdataroot = true;
+        if (!empty($CFG->file_lock_root) && strpos($CFG->file_lock_root, $CFG->dataroot) !== 0) {
+            $lockdirisdataroot = false;
+        }
+        return !$preventfilelocking || !$lockdirisdataroot;
+    }
+
+    /**
+     * Multiple locks for the same resource cannot be held from a single process.
+     * @return boolean - False
+     */
+    public function supports_recursion() {
+        return false;
+    }
+
+    /**
+     * Get some info that might be useful for debugging.
+     * @return boolean - string
+     */
+    protected function get_debug_info() {
+        return 'host:' . php_uname('n') . ', pid:' . getmypid() . ', time:' . time();
+    }
+
+    /**
+     * Get a lock within the specified timeout or return false.
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     * @param int $maxlifetime - Unused by this lock type.
+     * @return boolean - true if a lock was obtained.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+        $giveuptime = time() + $timeout;
+
+        $hash = md5($this->type . '_' . $resource);
+        $lockdir = $this->lockdirectory . '/' . substr($hash, 0, 2);
+
+        if (!check_dir_exists($lockdir, true, true)) {
+            return false;
+        }
+
+        $lockfilename = $lockdir . '/' . $hash;
+
+        $filehandle = fopen($lockfilename, "wb");
+
+        // Could not open the lock file.
+        if (!$filehandle) {
+            return false;
+        }
+
+        do {
+            // Will block on windows. So sad.
+            $wouldblock = false;
+            $locked = flock($filehandle, LOCK_EX | LOCK_NB, $wouldblock);
+            if (!$locked && $wouldblock) {
+                usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+            }
+            // Try until the giveup time.
+        } while (!$locked && $wouldblock && time() < $giveuptime);
+
+        if (!$locked) {
+            fclose($filehandle);
+            return false;
+        }
+        if ($this->verbose) {
+            fwrite($filehandle, $this->get_debug_info());
+        }
+        return new lock($filehandle, $this);
+    }
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     * @param lock $lock - A lock obtained from this factory.
+     * @return boolean - true if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock) {
+        $handle = $lock->get_key();
+
+        if (!$handle) {
+            // We didn't have a lock.
+            return false;
+        }
+
+        $result = flock($handle, LOCK_UN);
+        fclose($handle);
+        return $result;
+    }
+
+    /**
+     * Extend a lock that was previously obtained with @lock.
+     * @param lock $lock - not used
+     * @param int $maxlifetime - not used
+     * @return boolean - true if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        // Not supported by this factory.
+        return false;
+    }
+
+}
diff --git a/lib/classes/lock/lock.php b/lib/classes/lock/lock.php
new file mode 100644 (file)
index 0000000..9bdb040
--- /dev/null
@@ -0,0 +1,113 @@
+<?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/>.
+
+/**
+ * Class representing a lock
+ *
+ * The methods available for a specific lock type are only known by it's factory.
+ *
+ * @package    core
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Class representing a lock
+ *
+ * The methods available for a specific lock type are only known by it's factory.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock {
+
+    /** @var string|int $key A unique key representing a held lock */
+    protected $key = '';
+
+    /** @var lock_factory $factory The factory that generated this lock */
+    protected $factory;
+
+    /** @var bool $released Has this lock been released? If a lock falls out of scope without being released - show a warning. */
+    protected $released;
+
+    /**
+     * Construct a lock containing the unique key required to release it.
+     * @param mixed $key - The lock key. The type of this is up to the lock_factory being used.
+     *      For file locks this is a file handle. For MySQL this is a string.
+     * @param lock_factory $factory - The factory that generated this lock.
+     */
+    public function __construct($key, $factory) {
+        $this->factory = $factory;
+        $this->key = $key;
+        $this->released = false;
+    }
+
+    /**
+     * Return the unique key representing this lock.
+     * @return string|int lock key.
+     */
+    public function get_key() {
+        return $this->key;
+    }
+
+    /**
+     * Extend the lifetime of this lock. Not supported by all factories.
+     * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+     * @return bool
+     */
+    public function extend($maxlifetime = 86400) {
+        if ($this->factory) {
+            return $this->factory->extend_lock($this, $maxlifetime);
+        }
+        return false;
+    }
+
+    /**
+     * Release this lock
+     * @return bool
+     */
+    public function release() {
+        $this->released = true;
+        if (empty($this->factory)) {
+            return false;
+        }
+        $result = $this->factory->release_lock($this);
+        // Release any held references to the factory.
+        unset($this->factory);
+        $this->factory = null;
+        $this->key = '';
+        return $result;
+    }
+
+    /**
+     * Print debugging if this lock falls out of scope before being released.
+     */
+    public function __destruct() {
+        if (!$this->released && defined('PHPUNIT_TEST')) {
+            $this->release();
+            throw new \coding_exception('\core\lock\lock(' . $this->key . ') has fallen out of scope ' .
+                                        'without being released.' . "\n" .
+                                        'Locks must ALWAYS be released by calling $mylock->release().');
+        }
+    }
+
+}
diff --git a/lib/classes/lock/lock_config.php b/lib/classes/lock/lock_config.php
new file mode 100644 (file)
index 0000000..43541a6
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Lock configuration class, used to get an instance of the currently configured lock factory.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Lock configuration class, used to get an instance of the currently configured lock factory.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_config {
+
+    /**
+     * Get an instance of the currently configured locking subclass.
+     *
+     * @param string $type - Unique namespace for the locks generated by this factory. e.g. core_cron
+     * @return \core\lock\lock_factory
+     * @throws \coding_exception
+     */
+    public static function get_lock_factory($type) {
+        global $CFG, $DB;
+        $lockfactory = null;
+
+        if (isset($CFG->lock_factory) && $CFG->lock_factory != 'auto') {
+            if (!class_exists($CFG->lock_factory)) {
+                // In this case I guess it is not safe to continue. Different cluster nodes could end up using different locking
+                // types because of an installation error.
+                throw new \coding_exception('Lock factory set in $CFG does not exist: ' . $CFG->lock_factory);
+            }
+            $lockfactoryclass = $CFG->lock_factory;
+            $lockfactory = new $lockfactoryclass($type);
+        } else {
+            $dbtype = clean_param($DB->get_dbfamily(), PARAM_ALPHA);
+
+            // DB Specific lock factory is preferred - should support auto-release.
+            $lockfactoryclass = "\\core\\lock\\${dbtype}_lock_factory";
+            if (!class_exists($lockfactoryclass)) {
+                $lockfactoryclass = '\core\lock\file_lock_factory';
+            }
+            /* @var lock_factory $lockfactory */
+            $lockfactory = new $lockfactoryclass($type);
+            if (!$lockfactory->is_available()) {
+                // Final fallback - DB row locking.
+                $lockfactory = new \core\lock\db_record_lock_factory($type);
+            }
+        }
+
+        return $lockfactory;
+    }
+
+}
diff --git a/lib/classes/lock/lock_factory.php b/lib/classes/lock/lock_factory.php
new file mode 100644 (file)
index 0000000..b03e774
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+/**
+ * Defines abstract factory class for generating locks.
+ *
+ * @package    core
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Defines abstract factory class for generating locks.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+interface lock_factory {
+
+    /**
+     * Define the constructor signature required by the lock_config class.
+     *
+     * @param string $type - The type this lock is used for (e.g. cron, cache)
+     */
+    public function __construct($type);
+
+    /**
+     * Return information about the blocking behaviour of the locks on this platform.
+     *
+     * @return boolean - False if attempting to get a lock will block indefinitely.
+     */
+    public function supports_timeout();
+
+    /**
+     * Will this lock be automatically released when the process ends.
+     * This should never be relied upon in code - but is useful in the case of
+     * fatal errors. If a lock type does not support this auto release,
+     * the max lock time parameter must be obeyed to eventually clean up a lock.
+     *
+     * @return boolean - True if this lock type will be automatically released when the current process ends.
+     */
+    public function supports_auto_release();
+
+    /**
+     * Supports recursion.
+     *
+     * @return boolean - True if attempting to get 2 locks on the same resource will "stack"
+     */
+    public function supports_recursion();
+
+    /**
+     * Is available.
+     *
+     * @return boolean - True if this lock type is available in this environment.
+     */
+    public function is_available();
+
+    /**
+     * Get a lock within the specified timeout or return false.
+     *
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     *                       Not all lock types will support this.
+     * @param int $maxlifetime - The number of seconds to wait before reclaiming a stale lock.
+     *                       Not all lock types will use this - e.g. if they support auto releasing
+     *                       a lock when a process ends.
+     * @return \core\lock\lock|boolean - An instance of \core\lock\lock if the lock was obtained, or false.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400);
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     *
+     * @param lock $lock - The lock to release.
+     * @return boolean - True if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock);
+
+    /**
+     * Extend the timeout on a held lock.
+     *
+     * @param lock $lock - lock obtained from this factory
+     * @param int $maxlifetime - new max time to hold the lock
+     * @return boolean - True if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400);
+}
diff --git a/lib/classes/lock/postgres_lock_factory.php b/lib/classes/lock/postgres_lock_factory.php
new file mode 100644 (file)
index 0000000..f27d532
--- /dev/null
@@ -0,0 +1,243 @@
+<?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/>.
+
+/**
+ * Postgres advisory locking factory.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  Damyon Wiese 2013
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\lock;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Postgres advisory locking factory.
+ *
+ * Postgres locking implementation using advisory locks. Some important points. Postgres has
+ * 2 different forms of lock functions, some accepting a single int, and some accepting 2 ints. This implementation
+ * uses the 2 int version so that it uses a separate namespace from the session locking. The second note,
+ * is because postgres uses integer keys for locks, we first need to map strings to a unique integer. This is
+ * done by storing the strings in the lock_db table and using the auto-id returned. There is a static cache for
+ * id's in this function.
+ *
+ * @package   core
+ * @category  lock
+ * @copyright Damyon Wiese 2013
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class postgres_lock_factory implements lock_factory {
+
+    /** @var int $dblockid - used as a namespace for these types of locks (separate from session locks) */
+    protected $dblockid = -1;
+
+    /** @var array $lockidcache - static cache for string -> int conversions required for pg advisory locks. */
+    protected static $lockidcache = array();
+
+    /** @var \moodle_database $db Hold a reference to the global $DB */
+    protected $db;
+
+    /** @var string $type Used to prefix lock keys */
+    protected $type;
+
+    /** @var array $openlocks - List of held locks - used by auto-release */
+    protected $openlocks = array();
+
+    /**
+     * Calculate a unique instance id based on the database name and prefix.
+     * @return int.
+     */
+    protected function get_unique_db_instance_id() {
+        global $CFG;
+
+        $strkey = $CFG->dbname . ':' . $CFG->prefix;
+        $intkey = crc32($strkey);
+        // Normalize between 64 bit unsigned int and 32 bit signed ints. Php could return either from crc32.
+        if (PHP_INT_SIZE == 8) {
+            if ($intkey > 0x7FFFFFFF) {
+                $intkey -= 0x100000000;
+            }
+        }
+
+        return $intkey;
+    }
+
+    /**
+     * Almighty constructor.
+     * @param string $type - Used to prefix lock keys.
+     */
+    public function __construct($type) {
+        global $DB;
+
+        $this->type = $type;
+        $this->dblockid = $this->get_unique_db_instance_id();
+        // Save a reference to the global $DB so it will not be released while we still have open locks.
+        $this->db = $DB;
+
+        \core_shutdown_manager::register_function(array($this, 'auto_release'));
+    }
+
+    /**
+     * Is available.
+     * @return boolean - True if this lock type is available in this environment.
+     */
+    public function is_available() {
+        return $this->db->get_dbfamily() === 'postgres';
+    }
+
+    /**
+     * Return information about the blocking behaviour of the lock type on this platform.
+     * @return boolean - Defer to the DB driver.
+     */
+    public function supports_timeout() {
+        return true;
+    }
+
+    /**
+     * Will this lock type will be automatically released when a process ends.
+     *
+     * @return boolean - Via shutdown handler.
+     */
+    public function supports_auto_release() {
+        return true;
+    }
+
+    /**
+     * Multiple locks for the same resource can be held by a single process.
+     * @return boolean - Defer to the DB driver.
+     */
+    public function supports_recursion() {
+        return true;
+    }
+
+    /**
+     * This function generates the unique index for a specific lock key.
+     * Once an index is assigned to a key, it never changes - so this is
+     * statically cached.
+     *
+     * @param string $key
+     * @return int
+     * @throws \moodle_exception
+     */
+    protected function get_index_from_key($key) {
+        if (isset(self::$lockidcache[$key])) {
+            return self::$lockidcache[$key];
+        }
+
+        $index = 0;
+        $record = $this->db->get_record('lock_db', array('resourcekey' => $key));
+        if ($record) {
+            $index = $record->id;
+        }
+
+        if (!$index) {
+            $record = new \stdClass();
+            $record->resourcekey = $key;
+            try {
+                $index = $this->db->insert_record('lock_db', $record);
+            } catch (\dml_exception $de) {
+                // Race condition - never mind - now the value is guaranteed to exist.
+                $record = $this->db->get_record('lock_db', array('resourcekey' => $key));
+                if ($record) {
+                    $index = $record->id;
+                }
+            }
+        }
+
+        if (!$index) {
+            throw new \moodle_exception('Could not generate unique index for key');
+        }
+
+        self::$lockidcache[$key] = $index;
+        return $index;
+    }
+
+    /**
+     * Create and get a lock
+     * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
+     * @param int $timeout - The number of seconds to wait for a lock before giving up.
+     * @param int $maxlifetime - Unused by this lock type.
+     * @return boolean - true if a lock was obtained.
+     */
+    public function get_lock($resource, $timeout, $maxlifetime = 86400) {
+        $giveuptime = time() + $timeout;
+
+        $token = $this->get_index_from_key($resource);
+
+        $params = array('locktype' => $this->dblockid,
+                        'token' => $token);
+
+        $locked = false;
+
+        do {
+            $result = $this->db->get_record_sql('SELECT pg_try_advisory_lock(:locktype, :token) AS locked', $params);
+            $locked = $result->locked === 't';
+            if (!$locked) {
+                usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
+            }
+            // Try until the giveup time.
+        } while (!$locked && time() < $giveuptime);
+
+        if ($locked) {
+            $this->openlocks[$token] = 1;
+            return new lock($token, $this);
+        }
+        return false;
+    }
+
+    /**
+     * Release a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @return boolean - true if the lock is no longer held (including if it was never held).
+     */
+    public function release_lock(lock $lock) {
+        $params = array('locktype' => $this->dblockid,
+                        'token' => $lock->get_key());
+        $result = $this->db->get_record_sql('SELECT pg_advisory_unlock(:locktype, :token) AS unlocked', $params);
+        $result = $result->unlocked === 't';
+        if ($result) {
+            unset($this->openlocks[$lock->get_key()]);
+        }
+        return $result;
+    }
+
+    /**
+     * Extend a lock that was previously obtained with @lock.
+     * @param lock $lock - a lock obtained from this factory.
+     * @param int $maxlifetime - the new lifetime for the lock (in seconds).
+     * @return boolean - true if the lock was extended.
+     */
+    public function extend_lock(lock $lock, $maxlifetime = 86400) {
+        // Not supported by this factory.
+        return false;
+    }
+
+    /**
+     * Auto release any open locks on shutdown.
+     * This is required, because we may be using persistent DB connections.
+     */
+    public function auto_release() {
+        // Called from the shutdown handler. Must release all open locks.
+        foreach ($this->openlocks as $key => $unused) {
+            $lock = new lock($key, $this);
+            $this->release_lock($lock);
+        }
+    }
+
+}
index 40bd6b3..f0d7455 100644 (file)
@@ -162,6 +162,7 @@ class core_plugin_manager {
 
         $this->installedplugins = array();
 
+        // TODO: Delete this block once Moodle 2.6 or later becomes minimum required version to upgrade.
         if ($CFG->version < 2013092001.02) {
             // We did not upgrade the database yet.
             $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
index 99a8360..4dd6833 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20140112" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20140115" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="lock_db" COMMENT="Stores active and inactive lock types for db locking method.">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="resourcekey" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="String identifying the resource to be locked. Should use frankenstyle format."/>
+        <FIELD NAME="expires" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Expiry time for an active lock."/>
+        <FIELD NAME="owner" TYPE="char" LENGTH="36" NOTNULL="false" SEQUENCE="false" COMMENT="uuid indicating the owner of the lock."/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+      </KEYS>
+      <INDEXES>
+        <INDEX NAME="resourcekey_uniq" UNIQUE="true" FIELDS="resourcekey" COMMENT="Unique index for resourcekey"/>
+        <INDEX NAME="expires_idx" UNIQUE="false" FIELDS="expires" COMMENT="Index on expires column"/>
+        <INDEX NAME="owner_idx" UNIQUE="false" FIELDS="owner" COMMENT="Index on owner"/>
+      </INDEXES>
+    </TABLE>
   </TABLES>
 </XMLDB>
index 4937628..0fe0e43 100644 (file)
@@ -2927,5 +2927,46 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014011701.00);
     }
 
+    if ($oldversion < 2014012300.01) {
+        // Remove deleted users home pages.
+        $sql = "DELETE FROM {my_pages}
+                WHERE EXISTS (SELECT {user}.id
+                                  FROM {user}
+                                  WHERE {user}.id = {my_pages}.userid
+                                  AND {user}.deleted = 1)
+                AND {my_pages}.private = 1";
+        $DB->execute($sql);
+
+        // Reached main savepoint.
+        upgrade_main_savepoint(true, 2014012300.01);
+    }
+
+    if ($oldversion < 2014012400.00) {
+        // Define table lock_db to be created.
+        $table = new xmldb_table('lock_db');
+
+        // Adding fields to table lock_db.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('resourcekey', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('expires', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
+        $table->add_field('owner', XMLDB_TYPE_CHAR, '36', null, null, null, null);
+
+        // Adding keys to table lock_db.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+
+        // Adding indexes to table lock_db.
+        $table->add_index('resourcekey_uniq', XMLDB_INDEX_UNIQUE, array('resourcekey'));
+        $table->add_index('expires_idx', XMLDB_INDEX_NOTUNIQUE, array('expires'));
+        $table->add_index('owner_idx', XMLDB_INDEX_NOTUNIQUE, array('owner'));
+
+        // Conditionally launch create table for lock_db.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2014012400.00);
+    }
+
     return true;
 }
index 1112baf..741f790 100644 (file)
@@ -69,26 +69,21 @@ class database_manager {
     /**
      * This function will execute an array of SQL commands.
      *
-     * @param array $sqlarr Array of sql statements to execute.
-     * @throws ddl_exception This exception is thrown if any error is found.
+     * @param string[] $sqlarr Array of sql statements to execute.
+     * @throws ddl_change_structure_exception This exception is thrown if any error is found.
      */
     protected function execute_sql_arr(array $sqlarr) {
-        foreach ($sqlarr as $sql) {
-            $this->execute_sql($sql);
-        }
+        $this->mdb->change_database_structure($sqlarr);
     }
 
     /**
      * Execute a given sql command string.
      *
      * @param string $sql The sql string you wish to be executed.
-     * @throws ddl_exception This exception is thrown if any error is found.
+     * @throws ddl_change_structure_exception This exception is thrown if any error is found.
      */
     protected function execute_sql($sql) {
-        if (!$this->mdb->change_database_structure($sql)) {
-            // in case driver does not throw exceptions yet ;-)
-            throw new ddl_change_structure_exception($this->mdb->get_last_error(), $sql);
-        }
+        $this->mdb->change_database_structure($sql);
     }
 
     /**
index bbd6de2..7c11f47 100644 (file)
@@ -124,22 +124,66 @@ class mysql_sql_generator extends sql_generator {
 
         $sqlarr = parent::getCreateTableSQL($xmldb_table);
 
-        // Let's inject the extra MySQL tweaks.
-        foreach ($sqlarr as $i=>$sql) {
-            if (strpos($sql, 'CREATE TABLE ') === 0) {
+        // This is a very nasty hack that tries to use just one query per created table
+        // because MySQL is stupidly slow when modifying empty tables.
+        // Note: it is safer to inject everything on new lines because there might be some trailing -- comments.
+        $sqls = array();
+        $prevcreate = null;
+        $matches = null;
+        foreach ($sqlarr as $sql) {
+            if (preg_match('/^CREATE TABLE ([^ ]+)/', $sql, $matches)) {
+                $prevcreate = $matches[1];
+                $sql = preg_replace('/\s*\)\s*$/s', '/*keyblock*/)', $sql);
+                // Let's inject the extra MySQL tweaks here.
                 if ($engine) {
-                    $sqlarr[$i] .= " ENGINE = $engine";
+                    $sql .= "\n ENGINE = $engine";
                 }
                 if ($collation) {
                     if (strpos($collation, 'utf8_') === 0) {
-                        $sqlarr[$i] .= " DEFAULT CHARACTER SET utf8";
+                        $sql .= "\n DEFAULT CHARACTER SET utf8";
                     }
-                    $sqlarr[$i] .= " DEFAULT COLLATE = $collation";
+                    $sql .= "\n DEFAULT COLLATE = $collation";
                 }
+                $sqls[] = $sql;
+                continue;
             }
+            if ($prevcreate) {
+                if (preg_match('/^ALTER TABLE '.$prevcreate.' COMMENT=(.*)$/s', $sql, $matches)) {
+                    $prev = array_pop($sqls);
+                    $prev .= "\n COMMENT=$matches[1]";
+                    $sqls[] = $prev;
+                    continue;
+                }
+                if (preg_match('/^CREATE INDEX ([^ ]+) ON '.$prevcreate.' (.*)$/s', $sql, $matches)) {
+                    $prev = array_pop($sqls);
+                    if (strpos($prev, '/*keyblock*/')) {
+                        $prev = str_replace('/*keyblock*/', "\n, KEY $matches[1] $matches[2]/*keyblock*/", $prev);
+                        $sqls[] = $prev;
+                        continue;
+                    } else {
+                        $sqls[] = $prev;
+                    }
+                }
+                if (preg_match('/^CREATE UNIQUE INDEX ([^ ]+) ON '.$prevcreate.' (.*)$/s', $sql, $matches)) {
+                    $prev = array_pop($sqls);
+                    if (strpos($prev, '/*keyblock*/')) {
+                        $prev = str_replace('/*keyblock*/', "\n, UNIQUE KEY $matches[1] $matches[2]/*keyblock*/", $prev);
+                        $sqls[] = $prev;
+                        continue;
+                    } else {
+                        $sqls[] = $prev;
+                    }
+                }
+            }
+            $prevcreate = null;
+            $sqls[] = $sql;
         }
 
-        return $sqlarr;
+        foreach ($sqls as $key => $sql) {
+            $sqls[$key] = str_replace('/*keyblock*/', "\n", $sql);
+        }
+
+        return $sqls;
     }
 
     /**
index 070f0cb..d53a172 100644 (file)
@@ -91,6 +91,8 @@ abstract class moodle_database {
     protected $reads = 0;
     /** @var int The database writes (performance counter).*/
     protected $writes = 0;
+    /** @var float Time queries took to finish, seconds with microseconds.*/
+    protected $queriestime = 0;
 
     /** @var int Debug level. */
     protected $debug  = 0;
@@ -459,7 +461,10 @@ abstract class moodle_database {
         $logerrors = !empty($this->dboptions['logerrors']);
         $iserror   = ($error !== false);
 
-        $time = microtime(true) - $this->last_time;
+        $time = $this->query_time();
+
+        // Will be shown or not depending on MDL_PERF values rather than in dboptions['log*].
+        $this->queriestime = $this->queriestime + $time;
 
         if ($logall or ($logslow and ($logslow < ($time+0.00001))) or ($iserror and $logerrors)) {
             $this->loggingquery = true;
@@ -489,6 +494,14 @@ abstract class moodle_database {
         }
     }
 
+    /**
+     * Returns the time elapsed since the query started.
+     * @return float Seconds with microseconds
+     */
+    protected function query_time() {
+        return microtime(true) - $this->last_time;
+    }
+
     /**
      * Returns database server info array
      * @return array Array containing 'description' and 'version' at least.
@@ -543,7 +556,7 @@ abstract class moodle_database {
         if (!$this->get_debug()) {
             return;
         }
-        $time = microtime(true) - $this->last_time;
+        $time = $this->query_time();
         $message = "Query took: {$time} seconds.\n";
         if (CLI_SCRIPT) {
             echo $message;
@@ -1079,9 +1092,9 @@ abstract class moodle_database {
 
     /**
      * Do NOT use in code, this is for use by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query or array of queries
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public abstract function change_database_structure($sql);
 
@@ -1595,6 +1608,45 @@ abstract class moodle_database {
      */
     public abstract function insert_record($table, $dataobject, $returnid=true, $bulk=false);
 
+    /**
+     * Insert multiple records into database as fast as possible.
+     *
+     * Order of inserts is maintained, but the operation is not atomic,
+     * use transactions if necessary.
+     *
+     * This method is intended for inserting of large number of small objects,
+     * do not use for huge objects with text or binary fields.
+     *
+     * @since 2.7
+     *
+     * @param string $table  The database table to be inserted into
+     * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+     * @return void does not return new record ids
+     *
+     * @throws coding_exception if data objects have different structure
+     * @throws dml_exception A DML specific exception is thrown for any errors.
+     */
+    public function insert_records($table, $dataobjects) {
+        if (!is_array($dataobjects) and !($dataobjects instanceof Traversable)) {
+            throw new coding_exception('insert_records() passed non-traversable object');
+        }
+
+        $fields = null;
+        // Note: override in driver if there is a faster way.
+        foreach ($dataobjects as $dataobject) {
+            if (!is_array($dataobject) and !is_object($dataobject)) {
+                throw new coding_exception('insert_records() passed invalid record object');
+            }
+            $dataobject = (array)$dataobject;
+            if ($fields === null) {
+                $fields = array_keys($dataobject);
+            } else if ($fields !== array_keys($dataobject)) {
+                throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+            }
+            $this->insert_record($table, $dataobject, false);
+        }
+    }
+
     /**
      * Import a record into a table, id field is required.
      * Safety checks are NOT carried out. Lobs are supported.
@@ -2466,4 +2518,12 @@ abstract class moodle_database {
     public function perf_get_queries() {
         return $this->writes + $this->reads;
     }
+
+    /**
+     * Time waiting for the database engine to finish running all queries.
+     * @return float Number of seconds with microseconds
+     */
+    public function perf_get_queries_time() {
+        return $this->queriestime;
+    }
 }
index 0a324b1..9b8e98f 100644 (file)
@@ -595,17 +595,26 @@ class mssql_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        $sqls = (array)$sql;
 
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $result = mssql_query($sql, $this->mssql);
-        $this->query_end($result);
+        try {
+            foreach ($sqls as $sql) {
+                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+                $result = mssql_query($sql, $this->mssql);
+                $this->query_end($result);
+            }
+        } catch (ddl_change_structure_exception $e) {
+            $this->reset_caches();
+            throw $e;
+        }
 
+        $this->reset_caches();
         return true;
     }
 
index 3cb62a3..c3c2769 100644 (file)
@@ -37,6 +37,7 @@ require_once(__DIR__.'/mysqli_native_moodle_temptables.php');
  */
 class mysqli_native_moodle_database extends moodle_database {
 
+    /** @var mysqli $mysqli */
     protected $mysqli = null;
 
     private $transactions_supported = null;
@@ -506,7 +507,7 @@ class mysqli_native_moodle_database extends moodle_database {
      * Returns detailed information about columns in table. This information is cached internally.
      * @param string $table name
      * @param bool $usecache
-     * @return array array of database_column_info objects indexed with column names
+     * @return database_column_info[] array of database_column_info objects indexed with column names
      */
     public function get_columns($table, $usecache=true) {
 
@@ -821,17 +822,38 @@ class mysqli_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        if (is_array($sql)) {
+            $sql = implode("\n;\n", $sql);
+        }
 
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $result = $this->mysqli->query($sql);
-        $this->query_end($result);
+        try {
+            $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+            $result = $this->mysqli->multi_query($sql);
+            if ($result === false) {
+                $this->query_end(false);
+            }
+            while ($this->mysqli->more_results()) {
+                $result = $this->mysqli->next_result();
+                if ($result === false) {
+                    $this->query_end(false);
+                }
+            }
+            $this->query_end(true);
+        } catch (ddl_change_structure_exception $e) {
+            while (@$this->mysqli->more_results()) {
+                @$this->mysqli->next_result();
+            }
+            $this->reset_caches();
+            throw $e;
+        }
 
+        $this->reset_caches();
         return true;
     }
 
@@ -1122,6 +1144,124 @@ class mysqli_native_moodle_database extends moodle_database {
         return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
     }
 
+    /**
+     * Insert multiple records into database as fast as possible.
+     *
+     * Order of inserts is maintained, but the operation is not atomic,
+     * use transactions if necessary.
+     *
+     * This method is intended for inserting of large number of small objects,
+     * do not use for huge objects with text or binary fields.
+     *
+     * @since 2.7
+     *
+     * @param string $table  The database table to be inserted into
+     * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+     * @return void does not return new record ids
+     *
+     * @throws coding_exception if data objects have different structure
+     * @throws dml_exception A DML specific exception is thrown for any errors.
+     */
+    public function insert_records($table, $dataobjects) {
+        if (!is_array($dataobjects) and !$dataobjects instanceof Traversable) {
+            throw new coding_exception('insert_records() passed non-traversable object');
+        }
+
+        // MySQL has a relatively small query length limit by default,
+        // make sure 'max_allowed_packet' in my.cnf is high enough
+        // if you change the following default...
+        static $chunksize = null;
+        if ($chunksize === null) {
+            if (!empty($this->dboptions['bulkinsertsize'])) {
+                $chunksize = (int)$this->dboptions['bulkinsertsize'];
+
+            } else {
+                if (PHP_INT_SIZE === 4) {
+                    // Bad luck for Windows, we cannot do any maths with large numbers.
+                    $chunksize = 5;
+                } else {
+                    $sql = "SHOW VARIABLES LIKE 'max_allowed_packet'";
+                    $this->query_start($sql, null, SQL_QUERY_AUX);
+                    $result = $this->mysqli->query($sql);
+                    $this->query_end($result);
+                    $size = 0;
+                    if ($rec = $result->fetch_assoc()) {
+                        $size = $rec['Value'];
+                    }
+                    $result->close();
+                    // Hopefully 200kb per object are enough.
+                    $chunksize = (int)($size / 200000);
+                    if ($chunksize > 50) {
+                        $chunksize = 50;
+                    }
+                }
+            }
+        }
+
+        $columns = $this->get_columns($table, true);
+        $fields = null;
+        $count = 0;
+        $chunk = array();
+        foreach ($dataobjects as $dataobject) {
+            if (!is_array($dataobject) and !is_object($dataobject)) {
+                throw new coding_exception('insert_records() passed invalid record object');
+            }
+            $dataobject = (array)$dataobject;
+            if ($fields === null) {
+                $fields = array_keys($dataobject);
+                $columns = array_intersect_key($columns, $dataobject);
+                unset($columns['id']);
+            } else if ($fields !== array_keys($dataobject)) {
+                throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+            }
+
+            $count++;
+            $chunk[] = $dataobject;
+
+            if ($count === $chunksize) {
+                $this->insert_chunk($table, $chunk, $columns);
+                $chunk = array();
+                $count = 0;
+            }
+        }
+
+        if ($count) {
+            $this->insert_chunk($table, $chunk, $columns);
+        }
+    }
+
+    /**
+     * Insert records in chunks.
+     *
+     * Note: can be used only from insert_records().
+     *
+     * @param string $table
+     * @param array $chunk
+     * @param database_column_info[] $columns
+     */
+    protected function insert_chunk($table, array $chunk, array $columns) {
+        $fieldssql = '('.implode(',', array_keys($columns)).')';
+
+        $valuessql = '('.implode(',', array_fill(0, count($columns), '?')).')';
+        $valuessql = implode(',', array_fill(0, count($chunk), $valuessql));
+
+        $params = array();
+        foreach ($chunk as $dataobject) {
+            foreach ($columns as $field => $column) {
+                $params[] = $this->normalise_value($column, $dataobject[$field]);
+            }
+        }
+
+        $sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
+
+        list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
+        $rawsql = $this->emulate_bound_params($sql, $params);
+
+        $this->query_start($sql, $params, SQL_QUERY_INSERT);
+        $result = $this->mysqli->query($rawsql);
+        $this->query_end($result);
+    }
+
     /**
      * Import a record into a table, id field is required.
      * Safety checks are NOT carried out. Lobs are supported.
index 00f79d6..8e99da4 100644 (file)
@@ -888,19 +888,28 @@ class oci_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
-
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $stmt = $this->parse_query($sql);
-        $result = oci_execute($stmt, $this->commit_status);
-        $this->query_end($result, $stmt);
-        oci_free_statement($stmt);
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        $sqls = (array)$sql;
+
+        try {
+            foreach ($sqls as $sql) {
+                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+                $stmt = $this->parse_query($sql);
+                $result = oci_execute($stmt, $this->commit_status);
+                $this->query_end($result, $stmt);
+                oci_free_statement($stmt);
+            }
+        } catch (ddl_change_structure_exception $e) {
+            $this->reset_caches();
+            throw $e;
+        }
 
+        $this->reset_caches();
         return true;
     }
 
index 53fd8d5..29cdd65 100644 (file)
@@ -174,22 +174,34 @@ abstract class pdo_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
-     * @return bool success
+     * @param string|array $sql query
+     * @return bool true
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $result = true;
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        $sqls = (array)$sql;
 
         try {
-            $this->pdb->exec($sql);
+            foreach ($sqls as $sql) {
+                $result = true;
+                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+
+                try {
+                    $this->pdb->exec($sql);
+                } catch (PDOException $ex) {
+                    $this->lastError = $ex->getMessage();
+                    $result = false;
+                }
+                $this->query_end($result);
+            }
+        } catch (ddl_change_structure_exception $e) {
             $this->reset_caches();
-        } catch (PDOException $ex) {
-            $this->lastError = $ex->getMessage();
-            $result = false;
+            throw $e;
         }
-        $this->query_end($result);
-        return $result;
+
+        $this->reset_caches();
+        return true;
     }
 
     public function delete_records_select($table, $select, array $params=null) {
index 2ee1635..14b2cc3 100644 (file)
@@ -37,6 +37,7 @@ require_once(__DIR__.'/pgsql_native_moodle_temptables.php');
  */
 class pgsql_native_moodle_database extends moodle_database {
 
+    /** @var resource $pgsql database resource */
     protected $pgsql     = null;
     protected $bytea_oid = null;
 
@@ -381,7 +382,7 @@ class pgsql_native_moodle_database extends moodle_database {
      * Returns detailed information about columns in table. This information is cached internally.
      * @param string $table name
      * @param bool $usecache
-     * @return array array of database_column_info objects indexed with column names
+     * @return database_column_info[] array of database_column_info objects indexed with column names
      */
     public function get_columns($table, $usecache=true) {
         if ($usecache) {
@@ -625,18 +626,35 @@ class pgsql_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        if (is_array($sql)) {
+            $sql = implode("\n;\n", $sql);
+        }
+        if (!$this->is_transaction_started()) {
+            // It is better to do all or nothing, this helps with recovery...
+            $sql = "BEGIN ISOLATION LEVEL SERIALIZABLE;\n$sql\n; COMMIT";
+        }
 
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $result = pg_query($this->pgsql, $sql);
-        $this->query_end($result);
+        try {
+            $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+            $result = pg_query($this->pgsql, $sql);
+            $this->query_end($result);
+            pg_free_result($result);
+        } catch (ddl_change_structure_exception $e) {
+            if (!$this->is_transaction_started()) {
+                $result = @pg_query($this->pgsql, "ROLLBACK");
+                @pg_free_result($result);
+            }
+            $this->reset_caches();
+            throw $e;
+        }
 
-        pg_free_result($result);
+        $this->reset_caches();
         return true;
     }
 
@@ -919,6 +937,108 @@ class pgsql_native_moodle_database extends moodle_database {
 
     }
 
+    /**
+     * Insert multiple records into database as fast as possible.
+     *
+     * Order of inserts is maintained, but the operation is not atomic,
+     * use transactions if necessary.
+     *
+     * This method is intended for inserting of large number of small objects,
+     * do not use for huge objects with text or binary fields.
+     *
+     * @since 2.7
+     *
+     * @param string $table  The database table to be inserted into
+     * @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
+     * @return void does not return new record ids
+     *
+     * @throws coding_exception if data objects have different structure
+     * @throws dml_exception A DML specific exception is thrown for any errors.
+     */
+    public function insert_records($table, $dataobjects) {
+        if (!is_array($dataobjects) and !($dataobjects instanceof Traversable)) {
+            throw new coding_exception('insert_records() passed non-traversable object');
+        }
+
+        // PostgreSQL does not seem to have problems with huge queries.
+        $chunksize = 500;
+        if (!empty($this->dboptions['bulkinsertsize'])) {
+            $chunksize = (int)$this->dboptions['bulkinsertsize'];
+        }
+
+        $columns = $this->get_columns($table, true);
+
+        // Make sure there are no nasty blobs!
+        foreach ($columns as $column) {
+            if ($column->binary) {
+                parent::insert_records($table, $dataobjects);
+                return;
+            }
+        }
+
+        $fields = null;
+        $count = 0;
+        $chunk = array();
+        foreach ($dataobjects as $dataobject) {
+            if (!is_array($dataobject) and !is_object($dataobject)) {
+                throw new coding_exception('insert_records() passed invalid record object');
+            }
+            $dataobject = (array)$dataobject;
+            if ($fields === null) {
+                $fields = array_keys($dataobject);
+                $columns = array_intersect_key($columns, $dataobject);
+                unset($columns['id']);
+            } else if ($fields !== array_keys($dataobject)) {
+                throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
+            }
+
+            $count++;
+            $chunk[] = $dataobject;
+
+            if ($count === $chunksize) {
+                $this->insert_chunk($table, $chunk, $columns);
+                $chunk = array();
+                $count = 0;
+            }
+        }
+
+        if ($count) {
+            $this->insert_chunk($table, $chunk, $columns);
+        }
+    }
+
+    /**
+     * Insert records in chunks, no binary support, strict param types...
+     *
+     * Note: can be used only from insert_records().
+     *
+     * @param string $table
+     * @param array $chunk
+     * @param database_column_info[] $columns
+     */
+    protected function insert_chunk($table, array $chunk, array $columns) {
+        $i = 1;
+        $params = array();
+        $values = array();
+        foreach ($chunk as $dataobject) {
+            $vals = array();
+            foreach ($columns as $field => $column) {
+                $params[] = $this->normalise_value($column, $dataobject[$field]);
+                $vals[] = "\$".$i++;
+            }
+            $values[] = '('.implode(',', $vals).')';
+        }
+
+        $fieldssql = '('.implode(',', array_keys($columns)).')';
+        $valuessql = implode(',', $values);
+
+        $sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
+        $this->query_start($sql, $params, SQL_QUERY_INSERT);
+        $result = pg_query_params($this->pgsql, $sql, $params);
+        $this->query_end($result);
+        pg_free_result($result);
+    }
+
     /**
      * Import a record into a table, id field is required.
      * Safety checks are NOT carried out. Lobs are supported.
index f6cab36..5110a32 100644 (file)
@@ -669,17 +669,26 @@ class sqlsrv_native_moodle_database extends moodle_database {
 
     /**
      * Do NOT use in code, to be used by database_manager only!
-     * @param string $sql query
+     * @param string|array $sql query
      * @return bool true
-     * @throws dml_exception A DML specific exception is thrown for any errors.
+     * @throws ddl_change_structure_exception A DDL specific exception is thrown for any errors.
      */
     public function change_database_structure($sql) {
-        $this->reset_caches();
-
-        $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
-        $result = sqlsrv_query($this->sqlsrv, $sql);
-        $this->query_end($result);
+        $this->get_manager(); // Includes DDL exceptions classes ;-)
+        $sqls = (array)$sql;
+
+        try {
+            foreach ($sqls as $sql) {
+                $this->query_start($sql, null, SQL_QUERY_STRUCTURE);
+                $result = sqlsrv_query($this->sqlsrv, $sql);
+                $this->query_end($result);
+            }
+        } catch (ddl_change_structure_exception $e) {
+            $this->reset_caches();
+            throw $e;
+        }
 
+        $this->reset_caches();
         return true;
     }
 
index 869d6ba..223eaee 100644 (file)
@@ -2263,6 +2263,114 @@ class core_dml_testcase extends database_driver_testcase {
         }
     }
 
+    public function test_insert_records() {
+        $DB = $this->tdb;
+        $dbman = $DB->get_manager();
+
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_field('oneint', XMLDB_TYPE_INTEGER, '10', null, null, null, 100);
+        $table->add_field('onenum', XMLDB_TYPE_NUMBER, '10,2', null, null, null, 200);
+        $table->add_field('onechar', XMLDB_TYPE_CHAR, '100', null, null, null, 'onestring');
+        $table->add_field('onetext', XMLDB_TYPE_TEXT, 'big', null, null, null);
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $this->assertCount(0, $DB->get_records($tablename));
+
+        $record = new stdClass();
+        $record->id = '1';
+        $record->course = '1';
+        $record->oneint = null;
+        $record->onenum = '1.00';
+        $record->onechar = 'a';
+        $record->onetext = 'aaa';
+
+        $expected = array();
+        $records = array();
+        for ($i = 1; $i <= 2000; $i++) { // This may take a while, it should be higher than defaults in DML drivers.
+            $rec = clone($record);
+            $rec->id = (string)$i;
+            $rec->oneint = (string)$i;
+            $expected[$i] = $rec;
+            $rec = clone($rec);
+            unset($rec->id);
+            $records[$i] = $rec;
+        }
+
+        $DB->insert_records($tablename, $records);
+        $stored = $DB->get_records($tablename, array(), 'id ASC');
+        $this->assertEquals($expected, $stored);
+
+        // Test there can be some extra properties including id.
+        $count = $DB->count_records($tablename);
+        $rec1 = (array)$record;
+        $rec1['xxx'] = 1;
+        $rec2 = (array)$record;
+        $rec2['xxx'] = 2;
+
+        $records = array($rec1, $rec2);
+        $DB->insert_records($tablename, $records);
+        $this->assertEquals($count + 2, $DB->count_records($tablename));
+
+        // Test not all properties are necessary.
+        $rec1 = (array)$record;
+        unset($rec1['course']);
+        $rec2 = (array)$record;
+        unset($rec2['course']);
+
+        $records = array($rec1, $rec2);
+        $DB->insert_records($tablename, $records);
+
+        // Make sure no changes in data object structure are tolerated.
+        $rec1 = (array)$record;
+        unset($rec1['id']);
+        $rec2 = (array)$record;
+        unset($rec2['id']);
+
+        $records = array($rec1, $rec2);
+        $DB->insert_records($tablename, $records);
+
+        $rec2['xx'] = '1';
+        $records = array($rec1, $rec2);
+        try {
+            $DB->insert_records($tablename, $records);
+            $this->fail('coding_exception expected when insert_records receives different object data structures');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        unset($rec2['xx']);
+        unset($rec2['course']);
+        $rec2['course'] = '1';
+        $records = array($rec1, $rec2);
+        try {
+            $DB->insert_records($tablename, $records);
+            $this->fail('coding_exception expected when insert_records receives different object data structures');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        $records = 1;
+        try {
+            $DB->insert_records($tablename, $records);
+            $this->fail('coding_exception expected when insert_records receives non-traversable data');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+
+        $records = array(1);
+        try {
+            $DB->insert_records($tablename, $records);
+            $this->fail('coding_exception expected when insert_records receives non-objet record');
+        } catch (moodle_exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+    }
+
     public function test_import_record() {
         // All the information in this test is fetched from DB by get_recordset() so we
         // have such method properly tested against nulls, empties and friends...
index 0ac6de5..0fb40ba 100644 (file)
@@ -456,8 +456,10 @@ class tgz_extractor {
                         'Failed to close output file: ' .  $this->currentfile);
             }
 
-            // Update modified time.
-            touch($this->currentfile, $this->currentmtime);
+            // At this point we should touch the file to set its modified
+            // time to $this->currentmtime. However, when extracting to the
+            // temp directory, cron will delete files more than a week old,
+            // so to avoid problems we leave all files at their current time.
         }
 
         if ($this->currentarchivepath === tgz_packer::ARCHIVE_INDEX_FILE) {
index 312ac4e..d84c2e8 100644 (file)
@@ -3210,8 +3210,8 @@ function require_logout() {
  */
 function require_course_login($courseorid, $autologinguest = true, $cm = null, $setwantsurltome = true, $preventredirect = false) {
     global $CFG, $PAGE, $SITE;
-    $issite = (is_object($courseorid) and $courseorid->id == SITEID)
-          or (!is_object($courseorid) and $courseorid == SITEID);
+    $issite = ((is_object($courseorid) and $courseorid->id == SITEID)
+          or (!is_object($courseorid) and $courseorid == SITEID));
     if ($issite && !empty($cm) && !($cm instanceof cm_info)) {
         // Note: nearly all pages call get_fast_modinfo anyway and it does not make any
         // db queries so this is not really a performance concern, however it is obviously
@@ -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;
 }
 
@@ -4233,6 +4233,9 @@ function delete_user(stdClass $user) {
     // Remove users private keys.
     $DB->delete_records('user_private_key', array('userid' => $user->id));
 
+    // Remove users customised pages.
+    $DB->delete_records('my_pages', array('userid' => $user->id, 'private' => 1));
+
     // Force logout - may fail if file based sessions used, sorry.
     \core\session\manager::kill_user_sessions($user->id);
 
@@ -8857,6 +8860,10 @@ function get_performance_info() {
     $info['html'] .= '<span class="dbqueries">DB reads/writes: '.$info['dbqueries'].'</span> ';
     $info['txt'] .= 'db reads/writes: '.$info['dbqueries'].' ';
 
+    $info['dbtime'] = round($DB->perf_get_queries_time(), 5);
+    $info['html'] .= '<span class="dbtime">DB queries time: '.$info['dbtime'].' secs</span> ';
+    $info['txt'] .= 'db queries time: ' . $info['dbtime'] . 's ';
+
     if (function_exists('posix_times')) {
         $ptimes = posix_times();
         if (is_array($ptimes)) {
index d43d7c7..1f39798 100644 (file)
@@ -539,9 +539,6 @@ class core_renderer extends renderer_base {
               <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=0&amp;warnp2n3e=1&amp;url1=' . urlencode(qualified_me()) . '">WCAG 1 (2,3) Check</a></li>
             </ul></div>';
         }
-        if (!empty($CFG->additionalhtmlfooter)) {
-            $output .= "\n".$CFG->additionalhtmlfooter;
-        }
         return $output;
     }
 
@@ -563,15 +560,22 @@ class core_renderer extends renderer_base {
 
     /**
      * The standard tags (typically script tags that are not needed earlier) that
-     * should be output after everything else. Designed to be called in theme layout.php files.
+     * should be output after everything else. Designed to be called in theme layout.php files.
      *
      * @return string HTML fragment.
      */
     public function standard_end_of_body_html() {
+        global $CFG;
+
         // This function is normally called from a layout.php file in {@link core_renderer::header()}
         // but some of the content won't be known until later, so we return a placeholder
         // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
-        return $this->unique_end_html_token;
+        $output = '';
+        if (!empty($CFG->additionalhtmlfooter)) {
+            $output .= "\n".$CFG->additionalhtmlfooter;
+        }
+        $output .= $this->unique_end_html_token;
+        return $output;
     }
 
     /**
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 34b086e..04bd791 100644 (file)
@@ -343,6 +343,34 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
         $this->assertEquals($expected, $legacydata, $message);
     }
 
+    /**
+     * Assert that an event is not using event->contxet.
+     * While restoring context might not be valid and it should not be used by event url
+     * or description methods.
+     *
+     * @param \core\event\base $event the event object.
+     * @param string $message
+     * @return void
+     */
+    public function assertEventContextNotUsed(\core\event\base $event, $message = '') {
+        // Save current event->context and set it to false.
+        $eventcontext = phpunit_event_mock::testable_get_event_context($event);
+        phpunit_event_mock::testable_set_event_context($event, false);
+        if ($message === '') {
+            $message = 'Event should not use context property of event in any method.';
+        }
+
+        // Test event methods should not use event->context.
+        $event->get_url();
+        $event->get_description();
+        $event->get_legacy_eventname();
+        phpunit_event_mock::testable_get_legacy_eventdata($event);
+        phpunit_event_mock::testable_get_legacy_logdata($event);
+
+        // Restore event->context.
+        phpunit_event_mock::testable_set_event_context($event, $eventcontext);
+    }
+
     /**
      * Stores current time as the base for assertTimeCurrent().
      *
index 26b99d1..578f300 100644 (file)
@@ -37,6 +37,8 @@ abstract class phpunit_event_mock extends \core\event\base {
 
     /**
      * Returns the log data of the event.
+     *
+     * @param \core\event\base $event event to get legacy eventdata from.
      * @return array
      */
     public static function testable_get_legacy_eventdata($event) {
@@ -45,10 +47,31 @@ abstract class phpunit_event_mock extends \core\event\base {
 
     /**
      * Returns the log data of the event.
+     *
+     * @param \core\event\base $event event to get legacy logdata from.
      * @return array
      */
     public static function testable_get_legacy_logdata($event) {
         return $event->get_legacy_logdata();
     }
 
+    /**
+     * Returns event context.
+     *
+     * @param \core\event\base $event event to get context for.
+     * @return context event context
+     */
+    public static function testable_get_event_context($event) {
+        return $event->context;
+    }
+
+    /**
+     * Sets event context.
+     *
+     * @param \core\event\base $event event to set context for.
+     * @param context $context context to set.
+     */
+    public static function testable_set_event_context($event, $context) {
+        $event->context = $context;
+    }
 }
index 0e6c477..799e53d 100644 (file)
@@ -1255,7 +1255,7 @@ function disable_output_buffering() {
  */
 function redirect_if_major_upgrade_required() {
     global $CFG;
-    $lastmajordbchanges = 2013100400.02;
+    $lastmajordbchanges = 2014012400.00;
     if (empty($CFG->version) or (float)$CFG->version < $lastmajordbchanges or
             during_initial_install() or !empty($CFG->adminsetuppending)) {
         try {
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 5bf603d..d89c9c7 100644 (file)
@@ -321,7 +321,7 @@ class behat_hooks extends behat_base {
 
         // All the run screenshots in the same parent dir.
         if (!$screenshotsdirname = self::get_run_screenshots_dir()) {
-            $screenshotsdirname = self::$screenshotsdirname = date('Ymd_Hi');
+            $screenshotsdirname = self::$screenshotsdirname = date('Ymd_His');
 
             $dir = $CFG->behat_screenshots_path . DIRECTORY_SEPARATOR . $screenshotsdirname;
 
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 859173e..7e5579f 100644 (file)
@@ -75,6 +75,7 @@ class core_event_content_viewed_testcase extends advanced_testcase {
         $result = $sink->get_events();
         $event = $result[1];
         $this->assertEventLegacyLogData(null, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -89,6 +90,7 @@ class core_event_content_viewed_testcase extends advanced_testcase {
         $pageevent = \core_tests\event\content_viewed::create();
         $pageevent->set_page_detail();
         $pageevent->trigger();
+        $this->assertEventContextNotUsed($pageevent);
     }
 }
 
index b129c78..3789ab7 100644 (file)
@@ -57,6 +57,7 @@ class core_event_course_module_instance_list_viewed_testcase extends advanced_te
         $this->assertEventLegacyLogData($legacydata, $event);
         $url = new moodle_url('/mod/unittests/index.php', array('id' => $course->id));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
 
     }
 
index 7977642..28e7bd1 100644 (file)
@@ -64,6 +64,7 @@ class core_event_course_module_viewed_testcase extends advanced_testcase {
         $this->assertSame('feedback', $event->objecttable);
         $url = new moodle_url('/mod/feedback/view.php', array('id' => $cm->id));
         $this->assertEquals($url, $event->get_url());
+        $this->assertEventContextNotUsed($event);
 
     }
 
index a85276d..713fc14 100644 (file)
@@ -749,4 +749,16 @@ class core_event_testcase extends advanced_testcase {
 
         $this->assertSame($event->get_data(), $data);
     }
+
+    /**
+     * @expectedException PHPUnit_Framework_Error_Notice
+     */
+    public function test_context_not_used() {
+        $event = \core_tests\event\context_used_in_event::create(array('courseid' => 1, 'other' => array('sample' => 1, 'xx' => 10)));
+        $this->assertEventContextNotUsed($event);
+
+        $eventcontext = phpunit_event_mock::testable_get_event_context($event);
+        phpunit_event_mock::testable_set_event_context($event, null);
+        $this->assertEventContextNotUsed($event);
+    }
 }
index e298bcc..cd54a1c 100644 (file)
@@ -51,6 +51,7 @@ class core_events_testcase extends advanced_testcase {
         $this->assertEquals(context_coursecat::instance($category->id), $event->get_context());
         $expected = array(SITEID, 'category', 'add', 'editcategory.php?id=' . $category->id, $category->id);
         $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -139,6 +140,7 @@ class core_events_testcase extends advanced_testcase {
         $this->assertEquals(context_coursecat::instance($category2->id), $event->get_context());
         $expected = array(SITEID, 'category', 'show', 'editcategory.php?id=' . $category2->id, $category2->id);
         $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     /**
@@ -170,5 +172,6 @@ class core_events_testcase extends advanced_testcase {
         $this->assertEquals(context_system::instance(), $event->get_context());
         $expected = array(SITEID, 'library', 'mailer', qualified_me(), 'ERROR: The email failed to send!');
         $this->assertEventLegacyLogData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 }
index a516184..a72532a 100644 (file)
@@ -258,3 +258,29 @@ class course_module_viewed extends \core\event\course_module_viewed {
 class course_module_viewed_noinit extends \core\event\course_module_viewed {
 }
 
+/**
+ * Event to test context used in event functions
+ */
+class context_used_in_event extends \core\event\base {
+    public function get_description() {
+        return $this->context->instanceid . " Description";
+    }
+
+    protected function init() {
+        $this->data['crud'] = 'u';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+        $this->context = \context_system::instance();
+    }
+
+    public function get_url() {
+        return new \moodle_url('/somepath/somefile.php', array('id' => $this->context->instanceid));
+    }
+
+    protected function get_legacy_eventdata() {
+        return array($this->data['courseid'], $this->context->instanceid);
+    }
+
+    protected function get_legacy_logdata() {
+        return array($this->data['courseid'], 'core_unittest', 'view', 'unittest.php?id=' . $this->context->instanceid);
+    }
+}
diff --git a/lib/tests/lock_config_test.php b/lib/tests/lock_config_test.php
new file mode 100644 (file)
index 0000000..63aef07
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+
+/**
+ * lock unit tests
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Unit tests for our locking configuration.
+ *
+ * @package    core
+ * @category   lock
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_config_testcase extends advanced_testcase {
+
+    /**
+     * Tests the static parse charset method
+     * @return void
+     */
+    public function test_lock_config() {
+        global $CFG;
+        $original = null;
+        if (isset($CFG->lock_factory)) {
+            $original = $CFG->lock_factory;
+        }
+
+        // Test no configuration.
+        unset($CFG->lock_factory);
+
+        $factory = \core\lock\lock_config::get_lock_factory('cache');
+
+        $this->assertNotEmpty($factory, 'Get a default factory with no configuration');
+
+        $CFG->lock_factory = '\core\lock\file_lock_factory';
+
+        $factory = \core\lock\lock_config::get_lock_factory('cache');
+        $this->assertTrue($factory instanceof \core\lock\file_lock_factory,
+                          'Get a default factory with a set configuration');
+
+        $CFG->lock_factory = '\core\lock\db_record_lock_factory';
+
+        $factory = \core\lock\lock_config::get_lock_factory('cache');
+        $this->assertTrue($factory instanceof \core\lock\db_record_lock_factory,
+                          'Get a default factory with a changed configuration');
+
+        if ($original) {
+            $CFG->lock_factory = $original;
+        } else {
+            unset($CFG->lock_factory);
+        }
+    }
+}
+
diff --git a/lib/tests/lock_test.php b/lib/tests/lock_test.php
new file mode 100644 (file)
index 0000000..17c8e57
--- /dev/null
@@ -0,0 +1,111 @@
+<?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/>.
+
+/**
+ * lock unit tests
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+/**
+ * Unit tests for our locking implementations.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 Damyon Wiese
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class lock_testcase extends advanced_testcase {
+
+    /**
+     * Some lock types will store data in the database.
+     */
+    protected function setUp() {
+        $this->resetAfterTest(true);
+    }
+
+    /**
+     * Run a suite of tests on a lock factory.
+     * @param \core\lock\lock_factory $lockfactory - A lock factory to test
+     */
+    protected function run_on_lock_factory(\core\lock\lock_factory $lockfactory) {
+
+        if ($lockfactory->is_available()) {
+            // This should work.
+            $lock1 = $lockfactory->get_lock('abc', 2);
+            $this->assertNotEmpty($lock1, 'Get a lock');
+
+            if ($lockfactory->supports_timeout()) {
+                if ($lockfactory->supports_recursion()) {
+                    $lock2 = $lockfactory->get_lock('abc', 2);
+                    $this->assertNotEmpty($lock2, 'Get a stacked lock');
+                    $this->assertTrue($lock2->release(), 'Release a stacked lock');
+                } else {
+                    // This should timeout.
+                    $lock2 = $lockfactory->get_lock('abc', 2);
+                    $this->assertFalse($lock2, 'Cannot get a stacked lock');
+                }
+            }
+            // Release the lock.
+            $this->assertTrue($lock1->release(), 'Release a lock');
+            // Get it again.
+            $lock3 = $lockfactory->get_lock('abc', 2);
+
+            $this->assertNotEmpty($lock3, 'Get a lock again');
+            // Release the lock again.
+            $this->assertTrue($lock3->release(), 'Release a lock again');
+            // Release the lock again (shouldn't hurt).
+            $this->assertFalse($lock3->release(), 'Release a lock that is not held');
+            if (!$lockfactory->supports_auto_release()) {
+                // Test that a lock can be claimed after the timeout period.
+                $lock4 = $lockfactory->get_lock('abc', 2, 2);
+                $this->assertNotEmpty($lock4, 'Get a lock');
+                sleep(3);
+
+                $lock5 = $lockfactory->get_lock('abc', 2, 2);
+                $this->assertNotEmpty($lock5, 'Get another lock after a timeout');
+                $this->assertTrue($lock5->release(), 'Release the lock');
+                $this->assertTrue($lock4->release(), 'Release the lock');
+            }
+        }
+    }
+
+    /**
+     * Tests the testable lock factories.
+     * @return void
+     */
+    public function test_locks() {
+        // Run the suite on the current configured default (may be non-core).
+        $defaultfactory = \core\lock\lock_config::get_lock_factory('default');
+        $this->run_on_lock_factory($defaultfactory);
+
+        // Manually create the core no-configuration factories.
+        $dblockfactory = new \core\lock\db_record_lock_factory('test');
+        $this->run_on_lock_factory($dblockfactory);
+
+        $filelockfactory = new \core\lock\file_lock_factory('test');
+        $this->run_on_lock_factory($filelockfactory);
+
+    }
+
+}
+
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 5971a4a..2e73ea2 100644 (file)
@@ -1889,6 +1889,7 @@ class core_moodlelib_testcase extends advanced_testcase {
         $this->assertSame($eventdata['other']['picture'], $user->picture);
         $this->assertSame($eventdata['other']['mnethostid'], $user->mnethostid);
         $this->assertEquals($user, $event->get_record_snapshot('user', $event->objectid));
+        $this->assertEventContextNotUsed($event);
 
         // Try invalid params.
         $record = new stdClass();
@@ -2451,14 +2452,13 @@ 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);
         $this->assertEquals(context_system::instance()->id, $event->contextid);
+        $this->assertEventContextNotUsed($event);
 
         $user = $DB->get_record('user', array('id'=>$user->id));
 
@@ -2468,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']);
@@ -2504,6 +2503,7 @@ class core_moodlelib_testcase extends advanced_testcase {
         $expectedlogdata = array(SITEID, 'user', 'logout', 'view.php?id='.$event->objectid.'&course='.SITEID, $event->objectid, 0,
             $event->objectid);
         $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
     public function test_email_to_user() {
@@ -2586,6 +2586,7 @@ class core_moodlelib_testcase extends advanced_testcase {
             $this->assertEquals(context_user::instance($user->id), $event->get_context());
             $expectedlogdata = array(SITEID, 'user', 'update', 'view.php?id='.$user->id, '');
             $this->assertEventLegacyLogData($expectedlogdata, $event);
+            $this->assertEventContextNotUsed($event);
         }
     }
 
index bd1a652..508fa8d 100644 (file)
@@ -6,6 +6,7 @@ information provided here is intended especially for developers.
 * $core_renderer->block_move_target() changed to support more verbose move-block-here descriptions.
 
 DEPRECATIONS:
+* $module uses in mod/xxx/version.php files is now deprecated. Please use $plugin instead. It will be removed in Moodle 2.10.
 * Update init methods in all event classes - "level" property was renamed to "edulevel", the level property is now deprecated.
 * Abstract class \core\event\course_module_instances_list_viewed is deprecated now, use \core\event\instances_list_viewed instead.
 * mod_book\event\instances_list_viewed has been deprecated. Please use mod_book\event\course_module_instance_list_viewed instead.
@@ -27,6 +28,8 @@ JavaSript:
     * The findChildNodes global function has been deprecated. Y.all should
       be used instead.
 
+* New locking api and admin settings to configure the system locking type.
+
 === 2.6 ===
 
 * Use new methods from core_component class instead of get_core_subsystems(), get_plugin_types(),
index 24563fd..338a5cc 100644 (file)
@@ -565,10 +565,11 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
             throw new plugin_defective_exception($component, 'Missing version.php');
         }
 
+        // TODO: Support for $module will end with Moodle 2.10 by MDL-43896. Was deprecated for Moodle 2.7 by MDL-43040.
         $plugin = new stdClass();
         $plugin->version = null;
         $module = $plugin;
-        require($fullmod .'/version.php');  // Defines $module/$plugin with version etc.
+        require($fullmod .'/version.php');  // Defines $plugin with version etc.
         $plugin = clone($module);
         unset($module->version);
         unset($module->component);
index a31d211..69ab2e7 100644 (file)
Binary files a/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js and b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-debug.js differ
index fa8b2e3..3826d94 100644 (file)
Binary files a/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js and b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue-min.js differ
index a31d211..69ab2e7 100644 (file)
Binary files a/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js and b/lib/yui/build/moodle-core-chooserdialogue/moodle-core-chooserdialogue.js differ
diff --git a/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-debug.js b/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-debug.js
new file mode 100644 (file)
index 0000000..bd96b87
Binary files /dev/null and b/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-debug.js differ
diff --git a/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-min.js b/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-min.js
new file mode 100644 (file)
index 0000000..14c4146
Binary files /dev/null and b/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll-min.js differ
diff --git a/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll.js b/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll.js
new file mode 100644 (file)
index 0000000..5987452
Binary files /dev/null and b/lib/yui/build/moodle-core-lockscroll/moodle-core-lockscroll.js differ
index ff36c28..2e691ac 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 62cd2fe..138747b 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 0c3269e..cf96f86 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 a2af79d..7944b8b 100644 (file)
@@ -213,6 +213,20 @@ Y.extend(CHOOSERDIALOGUE, Y.Base, {
             }
         }
 
+        // If the dialogue is larger than a reasonable minimum height, we
+        // disable the page scrollbars.
+        if (newheight > this.get('minheight')) {
+            // Disable the page scrollbars.
+            if (this.panel.lockScroll && !this.panel.lockScroll.isActive()) {
+                this.panel.lockScroll.enableScrollLock();
+            }
+        } else {
+            // Re-enable the page scrollbars.
+            if (this.panel.lockScroll && this.panel.lockScroll.isActive()) {
+                this.panel.lockScroll.disableScrollLock();
+            }
+        }
+
         // Take off 15px top and bottom for borders, plus 40px each for the title and button area before setting the
         // new max-height
         totalheight = newheight;
diff --git a/lib/yui/src/lockscroll/build.json b/lib/yui/src/lockscroll/build.json
new file mode 100644 (file)
index 0000000..6e88f5e
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "moodle-core-lockscroll",
+  "builds": {
+    "moodle-core-lockscroll": {
+      "jsfiles": [
+        "lockscroll.js"
+      ]
+    }
+  }
+}
diff --git a/lib/yui/src/lockscroll/js/lockscroll.js b/lib/yui/src/lockscroll/js/lockscroll.js
new file mode 100644 (file)
index 0000000..10f5ad4
--- /dev/null
@@ -0,0 +1,123 @@
+/**
+ * Provides the ability to lock the scroll for a page, allowing nested
+ * locking.
+ *
+ * @module moodle-core-lockscroll
+ */
+
+/**
+ * Provides the ability to lock the scroll for a page.
+ *
+ * This is achieved by applying the class 'lockscroll' to the body Node.
+ *
+ * Nested widgets are also supported and the scroll lock is only removed
+ * when the final plugin instance is disabled.
+ *
+ * @class M.core.LockScroll
+ * @extends Plugin.Base
+ */
+Y.namespace('M.core').LockScroll = Y.Base.create('lockScroll', Y.Plugin.Base, [], {
+
+    /**
+     * Whether the LockScroll has been activated.
+     *
+     * @property _enabled
+     * @type Boolean
+     * @protected
+     */
+    _enabled: false,
+
+    /**
+     * Handle destruction of the lockScroll instance, including disabling
+     * of the current instance.
+     *
+     * @method destructor
+     */
+    destructor: function() {
+        this.disableScrollLock();
+    },
+
+    /**
+     * Start locking the page scroll.
+     *
+     * This is achieved by applying the lockscroll class to the body Node.
+     *
+     * A count of the total number of active, and enabled, lockscroll instances is also kept on
+     * the body to ensure that premature disabling does not occur.
+     *
+     * @method enableScrollLock
+     * @chainable
+     */
+    enableScrollLock: function() {
+        if (this.isActive()) {
+            Y.log('LockScroll already active. Ignoring enable request', 'warn', 'moodle-core-lockscroll');
+            return;
+        }
+
+        Y.log('Enabling LockScroll.', 'debug', 'moodle-core-lockscroll');
+        this._enabled = true;
+        var body = Y.one(Y.config.doc.body);
+
+        // We use a CSS class on the body to handle the actual locking.
+        body.addClass('lockscroll');
+
+        // Increase the count of active instances - this is used to ensure that we do not
+        // remove the locking when parent windows are still open.
+        // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them.
+        var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 0,
+            newCount = currentCount + 1;
+        body.setAttribute('data-activeScrollLocks', newCount);
+        Y.log("Setting the activeScrollLocks count from " + currentCount + " to " + newCount,
+                'debug', 'moodle-core-lockscroll');
+
+        return this;
+    },
+
+    /**
+     * Stop locking the page scroll.
+     *
+     * The instance may be disabled but the scroll lock not removed if other instances of the
+     * plugin are also active.
+     *
+     * @method disableScrollLock
+     * @chainable
+     */
+    disableScrollLock: function() {
+        if (this.isActive()) {
+            Y.log('Disabling LockScroll.', 'debug', 'moodle-core-lockscroll');
+            this._enabled = false;
+
+            var body = Y.one(Y.config.doc.body);
+
+            // Decrease the count of active instances.
+            // Note: We cannot use getData here because data attributes are sandboxed to the instance that created them.
+            var currentCount = parseInt(body.getAttribute('data-activeScrollLocks'), 10) || 1,
+                newCount = currentCount - 1;
+
+            if (currentCount === 1) {
+                body.removeClass('lockscroll');
+            }
+
+            body.setAttribute('data-activeScrollLocks', currentCount - 1);
+            Y.log("Setting the activeScrollLocks count from " + currentCount + " to " + newCount,
+                    'debug', 'moodle-core-lockscroll');
+        }
+
+        return this;
+    },
+
+    /**
+     * Return whether scroll locking is active.
+     *
+     * @method isActive
+     * @return Boolean
+     */
+    isActive: function() {
+        return this._enabled;
+    }
+
+}, {
+    NS: 'lockScroll',
+    ATTRS: {
+    }
+});
diff --git a/lib/yui/src/lockscroll/meta/lockscroll.json b/lib/yui/src/lockscroll/meta/lockscroll.json
new file mode 100644 (file)
index 0000000..9a619e4
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "moodle-core-lockscroll": {
+    "requires": [
+        "plugin",
+        "base-build"
+    ]
+  }
+}
index edd85e8..25a0c83 100644 (file)
@@ -87,6 +87,10 @@ Y.extend(DIALOGUE, Y.Panel, {
         }
         this.set('COUNT', COUNT);
 
+        if (this.get('modal')) {
+            this.plug(Y.M.core.LockScroll);
+        }
+
         // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
         // and allow setting of z-index in theme.
         bb = this.get('boundingBox');
@@ -286,6 +290,11 @@ Y.extend(DIALOGUE, Y.Panel, {
             header = this.headerNode,
             content = this.bodyNode;
 
+        // Lock scroll if the plugin is present.
+        if (this.lockScroll) {
+            this.lockScroll.enableScrollLock();
+        }
+
         result = DIALOGUE.superclass.show.call(this);
         if (header && header !== '') {
             header.focus();
@@ -294,6 +303,15 @@ Y.extend(DIALOGUE, Y.Panel, {
         }
         return result;
     },
+
+    hide: function() {
+        // Unlock scroll if the plugin is present.
+        if (this.lockScroll) {
+            this.lockScroll.disableScrollLock();
+        }
+
+        return DIALOGUE.superclass.hide.call(this, arguments);
+    },
     /**
      * Setup key delegation to keep tabbing within the open dialogue.
      *
index ef33211..c3c0bfe 100644 (file)
@@ -14,7 +14,8 @@
         "node",
         "panel",
         "event-key",
-        "dd-plugin"
+        "dd-plugin",
+        "moodle-core-lockscroll"
     ]
   },
   "moodle-core-notification-alert": {
diff --git a/message/tests/behat/message_participants.feature b/message/tests/behat/message_participants.feature
new file mode 100644 (file)
index 0000000..feeb42b
--- /dev/null
@@ -0,0 +1,39 @@
+@core @core_message
+Feature: An user can message course participants
+  In order to communicate efficiently with my students
+  As a teacher
+  I need to message them all
+
+  @javascript
+  Scenario: An user can message multiple course participants including him/her self
+    Given 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 |
+      | student3 | Student | 3 | student3@asd.com |
+    And the following "courses" exists:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exists:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Participants"
+    When I click on "input[type='checkbox']" "css_element" in the "Teacher 1" "table_row"
+    And I click on "input[type='checkbox']" "css_element" in the "Student 1" "table_row"
+    And I select "Send a message" from "With selected users..."
+    And I fill the moodle form with:
+      | messagebody | Here it is, the message content |
+    And I press "Preview"
+    And I press "Send message"
+    And I follow "Home"
+    And I expand "My profile" node
+    And I follow "Messages"
+    And I select "Recent conversations" from "Message navigation:"
+    Then I should see "Here it is, the message content"
+    And I should see "Student 1"
+    And I click on "this conversation" "link" in the "//div[@class='singlemessage'][contains(., 'Teacher 1')]" "xpath_element"
+    And I should see "Here it is, the message content"
index b33f689..6ceb783 100644 (file)
@@ -75,7 +75,7 @@ class all_submissions_downloaded extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index 5e7d6e8..2f519f0 100644 (file)
@@ -65,7 +65,7 @@ class assessable_submitted extends \core\event\assessable_submitted {
     protected function get_legacy_eventdata() {
         $eventdata = new \stdClass();
         $eventdata->modulename = 'assign';
-        $eventdata->cmid = $this->context->instanceid;
+        $eventdata->cmid = $this->contextinstanceid;
         $eventdata->itemid = $this->objectid;
         $eventdata->courseid = $this->courseid;
         $eventdata->userid = $this->userid;
@@ -106,7 +106,7 @@ class assessable_submitted extends \core\event\assessable_submitted {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index 287f847..07eae94 100644 (file)
@@ -75,7 +75,7 @@ class extension_granted extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index 3dc67b1..f6b726c 100644 (file)
@@ -75,7 +75,7 @@ class identities_revealed extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index b59ecd1..20b20f2 100644 (file)
@@ -81,7 +81,7 @@ class marker_updated extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index 6381682..4169ea6 100644 (file)
@@ -75,7 +75,7 @@ class statement_accepted extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index a5bf9c0..b52b984 100644 (file)
@@ -75,7 +75,7 @@ class submission_duplicated extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index cac5fac..bc3f474 100644 (file)
@@ -75,7 +75,7 @@ class submission_graded extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index 082a1a4..7d94453 100644 (file)
@@ -75,7 +75,7 @@ class submission_locked extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index e0d21c9..cbf526f 100644 (file)
@@ -81,7 +81,7 @@ class submission_status_updated extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index b50bd36..e5594b1 100644 (file)
@@ -75,7 +75,7 @@ class submission_unlocked extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index f9e6e47..505452b 100644 (file)
@@ -75,7 +75,7 @@ class submission_updated extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index a52b93a..1ef9037 100644 (file)
@@ -81,7 +81,7 @@ class workflow_state_updated extends \core\event\base {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
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 0357606..eacc25c 100644 (file)
@@ -73,8 +73,9 @@ 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);
     }
 
     /**
@@ -110,7 +111,8 @@ 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);
     }
 }
index b32eb00..1b33df2 100644 (file)
@@ -66,7 +66,7 @@ class assessable_uploaded extends \core\event\assessable_uploaded {
     protected function get_legacy_eventdata() {
         $eventdata = new \stdClass();
         $eventdata->modulename = 'assign';
-        $eventdata->cmid = $this->context->instanceid;
+        $eventdata->cmid = $this->contextinstanceid;
         $eventdata->itemid = $this->objectid;
         $eventdata->courseid = $this->courseid;
         $eventdata->userid = $this->userid;
@@ -102,7 +102,7 @@ class assessable_uploaded extends \core\event\assessable_uploaded {
      * @return \moodle_url
      */
     public function get_url() {
-        return new \moodle_url('/mod/assign/view.php', array('id' => $this->context->instanceid));
+        return new \moodle_url('/mod/assign/view.php', array('id' => $this->contextinstanceid));
     }
 
     /**
index 1aaa871..a6816a3 100644 (file)
@@ -90,6 +90,7 @@ class assignsubmission_file_events_testcase extends advanced_testcase {
         $expected->files = $files;
         $expected->pathnamehashes = array($fi->get_pathnamehash(), $fi2->get_pathnamehash());
         $this->assertEventLegacyData($expected, $event);
+        $this->assertEventContextNotUsed($event);
     }
 
 }