Merge branch 'MDL-46079-master' of https://github.com/sammarshallou/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 24 Jun 2014 23:32:11 +0000 (01:32 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 24 Jun 2014 23:32:11 +0000 (01:32 +0200)
255 files changed:
admin/cli/check_database_schema.php [new file with mode: 0644]
admin/mnet/index.php
auth/shibboleth/README.txt
backup/util/dbops/tests/backup_dbops_test.php
backup/util/plan/restore_plan.class.php
backup/util/structure/tests/structure_test.php
backup/util/ui/renderer.php
blocks/community/block_community.php
blog/tests/bloglib_test.php
cache/README.md
config-dist.php
course/dnduploadlib.php
course/lib.php
course/modduplicate.php
course/modlib.php
course/renderer.php
course/tests/courselib_test.php
files/renderer.php
filter/algebra/algebradebug.php
filter/algebra/pix.php
filter/tex/latex.php
filter/tex/lib.php
filter/tex/pix.php
filter/tex/settings.php
filter/tex/texdebug.php
grade/report/grader/classes/event/report_viewed.php [new file with mode: 0644]
grade/report/grader/index.php
grade/report/grader/lang/en/gradereport_grader.php
grade/report/outcomes/classes/event/report_viewed.php [new file with mode: 0644]
grade/report/outcomes/index.php
grade/report/outcomes/lang/en/gradereport_outcomes.php
grade/report/overview/classes/event/report_viewed.php [new file with mode: 0644]
grade/report/overview/index.php
grade/report/overview/lang/en/gradereport_overview.php
grade/report/user/classes/event/report_viewed.php [new file with mode: 0644]
grade/report/user/index.php
grade/report/user/lang/en/gradereport_user.php
index.php
lang/en/admin.php
lang/en/grades.php
lang/en/my.php
lib/bennu/iCalendar_rfc2445.php
lib/bennu/readme_moodle.txt
lib/classes/event/blog_association_created.php
lib/classes/event/blog_entries_viewed.php
lib/classes/event/blog_entry_created.php
lib/classes/event/blog_entry_deleted.php
lib/classes/event/blog_entry_updated.php
lib/classes/event/cohort_member_added.php
lib/classes/event/cohort_member_removed.php
lib/classes/event/content_viewed.php
lib/classes/event/course_category_deleted.php
lib/classes/event/course_completed.php
lib/classes/event/course_content_deleted.php
lib/classes/event/course_created.php
lib/classes/event/course_deleted.php
lib/classes/event/course_module_created.php
lib/classes/event/course_restored.php
lib/classes/event/course_updated.php
lib/classes/event/course_user_report_viewed.php
lib/classes/event/course_viewed.php
lib/classes/event/email_failed.php
lib/classes/event/grade_report_viewed.php [new file with mode: 0644]
lib/classes/event/group_member_added.php
lib/classes/event/group_member_removed.php
lib/classes/event/mnet_access_control_created.php
lib/classes/event/mnet_access_control_updated.php
lib/classes/event/note_created.php
lib/classes/event/note_deleted.php
lib/classes/event/note_updated.php
lib/classes/event/notes_viewed.php
lib/classes/event/recent_activity_viewed.php
lib/classes/event/role_assigned.php
lib/classes/event/role_deleted.php
lib/classes/event/role_unassigned.php
lib/classes/event/user_graded.php
lib/classes/event/user_list_viewed.php
lib/classes/event/user_loggedinas.php
lib/classes/event/user_loggedout.php
lib/classes/event/user_login_failed.php
lib/classes/event/user_profile_viewed.php
lib/classes/event/webservice_function_called.php
lib/classes/event/webservice_service_created.php
lib/classes/event/webservice_service_deleted.php
lib/classes/event/webservice_service_updated.php
lib/classes/event/webservice_service_user_added.php
lib/classes/event/webservice_service_user_removed.php
lib/classes/event/webservice_token_created.php
lib/classes/event/webservice_token_sent.php
lib/classes/plugininfo/theme.php
lib/ddl/database_manager.php
lib/dtl/database_exporter.php
lib/dtl/database_importer.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js
lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js
lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-debug.js
lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-min.js
lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button.js
lib/editor/atto/plugins/accessibilityhelper/yui/src/button/js/button.js
lib/editor/atto/plugins/equation/tests/behat/equation.feature
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js
lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js
lib/editor/atto/plugins/equation/yui/src/button/js/button.js
lib/editor/atto/plugins/image/tests/behat/image.feature
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js
lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js
lib/editor/atto/plugins/image/yui/src/button/js/button.js
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/pagelib.php
lib/phpunit/tests/advanced_test.php
lib/testing/classes/util.php
lib/tests/accesslib_test.php
lib/tests/events_test.php
lib/tests/fixtures/event_fixtures.php
lib/tests/grades_externallib_test.php
lib/tests/outputrequirementslib_test.php
lib/upgrade.txt
mod/assign/classes/event/reveal_identities_confirmation_page_viewed.php
mod/assign/classes/event/submission_confirmation_form_viewed.php
mod/assign/classes/event/submission_status_viewed.php
mod/assign/classes/event/workflow_state_updated.php
mod/assign/db/access.php
mod/assign/feedback/editpdf/ajax.php
mod/assign/feedback/editpdf/ajax_progress.php
mod/assign/feedback/editpdf/backup/moodle2/backup_assignfeedback_editpdf_subplugin.class.php
mod/assign/feedback/editpdf/backup/moodle2/restore_assignfeedback_editpdf_subplugin.class.php
mod/assign/feedback/editpdf/classes/document_services.php
mod/assign/feedback/editpdf/classes/page_editor.php
mod/assign/feedback/editpdf/locallib.php
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/file/lib.php
mod/assign/gradingtable.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/styles.css
mod/assign/submission/comments/lib.php
mod/assign/submission/file/lib.php
mod/assign/submission/onlinetext/classes/event/assessable_uploaded.php
mod/assign/submission/onlinetext/classes/event/submission_updated.php
mod/assign/submission/onlinetext/lib.php
mod/assign/tests/events_test.php
mod/assign/tests/externallib_test.php
mod/assign/tests/locallib_test.php
mod/assign/version.php
mod/assignment/lib.php
mod/choice/classes/event/answer_submitted.php
mod/choice/classes/event/answer_updated.php
mod/choice/tests/events_test.php
mod/data/tests/lib_test.php
mod/data/tests/search_test.php
mod/feedback/classes/event/response_deleted.php
mod/feedback/classes/event/response_submitted.php
mod/feedback/tests/events_test.php
mod/feedback/upgrade.txt [new file with mode: 0644]
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/classes/event/discussion_subscription_created.php [new file with mode: 0644]
mod/forum/classes/event/discussion_subscription_deleted.php [new file with mode: 0644]
mod/forum/classes/observer.php
mod/forum/classes/post_form.php
mod/forum/classes/subscriptions.php [new file with mode: 0644]
mod/forum/db/install.xml
mod/forum/db/upgrade.php
mod/forum/deprecatedlib.php
mod/forum/discuss.php
mod/forum/index.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/pix/t/subscribed.png [new file with mode: 0644]
mod/forum/pix/t/subscribed.svg [new file with mode: 0644]
mod/forum/pix/t/unsubscribed.png [new file with mode: 0644]
mod/forum/pix/t/unsubscribed.svg [new file with mode: 0644]
mod/forum/post.php
mod/forum/renderer.php
mod/forum/styles.css
mod/forum/subscribe.php
mod/forum/subscribe_ajax.php [new file with mode: 0644]
mod/forum/subscribers.php
mod/forum/tests/behat/discussion_subscriptions.feature [new file with mode: 0644]
mod/forum/tests/behat/forum_subscriptions.feature [new file with mode: 0644]
mod/forum/tests/events_test.php
mod/forum/tests/externallib_test.php
mod/forum/tests/generator_test.php
mod/forum/tests/lib_test.php
mod/forum/tests/mail_test.php [new file with mode: 0644]
mod/forum/tests/maildigest_test.php
mod/forum/tests/subscriptions_test.php [new file with mode: 0644]
mod/forum/unsubscribeall.php
mod/forum/upgrade.txt
mod/forum/version.php
mod/forum/view.php
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-debug.js [new file with mode: 0644]
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle-min.js [new file with mode: 0644]
mod/forum/yui/build/moodle-mod_forum-subscriptiontoggle/moodle-mod_forum-subscriptiontoggle.js [new file with mode: 0644]
mod/forum/yui/src/subscriptiontoggle/build.json [new file with mode: 0644]
mod/forum/yui/src/subscriptiontoggle/js/toggle.js [new file with mode: 0644]
mod/forum/yui/src/subscriptiontoggle/meta/subscriptiontoggle.json [new file with mode: 0644]
mod/glossary/classes/event/entry_created.php
mod/glossary/classes/event/entry_deleted.php
mod/glossary/classes/event/entry_updated.php
mod/lesson/classes/event/essay_attempt_viewed.php
mod/lesson/tests/events_test.php
mod/lti/lib.php
mod/lti/locallib.php
mod/quiz/classes/event/attempt_abandoned.php
mod/quiz/classes/event/attempt_becameoverdue.php
mod/quiz/classes/event/attempt_submitted.php
mod/quiz/report/overview/report.php
mod/quiz/report/reportlib.php
mod/quiz/tests/editlib_test.php
mod/quiz/tests/reportlib_test.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/survey/lang/en/survey.php
mod/wiki/classes/event/page_locks_deleted.php
mod/wiki/classes/event/page_updated.php
mod/wiki/tests/events_test.php
mod/workshop/classes/event/assessable_uploaded.php
mod/workshop/classes/event/assessment_evaluated.php
mod/workshop/classes/event/assessment_evaluations_reset.php
mod/workshop/classes/event/assessment_reevaluated.php
mod/workshop/classes/event/assessments_reset.php
mod/workshop/classes/event/phase_switched.php
mod/workshop/classes/event/submission_assessed.php
mod/workshop/classes/event/submission_created.php
mod/workshop/classes/event/submission_reassessed.php
mod/workshop/classes/event/submission_updated.php
mod/workshop/classes/event/submission_viewed.php
mod/workshop/lang/en/workshop.php
mod/workshop/tests/events_test.php
report/completion/classes/event/user_report_viewed.php
report/questioninstances/classes/event/report_viewed.php
repository/boxnet/lib.php
repository/equella/lib.php
theme/base/style/core.css
theme/base/style/dock.css
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/renderers/core_renderer.php
theme/bootstrapbase/style/moodle.css
theme/upgrade.txt
user/profile.php
user/view.php

diff --git a/admin/cli/check_database_schema.php b/admin/cli/check_database_schema.php
new file mode 100644 (file)
index 0000000..3164a27
--- /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/>.
+
+/**
+ * Validate that the current db structure matches the install.xml files.
+ *
+ * @package   core
+ * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author    Petr Skoda <petr.skoda@totaralms.com>
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+$help = "Validate database structure
+
+Options:
+-h, --help            Print out this help.
+
+Example:
+\$ sudo -u www-data /usr/bin/php admin/cli/check_database_schema.php
+";
+
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'help' => false,
+    ),
+    array(
+        'h' => 'help',
+    )
+);
+
+if ($options['help']) {
+    echo $help;
+    exit(0);
+}
+
+if (empty($CFG->version)) {
+    echo "Database is not yet installed.\n";
+    exit(2);
+}
+
+$dbmanager = $DB->get_manager();
+$schema = $dbmanager->get_install_xml_schema();
+
+if (!$errors = $dbmanager->check_database_schema($schema)) {
+    echo "Database structure is ok.\n";
+    exit(0);
+}
+
+foreach ($errors as $table => $items) {
+    cli_separator();
+    echo "$table\n";
+    foreach ($items as $item) {
+        echo " * $item\n";
+    }
+}
+cli_separator();
+
+exit(1);
index fb4e2c4..d6e544e 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
 
     // Allows the admin to configure mnet stuff
 
index a343c85..849dad2 100644 (file)
@@ -203,7 +203,7 @@ can directly edit the object $result.
 Example file:
 
 --
-<?PHP
+<?php
 
     // Set the zip code and the adress
     if ($_SERVER[$this->config->field_map_address] != '')
index e57dc7e..219b530 100644 (file)
@@ -47,7 +47,7 @@ class backup_dbops_testcase extends advanced_testcase {
         $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id), array('section'=>3));
         $coursemodule = $DB->get_record('course_modules', array('id'=>$page->cmid));
 
-        $this->moduleid  = $coursemodule->id;
+        $this->moduleid  = $page->cmid;
         $this->sectionid = $DB->get_field("course_sections", 'id', array("section"=>$coursemodule->section, "course"=>$course->id));
         $this->courseid  = $coursemodule->course;
         $this->userid = 2; // admin
@@ -180,7 +180,7 @@ class backup_dbops_testcase extends advanced_testcase {
         $this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 0);
 
         // A MODE_SAMESITE controller - should not include files
-        $bc = new mock_backup_controller4dbops(backup::TYPE_1COURSE, $this->moduleid, backup::FORMAT_MOODLE,
+        $bc = new mock_backup_controller4dbops(backup::TYPE_1COURSE, $this->courseid, backup::FORMAT_MOODLE,
             backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $this->userid);
         $this->assertEquals(backup_controller_dbops::backup_includes_files($bc->get_backupid()), 0);
     }
index 6f5b5b5..acfb7b3 100644 (file)
@@ -167,18 +167,21 @@ class restore_plan extends base_plan implements loggable {
         parent::execute();
         $this->controller->set_status(backup::STATUS_FINISHED_OK);
 
-        // Trigger a course restored event.
-        $event = \core\event\course_restored::create(array(
-            'objectid' => $this->get_courseid(),
-            'userid' => $this->get_userid(),
-            'context' => context_course::instance($this->get_courseid()),
-            'other' => array('type' => $this->controller->get_type(),
-                             'target' => $this->controller->get_target(),
-                             'mode' => $this->controller->get_mode(),
-                             'operation' => $this->controller->get_operation(),
-                             'samesite' => $this->controller->is_samesite())
-        ));
-        $event->trigger();
+        // Check if we are restoring a course.
+        if ($this->controller->get_type() === backup::TYPE_1COURSE) {
+            // Trigger a course restored event.
+            $event = \core\event\course_restored::create(array(
+                'objectid' => $this->get_courseid(),
+                'userid' => $this->get_userid(),
+                'context' => context_course::instance($this->get_courseid()),
+                'other' => array('type' => $this->controller->get_type(),
+                                 'target' => $this->controller->get_target(),
+                                 'mode' => $this->controller->get_mode(),
+                                 'operation' => $this->controller->get_operation(),
+                                 'samesite' => $this->controller->is_samesite())
+            ));
+            $event->trigger();
+        }
     }
 
     /**
index c8870cc..93a51a7 100644 (file)
@@ -35,8 +35,22 @@ require_once($CFG->dirroot . '/backup/util/xml/output/memory_xml_output.class.ph
  */
 class backup_structure_testcase extends advanced_testcase {
 
-    protected $forumid;   // To store the inserted forum->id
-    protected $contextid; // Official contextid for these tests
+    /** @var int Store the inserted forum->id for use in test functions */
+    protected $forumid;
+    /** @var int Store the inserted discussion1->id for use in test functions */
+    protected $discussionid1;
+    /** @var int Store the inserted discussion2->id for use in test functions */
+    protected $discussionid2;
+    /** @var int Store the inserted post1->id for use in test functions */
+    protected $postid1;
+    /** @var int Store the inserted post2->id for use in test functions */
+    protected $postid2;
+    /** @var int Store the inserted post3->id for use in test functions */
+    protected $postid3;
+    /** @var int Store the inserted post4->id for use in test functions */
+    protected $postid4;
+    /** @var int Official contextid for these tests */
+    protected $contextid;
 
 
     protected function setUp() {
@@ -72,30 +86,30 @@ class backup_structure_testcase extends advanced_testcase {
 
         // Create two discussions
         $discussion1 = (object)array('course' => 1, 'forum' => $this->forumid, 'name' => 'd1', 'userid' => 100, 'groupid' => 200);
-        $d1id = $DB->insert_record('forum_discussions', $discussion1);
+        $this->discussionid1 = $DB->insert_record('forum_discussions', $discussion1);
         $discussion2 = (object)array('course' => 1, 'forum' => $this->forumid, 'name' => 'd2', 'userid' => 101, 'groupid' => 201);
-        $d2id = $DB->insert_record('forum_discussions', $discussion2);
+        $this->discussionid2 = $DB->insert_record('forum_discussions', $discussion2);
 
         // Create four posts
-        $post1 = (object)array('discussion' => $d1id, 'userid' => 100, 'subject' => 'p1', 'message' => 'm1');
-        $p1id = $DB->insert_record('forum_posts', $post1);
-        $post2 = (object)array('discussion' => $d1id, 'parent' => $p1id, 'userid' => 102, 'subject' => 'p2', 'message' => 'm2');
-        $p2id = $DB->insert_record('forum_posts', $post2);
-        $post3 = (object)array('discussion' => $d1id, 'parent' => $p2id, 'userid' => 103, 'subject' => 'p3', 'message' => 'm3');
-        $p3id = $DB->insert_record('forum_posts', $post3);
-        $post4 = (object)array('discussion' => $d2id, 'userid' => 101, 'subject' => 'p4', 'message' => 'm4');
-        $p4id = $DB->insert_record('forum_posts', $post4);
+        $post1 = (object)array('discussion' => $this->discussionid1, 'userid' => 100, 'subject' => 'p1', 'message' => 'm1');
+        $this->postid1 = $DB->insert_record('forum_posts', $post1);
+        $post2 = (object)array('discussion' => $this->discussionid1, 'parent' => $this->postid1, 'userid' => 102, 'subject' => 'p2', 'message' => 'm2');
+        $this->postid2 = $DB->insert_record('forum_posts', $post2);
+        $post3 = (object)array('discussion' => $this->discussionid1, 'parent' => $this->postid2, 'userid' => 103, 'subject' => 'p3', 'message' => 'm3');
+        $this->postid3 = $DB->insert_record('forum_posts', $post3);
+        $post4 = (object)array('discussion' => $this->discussionid2, 'userid' => 101, 'subject' => 'p4', 'message' => 'm4');
+        $this->postid4 = $DB->insert_record('forum_posts', $post4);
         // With two related file
         $f1_post1 = (object)array(
             'contenthash' => 'testp1', 'contextid' => $this->contextid, 'component'=>'mod_forum',
-            'filearea' => 'post', 'filename' => 'tp1', 'itemid' => $p1id,
+            'filearea' => 'post', 'filename' => 'tp1', 'itemid' => $this->postid1,
             'filesize' => 123, 'timecreated' => 0, 'timemodified' => 0,
             'pathnamehash' => 'testp1'
         );
         $DB->insert_record('files', $f1_post1);
         $f1_post2 = (object)array(
             'contenthash' => 'testp2', 'contextid' => $this->contextid, 'component'=>'mod_forum',
-            'filearea' => 'attachment', 'filename' => 'tp2', 'itemid' => $p2id,
+            'filearea' => 'attachment', 'filename' => 'tp2', 'itemid' => $this->postid2,
             'filesize' => 123, 'timecreated' => 0, 'timemodified' => 0,
             'pathnamehash' => 'testp2'
         );
@@ -103,16 +117,16 @@ class backup_structure_testcase extends advanced_testcase {
 
         // Create two ratings
         $rating1 = (object)array(
-            'contextid' => $this->contextid, 'userid' => 104, 'itemid' => $p1id, 'rating' => 2,
+            'contextid' => $this->contextid, 'userid' => 104, 'itemid' => $this->postid1, 'rating' => 2,
             'scaleid' => -1, 'timecreated' => time(), 'timemodified' => time());
         $r1id = $DB->insert_record('rating', $rating1);
         $rating2 = (object)array(
-            'contextid' => $this->contextid, 'userid' => 105, 'itemid' => $p1id, 'rating' => 3,
+            'contextid' => $this->contextid, 'userid' => 105, 'itemid' => $this->postid1, 'rating' => 3,
             'scaleid' => -1, 'timecreated' => time(), 'timemodified' => time());
         $r2id = $DB->insert_record('rating', $rating2);
 
         // Create 1 reads
-        $read1 = (object)array('userid' => 102, 'forumid' => $this->forumid, 'discussionid' => $d2id, 'postid' => $p4id);
+        $read1 = (object)array('userid' => 102, 'forumid' => $this->forumid, 'discussionid' => $this->discussionid2, 'postid' => $this->postid4);
         $DB->insert_record('forum_read', $read1);
     }
 
@@ -195,11 +209,11 @@ class backup_structure_testcase extends advanced_testcase {
 
         // Let's add 1 optigroup with 4 elements
         $alternative1 = new backup_optigroup_element('alternative1',
-            array('name', 'value'), '../../id', 1);
+            array('name', 'value'), '../../id', $this->postid1);
         $alternative2 = new backup_optigroup_element('alternative2',
-            array('name', 'value'), backup::VAR_PARENTID, 2);
+            array('name', 'value'), backup::VAR_PARENTID, $this->postid2);
         $alternative3 = new backup_optigroup_element('alternative3',
-            array('name', 'value'), '/forum/discussions/discussion/posts/post/id', 3);
+            array('name', 'value'), '/forum/discussions/discussion/posts/post/id', $this->postid3);
         $alternative4 = new backup_optigroup_element('alternative4',
             array('forumtype', 'forumname')); // Alternative without conditions
         // Create the optigroup, adding one element
@@ -239,7 +253,7 @@ class backup_structure_testcase extends advanced_testcase {
             array(backup::VAR_PARENTID)
         );
 
-        $read->set_source_table('forum_read', array('id' => '../../id'));
+        $read->set_source_table('forum_read', array('forumid' => '../../id'));
 
         $inventeds->set_source_array(array((object)array('reason' => 'I love Moodle', 'version' => '1.0'),
             (object)array('reason' => 'I love Moodle', 'version' => '2.0'))); // 2 object array
@@ -334,83 +348,83 @@ class backup_structure_testcase extends advanced_testcase {
                     $ratarr[$node->nodeName] = $node->nodeValue;
                 }
             }
-            $this->assertEquals($ratarr['userid'], $DB->get_field('rating', 'userid', array('id' => $ratarr['id'])));
-            $this->assertEquals($ratarr['itemid'], $DB->get_field('rating', 'itemid', array('id' => $ratarr['id'])));
-            $this->assertEquals($ratarr['post_rating'], $DB->get_field('rating', 'rating', array('id' => $ratarr['id'])));
+            $this->assertEquals($DB->get_field('rating', 'userid', array('id' => $ratarr['id'])), $ratarr['userid']);
+            $this->assertEquals($DB->get_field('rating', 'itemid', array('id' => $ratarr['id'])), $ratarr['itemid']);
+            $this->assertEquals($DB->get_field('rating', 'rating', array('id' => $ratarr['id'])), $ratarr['post_rating']);
         }
 
         // Check forum has "blockeperiod" with value 0 (was declared by object instead of name)
         $query = '/forum[blockperiod="0"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check forum is missing "completiondiscussions" (as we are using mock_skip_final_element)
         $query = '/forum/completiondiscussions';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
+        $this->assertEquals(0, $result->length);
 
         // Check forum has "completionreplies" with value "original was 0, now changed" (because of mock_modify_final_element)
         $query = '/forum[completionreplies="original was 0, now changed"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check forum has "completionposts" with value "intercepted!" (because of mock_final_element_interceptor)
         $query = '/forum[completionposts="intercepted!"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check there isn't any alternative2 tag, as far as it hasn't source defined
         $query = '//alternative2';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
+        $this->assertEquals(0, $result->length);
 
         // Check there are 4 "field1" elements
         $query = '/forum/discussions/discussion/posts/post//field1';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 4);
+        $this->assertEquals(4, $result->length);
 
         // Check first post has one name element with value "alternative1"
-        $query = '/forum/discussions/discussion/posts/post[@id="1"][name="alternative1"]';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid1.'"][name="alternative1"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check there are two "dupetest1" elements
         $query = '/forum/discussions/discussion/posts/post//dupetest1';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 2);
+        $this->assertEquals(2, $result->length);
 
         // Check second post has one name element with value "dupetest2"
-        $query = '/forum/discussions/discussion/posts/post[@id="2"]/dupetest2';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/dupetest2';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check element "dupetest2" of second post has one field1 element with value "2"
-        $query = '/forum/discussions/discussion/posts/post[@id="2"]/dupetest2[field1="2"]';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/dupetest2[field1="2"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check forth post has no name element
-        $query = '/forum/discussions/discussion/posts/post[@id="4"]/name';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid4.'"]/name';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
+        $this->assertEquals(0, $result->length);
 
         // Check 1st, 2nd and 3rd posts have no forumtype element
-        $query = '/forum/discussions/discussion/posts/post[@id="1"]/forumtype';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid1.'"]/forumtype';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
-        $query = '/forum/discussions/discussion/posts/post[@id="2"]/forumtype';
+        $this->assertEquals(0, $result->length);
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid2.'"]/forumtype';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
-        $query = '/forum/discussions/discussion/posts/post[@id="3"]/forumtype';
+        $this->assertEquals(0, $result->length);
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid3.'"]/forumtype';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 0);
+        $this->assertEquals(0, $result->length);
 
         // Check 4th post has one forumtype element with value "general"
         // (because it doesn't matches alternatives 1, 2, 3, then alternative 4,
         // the one without conditions is being applied)
-        $query = '/forum/discussions/discussion/posts/post[@id="4"][forumtype="general"]';
+        $query = '/forum/discussions/discussion/posts/post[@id="'.$this->postid4.'"][forumtype="general"]';
         $result = $xpath->query($query);
-        $this->assertEquals($result->length, 1);
+        $this->assertEquals(1, $result->length);
 
         // Check annotations information against DB
         // Count records in original tables
index a297c48..e705270 100644 (file)
@@ -244,9 +244,20 @@ class core_backup_renderer extends plugin_renderer_base {
         global $CFG, $PAGE;
         require_once($CFG->dirroot.'/course/lib.php');
 
+        // These variables are used to check if the form using this function was submitted.
+        $target = optional_param('target', false, PARAM_INT);
+        $targetid = optional_param('targetid', null, PARAM_INT);
+
+        // Check if they submitted the form but did not provide all the data we need.
+        $missingdata = false;
+        if ($target and is_null($targetid)) {
+            $missingdata = true;
+        }
+
         $nextstageurl->param('sesskey', sesskey());
 
-        $form = html_writer::start_tag('form', array('method'=>'post', 'action'=>$nextstageurl->out_omit_querystring()));
+        $form = html_writer::start_tag('form', array('method' => 'post', 'action' => $nextstageurl->out_omit_querystring(),
+            'class' => 'mform'));
         foreach ($nextstageurl->params() as $key=>$value) {
             $form .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>$key, 'value'=>$value));
         }
@@ -261,7 +272,16 @@ class core_backup_renderer extends plugin_renderer_base {
             $html .= html_writer::start_tag('div', array('class'=>'bcs-new-course backup-section'));
             $html .= $this->output->heading(get_string('restoretonewcourse', 'backup'), 2, array('class'=>'header'));
             $html .= $this->backup_detail_input(get_string('restoretonewcourse', 'backup'), 'radio', 'target', backup::TARGET_NEW_COURSE, array('checked'=>'checked'));
-            $html .= $this->backup_detail_pair(get_string('selectacategory', 'backup'), $this->render($categories));
+            $selectacategoryhtml = $this->backup_detail_pair(get_string('selectacategory', 'backup'), $this->render($categories));
+            // Display the category selection as required if the form was submitted but this data was not supplied.
+            if ($missingdata && $target == backup::TARGET_NEW_COURSE) {
+                $html .= html_writer::span(get_string('required'), 'error');
+                $html .= html_writer::start_tag('fieldset', array('class' => 'error'));
+                $html .= $selectacategoryhtml;
+                $html .= html_writer::end_tag('fieldset');
+            } else {
+                $html .= $selectacategoryhtml;
+            }
             $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('continue'))));
             $html .= html_writer::end_tag('div');
             $html .= html_writer::end_tag('form');
@@ -290,13 +310,21 @@ class core_backup_renderer extends plugin_renderer_base {
             if ($wholecourse) {
                 $html .= $this->backup_detail_input(get_string('restoretoexistingcourseadding', 'backup'), 'radio', 'target', backup::TARGET_EXISTING_ADDING, array('checked'=>'checked'));
                 $html .= $this->backup_detail_input(get_string('restoretoexistingcoursedeleting', 'backup'), 'radio', 'target', backup::TARGET_EXISTING_DELETING);
-                $html .= $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
             } else {
                 // We only allow restore adding to existing for now. Enforce it here.
                 $html .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'target', 'value'=>backup::TARGET_EXISTING_ADDING));
                 $courses->invalidate_results(); // Clean list of courses
                 $courses->set_include_currentcourse(); // Show current course in the list
-                $html .= $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
+            }
+            $selectacoursehtml = $this->backup_detail_pair(get_string('selectacourse', 'backup'), $this->render($courses));
+            // Display the course selection as required if the form was submitted but this data was not supplied.
+            if ($missingdata && $target == backup::TARGET_EXISTING_ADDING) {
+                $html .= html_writer::span(get_string('required'), 'error');
+                $html .= html_writer::start_tag('fieldset', array('class' => 'error'));
+                $html .= $selectacoursehtml;
+                $html .= html_writer::end_tag('fieldset');
+            } else {
+                $html .= $selectacoursehtml;
             }
             $html .= $this->backup_detail_pair('', html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('continue'))));
             $html .= html_writer::end_tag('div');
index cead2b2..ecfa3b9 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
index cf1c1de..2db2265 100644 (file)
@@ -326,6 +326,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('associateid' => 2 , 'blogid' => 3, 'subject' => 'blog subject')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'associatetype\' value must be set in other and be a valid type.', $e->getMessage());
@@ -334,6 +335,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('associateid' => 2 , 'blogid' => 3, 'associatetype' => 'random', 'subject' => 'blog subject')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'associatetype\' value must be set in other and be a valid type.', $e->getMessage());
@@ -343,6 +345,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('blogid' => 3, 'associatetype' => 'course', 'subject' => 'blog subject')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'associateid\' value must be set in other.', $e->getMessage());
@@ -352,6 +355,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('associateid' => 3, 'associatetype' => 'course', 'subject' => 'blog subject')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'blogid\' value must be set in other.', $e->getMessage());
@@ -361,6 +365,7 @@ class core_bloglib_testcase extends advanced_testcase {
             \core\event\blog_association_created::create(array(
                 'contextid' => 1,
                 'objectid' => 3,
+                'relateduserid' => 2,
                 'other' => array('blogid' => 3, 'associateid' => 3, 'associatetype' => 'course')));
         } catch (coding_exception $e) {
             $this->assertContains('The \'subject\' value must be set in other.', $e->getMessage());
index 7745f0e..083f4e6 100644 (file)
@@ -266,3 +266,7 @@ There are a couple of considerations to using this method:
 * If you have configured your cache before setting $CFG->altcacheconfigpath you will need to copy it from moodledata/muc/config.php to the destination you specified.
 * This allows you to share a cache config between sites.
 * It also allows you to use unit tests to test your sites cache config.
+
+Please be aware that if you are using Memcache or Memcached it is recommended to use dedicated Memcached servers.
+When caches get purged the memcached servers you have configured get purged, any data stored within them whether it belongs to Moodle or not will be removed.
+If you are using Memcached for sessions as well as caching/testing and caches get purged your sessions will be removed prematurely and users will be need to start again.
\ No newline at end of file
index 82b1295..51de3b8 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
 ///////////////////////////////////////////////////////////////////////////
 //                                                                       //
 // Moodle configuration file                                             //
@@ -248,6 +248,11 @@ $CFG->admin = 'admin';
 //         less reliable. Use memcached where possible or if you encounter
 //         session problems. **
 //
+// Please be aware that when selecting either Memcached or Memcache for sessions that it is advised to use a dedicated
+// memcache server. The memcache and memcached extensions do not provide isolated environments for individual uses.
+// Using the same server for other purposes (MUC for example) can lead to sessions being prematurely removed should
+// the other uses of the server purge the cache.
+//
 // Following setting allows you to alter how frequently is timemodified updated in sessions table.
 //      $CFG->session_update_timemodified_frequency = 20; // In seconds.
 //
index 5d14623..16d51dc 100644 (file)
@@ -648,16 +648,7 @@ class dndupload_ajax_processor {
         $mod = $info->get_cm($this->cm->id);
 
         // Trigger course module created event.
-        $event = \core\event\course_module_created::create(array(
-            'courseid' => $this->course->id,
-            'context'  => context_module::instance($mod->id),
-            'objectid' => $mod->id,
-            'other'    => array(
-                'modulename' => $mod->modname,
-                'name'       => $mod->name,
-                'instanceid' => $instanceid
-            )
-        ));
+        $event = \core\event\course_module_created::create_from_cm($mod);
         $event->trigger();
 
         $this->send_response($mod);
index 6c13a7d..ffde87b 100644 (file)
@@ -3418,10 +3418,12 @@ function update_module($moduleinfo) {
 }
 
 /**
- * Duplicate a module on the course.
+ * Duplicate a module on the course for ajax.
  *
+ * @see mod_duplicate_module()
  * @param object $course The course
  * @param object $cm The course module to duplicate
+ * @param int $sr The section to link back to (used for creating the links)
  * @throws moodle_exception if the plugin doesn't support duplication
  * @return Object containing:
  * - fullcontent: The HTML markup for the created CM
@@ -3429,8 +3431,42 @@ function update_module($moduleinfo) {
  * - redirect: Whether to trigger a redirect following this change
  */
 function mod_duplicate_activity($course, $cm, $sr = null) {
-    global $CFG, $USER, $PAGE, $DB;
+    global $PAGE;
+
+    $newcm = duplicate_module($course, $cm);
+
+    $resp = new stdClass();
+    if ($newcm) {
+        $courserenderer = $PAGE->get_renderer('core', 'course');
+        $completioninfo = new completion_info($course);
+        $modulehtml = $courserenderer->course_section_cm($course, $completioninfo,
+                $newcm, null, array());
 
+        $resp->fullcontent = $courserenderer->course_section_cm_list_item($course, $completioninfo, $newcm, $sr);
+        $resp->cmid = $newcm->id;
+    } else {
+        // Trigger a redirect.
+        $resp->redirect = true;
+    }
+    return $resp;
+}
+
+/**
+ * Api to duplicate a module.
+ *
+ * @param object $course course object.
+ * @param object $cm course module object to be duplicated.
+ * @since Moodle 2.8
+ *
+ * @throws Exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ * @throws restore_controller_exception
+ *
+ * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm.
+ */
+function duplicate_module($course, $cm) {
+    global $CFG, $DB, $USER;
     require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
     require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
     require_once($CFG->libdir . '/filelib.php');
@@ -3440,10 +3476,10 @@ function mod_duplicate_activity($course, $cm, $sr = null) {
     $a->modname = format_string($cm->name);
 
     if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
-        throw new moodle_exception('duplicatenosupport', 'error');
+        throw new moodle_exception('duplicatenosupport', 'error', '', $a);
     }
 
-    // backup the activity
+    // Backup the activity.
 
     $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE,
             backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
@@ -3455,7 +3491,7 @@ function mod_duplicate_activity($course, $cm, $sr = null) {
 
     $bc->destroy();
 
-    // restore the backup immediately
+    // Restore the backup immediately.
 
     $rc = new restore_controller($backupid, $course->id,
             backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
@@ -3472,31 +3508,32 @@ function mod_duplicate_activity($course, $cm, $sr = null) {
 
     $rc->execute_plan();
 
-    // now a bit hacky part follows - we try to get the cmid of the newly
-    // restored copy of the module
+    // Now a bit hacky part follows - we try to get the cmid of the newly
+    // restored copy of the module.
     $newcmid = null;
     $tasks = $rc->get_plan()->get_tasks();
     foreach ($tasks as $task) {
-        error_log("Looking at a task");
         if (is_subclass_of($task, 'restore_activity_task')) {
-            error_log("Looking at a restore_activity_task task");
             if ($task->get_old_contextid() == $cmcontext->id) {
-                error_log("Contexts match");
                 $newcmid = $task->get_moduleid();
                 break;
             }
         }
     }
 
-    // if we know the cmid of the new course module, let us move it
+    // If we know the cmid of the new course module, let us move it
     // right below the original one. otherwise it will stay at the
-    // end of the section
+    // end of the section.
     if ($newcmid) {
         $info = get_fast_modinfo($course);
         $newcm = $info->get_cm($newcmid);
         $section = $DB->get_record('course_sections', array('id' => $cm->section, 'course' => $cm->course));
         moveto_module($newcm, $section, $cm);
         moveto_module($cm, $section, $newcm);
+
+        // Trigger course module created event. We can trigger the event only if we know the newcmid.
+        $event = \core\event\course_module_created::create_from_cm($newcm);
+        $event->trigger();
     }
     rebuild_course_cache($cm->course);
 
@@ -3506,20 +3543,7 @@ function mod_duplicate_activity($course, $cm, $sr = null) {
         fulldelete($backupbasepath);
     }
 
-    $resp = new stdClass();
-    if ($newcm) {
-        $courserenderer = $PAGE->get_renderer('core', 'course');
-        $completioninfo = new completion_info($course);
-        $modulehtml = $courserenderer->course_section_cm($course, $completioninfo,
-                $newcm, null, array());
-
-        $resp->fullcontent = $courserenderer->course_section_cm_list_item($course, $completioninfo, $newcm, $sr);
-        $resp->cmid = $newcm->id;
-    } else {
-        // Trigger a redirect
-        $resp->redirect = true;
-    }
-    return $resp;
+    return isset($newcm) ? $newcm : null;
 }
 
 /**
index 7247671..94e9e00 100644 (file)
@@ -28,9 +28,6 @@
  */
 
 require_once(dirname(dirname(__FILE__)) . '/config.php');
-require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
-require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
-require_once($CFG->libdir . '/filelib.php');
 
 $cmid           = required_param('cmid', PARAM_INT);
 $courseid       = required_param('course', PARAM_INT);
@@ -56,85 +53,20 @@ $PAGE->set_pagelayout('incourse');
 
 $output = $PAGE->get_renderer('core', 'backup');
 
+// Duplicate the module.
+$newcm = duplicate_module($course, $cm);
+
+echo $output->header();
+
 $a          = new stdClass();
 $a->modtype = get_string('modulename', $cm->modname);
 $a->modname = format_string($cm->name);
 
-if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
-    $url = course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn));
-    print_error('duplicatenosupport', 'error', $url, $a);
-}
-
-// backup the activity
-
-$bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE,
-        backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
-
-$backupid       = $bc->get_backupid();
-$backupbasepath = $bc->get_plan()->get_basepath();
-
-$bc->execute_plan();
-
-$bc->destroy();
-
-// restore the backup immediately
-
-$rc = new restore_controller($backupid, $courseid,
-        backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
-
-if (!$rc->execute_precheck()) {
-    $precheckresults = $rc->get_precheck_results();
-    if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
-        if (empty($CFG->keeptempdirectoriesonbackup)) {
-            fulldelete($backupbasepath);
-        }
-
-        echo $output->header();
-        echo $output->precheck_notices($precheckresults);
-        $url = course_get_url($course, $cm->sectionnum, array('sr' => $sectionreturn));
-        echo $output->continue_button($url);
-        echo $output->footer();
-        die();
-    }
-}
-
-$rc->execute_plan();
-
-// now a bit hacky part follows - we try to get the cmid of the newly
-// restored copy of the module
-$newcmid = null;
-$tasks = $rc->get_plan()->get_tasks();
-foreach ($tasks as $task) {
-    if (is_subclass_of($task, 'restore_activity_task')) {
-        if ($task->get_old_contextid() == $cmcontext->id) {
-            $newcmid = $task->get_moduleid();
-            break;
-        }
-    }
-}
-
-// if we know the cmid of the new course module, let us move it
-// right below the original one. otherwise it will stay at the
-// end of the section
-if ($newcmid) {
-    $newcm = get_coursemodule_from_id('', $newcmid, $course->id, true, MUST_EXIST);
-    moveto_module($newcm, $section, $cm);
-    moveto_module($cm, $section, $newcm);
-}
-
-$rc->destroy();
-
-if (empty($CFG->keeptempdirectoriesonbackup)) {
-    fulldelete($backupbasepath);
-}
-
-echo $output->header();
-
-if ($newcmid) {
+if (!empty($newcm)) {
     echo $output->confirm(
         get_string('duplicatesuccess', 'core', $a),
         new single_button(
-            new moodle_url('/course/modedit.php', array('update' => $newcmid, 'sr' => $sectionreturn)),
+            new moodle_url('/course/modedit.php', array('update' => $newcm->id, 'sr' => $sectionreturn)),
             get_string('duplicatecontedit'),
             'get'),
         new single_button(
index 2e03ff6..208a9ba 100644 (file)
@@ -148,16 +148,11 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) {
     $sectionid = course_add_cm_to_section($course, $moduleinfo->coursemodule, $moduleinfo->section);
 
     // Trigger event based on the action we did.
-    $event = \core\event\course_module_created::create(array(
-         'courseid' => $course->id,
-         'context'  => $modcontext,
-         'objectid' => $moduleinfo->coursemodule,
-         'other'    => array(
-             'modulename' => $moduleinfo->modulename,
-             'name'       => $moduleinfo->name,
-             'instanceid' => $moduleinfo->instance
-         )
-    ));
+    // Api create_from_cm expects modname and id property, and we don't want to modify $moduleinfo since we are returning it.
+    $eventdata = clone $moduleinfo;
+    $eventdata->modname = $eventdata->modulename;
+    $eventdata->id = $eventdata->coursemodule;
+    $event = \core\event\course_module_created::create_from_cm($eventdata, $modcontext);
     $event->trigger();
 
     $moduleinfo = edit_module_post_actions($moduleinfo, $course);
index 8450ceb..8780286 100644 (file)
@@ -69,9 +69,13 @@ class core_course_renderer extends plugin_renderer_base {
      */
     protected function add_modchoosertoggle() {
         global $CFG;
-        static $modchoosertoggleadded = false;
-        // Add the module chooser toggle to the course page
-        if ($modchoosertoggleadded || $this->page->state > moodle_page::STATE_PRINTING_HEADER ||
+
+        // Only needs to be done once per page.
+        if (!$this->page->requires->should_create_one_time_item_now('core_course_modchoosertoggle')) {
+            return;
+        }
+
+        if ($this->page->state > moodle_page::STATE_PRINTING_HEADER ||
                 $this->page->course->id == SITEID ||
                 !$this->page->user_is_editing() ||
                 !($context = context_course::instance($this->page->course->id)) ||
@@ -79,11 +83,11 @@ class core_course_renderer extends plugin_renderer_base {
                 !course_ajax_enabled($this->page->course) ||
                 !($coursenode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE)) ||
                 !($turneditingnode = $coursenode->get('turneditingonoff'))) {
-            // too late or we are on site page or we could not find the adjacent nodes in course settings menu
-            // or we are not allowed to edit
+            // Too late, or we are on site page, or we could not find the
+            // adjacent nodes in course settings menu, or we are not allowed to edit.
             return;
         }
-        $modchoosertoggleadded = true;
+
         if ($this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
             // We are on the course page, retain the current page params e.g. section.
             $modchoosertoggleurl = clone($this->page->url);
@@ -168,11 +172,9 @@ class core_course_renderer extends plugin_renderer_base {
      * @return string The composed HTML for the module
      */
     public function course_modchooser($modules, $course) {
-        static $isdisplayed = false;
-        if ($isdisplayed) {
+        if (!$this->page->requires->should_create_one_time_item_now('core_course_modchooser')) {
             return '';
         }
-        $isdisplayed = true;
 
         // Add the module chooser
         $this->page->requires->yui_module('moodle-course-modchooser',
@@ -1492,14 +1494,14 @@ class core_course_renderer extends plugin_renderer_base {
      * Make sure that javascript file for AJAX expanding of courses and categories content is included
      */
     protected function coursecat_include_js() {
-        global $CFG;
-        static $jsloaded = false;
-        if (!$jsloaded) {
-            // We must only load this module once.
-            $this->page->requires->yui_module('moodle-course-categoryexpander',
-                    'Y.Moodle.course.categoryexpander.init');
-            $jsloaded = true;
+        if (!$this->page->requires->should_create_one_time_item_now('core_course_categoryexpanderjsinit')) {
+            return;
         }
+
+        // We must only load this module once.
+        $this->page->requires->set_required_html_output('core_course_categoryexpanderjsinit');
+        $this->page->requires->yui_module('moodle-course-categoryexpander',
+                'Y.Moodle.course.categoryexpander.init');
     }
 
     /**
index 8f85102..b2e9fa4 100644 (file)
@@ -1958,9 +1958,8 @@ class core_course_courselib_testcase extends advanced_testcase {
         $modinfo = $this->create_specific_module_test('assign');
         $events = $sink->get_events();
         $event = array_pop($events);
-        $sink->close();
 
-        $cm = $DB->get_record('course_modules', array('id' => $modinfo->coursemodule), '*', MUST_EXIST);
+        $cm = get_coursemodule_from_id('assign', $modinfo->coursemodule, 0, false, MUST_EXIST);
         $mod = $DB->get_record('assign', array('id' => $modinfo->instance), '*', MUST_EXIST);
 
         // Validate event data.
@@ -1988,6 +1987,21 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($arr, $event);
         $this->assertEventContextNotUsed($event);
 
+        // Let us see if duplicating an activity results in a nice course module created event.
+        $sink->clear();
+        $course = get_course($mod->course);
+        $newcm = duplicate_module($course, $cm);
+        $events = $sink->get_events();
+        $event = array_pop($events);
+        $sink->close();
+
+        // Validate event data.
+        $this->assertInstanceOf('\core\event\course_module_created', $event);
+        $this->assertEquals($newcm->id, $event->objectid);
+        $this->assertEquals($USER->id, $event->userid);
+        $this->assertEquals($course->id, $event->courseid);
+        $url = new moodle_url('/mod/assign/view.php', array('id' => $newcm->id));
+        $this->assertEquals($url, $event->get_url());
     }
 
     /**
@@ -2477,4 +2491,26 @@ class core_course_courselib_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($expectedlegacydata, $event);
         $this->assertEventContextNotUsed($event);
     }
+
+    /**
+     * Test duplicate_module()
+     */
+    public function test_duplicate_module() {
+        $this->setAdminUser();
+        $this->resetAfterTest();
+        $course = self::getDataGenerator()->create_course();
+        $res = self::getDataGenerator()->create_module('resource', array('course' => $course));
+        $cm = get_coursemodule_from_id('resource', $res->cmid, 0, false, MUST_EXIST);
+
+        $newcm = duplicate_module($course, $cm);
+
+        // Make sure they are the same, except obvious id changes.
+        foreach ($cm as $prop => $value) {
+            if ($prop == 'id' || $prop == 'url' || $prop == 'instance' || $prop == 'added') {
+                // Ignore obviously different properties.
+                continue;
+            }
+            $this->assertEquals($value, $newcm->$prop);
+        }
+    }
 }
index f4e5ffa..7623b14 100644 (file)
@@ -102,7 +102,6 @@ class core_files_renderer extends plugin_renderer_base {
      * @return string HTML fragment
      */
     public function render_form_filemanager($fm) {
-        static $filemanagertemplateloaded;
         $html = $this->fm_print_generallayout($fm);
         $module = array(
             'name'=>'form_filemanager',
@@ -117,8 +116,7 @@ class core_files_renderer extends plugin_renderer_base {
                 array('confirmrenamefile', 'repository'), array('newfolder', 'repository'), array('edit', 'moodle')
             )
         );
-        if (empty($filemanagertemplateloaded)) {
-            $filemanagertemplateloaded = true;
+        if ($this->page->requires->should_create_one_time_item_now('core_file_managertemplate')) {
             $this->page->requires->js_init_call('M.form_filemanager.set_templates',
                     array($this->filemanager_js_templates()), true, $module);
         }
index 3939977..2ee949a 100644 (file)
@@ -342,7 +342,7 @@ http://www.forkosh.com/mimetex.zip</a>, or looking for an appropriate
 binary at <a href="http://moodle.org/download/mimetex/">
 http://moodle.org/download/mimetex/</a>. You may then also need to
 edit your moodle/filter/algebra/pix.php file to add
-<br /><?PHP echo "case &quot;" . PHP_OS . "&quot;:" ;?><br ?> to the list of operating systems
+<br /><?php echo "case &quot;" . PHP_OS . "&quot;:" ;?><br ?> to the list of operating systems
 in the switch (PHP_OS) statement. Windows users may have a problem properly
 unzipping mimetex.exe. Make sure that mimetex.exe is is <b>PRECISELY</b>
 433152 bytes in size. If not, download fresh copy from
index c4f29d9..8172b78 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
       // This function fetches math. images from the data directory
       // If not, it obtains the corresponding TeX expression from the cache_tex db table
       // and uses mimeTeX to create the image file
index a79a33e..b56336a 100644 (file)
@@ -94,6 +94,7 @@
             if (empty($pathlatex)) {
                 return false;
             }
+            $pathlatex = escapeshellarg(trim($pathlatex, " '\""));
 
             $doc = $this->construct_latex_document( $formula, $fontsize );
 
             }
 
             // run dvips (.dvi to .ps)
-            $pathdvips = get_config('filter_tex', 'pathdvips');
+            $pathdvips = escapeshellarg(trim(get_config('filter_tex', 'pathdvips'), " '\""));
             $command = "{$pathdvips} -E $dvi -o $ps";
             if ($this->execute($command, $log )) {
                 return false;
             } else {
                 $bg_opt = "";
             }
-            $pathconvert = get_config('filter_tex', 'pathconvert');
+            $pathconvert = escapeshellarg(trim(get_config('filter_tex', 'pathconvert'), " '\""));
             $command = "{$pathconvert} -density $density -trim $bg_opt $ps $img";
             if ($this->execute($command, $log )) {
                 return false;
index b77dfae..129ae29 100644 (file)
@@ -125,8 +125,9 @@ function filter_tex_updatedcallback($name) {
         return;
     }
 
-    $pathdvips = get_config('filter_tex', 'pathdvips');
-    $pathconvert = get_config('filter_tex', 'pathconvert');
+    $pathlatex = trim($pathlatex, " '\"");
+    $pathdvips = trim(get_config('filter_tex', 'pathdvips'), " '\"");
+    $pathconvert = trim(get_config('filter_tex', 'pathconvert'), " '\"");
 
     if (!(is_file($pathlatex) && is_executable($pathlatex) &&
           is_file($pathdvips) && is_executable($pathdvips) &&
index 4249e7f..43c9d1a 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
       // This function fetches math. images from the data directory
       // If not, it obtains the corresponding TeX expression from the cache_tex db table
       // and uses mimeTeX to create the image file
index 6c79a21..00ea31a 100644 (file)
@@ -50,9 +50,9 @@ if ($ADMIN->fulltree) {
     } else if (PHP_OS=='WINNT' or PHP_OS=='WIN32' or PHP_OS=='Windows') {
         // note: you need Ghostscript installed (standard), miktex (standard)
         // and ImageMagick (install at c:\ImageMagick)
-        $default_filter_tex_pathlatex   = "\"c:\\texmf\\miktex\\bin\\latex.exe\" ";
-        $default_filter_tex_pathdvips   = "\"c:\\texmf\\miktex\\bin\\dvips.exe\" ";
-        $default_filter_tex_pathconvert = "\"c:\\imagemagick\\convert.exe\" ";
+        $default_filter_tex_pathlatex   = "c:\\texmf\\miktex\\bin\\latex.exe";
+        $default_filter_tex_pathdvips   = "c:\\texmf\\miktex\\bin\\dvips.exe";
+        $default_filter_tex_pathconvert = "c:\\imagemagick\\convert.exe";
 
     } else {
         $default_filter_tex_pathlatex   = '';
@@ -60,6 +60,16 @@ if ($ADMIN->fulltree) {
         $default_filter_tex_pathconvert = '';
     }
 
+    $pathlatex = get_config('filter_tex', 'pathlatex');
+    $pathdvips = get_config('filter_tex', 'pathdvips');
+    $pathconvert = get_config('filter_tex', 'pathconvert');
+    if (strrpos($pathlatex . $pathdvips . $pathconvert, '"') or
+            strrpos($pathlatex . $pathdvips . $pathconvert, "'")) {
+        set_config('pathlatex', trim($pathlatex, " '\""), 'filter_tex');
+        set_config('pathdvips', trim($pathdvips, " '\""), 'filter_tex');
+        set_config('pathconvert', trim($pathconvert, " '\""), 'filter_tex');
+    }
+
     $items[] = new admin_setting_configexecutable('filter_tex/pathlatex', get_string('pathlatex', 'filter_tex'), '', $default_filter_tex_pathlatex);
     $items[] = new admin_setting_configexecutable('filter_tex/pathdvips', get_string('pathdvips', 'filter_tex'), '', $default_filter_tex_pathdvips);
     $items[] = new admin_setting_configexecutable('filter_tex/pathconvert', get_string('pathconvert', 'filter_tex'), '', $default_filter_tex_pathconvert);
index c56df3e..2612a2b 100644 (file)
         // first check if it is likely to work at all
         $output .= "<h3>Checking executables</h3>\n";
         $executables_exist = true;
-        $pathlatex = get_config('filter_tex', 'pathlatex');
+        $pathlatex = trim(get_config('filter_tex', 'pathlatex'), " '\"");
         if (is_file($pathlatex)) {
             $output .= "latex executable ($pathlatex) is readable<br />\n";
         }
             $executables_exist = false;
             $output .= "<b>Error:</b> latex executable ($pathlatex) is not readable<br />\n";
         }
-        $pathdvips = get_config('filter_tex', 'pathdvips');
+        $pathdvips = trim(get_config('filter_tex', 'pathdvips'), " '\"");
         if (is_file($pathdvips)) {
             $output .= "dvips executable ($pathdvips) is readable<br />\n";
         }
             $executables_exist = false;
             $output .= "<b>Error:</b> dvips executable ($pathdvips) is not readable<br />\n";
         }
-        $pathconvert = get_config('filter_tex', 'pathconvert');
+        $pathconvert = trim(get_config('filter_tex', 'pathconvert'), " '\"");
         if (is_file($pathconvert)) {
             $output .= "convert executable ($pathconvert) is readable<br />\n";
         }
         chdir($latex->temp_dir);
 
         // step 1: latex command
+        $pathlatex = escapeshellarg($pathlatex);
         $cmd = "$pathlatex --interaction=nonstopmode --halt-on-error $tex";
         $output .= execute($cmd);
 
         // step 2: dvips command
+        $pathdvips = escapeshellarg($pathdvips);
         $cmd = "$pathdvips -E $dvi -o $ps";
         $output .= execute($cmd);
 
         // step 3: convert command
+        $pathconvert = escapeshellarg($pathconvert);
         $cmd = "$pathconvert -density 240 -trim $ps $img ";
         $output .= execute($cmd);
 
@@ -364,7 +367,7 @@ http://www.forkosh.com/mimetex.zip</a>, or looking for an appropriate
 binary at <a href="http://moodle.org/download/mimetex/">
 http://moodle.org/download/mimetex/</a>. You may then also need to
 edit your moodle/filter/tex/pix.php file to add
-<br /><?PHP echo "case &quot;" . PHP_OS . "&quot;:" ;?><br ?> to the list of operating systems
+<br /><?php echo "case &quot;" . PHP_OS . "&quot;:" ;?><br ?> to the list of operating systems
 in the switch (PHP_OS) statement. Windows users may have a problem properly
 unzipping mimetex.exe. Make sure that mimetex.exe is is <b>PRECISELY</b>
 433152 bytes in size. If not, download a fresh copy from
diff --git a/grade/report/grader/classes/event/report_viewed.php b/grade/report/grader/classes/event/report_viewed.php
new file mode 100644 (file)
index 0000000..7d18f3d
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * Grader report viewed event.
+ *
+ * @package    gradereport_grader
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_grader\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grader report viewed event class.
+ *
+ * @package    gradereport_grader
+ * @since      Moodle 2.8
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_viewed extends \core\event\grade_report_viewed {
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventreportviewed', 'gradereport_grader');
+    }
+}
index e1c2b9f..adaccf6 100644 (file)
@@ -125,6 +125,14 @@ if (!empty($target) && !empty($action) && confirm_sesskey()) {
 
 $reportname = get_string('pluginname', 'gradereport_grader');
 
+$event = \gradereport_grader\event\report_viewed::create(
+    array(
+        'context' => $context,
+        'courseid' => $courseid,
+    )
+);
+$event->trigger();
+
 // Print header
 print_grade_page_head($COURSE->id, 'report', 'grader', $reportname, false, $buttons);
 
index ce3aa73..5507d64 100644 (file)
@@ -28,6 +28,7 @@ $string['ajaxerror'] = 'Error';
 $string['ajaxfailedupdate'] = 'Unable to update [1] for [2]';
 $string['ajaxfieldchanged'] = 'The field you are currently editing has changed, would you like to use the updated value?';
 $string['ajaxchoosescale'] = 'Choose';
+$string['eventreportviewed'] = 'Grader report viewed';
 $string['grader:manage'] = 'Manage the grader report';
 $string['grader:view'] = 'View the grader report';
 $string['pluginname'] = 'Grader report';
diff --git a/grade/report/outcomes/classes/event/report_viewed.php b/grade/report/outcomes/classes/event/report_viewed.php
new file mode 100644 (file)
index 0000000..7f226cd
--- /dev/null
@@ -0,0 +1,47 @@
+<?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/>.
+
+/**
+ * Outcomes report viewed event.
+ *
+ * @package    gradereport_outcomes
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_outcomes\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Outcomes report viewed event class.
+ *
+ * @package    gradereport_outcomes
+ * @since      Moodle 2.8
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_viewed extends \core\event\grade_report_viewed {
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventreportviewed', 'gradereport_outcomes');
+    }
+}
index 73729d8..03f7fd3 100644 (file)
@@ -170,7 +170,13 @@ foreach ($report_info as $outcomeid => $outcomedata) {
     $row++;
 }
 
-
+$event = \gradereport_outcomes\event\report_viewed::create(
+    array(
+        'context' => $context,
+        'courseid' => $courseid,
+    )
+);
+$event->trigger();
 
 $html .= '</table>';
 
index 9cc0633..015403b 100644 (file)
@@ -26,6 +26,7 @@
 $string['addoutcome'] = 'Add an outcome';
 $string['courseoutcomes'] = 'Course outcomes';
 $string['coursespecoutcome'] = 'Course outcomes';
+$string['eventreportviewed'] = 'Outcomes report viewed';
 $string['pluginname'] = 'Outcomes report';
 $string['outcomes:view'] = 'View the outcomes report';
 $string['usedgradeitem'] = 'Number of grade items';
diff --git a/grade/report/overview/classes/event/report_viewed.php b/grade/report/overview/classes/event/report_viewed.php
new file mode 100644 (file)
index 0000000..0702d9f
--- /dev/null
@@ -0,0 +1,60 @@
+<?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/>.
+
+/**
+ * Overview report viewed event.
+ *
+ * @package    gradereport_overview
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_overview\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Overview report viewed event class.
+ *
+ * @package    gradereport_overview
+ * @since      Moodle 2.8
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_viewed extends \core\event\grade_report_viewed {
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventreportviewed', 'gradereport_overview');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * Throw \coding_exception notice in case of any problems.
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' value must be set.');
+        }
+    }
+}
index baae232..51f2aa1 100644 (file)
@@ -148,6 +148,15 @@ if (has_capability('moodle/grade:viewall', $systemcontext)) { //Admins will see
     }
 }
 
+$event = \gradereport_overview\event\report_viewed::create(
+    array(
+        'context' => $context,
+        'courseid' => $courseid,
+        'relateduserid' => $userid,
+    )
+);
+$event->trigger();
+
 echo $OUTPUT->footer();
 
 
index 7829783..fd6fb8c 100644 (file)
@@ -22,5 +22,6 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['eventreportviewed'] = 'Overview report viewed';
 $string['pluginname'] = 'Overview report';
 $string['overview:view'] = 'View the overview report';
diff --git a/grade/report/user/classes/event/report_viewed.php b/grade/report/user/classes/event/report_viewed.php
new file mode 100644 (file)
index 0000000..93d9c00
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * User report viewed event.
+ *
+ * @package    gradereport_user
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace gradereport_user\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * User report viewed event class.
+ *
+ * @package    gradereport_user
+ * @since      Moodle 2.8
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class report_viewed extends \core\event\grade_report_viewed {
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventreportviewed', 'gradereport_user');
+    }
+
+    /**
+     * Custom validation.
+     *
+     * Throw \coding_exception notice in case of any problems.
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' value must be set.');
+        }
+    }
+}
index 587233b..3e8e3fd 100644 (file)
@@ -164,4 +164,13 @@ if (has_capability('moodle/grade:viewall', $context)) { //Teachers will see all
     }
 }
 
+$event = \gradereport_user\event\report_viewed::create(
+    array(
+        'context' => $context,
+        'courseid' => $courseid,
+        'relateduserid' => $userid,
+    )
+);
+$event->trigger();
+
 echo $OUTPUT->footer();
index 73c2474..56f6c1a 100644 (file)
@@ -22,6 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['eventreportviewed'] = 'User grade report viewed';
 $string['pluginname'] = 'User report';
 $string['user:view'] = 'View your own grade report';
 $string['tablesummary'] = 'The table is arranged as a list of graded items including categories of graded items. When items are in a category they will be indicated as such.';
index b3505ec..2a19e45 100644 (file)
--- a/index.php
+++ b/index.php
                     if (isloggedin()) {
                         $SESSION->fromdiscussion = $CFG->wwwroot;
                         $subtext = '';
-                        if (forum_is_subscribed($USER->id, $newsforum)) {
-                            if (!forum_is_forcesubscribed($newsforum)) {
+                        if (\mod_forum\subscriptions::is_subscribed($USER->id, $newsforum)) {
+                            if (!\mod_forum\subscriptions::is_forcesubscribed($newsforum)) {
                                 $subtext = get_string('unsubscribe', 'forum');
                             }
                         } else {
index 4e9db3d..dad616f 100644 (file)
@@ -165,13 +165,15 @@ $string['configcronremotepassword'] = 'This means that the cron.php script canno
     http://site.example.com/admin/cron.php?password=opensesame
 </pre>If this is left empty, no password is required.';
 $string['configcurlcache'] = 'Time-to-live for cURL cache, in seconds.';
-$string['configcustommenuitems'] = 'You can configure a custom menu here to be shown by themes. Each line consists of some menu text, a link URL (optional), a tooltip title (optional) and a language code or comma-separated list of codes (optional, for displaying the line to users of the specified language only), separated by pipe characters. You can specify a structure using hyphens. For example:
+$string['configcustommenuitems'] = 'You can configure a custom menu here to be shown by themes. Each line consists of some menu text, a link URL (optional), a tooltip title (optional) and a language code or comma-separated list of codes (optional, for displaying the line to users of the specified language only), separated by pipe characters. You can specify a structure using hyphens, and dividers can be used by adding a line of one or more # characters where desired. For example:
 <pre>
 Moodle community|https://moodle.org
 -Moodle free support|https://moodle.org/support
+-###
 -Moodle development|https://moodle.org/development
 --Moodle Docs|http://docs.moodle.org|Moodle Docs
 --German Moodle Docs|http://docs.moodle.org/de|Documentation in German|de
+#####
 Moodle.com|http://moodle.com/
 </pre>';
 $string['configdbsessions'] = 'If enabled, this setting will use the database to store information about current sessions. Note that changing this setting now will log out all current users (including you). If you are using MySQL please make sure that \'max_allowed_packet\' in my.cnf (or my.ini) is at least 4M. Other session drivers can be configured directly in config.php, see config-dist.php for more information. This option disappears if you specify session driver in config.php file.';
index 9ea00eb..e1c76b3 100644 (file)
@@ -186,6 +186,7 @@ $string['errorupdatinggradecategoryaggregateoutcomes'] = 'Error updating the "In
 $string['errorupdatinggradecategoryaggregatesubcats'] = 'Error updating the "Aggregate including subcategories" setting of grade category ID {$a->id}';
 $string['errorupdatinggradecategoryaggregation'] = 'Error updating the aggregation type of grade category ID {$a->id}';
 $string['errorupdatinggradeitemaggregationcoef'] = 'Error updating the aggregation coefficient (weight or extra credit) of grade item ID {$a->id}';
+$string['eventgradeviewed'] = 'Grades were viewed in the gradebook';
 $string['eventusergraded'] = 'User grade edited in gradebook';
 $string['excluded'] = 'Excluded';
 $string['excluded_help'] = 'If ticked, the grade will not be included in any aggregation.';
index 41a4688..56d9d5f 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP // $Id$ 
+<?php // $Id$
       // my.php - created with Moodle 1.7 beta + (2006101003)
 
 
@@ -13,4 +13,4 @@ $string['addpage'] = 'Add page';
 $string['delpage'] = 'Delete page';
 $string['managepages'] = 'Manage pages';
 $string['resetpage'] = 'Reset page to default';
-$string['reseterror'] = 'There was an error resetting your page';
\ No newline at end of file
+$string['reseterror'] = 'There was an error resetting your page';
index d221217..53fdd8a 100644 (file)
@@ -398,8 +398,8 @@ function rfc2445_is_valid_value($value, $type) {
 
             $parts = explode(';', strtoupper($value));
 
-            // First of all, we need at least a FREQ and a UNTIL or COUNT part, so...
-            if(count($parts) < 2) {
+            // We need at least one part for a valid rule, for example: "FREQ=DAILY".
+            if(empty($parts)) {
                 return false;
             }
 
index ce65055..783f27e 100644 (file)
@@ -3,4 +3,5 @@ Description of Bennu library import - customised library by author, this version
 modifications:
 1/ removed ereg functions deprecated as of php 5.3 (18 Nov 2009)
 2/ replaced mbstring functions with moodle core_text (28 Nov 2011)
-3/ replaced explode in iCalendar_component::unserialize() with preg_split to support various line breaks (20 Nov 2012)
\ No newline at end of file
+3/ replaced explode in iCalendar_component::unserialize() with preg_split to support various line breaks (20 Nov 2012)
+4/ updated rfc2445_is_valid_value() to accept single part rrule as a valid value (16 Jun 2014)
index ba083cb..62ae7c9 100644 (file)
@@ -104,14 +104,24 @@ class blog_association_created extends base {
     protected function validate_data() {
         parent::validate_data();
 
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
         if (empty($this->other['associatetype']) || ($this->other['associatetype'] !== 'course'
                 && $this->other['associatetype'] !== 'coursemodule')) {
             throw new \coding_exception('The \'associatetype\' value must be set in other and be a valid type.');
-        } else if (!isset($this->other['blogid'])) {
+        }
+
+        if (!isset($this->other['blogid'])) {
             throw new \coding_exception('The \'blogid\' value must be set in other.');
-        } else if (!isset($this->other['associateid'])) {
+        }
+
+        if (!isset($this->other['associateid'])) {
             throw new \coding_exception('The \'associateid\' value must be set in other.');
-        } else if (!isset($this->other['subject'])) {
+        }
+
+        if (!isset($this->other['subject'])) {
             throw new \coding_exception('The \'subject\' value must be set in other.');
         }
     }
index 117d0a4..632b892 100644 (file)
@@ -30,7 +30,14 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - int courseid: id of associated course.
+ *      - int entryid: (optional) id of the entry.
+ *      - int tagid: (optional) id of the tag.
+ *      - int userid: (optional) id of the user.
+ *      - int modid: (optional) id of the mod.
+ *      - int groupid: (optional) id of the group.
+ *      - int courseid: (optional) id of associated course.
+ *      - string search: (optional) the string used to search.
+ *      - int fromstart: (optional) the time to search from.
  * }
  *
  * @package    core
index 36acac5..1f39bed 100644 (file)
@@ -126,4 +126,18 @@ class blog_entry_created extends base {
         return array (SITEID, 'blog', 'add', 'index.php?userid=' . $this->relateduserid . '&entryid=' . $this->objectid,
             $this->blogentry->subject);
     }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index c7c3a63..2321e42 100644 (file)
@@ -117,4 +117,18 @@ class blog_entry_deleted extends base {
         return array (SITEID, 'blog', 'delete', 'index.php?userid=' . $this->relateduserid, 'deleted blog entry with entry id# '.
                 $this->objectid);
     }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index 893e830..4b92371 100644 (file)
@@ -124,5 +124,19 @@ class blog_entry_updated extends base {
         return array(SITEID, 'blog', 'update', 'index.php?userid=' . $this->relateduserid . '&entryid=' . $this->objectid,
                  $this->blogentry->subject);
     }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
 
index 6a944ea..018d854 100644 (file)
@@ -94,4 +94,18 @@ class cohort_member_added extends base {
         $data->userid = $this->relateduserid;
         return $data;
     }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index 68b29e3..25c902a 100644 (file)
@@ -95,4 +95,18 @@ class cohort_member_removed extends base {
         $data->userid = $this->relateduserid;
         return $data;
     }
+
+    /**
+     * Custom validations.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index d083cba..18231bf 100644 (file)
@@ -36,6 +36,12 @@ debugging('core\event\content_viewed has been deprecated. Please extend base eve
  *
  * This class has been deprecated, please extend base event or other relevent abstract class.
  *
+ * @property-read array $other {
+ *      Extra information about the event.
+ *
+ *      - string content: name of the content viewed.
+ * }
+ *
  * @package    core
  * @since      Moodle 2.6
  * @copyright  2013 Ankit Agarwal
index 5c6bbee..4d80eee 100644 (file)
@@ -122,4 +122,18 @@ class course_category_deleted extends base {
     protected function get_legacy_logdata() {
         return array(SITEID, 'category', 'delete', 'index.php', $this->other['name'] . '(ID ' . $this->objectid . ')');
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['name'])) {
+            throw new \coding_exception('The \'name\' value must be set in other.');
+        }
+    }
 }
index cd4bbdc..ed51fa9 100644 (file)
@@ -124,6 +124,14 @@ class course_completed extends base {
     protected function validate_data() {
         parent::validate_data();
 
-        // TODO: MDL-45445 add validation of relateduserid and other['relateduserid'].
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        // Check that the 'relateduserid' value is set in other as well. This is because we introduced this in 2.6
+        // and some observers may be relying on this value to be present.
+        if (!isset($this->other['relateduserid'])) {
+            throw new \coding_exception('The \'relateduserid\' value must be set in other.');
+        }
     }
 }
index 42161dd..dbea00d 100644 (file)
@@ -90,4 +90,18 @@ class course_content_deleted extends base {
 
         return $course;
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['options'])) {
+            throw new \coding_exception('The \'options\' value must be set in other.');
+        }
+    }
 }
index 56a6d61..699dcc1 100644 (file)
@@ -32,8 +32,8 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string shortname: shortname of course.
  *      - string fullname: fullname of course.
+ *      - string shortname: (optional) shortname of course.
  * }
  *
  * @package    core
@@ -105,4 +105,18 @@ class course_created extends base {
     protected function get_legacy_logdata() {
         return array(SITEID, 'course', 'new', 'view.php?id=' . $this->objectid, $this->other['fullname'] . ' (ID ' . $this->objectid . ')');
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['fullname'])) {
+            throw new \coding_exception('The \'fullname\' value must be set in other.');
+        }
+    }
 }
index ca27280..19641ff 100644 (file)
@@ -32,9 +32,9 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string shortname: shortname of course.
  *      - string fullname: fullname of course.
- *      - string idnumber: id number of course.
+ *      - string shortname: (optional) shortname of course.
+ *      - string idnumber: (optional) id number of course.
  * }
  *
  * @package    core
@@ -100,4 +100,18 @@ class course_deleted extends base {
     protected function get_legacy_logdata() {
         return array(SITEID, 'course', 'delete', 'view.php?id=' . $this->objectid, $this->other['fullname']  . '(ID ' . $this->objectid . ')');
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['fullname'])) {
+            throw new \coding_exception('The \'fullname\' value must be set in other.');
+        }
+    }
 }
index 672a09a..244a124 100644 (file)
@@ -54,6 +54,35 @@ class course_module_created extends base {
         $this->data['edulevel'] = self::LEVEL_TEACHING;
     }
 
+    /**
+     * Api to Create new event from course module.
+     *
+     * @since Moodle 2.6.4, 2.7.1
+     * @param \cm_info|\stdClass $cm course module instance, as returned by {@link get_coursemodule_from_id}
+     *                               or {@link get_coursemodule_from_instance}.
+     * @param \context_module $modcontext module context instance
+     *
+     * @return \core\event\base returns instance of new event
+     */
+    public static final function create_from_cm($cm, $modcontext = null) {
+        // If not set, get the module context.
+        if (empty($modcontext)) {
+            $modcontext = \context_module::instance($cm->id);
+        }
+
+        // Create event object for course module update action.
+        $event = static::create(array(
+            'context'  => $modcontext,
+            'objectid' => $cm->id,
+            'other'    => array(
+                'modulename' => $cm->modname,
+                'instanceid' => $cm->instance,
+                'name'       => $cm->name,
+            )
+        ));
+        return $event;
+    }
+
     /**
      * Returns localised general event name.
      *
index 8ccfc26..1d5230c 100644 (file)
@@ -107,4 +107,34 @@ class course_restored extends base {
             'samesite' => $this->other['samesite'],
         );
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['type'])) {
+            throw new \coding_exception('The \'type\' value must be set in other.');
+        }
+
+        if (!isset($this->other['target'])) {
+            throw new \coding_exception('The \'target\' value must be set in other.');
+        }
+
+        if (!isset($this->other['mode'])) {
+            throw new \coding_exception('The \'mode\' value must be set in other.');
+        }
+
+        if (!isset($this->other['operation'])) {
+            throw new \coding_exception('The \'operation\' value must be set in other.');
+        }
+
+        if (!isset($this->other['samesite'])) {
+            throw new \coding_exception('The \'samesite\' value must be set in other.');
+        }
+    }
 }
index e7053b2..a49e9c2 100644 (file)
@@ -32,8 +32,8 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string shortname: shortname of course.
- *      - string fullname: fullname of course.
+ *      - string shortname: (optional) shortname of course.
+ *      - string fullname: (optional) fullname of course.
  * }
  *
  * @package    core
index c465a03..1e55e56 100644 (file)
@@ -41,7 +41,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2014 Adrian Greeve <adrian@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class course_user_report_viewed extends \core\event\base {
+class course_user_report_viewed extends base {
 
     /**
      * Init method.
index 7148cde..1af160a 100644 (file)
@@ -41,7 +41,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2014 Adrian Greeve <adrian@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class course_viewed extends \core\event\base {
+class course_viewed extends base {
 
     /**
      * Init method.
index d62e2c7..461f458 100644 (file)
@@ -80,6 +80,10 @@ class email_failed extends base {
      */
     protected function validate_data() {
         parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
         if (!isset($this->other['subject'])) {
             throw new \coding_exception('The \'subject\' value must be set in other.');
         }
diff --git a/lib/classes/event/grade_report_viewed.php b/lib/classes/event/grade_report_viewed.php
new file mode 100644 (file)
index 0000000..5356a50
--- /dev/null
@@ -0,0 +1,89 @@
+<?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/>.
+
+/**
+ * Grade report viewed event.
+ *
+ * @package    core
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Grade report viewed event class.
+ *
+ * @package    core
+ * @since      Moodle 2.8
+ * @copyright  2014 Adrian Greeve <adrian@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class grade_report_viewed extends base {
+
+    /** string $reporttype The report type being viewed. */
+    protected $reporttype;
+
+    /**
+     * Initialise the event data.
+     */
+    protected function init() {
+        $reporttype = explode('\\', $this->eventname);
+        $shorttype = explode('_', $reporttype[1]);
+        $this->reporttype = $shorttype[1];
+
+        $this->data['crud'] = 'r';
+        $this->data['edulevel'] = self::LEVEL_PARTICIPATING;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventgradeviewed', 'grades');
+    }
+
+    /**
+     * Returns non-localised description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return "The user with id '$this->userid' viewed the $this->reporttype report in the gradebook.";
+    }
+
+    /**
+     * Returns relevant URL.
+     * @return \moodle_url
+     */
+    public function get_url() {
+        $url = '/grade/report/' . $this->reporttype . '/index.php';
+        return new \moodle_url($url, array('id' => $this->courseid));
+    }
+
+    /**
+     * Custom validation.
+     *
+     * To be overwritten by child classes.
+     */
+    protected function validate_data() {
+        parent::validate_data();
+    }
+}
index 2cb197f..1da73bc 100644 (file)
@@ -113,6 +113,10 @@ class group_member_added extends base {
     protected function validate_data() {
         parent::validate_data();
 
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
         if (!isset($this->other['component'])) {
             throw new \coding_exception('The \'component\' value must be set in other, even if empty.');
         }
index 6fca127..1acc778 100644 (file)
@@ -94,4 +94,18 @@ class group_member_removed extends base {
         $this->data['edulevel'] = self::LEVEL_OTHER;
         $this->data['objecttable'] = 'groups';
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index 90ae3ea..1725ddd 100644 (file)
@@ -30,6 +30,14 @@ defined('MOODLE_INTERNAL') || die();
 /**
  * Mnet access control created event class.
  *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string username: the username of the user.
+ *      - string hostname: the name of the host the user came from.
+ *      - string accessctrl: the access control value.
+ * }
+ *
  * @package    core
  * @since      Moodle 2.7
  * @copyright  2013 Mark Nelson <markn@moodle.com>
index e423985..cc8b875 100644 (file)
@@ -30,6 +30,14 @@ defined('MOODLE_INTERNAL') || die();
 /**
  * Mnet access control updated event class.
  *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - string username: the username of the user.
+ *      - string hostname: the name of the host the user came from.
+ *      - string accessctrl: the access control value.
+ * }
+ *
  * @package    core
  * @since      Moodle 2.7
  * @copyright  2013 Mark Nelson <markn@moodle.com>
index 62ce5bd..e6720eb 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string publishstate: the publish state.
+ *      - string publishstate: (optional) the publish state.
  * }
  *
  * @package    core
@@ -92,4 +92,18 @@ class note_created extends base {
         $logurl->set_anchor('note-' . $this->objectid);
         return array($this->courseid, 'notes', 'add', $logurl, 'add note');
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index 8af710b..0e127ca 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string publishstate: the publish state.
+ *      - string publishstate: (optional) the publish state.
  * }
  *
  * @package    core
@@ -82,4 +82,18 @@ class note_deleted extends base {
         $logurl->set_anchor('note-' . $this->objectid);
         return array($this->courseid, 'notes', 'delete', $logurl, 'delete note');
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index 43a0ed8..816816b 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string publishstate: the publish state.
+ *      - string publishstate: (optional) the publish state.
  * }
  *
  * @package    core
@@ -92,4 +92,18 @@ class note_updated extends base {
         $logurl->set_anchor('note-' . $this->objectid);
         return array($this->courseid, 'notes', 'update', $logurl, 'update note');
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index b3c4ce6..ccbbc5d 100644 (file)
@@ -70,6 +70,7 @@ class notes_viewed extends base {
 
     /**
      * Returns relevant URL.
+     *
      * @return \moodle_url
      */
     public function get_url() {
index b70c0f6..17fd3bb 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2014 Petr Skoda
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class recent_activity_viewed extends \core\event\base {
+class recent_activity_viewed extends base {
 
     /**
      * Init method.
index 4b86c8c..78682da 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  *
  *      - int id: role assigned id.
  *      - string component: name of component.
- *      - int itemid: id of the item.
+ *      - int itemid: (optional) id of the item.
  * }
  *
  * @package    core
@@ -109,4 +109,26 @@ class role_assigned extends base {
         return array($this->courseid, 'role', 'assign', 'admin/roles/assign.php?contextid='.$this->contextid.'&roleid='.$this->objectid,
                 $rolenames[$this->objectid], '', $this->userid);
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['id'])) {
+            throw new \coding_exception('The \'id\' value must be set in other.');
+        }
+
+        if (!isset($this->other['component'])) {
+            throw new \coding_exception('The \'component\' value must be set in other.');
+        }
+    }
 }
index a259419..9b5a2e2 100644 (file)
@@ -33,8 +33,8 @@ defined('MOODLE_INTERNAL') || die();
  *      Extra information about event.
  *
  *      - string shortname: shortname of role.
- *      - string description: role description.
- *      - string archetype: role type.
+ *      - string description: (optional) role description.
+ *      - string archetype: (optional) role type.
  * }
  *
  * @package    core
@@ -88,4 +88,18 @@ class role_deleted extends base {
         return array(SITEID, 'role', 'delete', 'admin/roles/manage.php?action=delete&roleid=' . $this->objectid,
             $this->other['shortname'], '');
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['shortname'])) {
+            throw new \coding_exception('The \'shortname\' value must be set in other.');
+        }
+    }
 }
index 31f00a0..bb788c2 100644 (file)
@@ -35,7 +35,7 @@ defined('MOODLE_INTERNAL') || die();
  *
  *      - int id: role assigned id.
  *      - string component: name of component.
- *      - int itemid: id of item.
+ *      - int itemid: (optional) id of item.
  * }
  *
  * @package    core
@@ -106,4 +106,26 @@ class role_unassigned extends base {
         return array($this->courseid, 'role', 'unassign', 'admin/roles/assign.php?contextid='.$this->contextid.'&roleid='.$this->objectid,
                 $rolenames[$this->objectid], '', $this->userid);
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['id'])) {
+            throw new \coding_exception('The \'id\' value must be set in other.');
+        }
+
+        if (!isset($this->other['component'])) {
+            throw new \coding_exception('The \'component\' value must be set in other.');
+        }
+    }
 }
index 2deb077..479b4dd 100644 (file)
@@ -36,8 +36,8 @@ defined('MOODLE_INTERNAL') || die();
  *      Extra information about the event.
  *
  *      - int itemid: grade item id.
- *      - bool overridden: Is this grade override?
- *      - float finalgrade: the final grade value.
+ *      - bool overridden: (optional) Is this grade override?
+ *      - float finalgrade: (optional) the final grade value.
  * }
  *
  * @package    core
@@ -72,6 +72,7 @@ class user_graded extends base {
     /**
      * Get grade object.
      *
+     * @throws \coding_exception
      * @return \grade_grade
      */
     public function get_grade() {
@@ -137,4 +138,22 @@ class user_graded extends base {
 
         return array($this->courseid, 'grade', 'update', $url, $info);
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['itemid'])) {
+            throw new \coding_exception('The \'itemid\' value must be set in other.');
+        }
+    }
 }
index 09bfc9a..002fbfe 100644 (file)
@@ -32,8 +32,8 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string courseshortname: the short name of course.
- *      - string coursefullname: the full name of course.
+ *      - string courseshortname: (optional) the short name of course.
+ *      - string coursefullname: (optional) the full name of course.
  * }
  *
  * @package    core
index 2e6b4dd..494038f 100644 (file)
@@ -90,4 +90,26 @@ class user_loggedinas extends base {
     public function get_url() {
         return new \moodle_url('/user/view.php', array('id' => $this->objectid));
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+
+        if (!isset($this->other['originalusername'])) {
+            throw new \coding_exception('The \'originalusername\' value must be set in other.');
+        }
+
+        if (!isset($this->other['loggedinasusername'])) {
+            throw new \coding_exception('The \'loggedinasusername\' value must be set in other.');
+        }
+    }
 }
index e64d872..7131a5a 100644 (file)
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string sessionid: session id.
+ *      - string sessionid: (optional) session id.
  * }
  *
  * @package    core
index 3186d25..cc1d1a2 100644 (file)
@@ -32,8 +32,8 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string username name of user.
- *      - int reason failure reason.
+ *      - string username: name of user.
+ *      - int reason: failure reason.
  * }
  *
  * @package    core
index ec11331..d254195 100644 (file)
@@ -32,9 +32,9 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - int courseid: id of course.
- *      - string courseshortname: short name of course.
- *      - string coursefullname: fullname of course.
+ *      - int courseid: (optional) id of course.
+ *      - string courseshortname: (optional) shortname of course.
+ *      - string coursefullname: (optional) fullname of course.
  * }
  *
  * @package    core
@@ -97,4 +97,18 @@ class user_profile_viewed extends base {
         }
         return null;
     }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception when validation does not pass.
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('The \'relateduserid\' must be set.');
+        }
+    }
 }
index 64ac2be..b1bde22 100644 (file)
@@ -105,5 +105,4 @@ class webservice_function_called extends base {
            throw new \coding_exception('The \'function\' value must be set in other.');
         }
     }
-
 }
index f6d27f5..6a64a6e 100644 (file)
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string: sessionid session id.
+ *      - string sessionid: (optional) session id.
  * }
  *
  * @package    core
@@ -91,5 +91,4 @@ class webservice_service_created extends base {
         $this->data['edulevel'] = self::LEVEL_OTHER;
         $this->data['objecttable'] = 'external_services';
     }
-
 }
index 56e40be..120dc70 100644 (file)
@@ -85,5 +85,4 @@ class webservice_service_deleted extends base {
         $this->data['edulevel'] = self::LEVEL_OTHER;
         $this->data['objecttable'] = 'external_services';
     }
-
 }
index e278b48..a84f4fc 100644 (file)
@@ -85,14 +85,4 @@ class webservice_service_updated extends base {
         $this->data['edulevel'] = self::LEVEL_OTHER;
         $this->data['objecttable'] = 'external_services';
     }
-
-    /**
-     * Set the legacy event log data.
-     *
-     * @return void
-     */
-    public function set_legacy_logdata($legacylogdata) {
-        $this->legacylogdata = $legacylogdata;
-    }
-
 }
index 9a3f3e6..2d42378 100644 (file)
@@ -98,5 +98,4 @@ class webservice_service_user_added extends base {
             throw new \coding_exception('The \'relateduserid\' must be set.');
         }
     }
-
 }
index c2248d8..50ae27c 100644 (file)
@@ -98,5 +98,4 @@ class webservice_service_user_removed extends base {
             throw new \coding_exception('The \'relateduserid\' must be set.');
         }
     }
-
 }
index 65da1bb..a816dbe 100644 (file)
@@ -103,5 +103,9 @@ class webservice_token_created extends base {
         if (!isset($this->relateduserid)) {
            throw new \coding_exception('The \'relateduserid\' must be set.');
         }
+
+        if (!isset($this->other['auto'])) {
+            throw new \coding_exception('The \'auto\' value must be set in other.');
+        }
     }
 }
index d73d182..f9d9f21 100644 (file)
@@ -73,5 +73,4 @@ class webservice_token_sent extends base {
         $this->data['edulevel'] = self::LEVEL_OTHER;
         $this->data['objecttable'] = 'external_tokens';
     }
-
 }
index d5106ea..f7a5868 100644 (file)
@@ -63,9 +63,25 @@ class theme extends base {
         $DB->set_field('user', 'theme', '', array('theme'=>$this->name));
         $DB->set_field('mnet_host', 'theme', '', array('theme'=>$this->name));
 
-        unset_config('thememobile');
-        unset_config('themetablet');
-        unset_config('themelegacy');
+        if (get_config('core', 'thememobile') === $this->name) {
+            unset_config('thememobile');
+        }
+        if (get_config('core', 'themetablet') === $this->name) {
+            unset_config('themetablet');
+        }
+        if (get_config('core', 'themelegacy') === $this->name) {
+            unset_config('themelegacy');
+        }
+
+        $themelist = get_config('core', 'themelist');
+        if (!empty($themelist)) {
+            $themes = explode(',', $themelist);
+            $key = array_search($this->name, $themes);
+            if ($key !== false) {
+                unset($themes[$key]);
+                set_config('themelist', implode(',', $themes));
+            }
+        }
 
         parent::uninstall_cleanup();
     }
index 741f790..eb94076 100644 (file)
@@ -904,67 +904,176 @@ class database_manager {
     /**
      * Checks the database schema against a schema specified by an xmldb_structure object
      * @param xmldb_structure $schema export schema describing all known tables
+     * @param array $options
      * @return array keyed by table name with array of difference messages as values
      */
-    public function check_database_schema(xmldb_structure $schema) {
+    public function check_database_schema(xmldb_structure $schema, array $options = null) {
+        $alloptions = array(
+            'extratables' => true,
+            'missingtables' => true,
+            'extracolumns' => true,
+            'missingcolumns' => true,
+            'changedcolumns' => true,
+        );
+
+        $typesmap = array(
+            'I' => XMLDB_TYPE_INTEGER,
+            'R' => XMLDB_TYPE_INTEGER,
+            'N' => XMLDB_TYPE_NUMBER,
+            'F' => XMLDB_TYPE_NUMBER, // Nobody should be using floats!
+            'C' => XMLDB_TYPE_CHAR,
+            'X' => XMLDB_TYPE_TEXT,
+            'B' => XMLDB_TYPE_BINARY,
+            'T' => XMLDB_TYPE_TIMESTAMP,
+            'D' => XMLDB_TYPE_DATETIME,
+        );
+
+        $options = (array)$options;
+        $options = array_merge($alloptions, $options);
+
+        // Note: the error descriptions are not supposed to be localised,
+        //       it is intended for developers and skilled admins only.
         $errors = array();
 
-        $dbtables = $this->mdb->get_tables();
-        $tables   = $schema->getTables();
+        /** @var string[] $dbtables */
+        $dbtables = $this->mdb->get_tables(false);
+        /** @var xmldb_table[] $tables */
+        $tables = $schema->getTables();
 
-        //TODO: maybe add several levels error/warning
-
-        // make sure that current and schema tables match exactly
         foreach ($tables as $table) {
             $tablename = $table->getName();
-            if (empty($dbtables[$tablename])) {
-                if (!isset($errors[$tablename])) {
-                    $errors[$tablename] = array();
+
+            if ($options['missingtables']) {
+                // Missing tables are a fatal problem.
+                if (empty($dbtables[$tablename])) {
+                    $errors[$tablename][] = "table is missing";
+                    continue;
                 }
-                $errors[$tablename][] = "Table $tablename is missing in database."; //TODO: localize
-                continue;
             }
 
-            // a) check for required fields
-            $dbfields = $this->mdb->get_columns($tablename);
-            $fields   = $table->getFields();
+            /** @var database_column_info[] $dbfields */
+            $dbfields = $this->mdb->get_columns($tablename, false);
+            /** @var xmldb_field[] $fields */
+            $fields = $table->getFields();
+
             foreach ($fields as $field) {
                 $fieldname = $field->getName();
                 if (empty($dbfields[$fieldname])) {
-                    if (!isset($errors[$tablename])) {
-                        $errors[$tablename] = array();
+                    if ($options['missingcolumns']) {
+                        // Missing columns are a fatal problem.
+                        $errors[$tablename][] = "column '$fieldname' is missing";
+                    }
+                } else if ($options['changedcolumns']) {
+                    $dbfield = $dbfields[$fieldname];
+
+                    if (!isset($typesmap[$dbfield->meta_type])) {
+                        $errors[$tablename][] = "column '$fieldname' has unsupported type '$dbfield->meta_type'";
+                    } else {
+                        $dbtype = $typesmap[$dbfield->meta_type];
+                        $type = $field->getType();
+                        if ($type == XMLDB_TYPE_FLOAT) {
+                            $type = XMLDB_TYPE_NUMBER;
+                        }
+                        if ($type != $dbtype) {
+                            if ($expected = array_search($type, $typesmap)) {
+                                $errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type', expected '$expected'";
+                            } else {
+                                $errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type'";
+                            }
+                        } else {
+                            if ($field->getNotNull() != $dbfield->not_null) {
+                                if ($field->getNotNull()) {
+                                    $errors[$tablename][] = "column '$fieldname' should be NOT NULL ($dbfield->meta_type)";
+                                } else {
+                                    $errors[$tablename][] = "column '$fieldname' should allow NULL ($dbfield->meta_type)";
+                                }
+                            }
+                            if ($dbtype == XMLDB_TYPE_TEXT) {
+                                // No length check necessary - there is one size only now.
+
+                            } else if ($dbtype == XMLDB_TYPE_NUMBER) {
+                                if ($field->getType() == XMLDB_TYPE_FLOAT) {
+                                    // Do not use floats in any new code, they are deprecated in XMLDB editor!
+
+                                } else if ($field->getLength() != $dbfield->max_length or $field->getDecimals() != $dbfield->scale) {
+                                    $size = "({$field->getLength()},{$field->getDecimals()})";
+                                    $dbsize = "($dbfield->max_length,$dbfield->scale)";
+                                    $errors[$tablename][] = "column '$fieldname' size is $dbsize, expected $size ($dbfield->meta_type)";
+                                }
+
+                            } else if ($dbtype == XMLDB_TYPE_CHAR) {
+                                // This is not critical, but they should ideally match.
+                                if ($field->getLength() != $dbfield->max_length) {
+                                    $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length, expected {$field->getLength()} ($dbfield->meta_type)";
+                                }
+
+                            } else if ($dbtype == XMLDB_TYPE_INTEGER) {
+                                // Integers may be bigger in some DBs.
+                                $length = $field->getLength();
+                                if ($length > 18) {
+                                    // Integers are not supposed to be bigger than 18.
+                                    $length = 18;
+                                }
+                                if ($length > $dbfield->max_length) {
+                                    $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length, expected at least {$field->getLength()} ($dbfield->meta_type)";
+                                }
+
+                            } else if ($dbtype == XMLDB_TYPE_BINARY) {
+                                // Ignore binary types.
+                                continue;
+
+                            } else if ($dbtype == XMLDB_TYPE_TIMESTAMP) {
+                                $errors[$tablename][] = "column '$fieldname' is a timestamp, this type is not supported ($dbfield->meta_type)";
+                                continue;
+
+                            } else if ($dbtype == XMLDB_TYPE_DATETIME) {
+                                $errors[$tablename][] = "column '$fieldname' is a datetime, this type is not supported ($dbfield->meta_type)";
+                                continue;
+
+                            } else {
+                                // Report all other unsupported types as problems.
+                                $errors[$tablename][] = "column '$fieldname' has unknown type ($dbfield->meta_type)";
+                                continue;
+                            }
+
+                            // Note: The empty string defaults are a bit messy...
+                            if ($field->getDefault() != $dbfield->default_value) {
+                                $default = is_null($field->getDefault()) ? 'NULL' : $field->getDefault();
+                                $dbdefault = is_null($dbfield->default_value) ? 'NULL' : $dbfield->default_value;
+                                $errors[$tablename][] = "column '$fieldname' has default '$dbdefault', expected '$default' ($dbfield->meta_type)";
+                            }
+                        }
                     }
-                    $errors[$tablename][] = "Field $fieldname is missing in table $tablename.";  //TODO: localize
                 }
                 unset($dbfields[$fieldname]);
             }
 
-            // b) check for extra fields (indicates unsupported hacks) - modify install.xml if you want the script to continue ;-)
-            foreach ($dbfields as $fieldname=>$info) {
-                if (!isset($errors[$tablename])) {
-                    $errors[$tablename] = array();
+            // Check for extra columns (indicates unsupported hacks) - modify install.xml if you want to pass validation.
+            foreach ($dbfields as $fieldname => $dbfield) {
+                if ($options['extracolumns']) {
+                    $errors[$tablename][] = "column '$fieldname' is not expected ($dbfield->meta_type)";
                 }
-                $errors[$tablename][] = "Field $fieldname is not expected in table $tablename.";  //TODO: localize
             }
             unset($dbtables[$tablename]);
         }
 
-        // look for unsupported tables - local custom tables should be in /local/xxxx/db/install.xml ;-)
-        // if there is no prefix, we can not say if tale is ours :-(
-        if ($this->generator->prefix !== '') {
-            foreach ($dbtables as $tablename=>$unused) {
-                if (strpos($tablename, 'pma_') === 0) {
-                    // ignore phpmyadmin tables for now
-                    continue;
-                }
-                if (strpos($tablename, 'test') === 0) {
-                    // ignore broken results of unit tests
-                    continue;
-                }
-                if (!isset($errors[$tablename])) {
-                    $errors[$tablename] = array();
+        if ($options['extratables']) {
+            // Look for unsupported tables - local custom tables should be in /local/xxxx/db/install.xml file.
+            // If there is no prefix, we can not say if table is ours, sorry.
+            if ($this->generator->prefix !== '') {
+                foreach ($dbtables as $tablename => $unused) {
+                    if (strpos($tablename, 'pma_') === 0) {
+                        // Ignore phpmyadmin tables.
+                        continue;
+                    }
+                    if (strpos($tablename, 'test') === 0) {
+                        // Legacy simple test db tables need to be eventually removed,
+                        // report them as problems!
+                        $errors[$tablename][] = "table is not expected (it may be a leftover after Simpletest unit tests)";
+                    } else {
+                        $errors[$tablename][] = "table is not expected";
+                    }
                 }
-                $errors[$tablename][] = "Table $tablename is not expected.";  //TODO: localize
             }
         }
 
index 206da10..23677f6 100644 (file)
@@ -129,7 +129,8 @@ abstract class database_exporter {
     public function export_database($description=null) {
         global $CFG;
 
-        if ($this->check_schema and $errors = $this->manager->check_database_schema($this->schema)) {
+        $options = array('changedcolumns' => false); // Column types may be fixed by transfer.
+        if ($this->check_schema and $errors = $this->manager->check_database_schema($this->schema, $options)) {
             $details = '';
             foreach ($errors as $table=>$items) {
                 $details .= '<div>'.get_string('tablex', 'dbtransfer', $table);
index d9cf1a7..b14bad7 100644 (file)
@@ -110,7 +110,8 @@ class database_importer {
             throw new dbtransfer_exception('importversionmismatchexception', $a);
         }
 
-        if ($this->check_schema and $errors = $this->manager->check_database_schema($this->schema)) {
+        $options = array('changedcolumns' => false); // Column types may be fixed by transfer.
+        if ($this->check_schema and $errors = $this->manager->check_database_schema($this->schema, $options)) {
             $details = '';
             foreach ($errors as $table=>$items) {
                 $details .= '<div>'.get_string('table').' '.$table.':';
index c456bc6..a861bd8 100644 (file)
@@ -6,10 +6,25 @@ Feature: Atto accessibility checker
   Scenario: Images with no alt
     Given I log in as "admin"
     And I navigate to "Edit profile" node in "My profile settings"
-    And I set the field "Description" to "<p>Some plain text</p><img src='/broken-image'/><p>Some more text</p>"
+    And I set the field "Description" to "<p>Some plain text</p><img src='/broken-image' width='1' height='1'/><p>Some more text</p>"
     When I click on "Show more buttons" "button"
     And I click on "Accessibility checker" "button"
     Then I should see "Images require alternative text."
+    And I follow "/broken-image"
+    And I wait "2" seconds
+    And I click on "Image" "button"
+    And the field "Enter URL" matches value "/broken-image"
+    And I set the field "Describe this image" to "No more warning!"
+    And I press "Save image"
+    And I press "Accessibility checker"
+    And I should see "Congratulations, no accessibility problems found!"
+    And I select the text in the "Description" Atto editor
+    And I click on "Image" "button"
+    And I set the field "Describe this image" to ""
+    And I set the field "Description not necessary" to "1"
+    And I press "Save image"
+    And I press "Accessibility checker"
+    And I should see "Congratulations, no accessibility problems found!"
 
   @javascript
   Scenario: Low contrast
index 4a8faae..cbdb87b 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js differ
index 0769b53..026e26b 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js differ
index d77ab25..9a4c177 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js differ
index 89ec9bc..5797448 100644 (file)
 var COMPONENT = 'atto_accessibilitychecker';
 
 Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
-    /**
-     * The warnings which are displayed.
-     *
-     * @property _displayedWarnings
-     * @type Object
-     * @private
-     */
-    _displayedWarnings: {},
 
     initializer: function() {
         this.addButton({
@@ -84,18 +76,12 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
             e.preventDefault();
 
             var host = this.get('host'),
-                index = e.target.getAttribute("data-index"),
-                node = this._displayedWarnings[index],
+                node = e.currentTarget.getData('sourceNode'),
                 dialogue = this.getDialogue();
 
-
             if (node) {
-                // Clear the dialogue's focusAfterHide to ensure we focus
-                // on the selection.
-                dialogue.set('focusAfterHide', null);
-
-                // Hide the dialogue.
-                dialogue.hide();
+                // Focus on the editor as we hide the dialogue.
+                dialogue.set('focusAfterHide', this.editor).hide();
 
                 // Then set the selection.
                 host.setSelection(host.getSelectionFromNode(node));
@@ -199,25 +185,24 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
      * @param {boolean} imagewarnings true if the warnings are related to images, false if text.
      */
     _addWarnings: function(list, description, nodes, imagewarnings) {
-        var warning, fails, i, key, src, textfield;
+        var warning, fails, i, src, textfield, li, link;
 
         if (nodes.length > 0) {
             warning = Y.Node.create('<p>' + description + '</p>');
             fails = Y.Node.create('<ol class="accessibilitywarnings"></ol>');
             i = 0;
             for (i = 0; i < nodes.length; i++) {
+                li = Y.Node.create('<li></li>');
                 if (imagewarnings) {
-                    key = 'image_'+i;
                     src = nodes[i].getAttribute('src');
-
-                    fails.append(Y.Node.create('<li><a data-index="'+key+'" href="#"><img data-index="'+key+'" src="' + src + '" /> '+src+'</a></li>'));
+                    link = Y.Node.create('<a href="#"><img src="' + src + '" /> ' + src + '</a>');
                 } else {
-                    key = 'text_' + i;
-
-                    textfield = ('innerText' in nodes[i])? 'innerText' : 'textContent';
-                    fails.append(Y.Node.create('<li><a href="#" data-index="'+key+'">' + nodes[i].get(textfield) + '</a></li>'));
+                    textfield = ('innerText' in nodes[i]) ? 'innerText' : 'textContent';
+                    link = Y.Node.create('<a href="#">' + nodes[i].get(textfield) + '</a>');
                 }
-                this._displayedWarnings[key] = nodes[i];
+                link.setData('sourceNode', nodes[i]);
+                li.append(link);
+                fails.append(li);
             }
 
             warning.append(fails);
index aa91046..0dd0891 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-debug.js and b/lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-debug.js differ
index 5840b00..a3f6851 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-min.js and b/lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button-min.js differ
index aa91046..0dd0891 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button.js and b/lib/editor/atto/plugins/accessibilityhelper/yui/build/moodle-atto_accessibilityhelper-button/moodle-atto_accessibilityhelper-button.js differ
index f6c1c74..d01d387 100644 (file)
@@ -65,15 +65,6 @@ var COMPONENT = 'atto_accessibilityhelper',
 
 Y.namespace('M.atto_accessibilityhelper').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
 
-    /**
-     * The warnings which are displayed.
-     *
-     * @property _displayedWarnings
-     * @type Object
-     * @private
-     */
-    _displayedWarnings: {},
-
     initializer: function() {
         this.addButton({
             icon: 'e/screenreader_helper',
@@ -125,28 +116,6 @@ Y.namespace('M.atto_accessibilityhelper').Button = Y.Base.create('button', Y.M.e
                 .empty()
                 .appendChild(this._listImages());
 
-        // Add ability to select problem areas in the editor.
-        content.delegate('click', function(e) {
-            e.preventDefault();
-
-            var host = this.get('host'),
-                index = e.target.getAttribute("data-index"),
-                node = this._displayedWarnings[index],
-                dialogue = this.getDialogue();
-
-
-            if (node) {
-                // Clear the dialogue's focusAfterHide to ensure we focus
-                // on the selection.
-                dialogue.set('focusAfterHide', null);
-                host.setSelection(host.getSelectionFromNode(node));
-            }
-
-            // Hide the dialogue.
-            dialogue.hide();
-
-        }, 'a', this);
-
         return content;
     },
 
index 0b20f46..9a24ad2 100644 (file)
@@ -19,3 +19,17 @@ Feature: Atto equation editor
     And I click on "Update profile" "button"
     Then "\infty" "text" should exist
 
+  @javascript
+  Scenario: Edit an equation
+    Given I log in as "admin"
+    When I navigate to "Edit profile" node in "My profile settings"
+    And I set the field "Description" to "<p>\( \pi \)</p>"
+    # Set field on the bottom of page, so equation editor dialogue is visible.
+    And I expand all fieldsets
+    And I set the field "Picture description" to "Test"
+    And I select the text in the "Description" Atto editor
+    And I click on "Show more buttons" "button"
+    And I click on "Equation editor" "button"
+    Then the field "Edit equation using" matches value " \pi "
+    And I click on "Save equation" "button"
+    And the field "Description" matches value "<p>\( \pi \)</p>"
index 1f4b0d8..83cdc40 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-debug.js differ
index 42f6886..99fc109 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button-min.js differ
index e5a5e72..b4429bc 100644 (file)
Binary files a/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js and b/lib/editor/atto/plugins/equation/yui/build/moodle-atto_equation-button/moodle-atto_equation-button.js differ
index 59c07c9..1124496 100644 (file)
@@ -204,6 +204,9 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
             return;
         }
 
+        // This needs to be done before the dialogue is opened because the focus will shift to the dialogue.
+        var equation = this._resolveEquation();
+
         var dialogue = this.getDialogue({
             headerContent: M.util.get_string('pluginname', COMPONENTNAME),
             focusAfterHide: true,
@@ -225,7 +228,6 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         // Trigger any JS filters to reprocess the new nodes.
         Y.fire(M.core.event.FILTER_CONTENT_UPDATED, {nodes: (new Y.NodeList(dialogue.get('boundingBox')))});
 
-        var equation = this._resolveEquation();
         if (equation) {
             content.one(SELECTORS.EQUATION_TEXT).set('text', equation);
         }
@@ -248,7 +250,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
             text,
             returnValue = false;
 
-        this.sourceEquation = null;
+        // Prevent resolving equations when we don't have focus.
+        if (!this.get('host').isActive()) {
+            return false;
+        }
 
         // Note this is a document fragment and YUI doesn't like them.
         if (!selectedNode) {
@@ -259,6 +264,9 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
         if (!selection || selection.length === 0) {
             return false;
         }
+
+        this.sourceEquation = null;
+
         selection = selection[0];
 
         text = Y.one(selectedNode).get('text');
@@ -324,6 +332,10 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
             }
         }, this);
 
+        // We trim the equation when we load it and then add spaces when we save it.
+        if (returnValue !== false) {
+            returnValue = returnValue.trim();
+        }
         return returnValue;
     },
 
@@ -358,6 +370,7 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
                 // Replace the equation.
                 selectedNode = Y.one(host.getSelectionParentNode());
                 text = selectedNode.get('text');
+                value = ' ' + value + ' ';
                 newText =   text.slice(0, this.sourceEquation.startInnerPosition) +
                             value +
                             text.slice(this.sourceEquation.endInnerPosition);
index 3620a89..19bd61d 100644 (file)
@@ -19,10 +19,50 @@ Feature: Add images to Atto
     And I set the field "Describe this image" to "It's the Moodle"
     # Wait for the page to "settle".
     And I wait "2" seconds
+    And the field "Width" matches value "204"
+    And the field "Height" matches value "61"
+    And I set the field "Auto size" to "1"
+    And I set the field "Width" to "2040"
+    # Trigger blur on the width field.
+    And I set the field "Alignment" to "Bottom"
+    And the field "Height" matches value "610"
+    And I set the field "Height" to "61"
+    # Trigger blur on the height field.
+    And I set the field "Alignment" to "Bottom"
+    And the field "Width" matches value "204"
+    And I set the field "Auto size" to "0"
+    And I set the field "Width" to "123"
+    And I set the field "Height" to "456"
+    # Trigger blur on the height field.
+    And I set the field "Alignment" to "Bottom"
+    And the field "Width" matches value "123"
+    And the field "Height" matches value "456"
     And I click on "Save image" "button"
     And I click on "Update profile" "button"
     And I follow "Edit profile"
     And I select the text in the "Description" Atto editor
     And I click on "Image" "button"
     Then the field "Describe this image" matches value "It's the Moodle"
+    And the field "Width" matches value "123"
+    And the field "Height" matches value "456"
 
+  @javascript
+  Scenario: Manually inserting an image
+    Given I log in as "admin"
+    And I navigate to "Edit profile" node in "My profile settings"
+    And I set the field "Description" to "<p>Image: <img src='/nothing/here'>.</p>"
+    And I select the text in the "Description" Atto editor
+    When I click on "Image" "button"
+    Then the field "Enter URL" matches value "/nothing/here"
+    And I set the field "Describe this image" to "Something"
+    And I set the field "Enter URL" to ""
+    And I press "Save image"
+    And I set the field "Description" to "<p>Image: <img src='/nothing/again' width='123' height='456' alt='Awesome!'>.</p>"
+    And I press "Update profile"
+    And I follow "Edit profile"
+    And I select the text in the "Description" Atto editor
+    And I click on "Image" "button"
+    And the field "Enter URL" matches value "/nothing/again"
+    And the field "Width" matches value "123"
+    And the field "Height" matches value "456"
+    And the field "Describe this image" matches value "Awesome!"
\ No newline at end of file
index 24abed2..4e9164a 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-debug.js differ
index cf58442..1a1ad36 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button-min.js differ
index 24abed2..4e9164a 100644 (file)
Binary files a/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js and b/lib/editor/atto/plugins/image/yui/build/moodle-atto_image-button/moodle-atto_image-button.js differ
index eba4d81..a8724c9 100644 (file)
@@ -241,6 +241,9 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             return;
         }
 
+        // Reset the image dimensions.
+        this._rawImageDimensions = null;
+
         var dialogue = this.getDialogue({
             headerContent: M.util.get_string('imageproperties', COMPONENTNAME),
             width: '480px',
@@ -386,6 +389,11 @@ Y.namespace('M.atto_image').Button = Y.Base.create('button', Y.M.editor_atto.Edi
             rawPercentage,
             rawSize;
 
+        // If we do not know the image size, do not do anything.
+        if (!this._rawImageDimensions) {
+            return;
+        }
+
         // Set the width back to default if it is empty.
         if (keyFieldValue === '') {
             keyFieldValue = this._rawImageDimensions[keyFieldType];
index 2a1f56e..a12b460 100644 (file)
@@ -3070,14 +3070,31 @@ EOD;
             $content .= html_writer::end_tag('div');
             $content .= html_writer::end_tag('li');
         } else {
-            // The node doesn't have children so produce a final menuitem
-            $content = html_writer::start_tag('li', array('class'=>'yui3-menuitem'));
-            if ($menunode->get_url() !== null) {
-                $url = $menunode->get_url();
+            // The node doesn't have children so produce a final menuitem.
+            // Also, if the node's text matches '####', add a class so we can treat it as a divider.
+            $content = '';
+            if (preg_match("/^#+$/", $menunode->get_text())) {
+
+                // This is a divider.
+                $content = html_writer::start_tag('li', array('class' => 'yui3-menuitem divider'));
             } else {
-                $url = '#';
+                $content = html_writer::start_tag(
+                    'li',
+                    array(
+                        'class' => 'yui3-menuitem'
+                    )
+                );
+                if ($menunode->get_url() !== null) {
+                    $url = $menunode->get_url();
+                } else {
+                    $url = '#';
+                }
+                $content .= html_writer::link(
+                    $url,
+                    $menunode->get_text(),
+                    array('class' => 'yui3-menuitem-content', 'title' => $menunode->get_title())
+                );
             }
-            $content .= html_writer::link($url, $menunode->get_text(), array('class'=>'yui3-menuitem-content', 'title'=>$menunode->get_title()));
             $content .= html_writer::end_tag('li');
         }
         // Return the sub menu
index 01700af..0442053 100644 (file)
@@ -115,6 +115,13 @@ class page_requirements_manager {
      */
     protected $extramodules = array();
 
+    /**
+     * @var array trackes the names of bits of HTML that are only required once
+     * per page. See {@link has_one_time_item_been_created()},
+     * {@link set_one_time_item_created()} and {@link should_create_one_time_item_now()}.
+     */
+    protected $onetimeitemsoutput = array();
+
     /**
      * @var bool Flag indicated head stuff already printed
      */
@@ -1492,6 +1499,68 @@ class page_requirements_manager {
     public function is_top_of_body_done() {
         return $this->topofbodydone;
     }
+
+    /**
+     * Should we generate a bit of content HTML that is only required once  on
+     * this page (e.g. the contents of the modchooser), now? Basically, we call
+     * {@link has_one_time_item_been_created()}, and if the thing has not already
+     * been output, we return true to tell the caller to generate it, and also
+     * call {@link set_one_time_item_created()} to record the fact that it is
+     * about to be generated.
+     *
+     * That is, a typical usage pattern (in a renderer method) is:
+     * <pre>
+     * if (!$this->page->requires->should_create_one_time_item_now($thing)) {
+     *     return '';
+     * }
+     * // Else generate it.
+     * </pre>
+     *
+     * @param string $thing identifier for the bit of content. Should be of the form
+     *      frankenstyle_things, e.g. core_course_modchooser.
+     * @return bool if true, the caller should generate that bit of output now, otherwise don't.
+     */
+    public function should_create_one_time_item_now($thing) {
+        if ($this->has_one_time_item_been_created($thing)) {
+            return false;
+        }
+
+        $this->set_one_time_item_created($thing);
+        return true;
+    }
+
+    /**
+     * Has a particular bit of HTML that is only required once  on this page
+     * (e.g. the contents of the modchooser) already been generated?
+     *
+     * Normally, you can use the {@link should_create_one_time_item_now()} helper
+     * method rather than calling this method directly.
+     *
+     * @param string $thing identifier for the bit of content. Should be of the form
+     *      frankenstyle_things, e.g. core_course_modchooser.
+     * @return bool whether that bit of output has been created.
+     */
+    public function has_one_time_item_been_created($thing) {
+        return isset($this->onetimeitemsoutput[$thing]);
+    }
+
+    /**
+     * Indicate that a particular bit of HTML that is only required once on this
+     * page (e.g. the contents of the modchooser) has been generated (or is about to be)?
+     *
+     * Normally, you can use the {@link should_create_one_time_item_now()} helper
+     * method rather than calling this method directly.
+     *
+     * @param string $thing identifier for the bit of content. Should be of the form
+     *      frankenstyle_things, e.g. core_course_modchooser.
+     */
+    public function set_one_time_item_created($thing) {
+        if ($this->has_one_time_item_been_created($thing)) {
+            throw new coding_exception($thing . ' is only supposed to be ouput ' .
+                    'once per page, but it seems to be being output again.');
+        }
+        return $this->onetimeitemsoutput[$thing] = true;
+    }
 }
 
 /**
index c650dad..d39b5f2 100644 (file)
@@ -965,7 +965,11 @@ class moodle_page {
             } else {
                 // We do not want devs to do weird switching of context levels on the fly because we might have used
                 // the context already such as in text filter in page title.
-                debugging("Coding problem: unsupported modification of PAGE->context from {$current} to {$context->contextlevel}");
+                // This is explicitly allowed for webservices though which may
+                // call "external_api::validate_context on many contexts in a single request.
+                if (!WS_SERVER) {
+                    debugging("Coding problem: unsupported modification of PAGE->context from {$current} to {$context->contextlevel}");
+                }
             }
         }
 
index 8811bcf..76ff14d 100644 (file)
@@ -142,10 +142,11 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
 
         $this->assertEquals(0, $DB->count_records('user_preferences'));
         $originaldisplayid = $DB->insert_record('user_preferences', array('userid'=>2, 'name'=> 'phpunittest', 'value'=>'x'));
-        $this->assertEquals(1, $originaldisplayid);
+        $this->assertEquals(1, $DB->count_records('user_preferences'));
 
+        $numcourses = $DB->count_records('course');
         $course = $this->getDataGenerator()->create_course();
-        $this->assertEquals(2, $course->id);
+        $this->assertEquals($numcourses + 1, $DB->count_records('course'));
 
         $this->assertEquals(2, $DB->count_records('user'));
         $DB->delete_records('user', array('id'=>1));
@@ -155,8 +156,10 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
 
         $this->assertEquals(1, $DB->count_records('course')); // Only frontpage in new site.
         $this->assertEquals(0, $DB->count_records('context_temp')); // Only frontpage in new site.
+
+        $numcourses = $DB->count_records('course');
         $course = $this->getDataGenerator()->create_course();
-        $this->assertEquals(2, $course->id);
+        $this->assertEquals($numcourses + 1, $DB->count_records('course'));
 
         $displayid = $DB->insert_record('user_preferences', array('userid'=>2, 'name'=> 'phpunittest', 'value'=>'x'));
         $this->assertEquals($originaldisplayid, $displayid);
@@ -164,20 +167,23 @@ class core_phpunit_advanced_testcase extends advanced_testcase {
         $this->assertEquals(2, $DB->count_records('user'));
         $DB->delete_records('user', array('id'=>2));
         $user = $this->getDataGenerator()->create_user();
-        $this->assertEquals(3, $user->id);
+        $this->assertEquals(2, $DB->count_records('user'));
+        $this->assertGreaterThan(2, $user->id);
 
         $this->resetAllData();
 
+        $numcourses = $DB->count_records('course');
         $course = $this->getDataGenerator()->create_course();
-        $this->assertEquals(2, $course->id);
+        $this->assertEquals($numcourses + 1, $DB->count_records('course'));
 
         $this->assertEquals(2, $DB->count_records('user'));
         $DB->delete_records('user', array('id'=>2));
 
         $this->resetAllData();
 
+        $numcourses = $DB->count_records('course');
         $course = $this->getDataGenerator()->create_course();
-        $this->assertEquals(2, $course->id);
+        $this->assertEquals($numcourses + 1, $DB->count_records('course'));
 
         $this->assertEquals(2, $DB->count_records('user'));
     }
index ed0ab99..56bf482 100644 (file)
@@ -74,6 +74,10 @@ abstract class testing_util {
      */
     private static $originaldatafilesjsonadded = false;
 
+    /**
+     * @var int next sequence value for a single test cycle.
+     */
+    protected static $sequencenextstartingid = null;
     /**
      * Return the name of the JSON file containing the init filenames.
      *
@@ -409,6 +413,27 @@ abstract class testing_util {
         }
     }
 
+    /**
+     * Determine the next unique starting id sequences.
+     *
+     * @static
+     * @param array $records The records to use to determine the starting value for the table.
+     * @return int The value the sequence should be set to.
+     */
+    private static function get_next_sequence_starting_value($records) {
+        $id = self::$sequencenextstartingid;
+
+        // If there are records, calculate the minimum id we can use.
+        // It must be bigger than the last record's id.
+        if (!empty($records)) {
+            $lastrecord = end($records);
+            $id = max($id, $lastrecord->id + 1);
+        }
+
+        self::$sequencenextstartingid = $id + 1000;
+        return $id;
+    }
+
     /**
      * Reset all database sequences to initial values.
      *
@@ -428,18 +453,20 @@ abstract class testing_util {
             return;
         }
 
+        // If all starting Id's are the same, it's difficult to detect coding and testing
+        // errors that use the incorrect id in tests.  The classic case is cmid vs instance id.
+        // To reduce the chance of the coding error, we start sequences at different values where possible.
+        // In a attempt to avoid tables with existing id's we start at a high number.
+        // Reset the value each time all database sequences are reset.
+        self::$sequencenextstartingid = 100000;
+
         $dbfamily = $DB->get_dbfamily();
         if ($dbfamily === 'postgres') {
             $queries = array();
             $prefix = $DB->get_prefix();
             foreach ($data as $table => $records) {
                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    if (empty($records)) {
-                        $nextid = 1;
-                    } else {
-                        $lastrecord = end($records);
-                        $nextid = $lastrecord->id + 1;
-                    }
+                    $nextid = self::get_next_sequence_starting_value($records);
                     $queries[] = "ALTER SEQUENCE {$prefix}{$table}_id_seq RESTART WITH $nextid";
                 }
             }
@@ -467,12 +494,7 @@ abstract class testing_util {
             foreach ($data as $table => $records) {
                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
                     if (isset($sequences[$table])) {
-                        if (empty($records)) {
-                            $nextid = 1;
-                        } else {
-                            $lastrecord = end($records);
-                            $nextid = $lastrecord->id + 1;
-                        }
+                        $nextid = self::get_next_sequence_starting_value($records);
                         if ($sequences[$table] != $nextid) {
                             $DB->change_database_structure("ALTER TABLE {$prefix}{$table} AUTO_INCREMENT = $nextid");
                         }
@@ -501,12 +523,7 @@ abstract class testing_util {
 
             foreach ($data as $table => $records) {
                 if (isset($structure[$table]['id']) and $structure[$table]['id']->auto_increment) {
-                    $lastrecord = end($records);
-                    if ($lastrecord) {
-                        $nextid = $lastrecord->id + 1;
-                    } else {
-                        $nextid = 1;
-                    }
+                    $nextid = self::get_next_sequence_starting_value($records);
                     if (!isset($current[$table])) {
                         $DB->get_manager()->reset_sequence($table);
                     } else if ($nextid == $current[$table]) {
@@ -522,6 +539,7 @@ abstract class testing_util {
 
         } else {
             // note: does mssql support any kind of faster reset?
+            // This also implies mssql will not use unique sequence values.
             if (is_null($empties)) {
                 $empties = self::guess_unmodified_empty_tables();
             }
index bb1f1e8..d4a821d 100644 (file)
@@ -385,7 +385,7 @@ class core_accesslib_testcase extends advanced_testcase {
         $permission = $DB->get_record('role_capabilities', array('contextid'=>$frontcontext->id, 'roleid'=>$student->id, 'capability'=>'moodle/backup:backupcourse'));
         $this->assertNotEmpty($permission);
         $this->assertEquals(CAP_ALLOW, $permission->permission);
-        $this->assertEquals(3, $permission->modifierid);
+        $this->assertEquals($user->id, $permission->modifierid);
 
         $result = assign_capability('moodle/backup:backupcourse', CAP_PROHIBIT, $student->id, $frontcontext->id, true);
         $this->assertTrue($result);
@@ -1796,7 +1796,7 @@ class core_accesslib_testcase extends advanced_testcase {
 
         // Add a resource to frontpage.
         $page = $generator->create_module('page', array('course'=>$SITE->id));
-        $testpages[] = $page->id;
+        $testpages[] = $page->cmid;
         $frontpagepagecontext = context_module::instance($page->cmid);
 
         // Add block to frontpage resource.
@@ -1839,7 +1839,7 @@ class core_accesslib_testcase extends advanced_testcase {
 
                 // Add a resource to each course.
                 $page = $generator->create_module('page', array('course'=>$course->id));
-                $testpages[] = $page->id;
+                $testpages[] = $page->cmid;
                 $modcontext = context_module::instance($page->cmid);
 
                 // Add block to each module.
@@ -2700,8 +2700,8 @@ class core_accesslib_testcase extends advanced_testcase {
         $this->assertEquals($url1, $url2);
         $this->assertInstanceOf('moodle_url', $url2);
 
-        $pagecm = get_coursemodule_from_instance('page', $testpages[7]);
-        $context = context_module::instance($pagecm->id);
+        $pagecm = get_coursemodule_from_id('page', $testpages[7]);
+        $context = context_module::instance($testpages[7]);
         $coursecontext1 = get_course_context($context);
         $this->assertDebuggingCalled('get_course_context() is deprecated, please use $context->get_course_context(true) instead.', DEBUG_DEVELOPER);
         $coursecontext2 = $context->get_course_context(true);
index 06fbdb6..baaa9e2 100644 (file)
@@ -25,6 +25,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+require_once(__DIR__.'/fixtures/event_fixtures.php');
+
 class core_events_testcase extends advanced_testcase {
 
     /**
@@ -338,4 +340,35 @@ class core_events_testcase extends advanced_testcase {
         $this->assertEventLegacyLogData($expected, $event);
         $this->assertEventContextNotUsed($event);
     }
+
+    /**
+     * There is no API associated with this event, so we will just test standard features.
+     */
+    public function test_grade_viewed() {
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $coursecontext = context_course::instance($course->id);
+
+        $event = \core_tests\event\grade_report_viewed::create(
+            array(
+                'context' => $coursecontext,
+                'courseid' => $course->id,
+                'userid' => $user->id,
+            )
+        );
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertInstanceOf('\core\event\grade_report_viewed', $event);
+        $this->assertEquals($event->courseid, $course->id);
+        $this->assertEquals($event->userid, $user->id);
+        $this->assertEventContextNotUsed($event);
+    }
 }
index 58cf2ab..e13c2d8 100644 (file)
@@ -250,6 +250,12 @@ class course_module_viewed extends \core\event\course_module_viewed {
 class course_module_viewed_noinit extends \core\event\course_module_viewed {
 }
 
+/**
+ * Event for testing core\event\grade_report_viewed.
+ */
+class grade_report_viewed extends \core\event\grade_report_viewed {
+}
+
 /**
  * Event to test context used in event functions
  */
index eb5a392..49bcf6c 100644 (file)
@@ -145,7 +145,7 @@ class core_grades_external_testcase extends externallib_advanced_testcase {
         $student2rawgrade = 20;
         list($course, $assignment, $student1, $student2, $teacher, $parent) =
             $this->load_test_data($assignmentname, $student1rawgrade, $student2rawgrade);
-        $assigmentcm = get_coursemodule_from_id('assign', $assignment->id, 0, false, MUST_EXIST);
+        $assigmentcm = get_coursemodule_from_id('assign', $assignment->cmid, 0, false, MUST_EXIST);
 
         // Student requesting their own grade for the assignment.
         $this->setUser($student1);
@@ -395,7 +395,7 @@ class core_grades_external_testcase extends externallib_advanced_testcase {
         $student2rawgrade = 20;
         list($course, $assignment, $student1, $student2, $teacher, $parent) =
             $this->load_test_data($assignmentname, $student1rawgrade, $student2rawgrade);
-        $assigmentcm = get_coursemodule_from_id('assign', $assignment->id, 0, false, MUST_EXIST);
+        $assigmentcm = get_coursemodule_from_id('assign', $assignment->cmid, 0, false, MUST_EXIST);
 
         $this->setUser($teacher);
 
index 863315b..c37ba55 100644 (file)
@@ -36,14 +36,31 @@ class core_outputrequirementslib_testcase extends advanced_testcase {
         $page = new moodle_page();
         $page->requires->string_for_js('course', 'moodle', 1);
         $page->requires->string_for_js('course', 'moodle', 1);
-        try {
-            $page->requires->string_for_js('course', 'moodle', 2);
-            $this->fail('Exception expected when the same string with different $a requested');
-        } catch (Exception $e) {
-            $this->assertInstanceOf('coding_exception', $e);
-        }
+        $this->setExpectedException('coding_exception');
+        $page->requires->string_for_js('course', 'moodle', 2);
 
         // Note: we can not switch languages in phpunit yet,
         //       it would be nice to test that the strings are actually fetched in the footer.
     }
+
+    public function test_one_time_output_normal_case() {
+        $page = new moodle_page();
+        $this->assertTrue($page->requires->should_create_one_time_item_now('test_item'));
+        $this->assertFalse($page->requires->should_create_one_time_item_now('test_item'));
+    }
+
+    public function test_one_time_output_repeat_output_throws() {
+        $page = new moodle_page();
+        $page->requires->set_one_time_item_created('test_item');
+        $this->setExpectedException('coding_exception');
+        $page->requires->set_one_time_item_created('test_item');
+    }
+
+    public function test_one_time_output_different_pages_independent() {
+        $firstpage = new moodle_page();
+        $secondpage = new moodle_page();
+        $this->assertTrue($firstpage->requires->should_create_one_time_item_now('test_item'));
+        $this->assertTrue($secondpage->requires->should_create_one_time_item_now('test_item'));
+    }
+
 }
index 9735720..e104dc6 100644 (file)
@@ -14,8 +14,32 @@ Events and Logging:
 * Significant changes in Logging API. For upgrading existing events_trigger() and
   add_to_log() see http://docs.moodle.org/dev/Migrating_logging_calls_in_plugins
   For accessing logs from plugins see http://docs.moodle.org/dev/Migrating_log_access_in_reports
-* The validation of the following events is now stricter:
-    - \core\event\course_section_updated
+* The validation of the following events is now stricter (see MDL-45445):
+    - \core\event\blog_entry_created
+    - \core\event\blog_entry_deleted
+    - \core\event\blog_entry_updated
+    - \core\event\cohort_member_added
+    - \core\event\cohort_member_removed
+    - \core\event\course_category_deleted
+    - \core\event\course_completed
+    - \core\event\course_content_deleted
+    - \core\event\course_created
+    - \core\event\course_deleted
+    - \core\event\course_restored
+    - \core\event\course_section_updated (see MDL-45229)
+    - \core\event\email_failed
+    - \core\event\group_member_added
+    - \core\event\group_member_removed
+    - \core\event\note_created
+    - \core\event\note_deleted
+    - \core\event\note_updated
+    - \core\event\role_assigned
+    - \core\event\role_deleted
+    - \core\event\role_unassigned
+    - \core\event\user_graded
+    - \core\event\user_loggedinas
+    - \core\event\user_profile_viewed
+    - \core\event\webservice_token_created
 
 DEPRECATIONS:
 * $module uses in mod/xxx/version.php files is now deprecated. Please use $plugin instead. It will be removed in Moodle 2.10.
index d76411f..3491330 100644 (file)
@@ -119,7 +119,7 @@ class reveal_identities_confirmation_page_viewed extends base {
         parent::validate_data();
 
         if (!isset($this->other['assignid'])) {
-            throw new \coding_exception('The \'assignid\' must be set in other.');
+            throw new \coding_exception('The \'assignid\' value must be set in other.');
         }
     }
 }
index 43980de..833d280 100644 (file)
@@ -120,7 +120,7 @@ class submission_confirmation_form_viewed extends base {
         parent::validate_data();
 
         if (!isset($this->other['assignid'])) {
-            throw new \coding_exception('The \'assignid\' must be set in other.');
+            throw new \coding_exception('The \'assignid\' value must be set in other.');
         }
     }
 }
index ce3ea15..898042d 100644 (file)
@@ -121,7 +121,7 @@ class submission_status_viewed extends base {
         parent::validate_data();
 
         if (!isset($this->other['assignid'])) {
-            throw new \coding_exception('The \'assignid\' must be set in other.');
+            throw new \coding_exception('The \'assignid\' value must be set in other.');
         }
     }
 }
index e758599..cfba9db 100644 (file)
@@ -32,7 +32,7 @@ defined('MOODLE_INTERNAL') || die();
  * @property-read array $other {
  *      Extra information about event.
  *
- *      - string newstatus: status of submission.
+ *      - string newstate: state of submission.
  * }
  *
  * @package    mod_assign
index 89815ab..f61579c 100644 (file)
@@ -159,6 +159,15 @@ $capabilities = array(
         )
     ),
 
+    'mod/assign:viewblinddetails' => array(
+        'riskbitmask' => RISK_PERSONAL,
+
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW
+        )
+    ),
 
 );
 
index 1451d5a..8baf3ff 100644 (file)
@@ -37,6 +37,7 @@ $action = optional_param('action', '', PARAM_ALPHANUM);
 $assignmentid = required_param('assignmentid', PARAM_INT);
 $userid = required_param('userid', PARAM_INT);
 $attemptnumber = required_param('attemptnumber', PARAM_INT);
+$readonly = optional_param('readonly', false, PARAM_BOOL);
 
 $cm = \get_coursemodule_from_instance('assign', $assignmentid, 0, false, MUST_EXIST);
 $context = \context_module::instance($cm->id);
@@ -53,12 +54,19 @@ if ($action == 'loadallpages') {
     $draft = true;
     if (!has_capability('mod/assign:grade', $context)) {
         $draft = false;
+        $readonly = true; // A student always sees the readonly version.
         require_capability('mod/assign:submit', $context);
     }
 
+    // Whoever is viewing the readonly version should not use the drafts, but the actual annotations.
+    if ($readonly) {
+        $draft = false;
+    }
+
     $pages = document_services::get_page_images_for_attempt($assignment,
                                                             $userid,
-                                                            $attemptnumber);
+                                                            $attemptnumber,
+                                                            $readonly);
 
     $response = new stdClass();
     $response->pagecount = count($pages);
@@ -66,13 +74,19 @@ if ($action == 'loadallpages') {
 
     $grade = $assignment->get_user_grade($userid, true);
 
+    // The readonly files are stored in a different file area.
+    $filearea = document_services::PAGE_IMAGE_FILEAREA;
+    if ($readonly) {
+        $filearea = document_services::PAGE_IMAGE_READONLY_FILEAREA;
+    }
+
     foreach ($pages as $id => $pagefile) {
         $index = count($response->pages);
         $page = new stdClass();
         $comments = page_editor::get_comments($grade->id, $index, $draft);
         $page->url = moodle_url::make_pluginfile_url($context->id,
                                                      'assignfeedback_editpdf',
-                                                     document_services::PAGE_IMAGE_FILEAREA,
+                                                     $filearea,
                                                      $grade->id,
                                                      '/',
                                                      $pagefile->get_filename())->out();
index 184a957..8154848 100644 (file)
@@ -49,6 +49,7 @@ try {
         throw new coding_exception('grade not found');
     }
 
+    // No need to handle the readonly files here, the should be already generated.
     $component = 'assignfeedback_editpdf';
     $filearea = document_services::PAGE_IMAGE_FILEAREA;
     $filepath = '/';
index 546b876..4fe8cc6 100644 (file)
@@ -61,8 +61,11 @@ class backup_assignfeedback_editpdf_subplugin extends backup_subplugin {
         $subpluginelementfiles->set_source_sql('SELECT id AS gradeid from {assign_grades} where id = :gradeid', array('gradeid' => backup::VAR_PARENTID));
         $subpluginelementannotation->set_source_table('assignfeedback_editpdf_annot', array('gradeid' => backup::VAR_PARENTID));
         $subpluginelementcomment->set_source_table('assignfeedback_editpdf_cmnt', array('gradeid' => backup::VAR_PARENTID));
-        // We only need to backup the files in the final pdf area - all the others can be regenerated.
-        $subpluginelementfiles->annotate_files('assignfeedback_editpdf', 'download', 'gradeid');
+        // We only need to backup the files in the final pdf area, and the readonly page images - the others can be regenerated.
+        $subpluginelementfiles->annotate_files('assignfeedback_editpdf',
+            \assignfeedback_editpdf\document_services::FINAL_PDF_FILEAREA, 'gradeid');
+        $subpluginelementfiles->annotate_files('assignfeedback_editpdf',
+            \assignfeedback_editpdf\document_services::PAGE_IMAGE_READONLY_FILEAREA, 'gradeid');
         $subpluginelementfiles->annotate_files('assignfeedback_editpdf', 'stamps', 'gradeid');
         return $subplugin;
     }
index cb7923e..c520e2b 100644 (file)
@@ -68,7 +68,10 @@ class restore_assignfeedback_editpdf_subplugin extends restore_subplugin {
         $data = (object)$data;
 
         // In this case the id is the old gradeid which will be mapped.
-        $this->add_related_files('assignfeedback_editpdf', 'download', 'grade', null, $data->gradeid);
+        $this->add_related_files('assignfeedback_editpdf',
+            \assignfeedback_editpdf\document_services::FINAL_PDF_FILEAREA, 'grade', null, $data->gradeid);
+        $this->add_related_files('assignfeedback_editpdf',
+            \assignfeedback_editpdf\document_services::PAGE_IMAGE_READONLY_FILEAREA, 'grade', null, $data->gradeid);
         $this->add_related_files('assignfeedback_editpdf', 'stamps', 'grade', null, $data->gradeid);
     }
 
index 468b103..912bd8a 100644 (file)
@@ -42,6 +42,8 @@ class document_services {
     const COMBINED_PDF_FILEAREA = 'combined';
     /** File area for page images */
     const PAGE_IMAGE_FILEAREA = 'pages';
+    /** File area for readonly page images */
+    const PAGE_IMAGE_READONLY_FILEAREA = 'readonlypages';
     /** Filename for combined pdf */
     const COMBINED_PDF_FILENAME = 'combined.pdf';
 
@@ -268,9 +270,10 @@ class document_services {
      * @param int|\assign $assignment
      * @param int $userid
      * @param int $attemptnumber (-1 means latest attempt)
+     * @param bool $readonly When true we get the number of pages for the readonly version.
      * @return int number of pages
      */
-    public static function page_number_for_attempt($assignment, $userid, $attemptnumber) {
+    public static function page_number_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) {
         global $CFG;
 
         require_once($CFG->libdir . '/pdflib.php');
@@ -281,6 +284,19 @@ class document_services {
             \print_error('nopermission');
         }
 
+        // When in readonly we can return the number of images in the DB because they should already exist,
+        // if for some reason they do not, then we proceed as for the normal version.
+        if ($readonly) {
+            $grade = $assignment->get_user_grade($userid, true, $attemptnumber);
+            $fs = get_file_storage();
+            $files = $fs->get_directory_files($assignment->get_context()->id, 'assignfeedback_editpdf',
+                self::PAGE_IMAGE_READONLY_FILEAREA, $grade->id, '/');
+            $pagecount = count($files);
+            if ($pagecount > 0) {
+                return $pagecount;
+            }
+        }
+
         // Get a combined pdf file from all submitted pdf files.
         $file = self::get_combined_pdf_for_attempt($assignment, $userid, $attemptnumber);
         if (!$file) {
@@ -363,12 +379,25 @@ class document_services {
 
     /**
      * This function returns a list of the page images from a pdf.
+     *
+     * The readonly version is different than the normal one. The readonly version contains a copy
+     * of the pages in the state they were when the PDF was annotated, by doing so we prevent the
+     * the pages that are displayed to change as soon as the submission changes.
+     *
+     * Though there is an edge case, if the PDF was annotated before MDL-45580, then it is possible
+     * that we do not find any readonly version of the pages. In that case, we will get the normal
+     * pages and copy them to the readonly area. This ensures that the pages will remain in that
+     * state until the submission is updated. When the normal files do not exist, we throw an exception
+     * because the readonly pages should only ever be displayed after a teacher has annotated the PDF,
+     * they would not exist until they do.
+     *
      * @param int|\assign $assignment
      * @param int $userid
      * @param int $attemptnumber (-1 means latest attempt)
+     * @param bool $readonly If true, then we are requesting the readonly version.
      * @return array(stored_file)
      */
-    public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber) {
+    public static function get_page_images_for_attempt($assignment, $userid, $attemptnumber, $readonly = false) {
 
         $assignment = self::get_assignment_from_param($assignment);
 
@@ -385,26 +414,39 @@ class document_services {
 
         $contextid = $assignment->get_context()->id;
         $component = 'assignfeedback_editpdf';
-        $filearea = self::PAGE_IMAGE_FILEAREA;
         $itemid = $grade->id;
         $filepath = '/';
+        $filearea = self::PAGE_IMAGE_FILEAREA;
 
         $fs = \get_file_storage();
 
+        // If we are after the readonly pages...
+        $copytoreadonly = false;
+        if ($readonly) {
+            $filearea = self::PAGE_IMAGE_READONLY_FILEAREA;
+            if ($fs->is_area_empty($contextid, $component, $filearea, $itemid)) {
+                // We have a problem here, we were supposed to find the files...
+                // let's fallback on the other area, and copy the files to the readonly area.
+                $copytoreadonly = true;
+                $filearea = self::PAGE_IMAGE_FILEAREA;
+            }
+        }
+
         $files = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath);
 
+        $pages = array();
         if (!empty($files)) {
             $first = reset($files);
-            if ($first->get_timemodified() < $submission->timemodified) {
-
+            if (!$readonly && $first->get_timemodified() < $submission->timemodified) {
+                // Image files are stale, we need to regenerate them, except in readonly mode.
+                // We also need to remove the draft annotations and comments associated with this attempt.
                 $fs->delete_area_files($contextid, $component, $filearea, $itemid);
-                // Image files are stale - regenerate them.
+                page_editor::delete_draft_content($itemid);
                 $files = array();
             } else {
 
                 // Need to reorder the files following their name.
                 // because get_directory_files() return a different order than generate_page_images_for_attempt().
-                $orderedfiles = array();
                 foreach($files as $file) {
                     // Extract the page number from the file name image_pageXXXX.png.
                     preg_match('/page([\d]+)\./', $file->get_filename(), $matches);
@@ -415,14 +457,26 @@ class document_services {
                     $pagenumber = (int)$matches[1];
 
                     // Save the page in the ordered array.
-                    $orderedfiles[$pagenumber] = $file;
+                    $pages[$pagenumber] = $file;
                 }
-                ksort($orderedfiles);
+                ksort($pages);
 
-                return $orderedfiles;
+                if ($copytoreadonly) {
+                    self::copy_pages_to_readonly_area($assignment, $grade);
+                }
             }
         }
-        return self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
+
+        if (empty($pages)) {
+            if ($readonly) {
+                // This should never happen, there should be a version of the pages available
+                // whenever we are requesting the readonly version.
+                throw new \moodle_exception('Could not find readonly pages for grade ' . $grade->id);
+            }
+            $pages = self::generate_page_images_for_attempt($assignment, $userid, $attemptnumber);
+        }
+
+        return $pages;
     }
 
     /**
@@ -465,6 +519,11 @@ class document_services {
 
     /**
      * This function takes the combined pdf and embeds all the comments and annotations.
+     *
+     * This also moves the annotations and comments from drafts to not drafts. And it will
+     * copy all the images stored to the readonly area, so that they can be viewed online, and
+     * not be overwritten when a new submission is sent.
+     *
      * @param int|\assign $assignment
      * @param int $userid
      * @param int $attemptnumber (-1 means latest attempt)
@@ -567,9 +626,41 @@ class document_services {
         @unlink($combined);
         @rmdir($tmpdir);
 
+        self::copy_pages_to_readonly_area($assignment, $grade);
+
         return $file;
     }
 
+    /**
+     * Copy the pages image to the readonly area.
+     *
+     * @param int|\assign $assignment The assignment.
+     * @param \stdClass $grade The grade record.
+     * @return void
+     */
+    public static function copy_pages_to_readonly_area($assignment, $grade) {
+        $fs = get_file_storage();
+        $assignment = self::get_assignment_from_param($assignment);
+        $contextid = $assignment->get_context()->id;
+        $component = 'assignfeedback_editpdf';
+        $itemid = $grade->id;
+
+        // Get all the pages.
+        $originalfiles = $fs->get_area_files($contextid, $component, self::PAGE_IMAGE_FILEAREA, $itemid);
+        if (empty($originalfiles)) {
+            // Nothing to do here...
+            return;
+        }
+
+        // Delete the old readonly files.
+        $fs->delete_area_files($contextid, $component, self::PAGE_IMAGE_READONLY_FILEAREA, $itemid);
+
+        // Do the copying.
+        foreach ($originalfiles as $originalfile) {
+            $fs->create_file_from_storedfile(array('filearea' => self::PAGE_IMAGE_READONLY_FILEAREA), $originalfile);
+        }
+    }
+
     /**
      * This function returns the generated pdf (if it exists).
      * @param int|\assign $assignment
index 1b1447a..3dfa071 100644 (file)
@@ -340,4 +340,21 @@ class page_editor {
 
         return true;
     }
+
+    /**
+     * Delete the draft annotations and comments.
+     *
+     * This is intended to be used when the version of the PDF has changed and the annotations
+     * might not be relevant any more, therefore we should delete them.
+     *
+     * @param int $gradeid The grade ID.
+     * @return bool
+     */
+    public static function delete_draft_content($gradeid) {
+        global $DB;
+        $conditions = array('gradeid' => $gradeid, 'draft' => 1);
+        $result = $DB->delete_records('assignfeedback_editpdf_annot', $conditions);
+        $result = $result && $DB->delete_records('assignfeedback_editpdf_cmnt', $conditions);
+        return $result;
+    }
 }
index f98bee9..be4a8a8 100644 (file)
@@ -137,7 +137,8 @@ class assign_feedback_editpdf extends assign_feedback_plugin {
         // Retrieve total number of pages.
         $pagetotal = document_services::page_number_for_attempt($this->assignment->get_instance()->id,
                 $userid,
-                $attempt);
+                $attempt,
+                $readonly);
 
         $widget = new assignfeedback_editpdf_widget($this->assignment->get_instance()->id,
                                                     $userid,
index 05b1dc5..3f4859a 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js differ
index e03ddc0..5e8c6bf 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js differ
index 05b1dc5..3f4859a 100644 (file)
Binary files a/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js and b/mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js differ
index 898179b..a0366ba 100644 (file)
@@ -346,7 +346,8 @@ EDITOR.prototype = {
                 action : 'loadallpages',
                 userid : this.get('userid'),
                 attemptnumber : this.get('attemptnumber'),
-                assignmentid : this.get('assignmentid')
+                assignmentid : this.get('assignmentid'),
+                readonly : this.get('readonly') ? 1 : 0
             },
             on: {
                 success: function(tid, response) {
index 6ae9635..5b74c7d 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
index 522dfa3..5506a6b 100644 (file)
@@ -90,6 +90,10 @@ class assign_grading_table extends table_sql implements renderable {
                                                   $this->assignment->get_context());
         $this->hasgrade = $this->assignment->can_grade();
 
+        // Check if we have the elevated view capablities to see the blind details.
+        $this->hasviewblind = has_capability('mod/assign:viewblinddetails',
+                $this->assignment->get_context());
+
         foreach ($assignment->get_feedback_plugins() as $plugin) {
             if ($plugin->is_visible() && $plugin->is_enabled()) {
                 foreach ($plugin->get_grading_batch_operations() as $action => $description) {
@@ -249,7 +253,7 @@ class assign_grading_table extends table_sql implements renderable {
         }
 
         // User picture.
-        if (!$this->assignment->is_blind_marking()) {
+        if ($this->hasviewblind || !$this->assignment->is_blind_marking()) {
             if (!$this->is_downloading()) {
                 $columns[] = 'picture';
                 $headers[] = get_string('pictureofuser');
@@ -272,6 +276,11 @@ class assign_grading_table extends table_sql implements renderable {
             $headers[] = get_string('recordid', 'assign');
         }
 
+        if ($this->hasviewblind) {
+                $columns[] = 'recordid';
+                $headers[] = get_string('recordid', 'assign');
+        }
+
         // Submission status.
         if ($assignment->is_any_submission_plugin_enabled()) {
             $columns[] = 'status';
index 6f7962c..1d06d51 100644 (file)
@@ -1,4 +1,4 @@
-<?PHP
+<?php
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
index 9c323eb..d3771cc 100644 (file)
@@ -3243,8 +3243,13 @@ class assign {
      */
     public function fullname($user) {
         if ($this->is_blind_marking()) {
-            $uniqueid = $this->get_uniqueid_for_user($user->id);
-            return get_string('participant', 'assign') . ' ' . $uniqueid;
+            $hasviewblind = has_capability('mod/assign:viewblinddetails', $this->get_context());
+            if ($hasviewblind) {
+                return fullname($user);
+            } else {
+                $uniqueid = $this->get_uniqueid_for_user($user->id);
+                return get_string('participant', 'assign') . ' ' . $uniqueid;
+            }
         } else {
             return fullname($user);
         }
@@ -4984,6 +4989,10 @@ class assign {
             $users[$userid] = $record;
         }
 
+        if (empty($users)) {
+            return get_string('nousersselected', 'assign');
+        }
+
         list($userids, $params) = $DB->get_in_or_equal(array_keys($users), SQL_PARAMS_NAMED);
         $params['assignid1'] = $this->get_instance()->id;
         $params['assignid2'] = $this->get_instance()->id;
index 880ec88..868d48d 100644 (file)
-div.gradingnavigation div {
+.path-mod-assign div.gradingnavigation div {
     float: left;
     margin-left: 2em;
 }
 
-
-div.submissionstatustable,
-div.submissionfull,
-div.submissionlinks,
-div.usersummary,
-div.feedback,
-div.gradingsummary {
+.path-mod-assign div.submissionstatustable,
+.path-mod-assign div.submissionfull,
+.path-mod-assign div.submissionlinks,
+.path-mod-assign div.usersummary,
+.path-mod-assign div.feedback,
+.path-mod-assign div.gradingsummary {
     margin-bottom: 5em;
 }
 
-
-div.submissionstatus .generaltable,
-div.submissionlinks .generaltable,
-div.feedback .generaltable,
-div.submissionsummarytable .generaltable,
-div.attempthistory table,
-div.gradingsummary .generaltable {
+.path-mod-assign div.submissionstatus .generaltable,
+.path-mod-assign div.submissionlinks .generaltable,
+.path-mod-assign div.feedback .generaltable,
+.path-mod-assign div.submissionsummarytable .generaltable,
+.path-mod-assign div.attempthistory table,
+.path-mod-assign div.gradingsummary .generaltable {
     width: 100%;
 }
 
-#page-mod-assign-view table.generaltable table td { border: 0px none; }
+.path-mod-assign table.generaltable table td {
+    border: 0px none;
+}
 
-.gradingsummarytable,
-.feedbacktable,
-.lockedsubmission,
-.submissionsummarytable {
+.path-mod-assign .gradingsummarytable,
+.path-mod-assign .feedbacktable,
+.path-mod-assign .lockedsubmission,
+.path-mod-assign .submissionsummarytable {
     margin-top: 1em;
 }
 
-div.submissionsummarytable table tbody tr td.c0 {
+.path-mod-assign div.submissionsummarytable table tbody tr td.c0 {
     width: 30%;
 }
 
-.submittedlate {
+.path-mod-assign .submittedlate {
     color: red;
     font-weight: 900;
 }
 
-.jsenabled .gradingoptionsform .fsubmit {display: none;}
-.jsenabled .gradingtable .c1 select {display: none;}
-.quickgradingform .mform fieldset { margin: 0px; padding: 0px; }
-.gradingbatchoperationsform .mform fieldset { margin: 0px; padding: 0px; }
+.path-mod-assign.jsenabled .gradingoptionsform .fsubmit {
+    display: none;
+}
+
+.path-mod-assign.jsenabled .gradingtable .c1 select {
+    display: none;
+}
+
+.path-mod-assign .quickgradingform .mform fieldset {
+    margin: 0px;
+    padding: 0px;
+}
+
+.path-mod-assign .gradingbatchoperationsform .mform fieldset {
+    margin: 0px;
+    padding: 0px;
+}
 
-td.submissionstatus,
-div.submissionstatus,
-a:link.submissionstatus {
+.path-mod-assign td.submissionstatus,
+.path-mod-assign div.submissionstatus,
+.path-mod-assign a:link.submissionstatus {
     color: black;
     background-color: #efefef;
 }
 
-td.submissionstatusdraft,
-div.submissionstatusdraft,
-a:link.submissionstatusdraft {
+.path-mod-assign td.submissionstatusdraft,
+.path-mod-assign div.submissionstatusdraft,
+.path-mod-assign a:link.submissionstatusdraft {
     color: black;
     background-color: #efefcf;
 }
 
-td.submissionstatussubmitted,
-div.submissionstatussubmitted,
-a:link.submissionstatussubmitted {
+.path-mod-assign td.submissionstatussubmitted,
+.path-mod-assign div.submissionstatussubmitted,
+.path-mod-assign a:link.submissionstatussubmitted {
     color: black;
     background-color: #cfefcf;
 }
 
 
-td.submissionlocked,
-div.submissionlocked {
+.path-mod-assign td.submissionlocked,
+.path-mod-assign div.submissionlocked {
     color: black;
     background-color: #efefcf;
 }
 
-td.submissionreopened,
-div.submissionreopened {
+.path-mod-assign td.submissionreopened,
+.path-mod-assign div.submissionreopened {
     color: black;
     background-color: #efefef;
 }
 
-td.submissiongraded,
-div.submissiongraded {
+.path-mod-assign td.submissiongraded,
+.path-mod-assign div.submissiongraded {
     color: black;
     background-color: #cfefcf;
 }
 
-td.submissionnotgraded,
-div.submissionnotgraded {
+.path-mod-assign td.submissionnotgraded,
+.path-mod-assign div.submissionnotgraded {
     color: black;
     background-color: #efefef;
 }
 
-td.latesubmission,
-a:link.latesubmission,
-div.latesubmission {
+.path-mod-assign td.latesubmission,
+.path-mod-assign a:link.latesubmission,
+.path-mod-assign div.latesubmission {
     color: black;
     background-color: #efcfcf;
 }
 
-td.earlysubmission,
-div.earlysubmission {
+.path-mod-assign td.earlysubmission,
+.path-mod-assign div.earlysubmission {
     color: black;
     background-color: #cfefcf;
 }
 
-.gradingtable .c0 { display: none; }
-.jsenabled .gradingtable .c0 { display: table-cell; }
-.gradingbatchoperationsform { display: none; }
-.jsenabled .gradingbatchoperationsform { display: block; }
+.path-mod-assign .gradingtable .c0 {
+    display: none;
+}
 
-.gradingtable tr.selectedrow td { background-color: #ffeecc; }
-.gradingtable tr.unselectedrow td { background-color: white; }
+.path-mod-assign.jsenabled .gradingtable .c0 {
+    display: table-cell;
+}
 
-.gradingtable .c0 div.selectall {margin-left: 7px;}
+.path-mod-assign .gradingbatchoperationsform {
+    display: none;
+}
 
-.gradingtable .yui3-menu ul {
+.path-mod-assign.jsenabled .gradingbatchoperationsform {
+    display: block;
+}
+
+.path-mod-assign .gradingtable tr.selectedrow td {
+    background-color: #ffeecc;
+}
+
+.path-mod-assign .gradingtable tr.unselectedrow td {
+    background-color: white;
+}
+
+.path-mod-assign .gradingtable .c0 div.selectall {
+    margin-left: 7px;
+}
+
+.path-mod-assign .gradingtable .yui3-menu ul {
     margin: 0px;
 }
 
-.gradingtable .yui3-menu-label {
+.path-mod-assign .gradingtable .yui3-menu-label {
     padding-left: 0px;
     line-height: 12px;
 }
-.gradingtable .yui3-menu-label img { padding: 0 3px; }
-.gradingtable .yui3-menu li {
+
+.path-mod-assign .gradingtable .yui3-menu-label img {
+    padding: 0 3px;
+}
+
+.path-mod-assign .gradingtable .yui3-menu li {
     list-style-type: none;
 }
 
-.jsenabled .gradingtable .yui3-loading {
+.path-mod-assign.jsenabled .gradingtable .yui3-loading {
     display: none;
 }
 
-.gradingtable .yui3-menu .yui3-menu-content {
+.path-mod-assign .gradingtable .yui3-menu .yui3-menu-content {
     border: 0px;
     padding-top: 0;
 }
 
-#page-mod-assign-view div.gradingtable tr .quickgrademodified {
+.path-mod-assign div.gradingtable tr .quickgrademodified {
     background-color: #FFCC99;
 }
 
-td.submissioneditable {
+.path-mod-assign td.submissioneditable {
     color: red;
 }
 
-.expandsummaryicon {
+.path-mod-assign .expandsummaryicon {
     cursor: pointer;
     display: none;
 }
 
-.jsenabled .expandsummaryicon {
+.path-mod-assign.jsenabled .expandsummaryicon {
     display: inline;
 }
 
-.hidefull {
+.path-mod-assign .hidefull {
     display: none;
 }
 
-.quickgradingform form .commentscontainer input,
-.quickgradingform form .commentscontainer textarea {
+.path-mod-assign .quickgradingform form .commentscontainer input,
+.path-mod-assign .quickgradingform form .commentscontainer textarea {
     display: none;
 }
 
-.jsenabled .quickgradingform form .commentscontainer input,
-.jsenabled .quickgradingform form .commentscontainer textarea {
+.path-mod-assign.jsenabled .quickgradingform form .commentscontainer input,
+.path-mod-assign.jsenabled .quickgradingform form .commentscontainer textarea {
     display: inline;
 }
 
-#page-mod-assign-view .previousfeedbackwarning {
+.path-mod-assign .previousfeedbackwarning {
     font-size: 140%;
     font-weight: bold;
     text-align: center;
     color: #500;
 }
 
-#page-mod-assign-view .submissionhistory {
+.path-mod-assign .submissionhistory {
     background-color: #b0b0b0;
 }
 
-#page-mod-assign-view .submissionhistory .cell.historytitle {
+.path-mod-assign .submissionhistory .cell.historytitle {
     background-color: #808080;
 }
 
-#page-mod-assign-view .submissionhistory .cell {
+.path-mod-assign .submissionhistory .cell {
     background-color: #d0d0d0;
 }
 
-#page-mod-assign-view .submissionhistory .singlebutton {
+.path-mod-assign .submissionhistory .singlebutton {
     display: inline-block;
     float: right;
 }
 
-#page-mod-assign-view.dir-rtl .submissionhistory .singlebutton {
+.path-mod-assign.dir-rtl .submissionhistory .singlebutton {
     float: left;
 }
 
-#page-mod-assign-view .submissionsummarytable .singlebutton {
+.path-mod-assign .submissionsummarytable .singlebutton {
     display: inline-block;
 }
 
-.jsenabled .mod-assign-history-link {
+.path-mod-assign.jsenabled .mod-assign-history-link {
     display: block;
     cursor: pointer;
     margin-bottom: 7px;
 }
 
-.jsenabled .mod-assign-history-link h4 {
+.path-mod-assign.jsenabled .mod-assign-history-link h4 {
     display: inline;
 }
 
-#page-mod-assign-view.jsenabled .attempthistory h4 {
+.path-mod-assign.jsenabled .attempthistory h4 {
     margin-bottom: 7px;
     text-align: left;
 }
 
-#page-mod-assign-view.jsenabled.dir_rtl .attempthistory h4 {
+.path-mod-assign.jsenabled.dir_rtl .attempthistory h4 {