Merge branch 'wip-MDL-41179-master' of git://github.com/marinaglancy/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 3 Sep 2013 03:48:45 +0000 (11:48 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 3 Sep 2013 03:48:45 +0000 (11:48 +0800)
109 files changed:
admin/roles/classes/define_role_table_advanced.php
admin/tool/generator/classes/backend.php
admin/tool/generator/classes/course_backend.php [new file with mode: 0644]
admin/tool/generator/classes/make_form.php
admin/tool/generator/classes/site_backend.php [new file with mode: 0644]
admin/tool/generator/cli/maketestcourse.php
admin/tool/generator/cli/maketestsite.php [new file with mode: 0644]
admin/tool/generator/lang/en/tool_generator.php
admin/tool/generator/maketestcourse.php
admin/tool/generator/tests/maketestcourse_test.php
admin/tool/generator/version.php
admin/tool/uploaduser/index.php
admin/user.php
auth/db/auth.php
auth/email/auth.php
auth/ldap/auth.php
auth/mnet/auth.php
backup/backup.php
backup/controller/backup_controller.class.php
backup/controller/restore_controller.class.php
backup/import.php
backup/moodle2/backup_final_task.class.php
backup/restore.php
backup/util/includes/backup_includes.php
backup/util/includes/restore_includes.php
backup/util/plan/backup_plan.class.php
backup/util/plan/base_plan.class.php
backup/util/plan/base_task.class.php
backup/util/plan/restore_plan.class.php
backup/util/plan/tests/fixtures/plan_fixtures.php
backup/util/progress/core_backup_display_progress.class.php [new file with mode: 0644]
backup/util/progress/core_backup_null_progress.class.php [new file with mode: 0644]
backup/util/progress/core_backup_progress.class.php [new file with mode: 0644]
backup/util/progress/tests/progress_test.php [new file with mode: 0644]
badges/classes/observer.php
badges/external.php
badges/mybackpack.php
badges/renderer.php
badges/tests/badgeslib_test.php
course/externallib.php
course/lib.php
course/tests/courselib_test.php
enrol/ldap/lang/en/enrol_ldap.php
enrol/ldap/lib.php
enrol/manual/locallib.php
enrol/meta/classes/observer.php [new file with mode: 0644]
enrol/meta/db/events.php
enrol/meta/locallib.php
enrol/meta/tests/plugin_test.php
enrol/tests/enrollib_test.php
files/renderer.php
grade/export/lib.php
grade/externallib.php
grade/grading/form/guide/lib.php
grade/grading/form/lib.php
grade/grading/form/rubric/lib.php
grade/tests/externallib_test.php
lang/en/badges.php
lang/en/cache.php
lang/en/enrol.php
lang/en/error.php
lang/en/moodle.php
lib/accesslib.php
lib/badgeslib.php
lib/behat/classes/util.php
lib/blocklib.php
lib/classes/event/user_created.php [new file with mode: 0644]
lib/classes/event/user_deleted.php [new file with mode: 0644]
lib/classes/event/user_enrolment_created.php [new file with mode: 0644]
lib/classes/event/user_enrolment_deleted.php [new file with mode: 0644]
lib/classes/event/user_enrolment_updated.php [new file with mode: 0644]
lib/classes/event/user_loggedout.php [new file with mode: 0644]
lib/classes/event/user_updated.php [new file with mode: 0644]
lib/configonlylib.php
lib/db/caches.php
lib/db/events.php
lib/db/services.php
lib/dml/mssql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/editor/tinymce/module.js
lib/enrollib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/tests/configonlylib_test.php
lib/tests/moodlelib_test.php
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/notification/js/dialogue.js
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/locallib.php
mod/data/view.php
mod/forum/classes/observer.php [new file with mode: 0644]
mod/forum/db/events.php
mod/forum/lib.php
mod/forum/tests/lib_test.php
repository/filepicker.js
theme/base/style/core.css
theme/bootstrapbase/less/moodle/backup-restore.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
user/edit.php
user/editadvanced.php
user/emailupdate.php
user/lib.php
user/messageselect.php
user/tests/userlib_test.php [new file with mode: 0644]
version.php

index cd7741c..66cc014 100644 (file)
@@ -49,14 +49,11 @@ class core_role_define_role_table_advanced extends core_role_capability_table_wi
         $this->displaypermissions = $this->allpermissions;
         $this->strperms[$this->allpermissions[CAP_INHERIT]] = get_string('notset', 'core_role');
 
-        $this->allcontextlevels = array(
-            CONTEXT_SYSTEM => get_string('coresystem'),
-            CONTEXT_USER => get_string('user'),
-            CONTEXT_COURSECAT => get_string('category'),
-            CONTEXT_COURSE => get_string('course'),
-            CONTEXT_MODULE => get_string('activitymodule'),
-            CONTEXT_BLOCK => get_string('block')
-        );
+        $this->allcontextlevels = array();
+        $levels = context_helper::get_all_levels();
+        foreach ($levels as $level => $classname) {
+            $this->allcontextlevels[$level] = context_helper::get_level_name($level);
+        }
     }
 
     protected function load_current_permissions() {
index beed421..20b3370 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+/**
+ * Backend generic code.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Backend code for the 'make large course' tool.
+ * Backend generic code for all tool_generator commands.
  *
+ * @abstract
  * @package tool_generator
  * @copyright 2013 The Open University
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class tool_generator_backend {
+abstract class tool_generator_backend {
     /**
      * @var int Lowest (smallest) size index
      */
@@ -38,126 +47,56 @@ class tool_generator_backend {
     const DEFAULT_SIZE = 3;
 
     /**
-     * @var array Number of sections in course
-     */
-    private static $paramsections = array(1, 10, 100, 500, 1000, 2000);
-    /**
-     * @var array Number of Page activities in course
-     */
-    private static $parampages = array(1, 50, 200, 1000, 5000, 10000);
-    /**
-     * @var array Number of students enrolled in course
-     */
-    private static $paramusers = array(1, 100, 1000, 10000, 50000, 100000);
-    /**
-     * Total size of small files: 1KB, 1MB, 10MB, 100MB, 1GB, 2GB.
-     *
-     * @var array Number of small files created in a single file activity
-     */
-    private static $paramsmallfilecount = array(1, 64, 128, 1024, 16384, 32768);
-    /**
-     * @var array Size of small files (to make the totals into nice numbers)
-     */
-    private static $paramsmallfilesize = array(1024, 16384, 81920, 102400, 65536, 65536);
-    /**
-     * Total size of big files: 8KB, 8MB, 80MB, 800MB, 8GB, 16GB.
-     *
-     * @var array Number of big files created as individual file activities
-     */
-    private static $parambigfilecount = array(1, 2, 5, 10, 10, 10);
-    /**
-     * @var array Size of each large file
-     */
-    private static $parambigfilesize = array(8192, 4194304, 16777216, 83886080,
-            858993459, 1717986918);
-    /**
-     * @var array Number of forum discussions
-     */
-    private static $paramforumdiscussions = array(1, 10, 100, 500, 1000, 2000);
-    /**
-     * @var array Number of forum posts per discussion
-     */
-    private static $paramforumposts = array(2, 2, 5, 10, 10, 10);
-
-    /**
-     * @var string Course shortname
-     */
-    private $shortname;
-
-    /**
-     * @var int Size code (index in the above arrays)
+     * @var bool True if we want a fixed dataset or false to generate random data
      */
-    private $size;
+    protected $fixeddataset;
 
     /**
      * @var bool True if displaying progress
      */
-    private $progress;
-
-    /**
-     * @var testing_data_generator Data generator
-     */
-    private $generator;
-
-    /**
-     * @var stdClass Course object
-     */
-    private $course;
+    protected $progress;
 
     /**
      * @var int Epoch time at which last dot was displayed
      */
-    private $lastdot;
+    protected $lastdot;
 
     /**
      * @var int Epoch time at which last percentage was displayed
      */
-    private $lastpercentage;
+    protected $lastpercentage;
 
     /**
      * @var int Epoch time at which current step (current set of dots) started
      */
-    private $starttime;
+    protected $starttime;
 
     /**
-     * @var array Array from test user number (1...N) to userid in database
+     * @var int Size code (index in the above arrays)
      */
-    private $userids;
+    protected $size;
 
     /**
-     * Constructs object ready to create course.
+     * Generic generator class
      *
-     * @param string $shortname Course shortname
      * @param int $size Size as numeric index
+     * @param bool $fixeddataset To use fixed or random data
      * @param bool $progress True if progress information should be displayed
-     * @return int Course id
      * @throws coding_exception If parameters are invalid
      */
-    public function __construct($shortname, $size, $progress = true) {
+    public function __construct($size, $fixeddataset = false, $progress = true) {
+
         // Check parameter.
         if ($size < self::MIN_SIZE || $size > self::MAX_SIZE) {
             throw new coding_exception('Invalid size');
         }
 
         // Set parameters.
-        $this->shortname = $shortname;
         $this->size = $size;
+        $this->fixeddataset = $fixeddataset;
         $this->progress = $progress;
     }
 
-    /**
-     * Gets a list of size choices supported by this backend.
-     *
-     * @return array List of size (int) => text description for display
-     */
-    public static function get_size_choices() {
-        $options = array();
-        for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
-            $options[$size] = get_string('size_' . $size, 'tool_generator');
-        }
-        return $options;
-    }
-
     /**
      * Converts a size name into the numeric constant.
      *
@@ -174,367 +113,13 @@ class tool_generator_backend {
         throw new coding_exception("Unknown size name '$sizename'");
     }
 
-    /**
-     * Checks that a shortname is available (unused).
-     *
-     * @param string $shortname Proposed course shortname
-     * @return string An error message if the name is unavailable or '' if OK
-     */
-    public static function check_shortname_available($shortname) {
-        global $DB;
-        $fullname = $DB->get_field('course', 'fullname',
-                array('shortname' => $shortname), IGNORE_MISSING);
-        if ($fullname !== false) {
-            // I wanted to throw an exception here but it is not possible to
-            // use strings from moodle.php in exceptions, and I didn't want
-            // to duplicate the string in tool_generator, so I changed this to
-            // not use exceptions.
-            return get_string('shortnametaken', 'moodle', $fullname);
-        }
-        return '';
-    }
-
-    /**
-     * Runs the entire 'make' process.
-     *
-     * @return int Course id
-     */
-    public function make() {
-        global $DB, $CFG;
-        require_once($CFG->dirroot . '/lib/phpunit/classes/util.php');
-
-        raise_memory_limit(MEMORY_EXTRA);
-
-        if ($this->progress && !CLI_SCRIPT) {
-            echo html_writer::start_tag('ul');
-        }
-
-        $entirestart = microtime(true);
-
-        // Start transaction.
-        $transaction = $DB->start_delegated_transaction();
-
-        // Get generator.
-        $this->generator = phpunit_util::get_data_generator();
-
-        // Make course.
-        $this->course = $this->create_course();
-        $this->create_users();
-        $this->create_pages();
-        $this->create_small_files();
-        $this->create_big_files();
-        $this->create_forum();
-
-        // Log total time.
-        $this->log('complete', round(microtime(true) - $entirestart, 1));
-
-        if ($this->progress && !CLI_SCRIPT) {
-            echo html_writer::end_tag('ul');
-        }
-
-        // Commit transaction and finish.
-        $transaction->allow_commit();
-        return $this->course->id;
-    }
-
-    /**
-     * Creates the actual course.
-     *
-     * @return stdClass Course record
-     */
-    private function create_course() {
-        $this->log('createcourse', $this->shortname);
-        $courserecord = array('shortname' => $this->shortname,
-                'fullname' => get_string('fullname', 'tool_generator',
-                    array('size' => get_string('shortsize_' . $this->size, 'tool_generator'))),
-                'numsections' => self::$paramsections[$this->size]);
-        return $this->generator->create_course($courserecord, array('createsections' => true));
-    }
-
-    /**
-     * Creates a number of user accounts and enrols them on the course.
-     * Note: Existing user accounts that were created by this system are
-     * reused if available.
-     */
-    private function create_users() {
-        global $DB;
-
-        // Work out total number of users.
-        $count = self::$paramusers[$this->size];
-
-        // Get existing users in order. We will 'fill up holes' in this up to
-        // the required number.
-        $this->log('checkaccounts', $count);
-        $nextnumber = 1;
-        $rs = $DB->get_recordset_select('user', $DB->sql_like('username', '?'),
-                array('tool_generator_%'), 'username', 'id, username');
-        foreach ($rs as $rec) {
-            // Extract number from username.
-            $matches = array();
-            if (!preg_match('~^tool_generator_([0-9]{6})$~', $rec->username, $matches)) {
-                continue;
-            }
-            $number = (int)$matches[1];
-
-            // Create missing users in range up to this.
-            if ($number != $nextnumber) {
-                $this->create_user_accounts($nextnumber, min($number - 1, $count));
-            } else {
-                $this->userids[$number] = (int)$rec->id;
-            }
-
-            // Stop if we've got enough users.
-            $nextnumber = $number + 1;
-            if ($number >= $count) {
-                break;
-            }
-        }
-        $rs->close();
-
-        // Create users from end of existing range.
-        if ($nextnumber <= $count) {
-            $this->create_user_accounts($nextnumber, $count);
-        }
-
-        // Assign all users to course.
-        $this->log('enrol', $count, true);
-
-        $enrolplugin = enrol_get_plugin('manual');
-        $instances = enrol_get_instances($this->course->id, true);
-        foreach ($instances as $instance) {
-            if ($instance->enrol === 'manual') {
-                break;
-            }
-        }
-        if ($instance->enrol !== 'manual') {
-            throw new coding_exception('No manual enrol plugin in course');
-        }
-        $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
-
-        for ($number = 1; $number <= $count; $number++) {
-            // Enrol user.
-            $enrolplugin->enrol_user($instance, $this->userids[$number], $role->id);
-            $this->dot($number, $count);
-        }
-
-        $this->end_log();
-    }
-
-    /**
-     * Creates user accounts with a numeric range.
-     *
-     * @param int $first Number of first user
-     * @param int $last Number of last user
-     */
-    private function create_user_accounts($first, $last) {
-        $this->log('createaccounts', (object)array('from' => $first, 'to' => $last), true);
-        $count = $last - $first + 1;
-        $done = 0;
-        for ($number = $first; $number <= $last; $number++, $done++) {
-            // Work out username with 6-digit number.
-            $textnumber = (string)$number;
-            while (strlen($textnumber) < 6) {
-                $textnumber = '0' . $textnumber;
-            }
-            $username = 'tool_generator_' . $textnumber;
-
-            // Create user account.
-            $record = array('firstname' => get_string('firstname', 'tool_generator'),
-                    'lastname' => $number, 'username' => $username);
-            $user = $this->generator->create_user($record);
-            $this->userids[$number] = (int)$user->id;
-            $this->dot($done, $count);
-        }
-        $this->end_log();
-    }
-
-    /**
-     * Creates a number of Page activities.
-     */
-    private function create_pages() {
-        // Set up generator.
-        $pagegenerator = $this->generator->get_plugin_generator('mod_page');
-
-        // Create pages.
-        $number = self::$parampages[$this->size];
-        $this->log('createpages', $number, true);
-        for ($i=0; $i<$number; $i++) {
-            $record = array('course' => $this->course->id);
-            $options = array('section' => $this->get_random_section());
-            $pagegenerator->create_instance($record, $options);
-            $this->dot($i, $number);
-        }
-
-        $this->end_log();
-    }
-
-    /**
-     * Creates one resource activity with a lot of small files.
-     */
-    private function create_small_files() {
-        $count = self::$paramsmallfilecount[$this->size];
-        $this->log('createsmallfiles', $count, true);
-
-        // Create resource with default textfile only.
-        $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
-        $record = array('course' => $this->course->id,
-                'name' => get_string('smallfiles', 'tool_generator'));
-        $options = array('section' => 0);
-        $resource = $resourcegenerator->create_instance($record, $options);
-
-        // Add files.
-        $fs = get_file_storage();
-        $context = context_module::instance($resource->cmid);
-        $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
-                'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/');
-        for ($i = 0; $i < $count; $i++) {
-            $filerecord['filename'] = 'smallfile' . $i . '.dat';
-
-            // Generate random binary data (different for each file so it
-            // doesn't compress unrealistically).
-            $data = self::get_random_binary(self::$paramsmallfilesize[$this->size]);
-
-            $fs->create_file_from_string($filerecord, $data);
-            $this->dot($i, $count);
-        }
-
-        $this->end_log();
-    }
-
-    /**
-     * Creates a string of random binary data. The start of the string includes
-     * the current time, in an attempt to avoid large-scale repetition.
-     *
-     * @param int $length Number of bytes
-     * @return Random data
-     */
-    private static function get_random_binary($length) {
-        $data = microtime(true);
-        if (strlen($data) > $length) {
-            // Use last digits of data.
-            return substr($data, -$length);
-        }
-        $length -= strlen($data);
-        for ($j=0; $j < $length; $j++) {
-            $data .= chr(rand(1, 255));
-        }
-        return $data;
-    }
-
-    /**
-     * Creates a number of resource activities with one big file each.
-     */
-    private function create_big_files() {
-        global $CFG;
-
-        // Work out how many files and how many blocks to use (up to 64KB).
-        $count = self::$parambigfilecount[$this->size];
-        $blocks = ceil(self::$parambigfilesize[$this->size] / 65536);
-        $blocksize = floor(self::$parambigfilesize[$this->size] / $blocks);
-
-        $this->log('createbigfiles', $count, true);
-
-        // Prepare temp area.
-        $tempfolder = make_temp_directory('tool_generator');
-        $tempfile = $tempfolder . '/' . rand();
-
-        // Create resources and files.
-        $fs = get_file_storage();
-        $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
-        for ($i = 0; $i < $count; $i++) {
-            // Create resource.
-            $record = array('course' => $this->course->id,
-                    'name' => get_string('bigfile', 'tool_generator', $i));
-            $options = array('section' => $this->get_random_section());
-            $resource = $resourcegenerator->create_instance($record, $options);
-
-            // Write file.
-            $handle = fopen($tempfile, 'w');
-            if (!$handle) {
-                throw new coding_exception('Failed to open temporary file');
-            }
-            for ($j = 0; $j < $blocks; $j++) {
-                $data = self::get_random_binary($blocksize);
-                fwrite($handle, $data);
-                $this->dot($i * $blocks + $j, $count * $blocks);
-            }
-            fclose($handle);
-
-            // Add file.
-            $context = context_module::instance($resource->cmid);
-            $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
-                    'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/',
-                    'filename' => 'bigfile' . $i . '.dat');
-            $fs->create_file_from_pathname($filerecord, $tempfile);
-        }
-
-        unlink($tempfile);
-        $this->end_log();
-    }
-
-    /**
-     * Creates one forum activity with a bunch of posts.
-     */
-    private function create_forum() {
-        global $DB;
-
-        $discussions = self::$paramforumdiscussions[$this->size];
-        $posts = self::$paramforumposts[$this->size];
-        $totalposts = $discussions * $posts;
-
-        $this->log('createforum', $totalposts, true);
-
-        // Create empty forum.
-        $forumgenerator = $this->generator->get_plugin_generator('mod_forum');
-        $record = array('course' => $this->course->id,
-                'name' => get_string('pluginname', 'forum'));
-        $options = array('section' => 0);
-        $forum = $forumgenerator->create_instance($record, $options);
-
-        // Add discussions and posts.
-        $sofar = 0;
-        for ($i=0; $i < $discussions; $i++) {
-            $record = array('forum' => $forum->id, 'course' => $this->course->id,
-                    'userid' => $this->get_random_user());
-            $discussion = $forumgenerator->create_discussion($record);
-            $parentid = $DB->get_field('forum_posts', 'id', array('discussion' => $discussion->id), MUST_EXIST);
-            $sofar++;
-            for ($j=0; $j < $posts - 1; $j++, $sofar++) {
-                $record = array('discussion' => $discussion->id,
-                        'userid' => $this->get_random_user(), 'parent' => $parentid);
-                $forumgenerator->create_post($record);
-                $this->dot($sofar, $totalposts);
-            }
-        }
-
-        $this->end_log();
-    }
-
-    /**
-     * Gets a random section number.
-     *
-     * @return int A section number from 1 to the number of sections
-     */
-    private function get_random_section() {
-        return rand(1, self::$paramsections[$this->size]);
-    }
-
-    /**
-     * Gets a random user id.
-     *
-     * @return int A user id for a random created user
-     */
-    private function get_random_user() {
-        return $this->userids[rand(1, self::$paramusers[$this->size])];
-    }
-
     /**
      * Displays information as part of progress.
      * @param string $langstring Part of langstring (after progress_)
      * @param mixed $a Optional lang string parameters
      * @param bool $leaveopen If true, doesn't close LI tag (ready for dots)
      */
-    private function log($langstring, $a = null, $leaveopen = false) {
+    protected function log($langstring, $a = null, $leaveopen = false) {
         if (!$this->progress) {
             return;
         }
@@ -564,7 +149,7 @@ class tool_generator_backend {
      * @param int $number Number of completed items
      * @param int $total Total number of items to complete
      */
-    private function dot($number, $total) {
+    protected function dot($number, $total) {
         if (!$this->progress) {
             return;
         }
@@ -592,7 +177,7 @@ class tool_generator_backend {
     /**
      * Ends a log string that was started using log function with $leaveopen.
      */
-    private function end_log() {
+    protected function end_log() {
         if (!$this->progress) {
             return;
         }
@@ -603,4 +188,5 @@ class tool_generator_backend {
             echo html_writer::end_tag('li');
         }
     }
+
 }
diff --git a/admin/tool/generator/classes/course_backend.php b/admin/tool/generator/classes/course_backend.php
new file mode 100644 (file)
index 0000000..07e1e7b
--- /dev/null
@@ -0,0 +1,507 @@
+<?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/>.
+
+/**
+ * tool_generator course backend code.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Backend code for the 'make large course' tool.
+ *
+ * @package tool_generator
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_generator_course_backend extends tool_generator_backend {
+    /**
+     * @var array Number of sections in course
+     */
+    private static $paramsections = array(1, 10, 100, 500, 1000, 2000);
+    /**
+     * @var array Number of Page activities in course
+     */
+    private static $parampages = array(1, 50, 200, 1000, 5000, 10000);
+    /**
+     * @var array Number of students enrolled in course
+     */
+    private static $paramusers = array(1, 100, 1000, 10000, 50000, 100000);
+    /**
+     * Total size of small files: 1KB, 1MB, 10MB, 100MB, 1GB, 2GB.
+     *
+     * @var array Number of small files created in a single file activity
+     */
+    private static $paramsmallfilecount = array(1, 64, 128, 1024, 16384, 32768);
+    /**
+     * @var array Size of small files (to make the totals into nice numbers)
+     */
+    private static $paramsmallfilesize = array(1024, 16384, 81920, 102400, 65536, 65536);
+    /**
+     * Total size of big files: 8KB, 8MB, 80MB, 800MB, 8GB, 16GB.
+     *
+     * @var array Number of big files created as individual file activities
+     */
+    private static $parambigfilecount = array(1, 2, 5, 10, 10, 10);
+    /**
+     * @var array Size of each large file
+     */
+    private static $parambigfilesize = array(8192, 4194304, 16777216, 83886080,
+            858993459, 1717986918);
+    /**
+     * @var array Number of forum discussions
+     */
+    private static $paramforumdiscussions = array(1, 10, 100, 500, 1000, 2000);
+    /**
+     * @var array Number of forum posts per discussion
+     */
+    private static $paramforumposts = array(2, 2, 5, 10, 10, 10);
+
+    /**
+     * @var string Course shortname
+     */
+    private $shortname;
+
+    /**
+     * @var testing_data_generator Data generator
+     */
+    protected $generator;
+
+    /**
+     * @var stdClass Course object
+     */
+    private $course;
+
+    /**
+     * @var array Array from test user number (1...N) to userid in database
+     */
+    private $userids;
+
+    /**
+     * Constructs object ready to create course.
+     *
+     * @param string $shortname Course shortname
+     * @param int $size Size as numeric index
+     * @param bool $fixeddataset To use fixed or random data
+     * @param bool $progress True if progress information should be displayed
+     * @return int Course id
+     */
+    public function __construct($shortname, $size, $fixeddataset = false, $progress = true) {
+
+        // Set parameters.
+        $this->shortname = $shortname;
+
+        parent::__construct($size, $fixeddataset, $progress);
+    }
+
+    /**
+     * Gets a list of size choices supported by this backend.
+     *
+     * @return array List of size (int) => text description for display
+     */
+    public static function get_size_choices() {
+        $options = array();
+        for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
+            $options[$size] = get_string('coursesize_' . $size, 'tool_generator');
+        }
+        return $options;
+    }
+
+    /**
+     * Checks that a shortname is available (unused).
+     *
+     * @param string $shortname Proposed course shortname
+     * @return string An error message if the name is unavailable or '' if OK
+     */
+    public static function check_shortname_available($shortname) {
+        global $DB;
+        $fullname = $DB->get_field('course', 'fullname',
+                array('shortname' => $shortname), IGNORE_MISSING);
+        if ($fullname !== false) {
+            // I wanted to throw an exception here but it is not possible to
+            // use strings from moodle.php in exceptions, and I didn't want
+            // to duplicate the string in tool_generator, so I changed this to
+            // not use exceptions.
+            return get_string('shortnametaken', 'moodle', $fullname);
+        }
+        return '';
+    }
+
+    /**
+     * Runs the entire 'make' process.
+     *
+     * @return int Course id
+     */
+    public function make() {
+        global $DB, $CFG;
+        require_once($CFG->dirroot . '/lib/phpunit/classes/util.php');
+
+        raise_memory_limit(MEMORY_EXTRA);
+
+        if ($this->progress && !CLI_SCRIPT) {
+            echo html_writer::start_tag('ul');
+        }
+
+        $entirestart = microtime(true);
+
+        // Start transaction.
+        $transaction = $DB->start_delegated_transaction();
+
+        // Get generator.
+        $this->generator = phpunit_util::get_data_generator();
+
+        // Make course.
+        $this->course = $this->create_course();
+        $this->create_users();
+        $this->create_pages();
+        $this->create_small_files();
+        $this->create_big_files();
+        $this->create_forum();
+
+        // Log total time.
+        $this->log('coursecompleted', round(microtime(true) - $entirestart, 1));
+
+        if ($this->progress && !CLI_SCRIPT) {
+            echo html_writer::end_tag('ul');
+        }
+
+        // Commit transaction and finish.
+        $transaction->allow_commit();
+        return $this->course->id;
+    }
+
+    /**
+     * Creates the actual course.
+     *
+     * @return stdClass Course record
+     */
+    private function create_course() {
+        $this->log('createcourse', $this->shortname);
+        $courserecord = array('shortname' => $this->shortname,
+                'fullname' => get_string('fullname', 'tool_generator',
+                    array('size' => get_string('shortsize_' . $this->size, 'tool_generator'))),
+                'numsections' => self::$paramsections[$this->size]);
+        return $this->generator->create_course($courserecord, array('createsections' => true));
+    }
+
+    /**
+     * Creates a number of user accounts and enrols them on the course.
+     * Note: Existing user accounts that were created by this system are
+     * reused if available.
+     */
+    private function create_users() {
+        global $DB;
+
+        // Work out total number of users.
+        $count = self::$paramusers[$this->size];
+
+        // Get existing users in order. We will 'fill up holes' in this up to
+        // the required number.
+        $this->log('checkaccounts', $count);
+        $nextnumber = 1;
+        $rs = $DB->get_recordset_select('user', $DB->sql_like('username', '?'),
+                array('tool_generator_%'), 'username', 'id, username');
+        foreach ($rs as $rec) {
+            // Extract number from username.
+            $matches = array();
+            if (!preg_match('~^tool_generator_([0-9]{6})$~', $rec->username, $matches)) {
+                continue;
+            }
+            $number = (int)$matches[1];
+
+            // Create missing users in range up to this.
+            if ($number != $nextnumber) {
+                $this->create_user_accounts($nextnumber, min($number - 1, $count));
+            } else {
+                $this->userids[$number] = (int)$rec->id;
+            }
+
+            // Stop if we've got enough users.
+            $nextnumber = $number + 1;
+            if ($number >= $count) {
+                break;
+            }
+        }
+        $rs->close();
+
+        // Create users from end of existing range.
+        if ($nextnumber <= $count) {
+            $this->create_user_accounts($nextnumber, $count);
+        }
+
+        // Assign all users to course.
+        $this->log('enrol', $count, true);
+
+        $enrolplugin = enrol_get_plugin('manual');
+        $instances = enrol_get_instances($this->course->id, true);
+        foreach ($instances as $instance) {
+            if ($instance->enrol === 'manual') {
+                break;
+            }
+        }
+        if ($instance->enrol !== 'manual') {
+            throw new coding_exception('No manual enrol plugin in course');
+        }
+        $role = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST);
+
+        for ($number = 1; $number <= $count; $number++) {
+            // Enrol user.
+            $enrolplugin->enrol_user($instance, $this->userids[$number], $role->id);
+            $this->dot($number, $count);
+        }
+
+        // Sets the pointer at the beginning to be aware of the users we use.
+        reset($this->userids);
+
+        $this->end_log();
+    }
+
+    /**
+     * Creates user accounts with a numeric range.
+     *
+     * @param int $first Number of first user
+     * @param int $last Number of last user
+     */
+    private function create_user_accounts($first, $last) {
+        $this->log('createaccounts', (object)array('from' => $first, 'to' => $last), true);
+        $count = $last - $first + 1;
+        $done = 0;
+        for ($number = $first; $number <= $last; $number++, $done++) {
+            // Work out username with 6-digit number.
+            $textnumber = (string)$number;
+            while (strlen($textnumber) < 6) {
+                $textnumber = '0' . $textnumber;
+            }
+            $username = 'tool_generator_' . $textnumber;
+
+            // Create user account.
+            $record = array('firstname' => get_string('firstname', 'tool_generator'),
+                    'lastname' => $number, 'username' => $username);
+            $user = $this->generator->create_user($record);
+            $this->userids[$number] = (int)$user->id;
+            $this->dot($done, $count);
+        }
+        $this->end_log();
+    }
+
+    /**
+     * Creates a number of Page activities.
+     */
+    private function create_pages() {
+        // Set up generator.
+        $pagegenerator = $this->generator->get_plugin_generator('mod_page');
+
+        // Create pages.
+        $number = self::$parampages[$this->size];
+        $this->log('createpages', $number, true);
+        for ($i=0; $i<$number; $i++) {
+            $record = array('course' => $this->course->id);
+            $options = array('section' => $this->get_target_section());
+            $pagegenerator->create_instance($record, $options);
+            $this->dot($i, $number);
+        }
+
+        $this->end_log();
+    }
+
+    /**
+     * Creates one resource activity with a lot of small files.
+     */
+    private function create_small_files() {
+        $count = self::$paramsmallfilecount[$this->size];
+        $this->log('createsmallfiles', $count, true);
+
+        // Create resource with default textfile only.
+        $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
+        $record = array('course' => $this->course->id,
+                'name' => get_string('smallfiles', 'tool_generator'));
+        $options = array('section' => 0);
+        $resource = $resourcegenerator->create_instance($record, $options);
+
+        // Add files.
+        $fs = get_file_storage();
+        $context = context_module::instance($resource->cmid);
+        $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
+                'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/');
+        for ($i = 0; $i < $count; $i++) {
+            $filerecord['filename'] = 'smallfile' . $i . '.dat';
+
+            // Generate random binary data (different for each file so it
+            // doesn't compress unrealistically).
+            $data = self::get_random_binary(self::$paramsmallfilesize[$this->size]);
+
+            $fs->create_file_from_string($filerecord, $data);
+            $this->dot($i, $count);
+        }
+
+        $this->end_log();
+    }
+
+    /**
+     * Creates a string of random binary data. The start of the string includes
+     * the current time, in an attempt to avoid large-scale repetition.
+     *
+     * @param int $length Number of bytes
+     * @return Random data
+     */
+    private static function get_random_binary($length) {
+        $data = microtime(true);
+        if (strlen($data) > $length) {
+            // Use last digits of data.
+            return substr($data, -$length);
+        }
+        $length -= strlen($data);
+        for ($j=0; $j < $length; $j++) {
+            $data .= chr(rand(1, 255));
+        }
+        return $data;
+    }
+
+    /**
+     * Creates a number of resource activities with one big file each.
+     */
+    private function create_big_files() {
+        global $CFG;
+
+        // Work out how many files and how many blocks to use (up to 64KB).
+        $count = self::$parambigfilecount[$this->size];
+        $blocks = ceil(self::$parambigfilesize[$this->size] / 65536);
+        $blocksize = floor(self::$parambigfilesize[$this->size] / $blocks);
+
+        $this->log('createbigfiles', $count, true);
+
+        // Prepare temp area.
+        $tempfolder = make_temp_directory('tool_generator');
+        $tempfile = $tempfolder . '/' . rand();
+
+        // Create resources and files.
+        $fs = get_file_storage();
+        $resourcegenerator = $this->generator->get_plugin_generator('mod_resource');
+        for ($i = 0; $i < $count; $i++) {
+            // Create resource.
+            $record = array('course' => $this->course->id,
+                    'name' => get_string('bigfile', 'tool_generator', $i));
+            $options = array('section' => $this->get_target_section());
+            $resource = $resourcegenerator->create_instance($record, $options);
+
+            // Write file.
+            $handle = fopen($tempfile, 'w');
+            if (!$handle) {
+                throw new coding_exception('Failed to open temporary file');
+            }
+            for ($j = 0; $j < $blocks; $j++) {
+                $data = self::get_random_binary($blocksize);
+                fwrite($handle, $data);
+                $this->dot($i * $blocks + $j, $count * $blocks);
+            }
+            fclose($handle);
+
+            // Add file.
+            $context = context_module::instance($resource->cmid);
+            $filerecord = array('component' => 'mod_resource', 'filearea' => 'content',
+                    'contextid' => $context->id, 'itemid' => 0, 'filepath' => '/',
+                    'filename' => 'bigfile' . $i . '.dat');
+            $fs->create_file_from_pathname($filerecord, $tempfile);
+        }
+
+        unlink($tempfile);
+        $this->end_log();
+    }
+
+    /**
+     * Creates one forum activity with a bunch of posts.
+     */
+    private function create_forum() {
+        global $DB;
+
+        $discussions = self::$paramforumdiscussions[$this->size];
+        $posts = self::$paramforumposts[$this->size];
+        $totalposts = $discussions * $posts;
+
+        $this->log('createforum', $totalposts, true);
+
+        // Create empty forum.
+        $forumgenerator = $this->generator->get_plugin_generator('mod_forum');
+        $record = array('course' => $this->course->id,
+                'name' => get_string('pluginname', 'forum'));
+        $options = array('section' => 0);
+        $forum = $forumgenerator->create_instance($record, $options);
+
+        // Add discussions and posts.
+        $sofar = 0;
+        for ($i=0; $i < $discussions; $i++) {
+            $record = array('forum' => $forum->id, 'course' => $this->course->id,
+                    'userid' => $this->get_target_user());
+            $discussion = $forumgenerator->create_discussion($record);
+            $parentid = $DB->get_field('forum_posts', 'id', array('discussion' => $discussion->id), MUST_EXIST);
+            $sofar++;
+            for ($j=0; $j < $posts - 1; $j++, $sofar++) {
+                $record = array('discussion' => $discussion->id,
+                        'userid' => $this->get_target_user(), 'parent' => $parentid);
+                $forumgenerator->create_post($record);
+                $this->dot($sofar, $totalposts);
+            }
+        }
+
+        $this->end_log();
+    }
+
+    /**
+     * Gets a section number.
+     *
+     * Depends on $this->fixeddataset.
+     *
+     * @return int A section number from 1 to the number of sections
+     */
+    private function get_target_section() {
+
+        if (!$this->fixeddataset) {
+            $key = rand(1, self::$paramsections[$this->size]);
+        } else {
+            // Using section 1.
+            $key = 1;
+        }
+
+        return $key;
+    }
+
+    /**
+     * Gets a user id.
+     *
+     * Depends on $this->fixeddataset.
+     *
+     * @return int A user id for a random created user
+     */
+    private function get_target_user() {
+
+        if (!$this->fixeddataset) {
+            $userid = $this->userids[rand(1, self::$paramusers[$this->size])];
+        } else if ($userid = current($this->userids)) {
+            // Moving pointer to the next user.
+            next($this->userids);
+        } else {
+            // Returning to the beginning if we reached the end.
+            $userid = reset($this->userids);
+        }
+
+        return $userid;
+    }
+
+}
index 25c8350..879364d 100644 (file)
@@ -31,8 +31,8 @@ class tool_generator_make_form extends moodleform {
         $mform = $this->_form;
 
         $mform->addElement('select', 'size', get_string('size', 'tool_generator'),
-                tool_generator_backend::get_size_choices());
-        $mform->setDefault('size', tool_generator_backend::DEFAULT_SIZE);
+                tool_generator_course_backend::get_size_choices());
+        $mform->setDefault('size', tool_generator_course_backend::DEFAULT_SIZE);
 
         $mform->addElement('text', 'shortname', get_string('shortnamecourse'));
         $mform->addRule('shortname', get_string('missingshortname'), 'required', null, 'client');
@@ -48,7 +48,7 @@ class tool_generator_make_form extends moodleform {
         // Check course doesn't already exist.
         if (!empty($data['shortname'])) {
             // Check shortname.
-            $error =  tool_generator_backend::check_shortname_available($data['shortname']);
+            $error =  tool_generator_course_backend::check_shortname_available($data['shortname']);
             if ($error) {
                 $errors['shortname'] = $error;
             }
diff --git a/admin/tool/generator/classes/site_backend.php b/admin/tool/generator/classes/site_backend.php
new file mode 100644 (file)
index 0000000..01bde97
--- /dev/null
@@ -0,0 +1,203 @@
+<?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/>.
+
+/**
+ * tool_generator site backend.
+ *
+ * @package tool_generator
+ * @copyright 2013 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Backend code for the site generator.
+ *
+ * @package tool_generator
+ * @copyright 2013 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_generator_site_backend extends tool_generator_backend {
+
+    /**
+     * @var string The course's shortname prefix.
+     */
+    const SHORTNAMEPREFIX = 'testcourse_';
+
+    /**
+     * @var bool If the debugging level checking was skipped.
+     */
+    protected $bypasscheck;
+
+    /**
+     * @var array Multidimensional array where the first level is the course size and the second the site size.
+     */
+    protected static $sitecourses = array(
+        array(2, 8, 64, 256, 1024, 4096),
+        array(1, 4, 8, 16, 32, 64),
+        array(0, 0, 1, 4, 8, 16),
+        array(0, 0, 0, 1, 0, 0),
+        array(0, 0, 0, 0, 1, 0),
+        array(0, 0, 0, 0, 0, 1)
+    );
+
+    /**
+     * Constructs object ready to make the site.
+     *
+     * @param int $size Size as numeric index
+     * @param bool $bypasscheck If debugging level checking was skipped.
+     * @param bool $fixeddataset To use fixed or random data
+     * @param bool $progress True if progress information should be displayed
+     * @return int Course id
+     */
+    public function __construct($size, $bypasscheck, $fixeddataset = false, $progress = true) {
+
+        // Set parameters.
+        $this->bypasscheck = $bypasscheck;
+
+        parent::__construct($size, $fixeddataset, $progress);
+    }
+
+    /**
+     * Gets a list of size choices supported by this backend.
+     *
+     * @return array List of size (int) => text description for display
+     */
+    public static function get_size_choices() {
+        $options = array();
+        for ($size = self::MIN_SIZE; $size <= self::MAX_SIZE; $size++) {
+            $options[$size] = get_string('sitesize_' . $size, 'tool_generator');
+        }
+        return $options;
+    }
+
+    /**
+     * Runs the entire 'make' process.
+     *
+     * @return int Course id
+     */
+    public function make() {
+        global $DB, $CFG;
+
+        raise_memory_limit(MEMORY_EXTRA);
+
+        if ($this->progress && !CLI_SCRIPT) {
+            echo html_writer::start_tag('ul');
+        }
+
+        $entirestart = microtime(true);
+
+        // Create courses.
+        $prevchdir = getcwd();
+        chdir($CFG->dirroot);
+        $ncourse = $this->get_last_testcourse_id();
+        foreach (self::$sitecourses as $coursesize => $ncourses) {
+            for ($i = 1; $i <= $ncourses[$this->size]; $i++) {
+                // Non language-dependant shortname.
+                $ncourse++;
+                $this->run_create_course(self::SHORTNAMEPREFIX . $ncourse, $coursesize);
+            }
+        }
+        chdir($prevchdir);
+
+        // Store last course id to return it (will be the bigger one).
+        $lastcourseid = $DB->get_field('course', 'id', array('shortname' => self::SHORTNAMEPREFIX . $ncourse));
+
+        // Log total time.
+        $this->log('sitecompleted', round(microtime(true) - $entirestart, 1));
+
+        if ($this->progress && !CLI_SCRIPT) {
+            echo html_writer::end_tag('ul');
+        }
+
+        return $lastcourseid;
+    }
+
+    /**
+     * Creates a course with the specified shortname, coursesize and the provided maketestsite options.
+     *
+     * @param string $shortname The course shortname
+     * @param int $coursesize One of the possible course sizes.
+     * @return void
+     */
+    protected function run_create_course($shortname, $coursesize) {
+
+        // We are in $CFG->dirroot.
+        $command = 'php admin/tool/generator/cli/maketestcourse.php';
+
+        $options = array(
+            '--shortname="' . $shortname . '"',
+            '--size="' . get_string('shortsize_' . $coursesize, 'tool_generator') . '"'
+        );
+
+        if (!$this->progress) {
+            $options[] = '--quiet';
+        }
+
+        // Extend options.
+        $optionstoextend = array(
+            'fixeddataset' => 'fixeddataset',
+            'bypasscheck' => 'bypasscheck',
+        );
+
+        // Getting an options string.
+        foreach ($optionstoextend as $attribute => $option) {
+            if (!empty($this->{$attribute})) {
+                $options[] = '--' . $option;
+            }
+        }
+        $options = implode(' ', $options);
+        if ($this->progress) {
+            system($command . ' ' . $options, $exitcode);
+        } else {
+            passthru($command . ' ' . $options, $exitcode);
+        }
+
+        if ($exitcode != 0) {
+            exit($exitcode);
+        }
+    }
+
+    /**
+     * Obtains the last unique sufix (numeric) using the test course prefix.
+     *
+     * @return int The last generated numeric value.
+     */
+    protected function get_last_testcourse_id() {
+        global $DB;
+
+        $params = array();
+        $params['shortnameprefix'] = $DB->sql_like_escape(self::SHORTNAMEPREFIX) . '%';
+        $like = $DB->sql_like('shortname', ':shortnameprefix');
+
+        if (!$testcourses = $DB->get_records_select('course', $like, $params, 'shortname DESC')) {
+            return 0;
+        }
+
+        // They come ordered by shortname DESC, so non-numeric values will be the first ones.
+        foreach ($testcourses as $testcourse) {
+            $sufix = substr($testcourse->shortname, strlen(self::SHORTNAMEPREFIX));
+            if (is_numeric($sufix)) {
+                return $sufix;
+            }
+        }
+
+        // If all sufixes are not numeric this is the fist make test site run.
+        return 0;
+    }
+
+}
index b646e2f..407a58f 100644 (file)
@@ -34,6 +34,7 @@ list($options, $unrecognized) = cli_get_params(
         'help' => false,
         'shortname' => false,
         'size' => false,
+        'fixeddataset' => false,
         'bypasscheck' => false,
         'quiet' => false
     ),
@@ -53,6 +54,7 @@ level.
 Options:
 --shortname    Shortname of course to create (required)
 --size         Size of course to create XS, S, M, L, XL, or XXL (required)
+--fixeddataset Use a fixed data set instead of randomly generated data
 --bypasscheck  Bypasses the developer-mode check (be careful!)
 --quiet        Do not show any output
 
@@ -73,16 +75,17 @@ if (empty($options['bypasscheck']) && !debugging('', DEBUG_DEVELOPER)) {
 // Get options.
 $shortname = $options['shortname'];
 $sizename = $options['size'];
+$fixeddataset = $options['fixeddataset'];
 
 // Check size.
 try {
-    $size = tool_generator_backend::size_for_name($sizename);
+    $size = tool_generator_course_backend::size_for_name($sizename);
 } catch (coding_exception $e) {
     cli_error("Invalid size ($sizename). Use --help for help.");
 }
 
 // Check shortname.
-if ($error = tool_generator_backend::check_shortname_available($shortname)) {
+if ($error = tool_generator_course_backend::check_shortname_available($shortname)) {
     cli_error($error);
 }
 
@@ -90,5 +93,5 @@ if ($error = tool_generator_backend::check_shortname_available($shortname)) {
 session_set_user(get_admin());
 
 // Do backend code to generate course.
-$backend = new tool_generator_backend($shortname, $size, empty($options['quiet']));
+$backend = new tool_generator_course_backend($shortname, $size, $fixeddataset, empty($options['quiet']));
 $id = $backend->make();
diff --git a/admin/tool/generator/cli/maketestsite.php b/admin/tool/generator/cli/maketestsite.php
new file mode 100644 (file)
index 0000000..bc91d7d
--- /dev/null
@@ -0,0 +1,95 @@
+<?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/>.
+
+/**
+ * CLI interface for creating a test site.
+ *
+ * @package tool_generator
+ * @copyright 2013 David Monllaó
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+define('NO_OUTPUT_BUFFERING', true);
+
+require(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir. '/clilib.php');
+
+// CLI options.
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'help' => false,
+        'size' => false,
+        'fixeddataset' => false,
+        'bypasscheck' => false,
+        'quiet' => false
+    ),
+    array(
+        'h' => 'help'
+    )
+);
+
+$sitesizes = '* ' . implode(PHP_EOL . '* ', tool_generator_site_backend::get_size_choices());
+
+// Display help.
+if (!empty($options['help']) || empty($options['size'])) {
+    echo "
+Utility to generate a standard test site data set.
+
+Not for use on live sites; only normally works if debugging is set to DEVELOPER
+level.
+
+Consider that, depending on the size you select, this CLI tool can really generate a lot of data, aproximated sizes:
+
+$sitesizes
+
+Options:
+--size         Size of the generated site, this value affects the number of courses and their size. Accepted values: XS, S, M, L, XL, or XXL (required)
+--fixeddataset Use a fixed data set instead of randomly generated data
+--bypasscheck  Bypasses the developer-mode check (be careful!)
+--quiet        Do not show any output
+
+-h, --help     Print out this help
+
+Example from Moodle root directory:
+\$ php admin/tool/generator/cli/maketestsite.php --size=S
+";
+    // Exit with error unless we're showing this because they asked for it.
+    exit(empty($options['help']) ? 1 : 0);
+}
+
+// Check debugging is set to developer level.
+if (empty($options['bypasscheck']) && !$CFG->debugdeveloper) {
+    cli_error(get_string('error_notdebugging', 'tool_generator'));
+}
+
+// Get options.
+$sizename = $options['size'];
+$fixeddataset = $options['fixeddataset'];
+
+// Check size.
+try {
+    $size = tool_generator_site_backend::size_for_name($sizename);
+} catch (coding_exception $e) {
+    cli_error("Invalid size ($sizename). Use --help for help.");
+}
+
+// Switch to admin user account.
+session_set_user(get_admin());
+
+// Do backend code to generate site.
+$backend = new tool_generator_site_backend($size, $options['bypasscheck'], $fixeddataset, empty($options['quiet']));
+$backend->make();
index 103a4fd..a204ce8 100644 (file)
  */
 
 $string['bigfile'] = 'Big file {$a}';
+$string['coursesize_0'] = 'XS (~10KB; create in ~1 second)';
+$string['coursesize_1'] = 'S (~10MB; create in ~30 seconds)';
+$string['coursesize_2'] = 'M (~100MB; create in ~5 minutes)';
+$string['coursesize_3'] = 'L (~1GB; create in ~1 hour)';
+$string['coursesize_4'] = 'XL (~10GB; create in ~4 hours)';
+$string['coursesize_5'] = 'XXL (~20GB; create in ~8 hours)';
 $string['createcourse'] = 'Create course';
 $string['creating'] = 'Creating course';
 $string['done'] = 'done ({$a}s)';
@@ -48,27 +54,28 @@ $string['error_notdebugging'] = 'Not available on this server because debugging
 $string['firstname'] = 'Test course user';
 $string['fullname'] = 'Test course: {$a->size}';
 $string['maketestcourse'] = 'Make test course';
-$string['pluginname'] = 'Random course generator';
+$string['pluginname'] = 'Development data generator';
 $string['progress_createcourse'] = 'Creating course {$a}';
 $string['progress_checkaccounts'] = 'Checking user accounts ({$a})';
+$string['progress_coursecompleted'] = 'Course completed ({$a}s)';
 $string['progress_createaccounts'] = 'Creating user accounts ({$a->from} - {$a->to})';
 $string['progress_createbigfiles'] = 'Creating big files ({$a})';
 $string['progress_createforum'] = 'Creating forum ({$a} posts)';
 $string['progress_createpages'] = 'Creating pages ({$a})';
 $string['progress_createsmallfiles'] = 'Creating small files ({$a})';
 $string['progress_enrol'] = 'Enrolling users into course ({$a})';
-$string['progress_complete'] = 'Complete ({$a}s)';
+$string['progress_sitecompleted'] = 'Site completed ({$a}s)';
 $string['shortsize_0'] = 'XS';
 $string['shortsize_1'] = 'S';
 $string['shortsize_2'] = 'M';
 $string['shortsize_3'] = 'L';
 $string['shortsize_4'] = 'XL';
 $string['shortsize_5'] = 'XXL';
+$string['sitesize_0'] = 'XS (~10MB; 3 courses, created in ~30 seconds)';
+$string['sitesize_1'] = 'S (~50MB; 8 courses, created in ~2 minutes)';
+$string['sitesize_2'] = 'M (~200MB; 73 courses, created in ~10 minutes)';
+$string['sitesize_3'] = 'L (~1\'5GB; 277 courses, created in ~1\'5 hours)';
+$string['sitesize_4'] = 'XL (~10GB; 1065 courses, created in ~5 hours)';
+$string['sitesize_5'] = 'XXL (~20GB; 4177 courses, created in ~10 hours)';
 $string['size'] = 'Size of course';
-$string['size_0'] = 'XS (~10KB; create in ~1 second)';
-$string['size_1'] = 'S (~10MB; create in ~30 seconds)';
-$string['size_2'] = 'M (~100MB; create in ~5 minutes)';
-$string['size_3'] = 'L (~1GB; create in ~1 hour)';
-$string['size_4'] = 'XL (~10GB; create in ~4 hours)';
-$string['size_5'] = 'XXL (~20GB; create in ~8 hours)';
 $string['smallfiles'] = 'Small files';
index f3ff9f7..82af097 100644 (file)
@@ -55,7 +55,7 @@ $mform = new tool_generator_make_form('maketestcourse.php');
 if ($data = $mform->get_data()) {
     // Do actual work.
     echo $OUTPUT->heading(get_string('creating', 'tool_generator'));
-    $backend = new tool_generator_backend($data->shortname, $data->size);
+    $backend = new tool_generator_course_backend($data->shortname, $data->size);
     $id = $backend->make();
 
     echo html_writer::div(
index 3b2244d..b8637f4 100644 (file)
@@ -35,7 +35,7 @@ class tool_generator_maketestcourse_testcase extends advanced_testcase {
         $this->setAdminUser();
 
         // Create the XS course.
-        $backend = new tool_generator_backend('TOOL_MAKELARGECOURSE_XS', 0, false);
+        $backend = new tool_generator_course_backend('TOOL_MAKELARGECOURSE_XS', 0, false, false);
         $courseid = $backend->make();
 
         // Get course details.
@@ -107,4 +107,48 @@ class tool_generator_maketestcourse_testcase extends advanced_testcase {
                     fd.forum = ?", array($forum->instance));
         $this->assertEquals(2, $posts);
     }
+
+    /**
+     * Creates an small test course with fixed data set and checks the used sections and users.
+     */
+    public function test_fixed_data_set() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Create the S course (more sections and activities than XS).
+        $backend = new tool_generator_course_backend('TOOL_S_COURSE_1', 1, true, false);
+        $courseid = $backend->make();
+
+        // Get course details.
+        $course = get_course($courseid);
+        $modinfo = get_fast_modinfo($course);
+
+        // Check module instances belongs to section 1.
+        $instances = $modinfo->get_instances_of('page');
+        $npageinstances = count($instances);
+        foreach ($instances as $instance) {
+            $this->assertEquals(1, $instance->sectionnum);
+        }
+
+        // Users that started discussions are the same.
+        $forums = $modinfo->get_instances_of('forum');
+        $nforuminstances = count($forums);
+        $discussions = forum_get_discussions(reset($forums), 'd.timemodified ASC');
+        $lastusernumber = 0;
+        $discussionstarters = array();
+        foreach ($discussions as $discussion) {
+            $usernumber = intval($discussion->lastname);
+
+            // Checks that the users are odd numbers.
+            $this->assertEquals(1, $usernumber % 2);
+
+            // Checks that the users follows an increasing order.
+            $this->assertGreaterThan($lastusernumber, $usernumber);
+            $lastusernumber = $usernumber;
+            $discussionstarters[$discussion->userid] = $discussion->subject;
+        }
+
+    }
 }
index 66aa662..3733d98 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version = 2013080700;
-$plugin->requires = 2013080200;
+$plugin->version = 2013090200;
+$plugin->requires = 2013090200;
 $plugin->component = 'tool_generator';
index c052d1c..7949d94 100644 (file)
@@ -27,6 +27,7 @@ require('../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 require_once($CFG->libdir.'/csvlib.class.php');
 require_once($CFG->dirroot.'/user/profile/lib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 require_once($CFG->dirroot.'/group/lib.php');
 require_once($CFG->dirroot.'/cohort/lib.php');
 require_once('locallib.php');
@@ -654,9 +655,8 @@ if ($formdata = $mform2->is_cancelled()) {
             }
 
             if ($doupdate or $existinguser->password !== $oldpw) {
-                // we want only users that were really updated
-
-                $DB->update_record('user', $existinguser);
+                // We want only users that were really updated.
+                user_update_user($existinguser, false);
 
                 $upt->track('status', $struserupdated);
                 $usersupdated++;
@@ -668,8 +668,6 @@ if ($formdata = $mform2->is_cancelled()) {
                     profile_save_data($existinguser);
                 }
 
-                events_trigger('user_updated', $existinguser);
-
                 if ($bulk == UU_BULK_UPDATED or $bulk == UU_BULK_ALL) {
                     if (!in_array($user->id, $SESSION->bulk_users)) {
                         $SESSION->bulk_users[] = $user->id;
@@ -789,8 +787,7 @@ if ($formdata = $mform2->is_cancelled()) {
                 $upt->track('password', '-', 'normal', false);
             }
 
-            // create user - insert_record ignores any extra properties
-            $user->id = $DB->insert_record('user', $user);
+            $user->id = user_create_user($user, false);
             $upt->track('username', html_writer::link(new moodle_url('/user/profile.php', array('id'=>$user->id)), s($user->username)), 'normal', false);
 
             // pre-process custom profile menu fields data from csv file
@@ -812,8 +809,6 @@ if ($formdata = $mform2->is_cancelled()) {
             // make sure user context exists
             context_user::instance($user->id);
 
-            events_trigger('user_created', $user);
-
             if ($bulk == UU_BULK_NEW or $bulk == UU_BULK_ALL) {
                 if (!in_array($user->id, $SESSION->bulk_users)) {
                     $SESSION->bulk_users[] = $user->id;
index 0f66603..699babb 100644 (file)
@@ -4,6 +4,7 @@
     require_once($CFG->libdir.'/adminlib.php');
     require_once($CFG->libdir.'/authlib.php');
     require_once($CFG->dirroot.'/user/filters/lib.php');
+    require_once($CFG->dirroot.'/user/lib.php');
 
     $delete       = optional_param('delete', 0, PARAM_INT);
     $confirm      = optional_param('confirm', '', PARAM_ALPHANUM);   //md5 confirmation hash
         if ($user = $DB->get_record('user', array('id'=>$suspend, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>0))) {
             if (!is_siteadmin($user) and $USER->id != $user->id and $user->suspended != 1) {
                 $user->suspended = 1;
-                $user->timemodified = time();
-                $DB->set_field('user', 'suspended', $user->suspended, array('id'=>$user->id));
-                $DB->set_field('user', 'timemodified', $user->timemodified, array('id'=>$user->id));
-                // force logout
+                // Force logout.
                 session_kill_user($user->id);
-                events_trigger('user_updated', $user);
+                user_update_user($user, false);
             }
         }
         redirect($returnurl);
         if ($user = $DB->get_record('user', array('id'=>$unsuspend, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>0))) {
             if ($user->suspended != 0) {
                 $user->suspended = 0;
-                $user->timemodified = time();
-                $DB->set_field('user', 'suspended', $user->suspended, array('id'=>$user->id));
-                $DB->set_field('user', 'timemodified', $user->timemodified, array('id'=>$user->id));
-                events_trigger('user_updated', $user);
+                user_update_user($user, false);
             }
         }
         redirect($returnurl);
index de5061d..a01fc6e 100644 (file)
@@ -284,6 +284,7 @@ class auth_plugin_db extends auth_plugin_base {
             $remove_users = $DB->get_records_sql($sql, $params);
 
             if (!empty($remove_users)) {
+                require_once($CFG->dirroot.'/user/lib.php');
                 $trace->output(get_string('auth_dbuserstoremove','auth_db', count($remove_users)));
 
                 foreach ($remove_users as $user) {
@@ -294,8 +295,7 @@ class auth_plugin_db extends auth_plugin_base {
                         $updateuser = new stdClass();
                         $updateuser->id   = $user->id;
                         $updateuser->suspended = 1;
-                        $updateuser->timemodified = time();
-                        $DB->update_record('user', $updateuser);
+                        user_update_user($updateuser, false);
                         $trace->output(get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1);
                     }
                 }
index e459004..a670cfc 100644 (file)
@@ -87,17 +87,15 @@ class auth_plugin_email extends auth_plugin_base {
     function user_signup($user, $notify=true) {
         global $CFG, $DB;
         require_once($CFG->dirroot.'/user/profile/lib.php');
+        require_once($CFG->dirroot.'/user/lib.php');
 
         $user->password = hash_internal_user_password($user->password);
 
-        $user->id = $DB->insert_record('user', $user);
+        $user->id = user_create_user($user, false);
 
-        /// Save any custom profile field information
+        // Save any custom profile field information.
         profile_save_data($user);
 
-        $user = $DB->get_record('user', array('id'=>$user->id));
-        events_trigger('user_created', $user);
-
         if (! send_confirmation_email($user)) {
             print_error('auth_emailnoemail','auth_email');
         }
index 866a838..1bad153 100644 (file)
@@ -78,6 +78,7 @@ if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
 
 require_once($CFG->libdir.'/authlib.php');
 require_once($CFG->libdir.'/ldaplib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 /**
  * LDAP authentication plugin.
@@ -550,7 +551,7 @@ class auth_plugin_ldap extends auth_plugin_base {
             print_error('auth_ldap_create_error', 'auth_ldap');
         }
 
-        $user->id = $DB->insert_record('user', $user);
+        $user->id = user_create_user($user, false);
 
         // Save any custom profile field information
         profile_save_data($user);
@@ -562,7 +563,6 @@ class auth_plugin_ldap extends auth_plugin_base {
         update_internal_user_password($user, $plainslashedpassword);
 
         $user = $DB->get_record('user', array('id'=>$user->id));
-        events_trigger('user_created', $user);
 
         if (! send_confirmation_email($user)) {
             print_error('noemail', 'auth_ldap');
@@ -612,12 +612,12 @@ class auth_plugin_ldap extends auth_plugin_base {
                 if (!$this->user_activate($username)) {
                     return AUTH_CONFIRM_FAIL;
                 }
-                $DB->set_field('user', 'confirmed', 1, array('id'=>$user->id));
+                $user->confirmed = 1;
                 if ($user->firstaccess == 0) {
-                    $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id));
+                    $user->firstaccess = time();
                 }
-                $euser = $DB->get_record('user', array('id' => $user->id));
-                events_trigger('user_updated', $euser);
+                require_once($CFG->dirroot.'/user/lib.php');
+                user_update_user($user, false);
                 return AUTH_CONFIRM_OK;
             }
         } else {
@@ -806,10 +806,8 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $updateuser = new stdClass();
                     $updateuser->id = $user->id;
                     $updateuser->suspended = 1;
-                    $DB->update_record('user', $updateuser);
+                    user_update_user($updateuser, false);
                     echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
-                    $euser = $DB->get_record('user', array('id' => $user->id));
-                    events_trigger('user_updated', $euser);
                     session_kill_user($user->id);
                 }
             } else {
@@ -835,10 +833,8 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $updateuser->id = $user->id;
                     $updateuser->auth = $this->authtype;
                     $updateuser->suspended = 0;
-                    $DB->update_record('user', $updateuser);
+                    user_update_user($updateuser, false);
                     echo "\t"; print_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
-                    $euser = $DB->get_record('user', array('id' => $user->id));
-                    events_trigger('user_updated', $euser);
                 }
             } else {
                 print_string('nouserentriestorevive', 'auth_ldap');
@@ -950,10 +946,10 @@ class auth_plugin_ldap extends auth_plugin_base {
                     $user->lang = $CFG->lang;
                 }
 
-                $id = $DB->insert_record('user', $user);
+                $id = user_create_user($user, false);
                 echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n";
                 $euser = $DB->get_record('user', array('id' => $id));
-                events_trigger('user_created', $euser);
+
                 if (!empty($this->config->forcechangepassword)) {
                     set_user_preference('auth_forcepasswordchange', 1, $id);
                 }
@@ -1011,22 +1007,25 @@ class auth_plugin_ldap extends auth_plugin_base {
                 $updatekeys = array_keys($newinfo);
             }
 
-            foreach ($updatekeys as $key) {
-                if (isset($newinfo[$key])) {
-                    $value = $newinfo[$key];
-                } else {
-                    $value = '';
-                }
+            if (!empty($updatekeys)) {
+                $newuser = new stdClass();
+                $newuser->id = $userid;
+
+                foreach ($updatekeys as $key) {
+                    if (isset($newinfo[$key])) {
+                        $value = $newinfo[$key];
+                    } else {
+                        $value = '';
+                    }
 
-                if (!empty($this->config->{'field_updatelocal_' . $key})) {
-                    if ($user->{$key} != $value) { // only update if it's changed
-                        $DB->set_field('user', $key, $value, array('id'=>$userid));
+                    if (!empty($this->config->{'field_updatelocal_' . $key})) {
+                        // Only update if it's changed.
+                        if ($user->{$key} != $value) {
+                            $newuser->$key = $value;
+                        }
                     }
                 }
-            }
-            if (!empty($updatekeys)) {
-                $euser = $DB->get_record('user', array('id' => $userid));
-                events_trigger('user_updated', $euser);
+                user_update_user($newuser, false);
             }
         } else {
             return false;
index c1861fa..2f626bd 100644 (file)
@@ -216,6 +216,7 @@ class auth_plugin_mnet extends auth_plugin_base {
         global $CFG, $DB;
         require_once $CFG->dirroot . '/mnet/xmlrpc/client.php';
         require_once $CFG->libdir . '/gdlib.php';
+        require_once($CFG->dirroot.'/user/lib.php');
 
         // verify the remote host is configured locally before attempting RPC call
         if (! $remotehost = $DB->get_record('mnet_host', array('wwwroot' => $remotepeer->wwwroot, 'deleted' => 0))) {
@@ -361,8 +362,7 @@ class auth_plugin_mnet extends auth_plugin_base {
         if (empty($localuser->firstaccess)) { // Now firstaccess, grab it here
             $localuser->firstaccess = time();
         }
-
-        $DB->update_record('user', $localuser);
+        user_update_user($localuser, false);
 
         if (!$firsttime) {
             // repeat customer! let the IDP know about enrolments
index cfe16ef..cf7d3be 100644 (file)
@@ -89,11 +89,6 @@ if (!($bc = backup_ui::load_controller($backupid))) {
 }
 $backup = new backup_ui($bc);
 $backup->process();
-if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
-    $backup->execute();
-} else {
-    $backup->save_controller();
-}
 
 $PAGE->set_title($heading.': '.$backup->get_stage_name());
 $PAGE->set_heading($heading);
@@ -104,6 +99,19 @@ echo $OUTPUT->header();
 if ($backup->enforce_changed_dependencies()) {
     debugging('Your settings have been altered due to unmet dependencies', DEBUG_DEVELOPER);
 }
+
+if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
+    // Display an extra progress bar so that we can show the progress first.
+    echo html_writer::start_div('', array('id' => 'executionprogress'));
+    echo $renderer->progress_bar($backup->get_progress_bar());
+    $backup->get_controller()->set_progress(new core_backup_display_progress());
+    $backup->execute();
+    echo html_writer::end_div();
+    echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
+} else {
+    $backup->save_controller();
+}
+
 echo $renderer->progress_bar($backup->get_progress_bar());
 echo $backup->display($renderer);
 $backup->destroy();
index b72c394..b7b87d2 100644 (file)
@@ -64,6 +64,11 @@ class backup_controller extends backup implements loggable {
     protected $destination; // Destination chain object (fs_moodle, fs_os, db, email...)
     protected $logger;      // Logging chain object (moodle, inline, fs, db, syslog)
 
+    /**
+     * @var core_backup_progress Progress reporting object.
+     */
+    protected $progress;
+
     protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses
 
     /**
@@ -109,6 +114,10 @@ class backup_controller extends backup implements loggable {
         // Default logger chain (based on interactive/execution)
         $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->backupid);
 
+        // By default there is no progress reporter. Interfaces that wish to
+        // display progress must set it.
+        $this->progress = new core_backup_null_progress();
+
         // Instantiate the output_controller singleton and active it if interactive and inmediate
         $oc = output_controller::get_instance();
         if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) {
@@ -302,6 +311,25 @@ class backup_controller extends backup implements loggable {
         return $this->logger;
     }
 
+    /**
+     * Gets the progress reporter, which can be used to report progress within
+     * the backup or restore process.
+     *
+     * @return core_backup_progress Progress reporting object
+     */
+    public function get_progress() {
+        return $this->progress;
+    }
+
+    /**
+     * Sets the progress reporter.
+     *
+     * @param core_backup_progress $progress Progress reporting object
+     */
+    public function set_progress(core_backup_progress $progress) {
+        $this->progress = $progress;
+    }
+
     /**
      * Executes the backup
      * @return void Throws and exception of completes
index ae3d58a..9d98df6 100644 (file)
@@ -57,6 +57,11 @@ class restore_controller extends backup implements loggable {
 
     protected $logger;      // Logging chain object (moodle, inline, fs, db, syslog)
 
+    /**
+     * @var core_backup_progress Progress reporting object.
+     */
+    protected $progress;
+
     protected $checksum; // Cache @checksumable results for lighter @is_checksum_correct() uses
 
     /**
@@ -101,6 +106,10 @@ class restore_controller extends backup implements loggable {
         // Default logger chain (based on interactive/execution)
         $this->logger = backup_factory::get_logger_chain($this->interactive, $this->execution, $this->restoreid);
 
+        // By default there is no progress reporter. Interfaces that wish to
+        // display progress must set it.
+        $this->progress = new core_backup_null_progress();
+
         // Instantiate the output_controller singleton and active it if interactive and inmediate
         $oc = output_controller::get_instance();
         if ($this->interactive == backup::INTERACTIVE_YES && $this->execution == backup::EXECUTION_INMEDIATE) {
@@ -300,6 +309,25 @@ class restore_controller extends backup implements loggable {
         return $this->logger;
     }
 
+    /**
+     * Gets the progress reporter, which can be used to report progress within
+     * the backup or restore process.
+     *
+     * @return core_backup_progress Progress reporting object
+     */
+    public function get_progress() {
+        return $this->progress;
+    }
+
+    /**
+     * Sets the progress reporter.
+     *
+     * @param core_backup_progress $progress Progress reporting object
+     */
+    public function set_progress(core_backup_progress $progress) {
+        $this->progress = $progress;
+    }
+
     public function execute_plan() {
         // Basic/initial prevention against time/memory limits
         set_time_limit(1 * 60 * 60); // 1 hour for 1 course initially granted
index fbd919a..13ad15a 100644 (file)
@@ -90,11 +90,25 @@ if ($backup->get_stage() == backup_ui::STAGE_CONFIRMATION) {
 
 // If it's the final stage process the import
 if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
+    echo $OUTPUT->header();
+
+    // Display an extra progress bar so that we can show the current stage.
+    echo html_writer::start_div('', array('id' => 'executionprogress'));
+    echo $renderer->progress_bar($backup->get_progress_bar());
+
+    // Start the progress display - we split into 2 chunks for backup and restore.
+    $progress = new core_backup_display_progress();
+    $progress->start_progress('', 2);
+    $backup->get_controller()->set_progress($progress);
+
     // First execute the backup
     $backup->execute();
     $backup->destroy();
     unset($backup);
 
+    // Note that we've done that progress.
+    $progress->progress(1);
+
     // Check whether the backup directory still exists. If missing, something
     // went really wrong in backup, throw error. Note that backup::MODE_IMPORT
     // backups don't store resulting files ever
@@ -106,6 +120,7 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
     // Prepare the restore controller. We don't need a UI here as we will just use what
     // ever the restore has (the user has just chosen).
     $rc = new restore_controller($backupid, $course->id, backup::INTERACTIVE_YES, backup::MODE_IMPORT, $USER->id, $restoretarget);
+    $rc->set_progress($progress);
     // Convert the backup if required.... it should NEVER happed
     if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) {
         $rc->convert();
@@ -140,8 +155,12 @@ if ($backup->get_stage() == backup_ui::STAGE_FINAL) {
     // Delete the temp directory now
     fulldelete($tempdestination);
 
+    // All progress complete. Hide progress area.
+    $progress->end_progress();
+    echo html_writer::end_div();
+    echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
+
     // Display a notification and a continue button
-    echo $OUTPUT->header();
     if ($warnings) {
         echo $OUTPUT->box_start();
         echo $OUTPUT->notification(get_string('warning'), 'notifywarning');
index ad5ac4a..2cd3754 100644 (file)
@@ -157,6 +157,11 @@ class backup_final_task extends backup_task {
         $this->built = true;
     }
 
+    public function get_weight() {
+        // The final task takes ages, so give it 20 times the weight of a normal task.
+        return 20;
+    }
+
 // Protected API starts here
 
     /**
index cd51cae..a31501c 100644 (file)
@@ -44,10 +44,28 @@ if ($stage & restore_ui::STAGE_CONFIRM + restore_ui::STAGE_DESTINATION) {
 }
 
 $outcome = $restore->process();
+$heading = $course->fullname;
+
+$PAGE->set_title($heading.': '.$restore->get_stage_name());
+$PAGE->set_heading($heading);
+$PAGE->navbar->add($restore->get_stage_name());
+
+$renderer = $PAGE->get_renderer('core','backup');
+echo $OUTPUT->header();
+if (!$restore->is_independent() && $restore->enforce_changed_dependencies()) {
+    debugging('Your settings have been altered due to unmet dependencies', DEBUG_DEVELOPER);
+}
+
 if (!$restore->is_independent()) {
     if ($restore->get_stage() == restore_ui::STAGE_PROCESS && !$restore->requires_substage()) {
         try {
+            // Display an extra progress bar so that we can show the progress first.
+            echo html_writer::start_div('', array('id' => 'executionprogress'));
+            echo $renderer->progress_bar($restore->get_progress_bar());
+            $restore->get_controller()->set_progress(new core_backup_display_progress());
             $restore->execute();
+            echo html_writer::end_div();
+            echo html_writer::script('document.getElementById("executionprogress").style.display = "none";');
         } catch(Exception $e) {
             $restore->cleanup();
             throw $e;
@@ -56,17 +74,7 @@ if (!$restore->is_independent()) {
         $restore->save_controller();
     }
 }
-$heading = $course->fullname;
-
-$PAGE->set_title($heading.': '.$restore->get_stage_name());
-$PAGE->set_heading($heading);
-$PAGE->navbar->add($restore->get_stage_name());
 
-$renderer = $PAGE->get_renderer('core','backup');
-echo $OUTPUT->header();
-if (!$restore->is_independent() && $restore->enforce_changed_dependencies()) {
-    debugging('Your settings have been altered due to unmet dependencies', DEBUG_DEVELOPER);
-}
 echo $renderer->progress_bar($restore->get_progress_bar());
 echo $restore->display($renderer);
 $restore->destroy();
index 12a3259..2a4f06b 100644 (file)
@@ -71,6 +71,9 @@ require_once($CFG->dirroot . '/backup/util/loggers/error_log_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php');
+require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php');
+require_once($CFG->dirroot . '/backup/util/progress/core_backup_null_progress.class.php');
+require_once($CFG->dirroot . '/backup/util/progress/core_backup_display_progress.class.php');
 require_once($CFG->dirroot . '/backup/util/settings/setting_dependency.class.php');
 require_once($CFG->dirroot . '/backup/util/settings/base_setting.class.php');
 require_once($CFG->dirroot . '/backup/util/settings/backup_setting.class.php');
index ae01911..78cd24b 100644 (file)
@@ -60,6 +60,9 @@ require_once($CFG->dirroot . '/backup/util/loggers/error_log_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/file_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/database_logger.class.php');
 require_once($CFG->dirroot . '/backup/util/loggers/output_indented_logger.class.php');
+require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php');
+require_once($CFG->dirroot . '/backup/util/progress/core_backup_null_progress.class.php');
+require_once($CFG->dirroot . '/backup/util/progress/core_backup_display_progress.class.php');
 require_once($CFG->dirroot . '/backup/util/factories/backup_factory.class.php');
 require_once($CFG->dirroot . '/backup/util/factories/restore_factory.class.php');
 require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php');
index 4378221..3d537d2 100644 (file)
@@ -87,6 +87,16 @@ class backup_plan extends base_plan implements loggable {
         return $this->controller->get_logger();
     }
 
+    /**
+     * Gets the progress reporter, which can be used to report progress within
+     * the backup or restore process.
+     *
+     * @return core_backup_progress Progress reporting object
+     */
+    public function get_progress() {
+        return $this->controller->get_progress();
+    }
+
     public function is_excluding_activities() {
         return $this->excludingdactivities;
     }
index d8c4b4d..7479094 100644 (file)
@@ -158,12 +158,33 @@ abstract class base_plan implements checksumable, executable {
         if (!$this->built) {
             throw new base_plan_exception('base_plan_not_built');
         }
+
+        // Calculate the total weight of all tasks and start progress tracking.
+        $progress = $this->get_progress();
+        $totalweight = 0;
+        foreach ($this->tasks as $task) {
+            $totalweight += $task->get_weight();
+        }
+        $progress->start_progress($this->get_name(), $totalweight);
+
+        // Build and execute all tasks.
         foreach ($this->tasks as $task) {
             $task->build();
             $task->execute();
         }
+
+        // Finish progress tracking.
+        $progress->end_progress();
     }
 
+    /**
+     * Gets the progress reporter, which can be used to report progress within
+     * the backup or restore process.
+     *
+     * @return core_backup_progress Progress reporting object
+     */
+    public abstract function get_progress();
+
     /**
      * Destroy all circular references. It helps PHP 5.2 a lot!
      */
index 440521b..e167b89 100644 (file)
@@ -67,6 +67,17 @@ abstract class base_task implements checksumable, executable, loggable {
         return $this->settings;
     }
 
+    /**
+     * Returns the weight of this task, an approximation of the amount of time
+     * it will take. By default this value is 1. It can be increased for longer
+     * tasks.
+     *
+     * @return int Weight
+     */
+    public function get_weight() {
+        return 1;
+    }
+
     public function get_setting($name) {
         // First look in task settings
         $result = null;
@@ -111,6 +122,16 @@ abstract class base_task implements checksumable, executable, loggable {
         return $this->plan->get_logger();
     }
 
+    /**
+     * Gets the progress reporter, which can be used to report progress within
+     * the backup or restore process.
+     *
+     * @return core_backup_progress Progress reporting object
+     */
+    public function get_progress() {
+        return $this->plan->get_progress();
+    }
+
     public function log($message, $level, $a = null, $depth = null, $display = false) {
         backup_helper::log($message, $level, $a, $depth, $display, $this->get_logger());
     }
@@ -149,6 +170,13 @@ abstract class base_task implements checksumable, executable, loggable {
         if ($this->executed) {
             throw new base_task_exception('base_task_already_executed', $this->name);
         }
+
+        // Starts progress based on the weight of this task and number of steps.
+        $progress = $this->get_progress();
+        $progress->start_progress($this->get_name(), count($this->steps), $this->get_weight());
+        $done = 0;
+
+        // Execute all steps.
         foreach ($this->steps as $step) {
             $result = $step->execute();
             // If step returns array, it will be forwarded to plan
@@ -156,11 +184,16 @@ abstract class base_task implements checksumable, executable, loggable {
             if (is_array($result) and !empty($result)) {
                 $this->add_result($result);
             }
+            $done++;
+            $progress->progress($done);
         }
         // Mark as executed if any step has been executed
         if (!empty($this->steps)) {
             $this->executed = true;
         }
+
+        // Finish progress for this task.
+        $progress->end_progress();
     }
 
     /**
index c9dd8fb..e173e0e 100644 (file)
@@ -94,6 +94,16 @@ class restore_plan extends base_plan implements loggable {
         return $this->controller->get_logger();
     }
 
+    /**
+     * Gets the progress reporter, which can be used to report progress within
+     * the backup or restore process.
+     *
+     * @return core_backup_progress Progress reporting object
+     */
+    public function get_progress() {
+        return $this->controller->get_progress();
+    }
+
     public function get_info() {
         return $this->controller->get_info();
     }
index daf1499..1283e7e 100644 (file)
@@ -34,6 +34,10 @@ require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
 class mock_base_plan extends base_plan {
     public function build() {
     }
+
+    public function get_progress() {
+        return null;
+    }
 }
 
 /**
diff --git a/backup/util/progress/core_backup_display_progress.class.php b/backup/util/progress/core_backup_display_progress.class.php
new file mode 100644 (file)
index 0000000..62696cb
--- /dev/null
@@ -0,0 +1,136 @@
+<?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/>.
+
+/**
+ * Progress handler that uses a standard Moodle progress bar to display
+ * progress. The Moodle progress bar cannot show indeterminate progress,
+ * so we do extra output in addition to the bar.
+ *
+ * @package core_backup
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_backup_display_progress extends core_backup_progress {
+    /**
+     * @var int Number of wibble states (state0...stateN-1 classes in CSS)
+     */
+    const WIBBLE_STATES = 13;
+
+    /**
+     * @var progress_bar Current progress bar.
+     */
+    private $bar;
+
+    private $lastwibble, $currentstate = 0, $direction = 1;
+
+    /**
+     * @var bool True to display names
+     */
+    protected $displaynames = false;
+
+    /**
+     * Constructs the progress reporter. This will output HTML code for the
+     * progress bar, and an indeterminate wibbler below it.
+     *
+     * @param bool $startnow If true, outputs HTML immediately.
+     */
+    public function __construct($startnow = true) {
+        if ($startnow) {
+            $this->start_html();
+        }
+    }
+
+    /**
+     * By default, the progress section names do not display because (in backup)
+     * these are usually untranslated and incomprehensible. To make them
+     * display, call this method.
+     *
+     * @param bool $displaynames True to display names
+     */
+    public function set_display_names($displaynames = true) {
+        $this->displaynames = $displaynames;
+    }
+
+    /**
+     * Starts to output progress.
+     *
+     * Called in constructor and in update_progress if required.
+     *
+     * @throws coding_exception If already started
+     */
+    public function start_html() {
+        if ($this->bar) {
+            throw new coding_exception('Already started');
+        }
+        $this->bar = new progress_bar();
+        $this->bar->create();
+        echo html_writer::start_div('wibbler');
+    }
+
+    /**
+     * Finishes output. (Progress can begin again later if there are more
+     * calls to update_progress.)
+     *
+     * Automatically called from update_progress when progress finishes.
+     */
+    public function end_html() {
+        // Finish progress bar.
+        $this->bar->update_full(100, '');
+        $this->bar = null;
+
+        // End wibbler div.
+        echo html_writer::end_div();
+    }
+
+    public function update_progress() {
+        // If finished...
+        if (!$this->is_in_progress_section()) {
+            if ($this->bar) {
+                $this->end_html();
+            }
+        } else {
+            if (!$this->bar) {
+                $this->start_html();
+            }
+            // In case of indeterminate or small progress, update the wibbler
+            // (up to once per second).
+            if (time() != $this->lastwibble) {
+                $this->lastwibble = time();
+                echo html_writer::div('', 'wibble state' . $this->currentstate);
+
+                // Go on to next colour.
+                $this->currentstate += $this->direction;
+                if ($this->currentstate < 0 || $this->currentstate >= self::WIBBLE_STATES) {
+                    $this->direction = -$this->direction;
+                    $this->currentstate += 2 * $this->direction;
+                }
+            }
+
+            // Get progress.
+            list ($min, $max) = $this->get_progress_proportion_range();
+
+            // Update progress bar.
+            $message = '';
+            if ($this->displaynames) {
+                $message = $this->get_current_description();
+            }
+            $this->bar->update_full($min * 100, $message);
+
+            // Flush output.
+            flush();
+        }
+    }
+}
diff --git a/backup/util/progress/core_backup_null_progress.class.php b/backup/util/progress/core_backup_null_progress.class.php
new file mode 100644 (file)
index 0000000..3e02369
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Progress handler that ignores progress entirely.
+ *
+ * @package core_backup
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_backup_null_progress extends core_backup_progress {
+    public function update_progress() {
+        // Do nothing.
+    }
+}
diff --git a/backup/util/progress/core_backup_progress.class.php b/backup/util/progress/core_backup_progress.class.php
new file mode 100644 (file)
index 0000000..2c092c9
--- /dev/null
@@ -0,0 +1,307 @@
+<?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/>.
+
+/**
+ * Base class for handling progress information during a backup and restore.
+ *
+ * Subclasses should generally override the current_progress function which
+ * summarises all progress information.
+ *
+ * @package core_backup
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class core_backup_progress {
+    /**
+     * @var int Constant indicating that the number of progress calls is unknown.
+     */
+    const INDETERMINATE = -1;
+
+    /**
+     * @var int The number of seconds that can pass without progress() calls.
+     */
+    const TIME_LIMIT_WITHOUT_PROGRESS = 120;
+
+    /**
+     * @var int Time of last progress call.
+     */
+    protected $lastprogresstime;
+
+    /**
+     * @var int Number of progress calls (restricted to ~ 1/second).
+     */
+    protected $count;
+
+    /**
+     * @var array Array of progress descriptions for each stack level.
+     */
+    protected $descriptions = array();
+
+    /**
+     * @var array Array of maximum progress values for each stack level.
+     */
+    protected $maxes = array();
+
+    /**
+     * @var array Array of current progress values.
+     */
+    protected $currents = array();
+
+    /**
+     * @var int Array of counts within parent progress entry (ignored for first)
+     */
+    protected $parentcounts = array();
+
+    /**
+     * Marks the start of an operation that will display progress.
+     *
+     * This can be called multiple times for nested progress sections. It must
+     * be paired with calls to end_progress.
+     *
+     * The progress maximum may be INDETERMINATE if the current operation has
+     * an unknown number of steps. (This is default.)
+     *
+     * Calling this function will always result in a new display, so this
+     * should not be called exceedingly frequently.
+     *
+     * When it is complete by calling end_progress, each start_progress section
+     * automatically adds progress to its parent, as defined by $parentcount.
+     *
+     * @param string $description Description to display
+     * @param int $max Maximum value of progress for this section
+     * @param int $parentcount How many progress points this section counts for
+     * @throws coding_exception If max is invalid
+     */
+    public function start_progress($description, $max = self::INDETERMINATE,
+            $parentcount = 1) {
+        if ($max != self::INDETERMINATE && $max <= 0) {
+            throw new coding_exception(
+                    'start_progress() max value cannot be zero or negative');
+        }
+        if ($parentcount < 1) {
+            throw new coding_exception(
+                    'start_progress() parent progress count must be at least 1');
+        }
+        if (!empty($this->descriptions)) {
+            $prevmax = end($this->maxes);
+            if ($prevmax !== self::INDETERMINATE) {
+                $prevcurrent = end($this->currents);
+                if ($prevcurrent + $parentcount > $prevmax) {
+                    throw new coding_exception(
+                            'start_progress() parent progress would exceed max');
+                }
+            }
+        } else {
+            if ($parentcount != 1) {
+                throw new coding_exception(
+                        'start_progress() progress count must be 1 when no parent');
+            }
+        }
+        $this->descriptions[] = $description;
+        $this->maxes[] = $max;
+        $this->currents[] = 0;
+        $this->parentcounts[] = $parentcount;
+        $this->update_progress();
+        $lastprogresstime = $this->get_time();
+    }
+
+    /**
+     * Marks the end of an operation that will display progress.
+     *
+     * This must be paired with each start_progress call.
+     *
+     * If there is a parent progress section, its progress will be increased
+     * automatically to reflect the end of the child section.
+     *
+     * @throws coding_exception If progress hasn't been started
+     */
+    public function end_progress() {
+        if (!count($this->descriptions)) {
+            throw new coding_exception('end_progress() without start_progress()');
+        }
+        array_pop($this->descriptions);
+        array_pop($this->maxes);
+        array_pop($this->currents);
+        $parentcount = array_pop($this->parentcounts);
+        if (!empty($this->descriptions)) {
+            $lastmax = end($this->maxes);
+            if ($lastmax != self::INDETERMINATE) {
+                $lastvalue = end($this->currents);
+                $this->currents[key($this->currents)] = $lastvalue + $parentcount;
+            }
+        }
+        $this->update_progress();
+    }
+
+    /**
+     * Indicates that progress has occurred.
+     *
+     * The progress value should indicate the total progress so far, from 0
+     * to the value supplied for $max (inclusive) in start_progress.
+     *
+     * You do not need to call this function for every value. It is OK to skip
+     * values. It is also OK to call this function as often as desired; it
+     * doesn't do anything if called more than once per second.
+     *
+     * It must be INDETERMINATE if start_progress was called with $max set to
+     * INDETERMINATE. Otherwise it must not be indeterminate.
+     *
+     * @param int $progress Progress so far
+     * @throws coding_exception If progress value is invalid
+     */
+    public function progress($progress = self::INDETERMINATE) {
+        // Ignore too-frequent progress calls (more than once per second).
+        $now = $this->get_time();
+        if ($now === $this->lastprogresstime) {
+            return;
+        }
+
+        // Check we are inside a progress section.
+        $max = end($this->maxes);
+        if ($max === false) {
+            throw new coding_exception(
+                    'progress() without start_progress');
+        }
+
+        // Check and apply new progress.
+        if ($progress === self::INDETERMINATE) {
+            // Indeterminate progress.
+            if ($max !== self::INDETERMINATE) {
+                throw new coding_exception(
+                        'progress() INDETERMINATE, expecting value');
+            }
+        } else {
+            // Determinate progress.
+            $current = end($this->currents);
+            if ($max === self::INDETERMINATE) {
+                throw new coding_exception(
+                        'progress() with value, expecting INDETERMINATE');
+            } else if ($progress < 0 || $progress > $max) {
+                throw new coding_exception(
+                        'progress() value out of range');
+            } else if ($progress < $current) {
+                throw new coding_Exception(
+                        'progress() value may not go backwards');
+            }
+            $this->currents[key($this->currents)] = $progress;
+        }
+
+        // Update progress.
+        $this->count++;
+        $this->lastprogresstime = $now;
+        set_time_limit(self::TIME_LIMIT_WITHOUT_PROGRESS);
+        $this->update_progress();
+    }
+
+    /**
+     * Gets time (this is provided so that unit tests can override it).
+     *
+     * @return int Current system time
+     */
+    protected function get_time() {
+        return time();
+    }
+
+    /**
+     * Called whenever new progress should be displayed.
+     */
+    protected abstract function update_progress();
+
+    /**
+     * @return bool True if currently inside a progress section
+     */
+    public function is_in_progress_section() {
+        return !empty($this->descriptions);
+    }
+
+    /**
+     * @return string Current progress section description
+     */
+    public function get_current_description() {
+        $description = end($this->descriptions);
+        if ($description === false) {
+            throw new coding_exception('Not inside progress section');
+        }
+        return $description;
+    }
+
+    /**
+     * Obtains current progress in a way suitable for drawing a progress bar.
+     *
+     * Progress is returned as a minimum and maximum value. If there is no
+     * indeterminate progress, these values will be identical. If there is
+     * intermediate progress, these values can be different. (For example, if
+     * the top level progress sections is indeterminate, then the values will
+     * always be 0.0 and 1.0.)
+     *
+     * @return array Minimum and maximum possible progress proportions
+     */
+    public function get_progress_proportion_range() {
+        // If there is no progress underway, we must have finished.
+        if (empty($this->currents)) {
+            return array(1.0, 1.0);
+        }
+        $count = count($this->currents);
+        $min = 0.0;
+        $max = 1.0;
+        for ($i = 0; $i < $count; $i++) {
+            // Get max value at that section - if it's indeterminate we can tell
+            // no more.
+            $sectionmax = $this->maxes[$i];
+            if ($sectionmax === self::INDETERMINATE) {
+                return array($min, $max);
+            }
+
+            // Special case if current value is max (this should only happen
+            // just before ending a section).
+            $sectioncurrent = $this->currents[$i];
+            if ($sectioncurrent === $sectionmax) {
+                return array($max, $max);
+            }
+
+            // Using the current value at that section, we know we are somewhere
+            // between 'current' and the next 'current' value which depends on
+            // the parentcount of the nested section (if any).
+            $newmin = ($sectioncurrent / $sectionmax) * ($max - $min) + $min;
+            $nextcurrent = $sectioncurrent + 1;
+            if ($i + 1 < $count) {
+                $weight = $this->parentcounts[$i + 1];
+                $nextcurrent = $sectioncurrent + $weight;
+            }
+            $newmax = ($nextcurrent / $sectionmax) * ($max - $min) + $min;
+            $min = $newmin;
+            $max = $newmax;
+        }
+
+        // If there was nothing indeterminate, we use the min value as current.
+        return array($min, $min);
+    }
+
+    /**
+     * Obtains current indeterminate progress in a way suitable for adding to
+     * the progress display.
+     *
+     * This returns the number of indeterminate calls (at any level) during the
+     * lifetime of this progress reporter, whether or not there is a current
+     * indeterminate step. (The number will not be ridiculously high because
+     * progress calls are limited to one per second.)
+     *
+     * @return int Number of indeterminate progress calls
+     */
+    public function get_progress_count() {
+        return $this->count;
+    }
+}
diff --git a/backup/util/progress/tests/progress_test.php b/backup/util/progress/tests/progress_test.php
new file mode 100644 (file)
index 0000000..3c673f7
--- /dev/null
@@ -0,0 +1,363 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the progress classes.
+ *
+ * @package core_backup
+ * @category phpunit
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+// Include all the needed stuff.
+global $CFG;
+require_once($CFG->dirroot . '/backup/util/progress/core_backup_progress.class.php');
+
+/**
+ * Progress tests.
+ */
+class backup_progress_testcase extends basic_testcase {
+
+    /**
+     * Tests for basic use with simple numeric progress.
+     */
+    public function test_basic() {
+        $progress = new core_backup_mock_progress();
+
+        // Check values of empty progress things.
+        $this->assertFalse($progress->is_in_progress_section());
+
+        // Start progress counting, check basic values and check that update
+        // gets called.
+        $progress->start_progress('hello', 10);
+        $this->assertTrue($progress->was_update_called());
+        $this->assertTrue($progress->is_in_progress_section());
+        $this->assertEquals('hello', $progress->get_current_description());
+
+        // Check numeric position and indeterminate count.
+        $this->assert_min_max(0.0, 0.0, $progress);
+        $this->assertEquals(0, $progress->get_progress_count());
+
+        // Make some progress and check that the time limit gets added.
+        $progress->step_time();
+        $progress->progress(2);
+        $this->assertTrue($progress->was_update_called());
+        $this->assertEquals(120, ini_get('max_execution_time'));
+
+        // Check the new value.
+        $this->assert_min_max(0.2, 0.2, $progress);
+
+        // Do another progress run at same time, it should be ignored.
+        $progress->progress(3);
+        $this->assertFalse($progress->was_update_called());
+        $this->assert_min_max(0.2, 0.2, $progress);
+
+        // End the section. This should cause an update.
+        $progress->end_progress();
+        $this->assertTrue($progress->was_update_called());
+
+        // Because there are no sections left open, it thinks we finished.
+        $this->assert_min_max(1.0, 1.0, $progress);
+
+        // There was 1 progress call.
+        $this->assertEquals(1, $progress->get_progress_count());
+
+        // Clear the time limit, otherwise phpunit complains.
+        set_time_limit(0);
+    }
+
+    /**
+     * Tests progress that is nested and/or indeterminate.
+     */
+    public function test_nested() {
+        // Outer progress goes from 0 to 10.
+        $progress = new core_backup_mock_progress();
+        $progress->start_progress('hello', 10);
+
+        // Get up to 4, check position.
+        $progress->step_time();
+        $progress->progress(4);
+        $this->assert_min_max(0.4, 0.4, $progress);
+        $this->assertEquals('hello', $progress->get_current_description());
+
+        // Now start indeterminate progress.
+        $progress->start_progress('world');
+        $this->assert_min_max(0.4, 0.5, $progress);
+        $this->assertEquals('world', $progress->get_current_description());
+
+        // Do some indeterminate progress and count it (once per second).
+        $progress->step_time();
+        $progress->progress();
+        $this->assertEquals(2, $progress->get_progress_count());
+        $progress->progress();
+        $this->assertEquals(2, $progress->get_progress_count());
+        $progress->step_time();
+        $progress->progress();
+        $this->assertEquals(3, $progress->get_progress_count());
+        $this->assert_min_max(0.4, 0.5, $progress);
+
+        // Exit the indeterminate section.
+        $progress->end_progress();
+        $this->assert_min_max(0.5, 0.5, $progress);
+
+        $progress->step_time();
+        $progress->progress(7);
+        $this->assert_min_max(0.7, 0.7, $progress);
+
+        // Enter a numbered section (this time with a range of 5).
+        $progress->start_progress('frogs', 5);
+        $this->assert_min_max(0.7, 0.7, $progress);
+        $progress->step_time();
+        $progress->progress(1);
+        $this->assert_min_max(0.72, 0.72, $progress);
+        $progress->step_time();
+        $progress->progress(3);
+        $this->assert_min_max(0.76, 0.76, $progress);
+
+        // Now enter another indeterminate section.
+        $progress->start_progress('and');
+        $this->assert_min_max(0.76, 0.78, $progress);
+
+        // Make some progress, should increment indeterminate count.
+        $progress->step_time();
+        $progress->progress();
+        $this->assertEquals(7, $progress->get_progress_count());
+
+        // Enter numbered section, won't make any difference to values.
+        $progress->start_progress('zombies', 2);
+        $progress->step_time();
+        $progress->progress(1);
+        $this->assert_min_max(0.76, 0.78, $progress);
+        $this->assertEquals(8, $progress->get_progress_count());
+
+        // Leaving it will make no difference too.
+        $progress->end_progress();
+
+        // Leaving the indeterminate section will though.
+        $progress->end_progress();
+        $this->assert_min_max(0.78, 0.78, $progress);
+
+        // Leave the two numbered sections.
+        $progress->end_progress();
+        $this->assert_min_max(0.8, 0.8, $progress);
+        $progress->end_progress();
+        $this->assertFalse($progress->is_in_progress_section());
+
+        set_time_limit(0);
+    }
+
+    /**
+     * Tests the feature for 'weighting' nested progress.
+     */
+    public function test_nested_weighted() {
+        $progress = new core_backup_mock_progress();
+        $progress->start_progress('', 10);
+
+        // First nested child has 2 units of its own and is worth 1 unit.
+        $progress->start_progress('', 2);
+        $progress->step_time();
+        $progress->progress(1);
+        $this->assert_min_max(0.05, 0.05, $progress);
+        $progress->end_progress();
+        $this->assert_min_max(0.1, 0.1, $progress);
+
+        // Next child has 2 units of its own but is worth 3 units.
+        $progress->start_progress('weighted', 2, 3);
+        $progress->step_time();
+        $progress->progress(1);
+        $this->assert_min_max(0.25, 0.25, $progress);
+        $progress->end_progress();
+        $this->assert_min_max(0.4, 0.4, $progress);
+
+        // Next indeterminate child is worth 6 units.
+        $progress->start_progress('', core_backup_progress::INDETERMINATE, 6);
+        $progress->step_time();
+        $progress->progress();
+        $this->assert_min_max(0.4, 1.0, $progress);
+        $progress->end_progress();
+        $this->assert_min_max(1.0, 1.0, $progress);
+
+        set_time_limit(0);
+    }
+
+    /**
+     * I had some issues with real use in backup/restore, this test is intended
+     * to be similar.
+     */
+    public function test_realistic() {
+        $progress = new core_backup_mock_progress();
+        $progress->start_progress('parent', 100);
+        $progress->start_progress('child', 1);
+        $progress->progress(1);
+        $this->assert_min_max(0.01, 0.01, $progress);
+        $progress->end_progress();
+        $this->assert_min_max(0.01, 0.01, $progress);
+
+        // Clear the time limit, otherwise phpunit complains.
+        set_time_limit(0);
+    }
+
+    /**
+     * Tests for any exceptions due to invalid calls.
+     */
+    public function test_exceptions() {
+        $progress = new core_backup_mock_progress();
+
+        // Check errors when empty.
+        try {
+            $progress->progress();
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~without start_progress~', $e->getMessage()));
+        }
+        try {
+            $progress->end_progress();
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~without start_progress~', $e->getMessage()));
+        }
+        try {
+            $progress->get_current_description();
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~Not inside progress~', $e->getMessage()));
+        }
+        try {
+            $progress->start_progress('', 1, 7);
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~must be 1~', $e->getMessage()));
+        }
+
+        // Check invalid start (0).
+        try {
+            $progress->start_progress('hello', 0);
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~cannot be zero or negative~', $e->getMessage()));
+        }
+
+        // Indeterminate when value expected.
+        $progress->start_progress('hello', 10);
+        try {
+            $progress->progress(core_backup_progress::INDETERMINATE);
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~expecting value~', $e->getMessage()));
+        }
+
+        // Value when indeterminate expected.
+        $progress->start_progress('hello');
+        try {
+            $progress->progress(4);
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~expecting INDETERMINATE~', $e->getMessage()));
+        }
+
+        // Illegal values.
+        $progress->start_progress('hello', 10);
+        try {
+            $progress->progress(-2);
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~out of range~', $e->getMessage()));
+        }
+        try {
+            $progress->progress(11);
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~out of range~', $e->getMessage()));
+        }
+
+        // You are allowed two with the same value...
+        $progress->progress(4);
+        $progress->step_time();
+        $progress->progress(4);
+        $progress->step_time();
+
+        // ...but not to go backwards.
+        try {
+            $progress->progress(3);
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~backwards~', $e->getMessage()));
+        }
+
+        // When you go forward, you can't go further than there is room.
+        try {
+            $progress->start_progress('', 1, 7);
+            $this->fail();
+        } catch (coding_exception $e) {
+            $this->assertEquals(1, preg_match('~would exceed max~', $e->getMessage()));
+        }
+
+        // Clear the time limit, otherwise phpunit complains.
+        set_time_limit(0);
+    }
+
+    /**
+     * Checks the current progress values are as expected.
+     *
+     * @param number $min Expected min progress
+     * @param number $max Expected max progress
+     * @param core_backup_mock_progress $progress
+     */
+    private function assert_min_max($min, $max, core_backup_mock_progress $progress) {
+        $this->assertEquals(array($min, $max),
+                $progress->get_progress_proportion_range());
+    }
+}
+
+/**
+ * Helper class that records when update_progress is called and allows time
+ * stepping.
+ */
+class core_backup_mock_progress extends core_backup_progress {
+    private $updatecalled = false;
+    private $time = 1;
+
+    /**
+     * Checks if update was called since the last call to this function.
+     *
+     * @return boolean True if update was called
+     */
+    public function was_update_called() {
+        if ($this->updatecalled) {
+            $this->updatecalled = false;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Steps the current time by 1 second.
+     */
+    public function step_time() {
+        $this->time++;
+    }
+
+    protected function update_progress() {
+        $this->updatecalled = true;
+    }
+
+    protected function get_time() {
+        return $this->time;
+    }
+}
index 6d7b054..39c2c4c 100644 (file)
@@ -72,7 +72,7 @@ class core_badges_observer {
     /**
      * Triggered when 'course_completed' event is triggered.
      *
-     * @param   \core\event\course_completed $event
+     * @param \core\event\course_completed $event
      */
     public static function course_criteria_review(\core\event\course_completed $event) {
         global $DB, $CFG;
@@ -105,4 +105,36 @@ class core_badges_observer {
             }
         }
     }
+
+    /**
+     * Triggered when 'user_updated' event happens.
+     *
+     * @param \core\event\user_updated $event event generated when user profile is updated.
+     */
+    public static function profile_criteria_review(\core\event\user_updated $event) {
+        global $DB, $CFG;
+
+        if (!empty($CFG->enablebadges)) {
+            require_once($CFG->dirroot.'/lib/badgeslib.php');
+            $userid = $event->objectid;
+
+            if ($rs = $DB->get_records('badge_criteria', array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE))) {
+                foreach ($rs as $r) {
+                    $badge = new badge($r->badgeid);
+                    if (!$badge->is_active() || $badge->is_issued($userid)) {
+                        continue;
+                    }
+
+                    if ($badge->criteria[BADGE_CRITERIA_TYPE_PROFILE]->review($userid)) {
+                        $badge->criteria[BADGE_CRITERIA_TYPE_PROFILE]->mark_complete($userid);
+
+                        if ($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($userid)) {
+                            $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($userid);
+                            $badge->issue($userid);
+                        }
+                    }
+                }
+            }
+        }
+    }
 }
index 5436f01..e9c4ab3 100644 (file)
 require_once(dirname(dirname(__FILE__)) . '/config.php');
 require_once($CFG->libdir . '/badgeslib.php');
 
-$json = required_param('badge', PARAM_RAW);
+$json = optional_param('badge', null, PARAM_RAW);
+// Redirect to homepage if users are trying to access external badge through old url.
+if ($json) {
+    redirect($CFG->wwwroot, get_string('invalidrequest', 'error'), 3);
+}
+
+$hash = required_param('hash', PARAM_ALPHANUM);
+$userid = required_param('user', PARAM_INT);
+
+$PAGE->set_url(new moodle_url('/badges/external.php', array('hash' => $hash, 'user' => $userid)));
+
+// Using the same setting as user profile page.
+if (!empty($CFG->forceloginforprofiles)) {
+    require_login();
+    if (isguestuser()) {
+        $SESSION->wantsurl = $PAGE->url->out(false);
+        redirect(get_login_url());
+    }
+} else if (!empty($CFG->forcelogin)) {
+    require_login();
+}
+
+// Get all external badges of a user.
+$out = get_backpack_settings($userid);
+$badges = $out->badges;
+
+// Loop through the badges and check if supplied badge hash exists in user external badges.
+foreach ($badges as $b) {
+    if ($hash == hash("md5", $b->hostedUrl)) {
+        $badge = $b;
+        break;
+    }
+}
+
+// If we didn't find the badge, a user might be trying to replace userid parameter.
+if (is_null($badge)) {
+    print_error(get_string('error:externalbadgedoesntexist', 'badges'));
+}
 
 $PAGE->set_context(context_system::instance());
 $output = $PAGE->get_renderer('core', 'badges');
 
-$badge = new external_badge(unserialize($json));
+$badge = new external_badge($badge, $userid);
 
-$PAGE->set_url('/badges/external.php');
 $PAGE->set_pagelayout('base');
 $PAGE->set_title(get_string('issuedbadge', 'badges'));
 
index 1f52453..875d025 100644 (file)
@@ -53,11 +53,13 @@ $PAGE->set_heading($title);
 $PAGE->set_pagelayout('mydashboard');
 
 $backpack = $DB->get_record('badge_backpack', array('userid' => $USER->id));
+$badgescache = cache::make('core', 'externalbadges');
 
 if ($disconnect && $backpack) {
     require_sesskey();
     $DB->delete_records('badge_external', array('backpackid' => $backpack->id));
     $DB->delete_records('badge_backpack', array('userid' => $USER->id));
+    $badgescache->delete($USER->id);
     redirect(new moodle_url('/badges/mybackpack.php'));
 }
 
@@ -103,6 +105,7 @@ if ($backpack) {
                 $DB->insert_record('badge_external', $obj);
             }
         }
+        $badgescache->delete($USER->id);
         redirect(new moodle_url('/badges/mybadges.php'));
     }
 } else {
index 088b443..3075dc1 100644 (file)
@@ -81,9 +81,10 @@ class core_badges_renderer extends plugin_renderer_base {
                 $url = new moodle_url('badge.php', array('hash' => $badge->uniquehash));
             } else {
                 if (!$external) {
-                    $url = new moodle_url($CFG->wwwroot . '/badges/badge.php', array('hash' => $badge->uniquehash));
+                    $url = new moodle_url('/badges/badge.php', array('hash' => $badge->uniquehash));
                 } else {
-                    $url = new moodle_url($CFG->wwwroot . '/badges/external.php', array('badge' => serialize($badge)));
+                    $hash = hash('md5', $badge->hostedUrl);
+                    $url = new moodle_url('/badges/external.php', array('hash' => $hash, 'user' => $userid));
                 }
             }
             $actions = html_writer::tag('div', $push . $download . $status, array('class' => 'badge-actions'));
@@ -276,6 +277,7 @@ class core_badges_renderer extends plugin_renderer_base {
     protected function render_issued_badge(issued_badge $ibadge) {
         global $USER, $CFG, $DB;
         $issued = $ibadge->issued;
+        $userinfo = $ibadge->recipient;
         $badge = new badge($ibadge->badgeid);
         $today_date = date('Y-m-d');
         $today = strtotime($today_date);
@@ -286,7 +288,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $imagetable = new html_table();
         $imagetable->attributes = array('class' => 'clearfix badgeissuedimage');
         $imagetable->data[] = array(html_writer::empty_tag('img', array('src' => $issued['badge']['image'])));
-        if ($USER->id == $ibadge->recipient && !empty($CFG->enablebadges)) {
+        if ($USER->id == $userinfo->id && !empty($CFG->enablebadges)) {
             $imagetable->data[] = array($this->output->single_button(
                         new moodle_url('/badges/badge.php', array('hash' => $ibadge->hash, 'bake' => true)),
                         get_string('download'),
@@ -307,11 +309,20 @@ class core_badges_renderer extends plugin_renderer_base {
         $datatable = new html_table();
         $datatable->attributes = array('class' => 'badgeissuedinfo');
         $datatable->colclasses = array('bfield', 'bvalue');
+
+        // Recipient information.
+        $datatable->data[] = array($this->output->heading(get_string('recipientdetails', 'badges'), 3), '');
+        $datatable->data[] = array(get_string('name'), fullname($userinfo));
+        if (empty($userinfo->backpackemail)) {
+            $datatable->data[] = array(get_string('email'), obfuscate_mailto($userinfo->accountemail));
+        } else {
+            $datatable->data[] = array(get_string('email'), obfuscate_mailto($userinfo->backpackemail));
+        }
+
         $datatable->data[] = array($this->output->heading(get_string('issuerdetails', 'badges'), 3), '');
         $datatable->data[] = array(get_string('issuername', 'badges'), $badge->issuername);
         if (isset($badge->issuercontact) && !empty($badge->issuercontact)) {
-            $datatable->data[] = array(get_string('contact', 'badges'),
-                html_writer::tag('a', $badge->issuercontact, array('href' => 'mailto:' . $badge->issuercontact)));
+            $datatable->data[] = array(get_string('contact', 'badges'), obfuscate_mailto($badge->issuercontact));
         }
         $datatable->data[] = array($this->output->heading(get_string('badgedetails', 'badges'), 3), '');
         $datatable->data[] = array(get_string('name'), $badge->name);
@@ -347,7 +358,7 @@ class core_badges_renderer extends plugin_renderer_base {
 
         // Print evidence.
         $agg = $badge->get_aggregation_methods();
-        $evidence = $badge->get_criteria_completions($ibadge->recipient);
+        $evidence = $badge->get_criteria_completions($userinfo->id);
         $eids = array_map(create_function('$o', 'return $o->critid;'), $evidence);
         unset($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]);
 
@@ -378,6 +389,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $issued = $ibadge->issued;
         $assertion = $issued->assertion;
         $issuer = $assertion->badge->issuer;
+        $userinfo = $ibadge->recipient;
         $table = new html_table();
 
         $imagetable = new html_table();
@@ -387,13 +399,29 @@ class core_badges_renderer extends plugin_renderer_base {
         $datatable = new html_table();
         $datatable->attributes = array('class' => 'badgeissuedinfo');
         $datatable->colclasses = array('bfield', 'bvalue');
+
+        // Recipient information.
+        $datatable->data[] = array($this->output->heading(get_string('recipientdetails', 'badges'), 3), '');
+        // Technically, we should alway have a user at this point, but added an extra check just in case.
+        if ($userinfo) {
+            $datatable->data[] = array(get_string('name'), fullname($userinfo));
+            if (!$ibadge->valid) {
+                $notify = $this->output->notification(get_string('recipientvalidationproblem', 'badges'), 'notifynotice');
+                $datatable->data[] = array(get_string('email'), obfuscate_mailto($userinfo->email) . $notify);
+            } else {
+                $datatable->data[] = array(get_string('email'), obfuscate_mailto($userinfo->email));
+            }
+        } else {
+            $notify = $this->output->notification(get_string('recipientidentificationproblem', 'badges'), 'notifynotice');
+            $datatable->data[] = array(get_string('name'), $notify);
+        }
+
         $datatable->data[] = array($this->output->heading(get_string('issuerdetails', 'badges'), 3), '');
         $datatable->data[] = array(get_string('issuername', 'badges'), $issuer->name);
         $datatable->data[] = array(get_string('issuerurl', 'badges'),
                 html_writer::tag('a', $issuer->origin, array('href' => $issuer->origin)));
         if (isset($issuer->contact)) {
-            $datatable->data[] = array(get_string('contact', 'badges'),
-                html_writer::tag('a', $issuer->contact, array('href' => 'mailto:' . $issuer->contact)));
+            $datatable->data[] = array(get_string('contact', 'badges'), obfuscate_mailto($issuer->contact));
         }
         $datatable->data[] = array($this->output->heading(get_string('badgedetails', 'badges'), 3), '');
         $datatable->data[] = array(get_string('name'), $assertion->badge->name);
@@ -875,7 +903,7 @@ class issued_badge implements renderable {
     public $issued;
 
     /** @var badge recipient */
-    public $recipient = 0;
+    public $recipient;
 
     /** @var badge visibility to others */
     public $visible = 0;
@@ -901,7 +929,12 @@ class issued_badge implements renderable {
                 WHERE ' . $DB->sql_compare_text('uniquehash', 40) . ' = ' . $DB->sql_compare_text(':hash', 40),
                 array('hash' => $hash), IGNORE_MISSING);
         if ($rec) {
-            $this->recipient = $rec->userid;
+            // Get a recipient from database.
+            $user = $DB->get_record_sql('SELECT u.id, u.lastname, u.firstname,
+                                                u.email AS accountemail, b.email AS backpackemail
+                        FROM {user} u LEFT JOIN {badge_backpack} b ON u.id = b.userid
+                        WHERE u.id = :userid', array('userid' => $rec->userid));
+            $this->recipient = $user;
             $this->visible = $rec->visible;
             $this->badgeid = $rec->badgeid;
         }
@@ -915,13 +948,51 @@ class external_badge implements renderable {
     /** @var issued badge */
     public $issued;
 
+    /** @var User ID */
+    public $recipient;
+
+    /** @var validation of external badge */
+    public $valid = true;
+
     /**
      * Initializes the badge to display
      *
-     * @param string $json External badge information.
+     * @param object $badge External badge information.
+     * @param int $recipient User id.
      */
-    public function __construct($json) {
-        $this->issued = $json;
+    public function __construct($badge, $recipient) {
+        global $DB;
+        // At this point a user has connected a backpack. So, we are going to get
+        // their backpack email rather than their account email.
+        $user = $DB->get_record_sql('SELECT u.lastname, u.firstname, b.email
+                    FROM {user} u INNER JOIN {badge_backpack} b ON u.id = b.userid
+                    WHERE userid = :userid', array('userid' => $recipient), IGNORE_MISSING);
+
+        $this->issued = $badge;
+        $this->recipient = $user;
+
+        // Check if recipient is valid.
+        // There is no way to be 100% sure that a badge belongs to a user.
+        // Backpack does not return any recipient information.
+        // All we can do is compare that backpack email hashed using salt
+        // provided in the assertion matches a badge recipient from the assertion.
+        if ($user) {
+            if (validate_email($badge->assertion->recipient) && $badge->assertion->recipient == $user->email) {
+                // If we have email, compare emails.
+                $this->valid = true;
+            } else if ($badge->assertion->recipient == 'sha256$' . hash('sha256', $user->email)) {
+                // If recipient is hashed, but no salt, compare hashes without salt.
+                $this->valid = true;
+            } else if ($badge->assertion->recipient == 'sha256$' . hash('sha256', $user->email . $badge->assertion->salt)) {
+                // If recipient is hashed, compare hashes.
+                $this->valid = true;
+            } else {
+                // Otherwise, we cannot be sure that this user is a recipient.
+                $this->valid = false;
+            }
+        } else {
+            $this->valid = false;
+        }
     }
 }
 
@@ -1016,7 +1087,7 @@ class badge_user_collection extends badge_collection implements renderable {
         parent::__construct($badges);
 
         if (!empty($CFG->badges_allowexternalbackpack)) {
-            $this->backpack = get_backpack_settings($userid);
+            $this->backpack = get_backpack_settings($userid, true);
         }
     }
 }
index fa7b7cb..a3b2878 100644 (file)
@@ -260,4 +260,23 @@ class core_badgeslib_testcase extends advanced_testcase {
         $this->assertDebuggingCalled('Error baking badge image!');
         $this->assertTrue($badge->is_issued($this->user->id));
     }
+
+    /**
+     * Test badges observer when user_updated event is fired.
+     */
+    public function test_badges_observer_profile_criteria_review() {
+        $badge = new badge($this->coursebadge);
+        $this->assertFalse($badge->is_issued($this->user->id));
+
+        $criteria_overall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
+        $criteria_overall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
+        $criteria_overall1 = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE, 'badgeid' => $badge->id));
+        $criteria_overall1->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ALL, 'field_address' => 'address'));
+
+        $this->user->address = 'Test address';
+        user_update_user($this->user, false);
+        // Check if badge is awarded.
+        $this->assertDebuggingCalled('Error baking badge image!');
+        $this->assertTrue($badge->is_issued($this->user->id));
+    }
 }
index 61232fd..26742db 100644 (file)
@@ -721,7 +721,7 @@ class core_course_external extends external_api {
                 if (array_key_exists('shortname', $course) && ($oldcourse->shortname != $course['shortname'])) {
                     require_capability('moodle/course:changeshortname', $context);
                     if ($DB->record_exists('course', array('shortname' => $course['shortname']))) {
-                        throw new moodle_exception('shortnametaken');
+                        throw new moodle_exception('shortnametaken', '', '', $course['shortname']);
                     }
                 }
 
@@ -729,7 +729,7 @@ class core_course_external extends external_api {
                 if (array_key_exists('idnumber', $course) && ($oldcourse->idnumber != $course['idnumber'])) {
                     require_capability('moodle/course:changeidnumber', $context);
                     if ($DB->record_exists('course', array('idnumber' => $course['idnumber']))) {
-                        throw new moodle_exception('idnumbertaken');
+                        throw new moodle_exception('courseidnumbertaken', '', '', $course['idnumber']);
                     }
                 }
 
index a0b5f5a..5cfa240 100644 (file)
@@ -2260,17 +2260,17 @@ function create_course($data, $editoroptions = NULL) {
     //check the categoryid - must be given for all new courses
     $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
 
-    //check if the shortname already exist
+    // Check if the shortname already exists.
     if (!empty($data->shortname)) {
         if ($DB->record_exists('course', array('shortname' => $data->shortname))) {
-            throw new moodle_exception('shortnametaken');
+            throw new moodle_exception('shortnametaken', '', '', $data->shortname);
         }
     }
 
-    //check if the id number already exist
+    // Check if the idnumber already exists.
     if (!empty($data->idnumber)) {
         if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) {
-            throw new moodle_exception('idnumbertaken');
+            throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
         }
     }
 
index 28837fc..4c42b9f 100644 (file)
@@ -602,6 +602,23 @@ class core_course_courselib_testcase extends advanced_testcase {
         // Ensure blocks have been associated to the course.
         $blockcount = $DB->count_records('block_instances', array('parentcontextid' => $context->id));
         $this->assertGreaterThan(0, $blockcount);
+
+        // Ensure that the shortname isn't duplicated.
+        try {
+            $created = create_course($course);
+            $this->fail('Exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertSame(get_string('shortnametaken', 'error', $course->shortname), $e->getMessage());
+        }
+
+        // Ensure that the idnumber isn't duplicated.
+        $course->shortname .= '1';
+        try {
+            $created = create_course($course);
+            $this->fail('Exception expected');
+        } catch (moodle_exception $e) {
+            $this->assertSame(get_string('courseidnumbertaken', 'error', $course->idnumber), $e->getMessage());
+        }
     }
 
     public function test_create_course_with_generator() {
index f6ec711..4d85032 100644 (file)
@@ -53,6 +53,7 @@ $string['course_summary_key'] = 'Summary';
 $string['createcourseextid'] = 'CREATE User enrolled to a nonexistant course \'{$a->courseextid}\'';
 $string['createnotcourseextid'] = 'User enrolled to a nonexistant course \'{$a->courseextid}\'';
 $string['creatingcourse'] =  'Creating course \'{$a}\'...';
+$string['duplicateshortname'] = "Course creation failed. Duplicate short name. Skipping course with idnumber '{\$a->idnumber}'...";
 $string['editlock'] = 'Lock value';
 $string['emptyenrolment'] = "Empty enrolment for role '{\$a->role_shortname}' in course '{\$a->course_shortname}'\n";
 $string['enrolname'] = 'LDAP';
index 9ebacb8..73eb118 100644 (file)
@@ -990,6 +990,12 @@ class enrol_ldap_plugin extends enrol_plugin {
             $course->summary = $course_ext[$this->get_config('course_summary')][0];
         }
 
+        // Check if the shortname already exists if it does - skip course creation.
+        if ($DB->record_exists('course', array('shortname' => $course->shortname))) {
+            $trace->output(get_string('duplicateshortname', 'enrol_ldap', $course));
+            return false;
+        }
+
         $newcourse = create_course($course);
         return $newcourse->id;
     }
index 5c3e345..a4f7a00 100644 (file)
@@ -257,7 +257,17 @@ class enrol_manual_editselectedusers_operation extends enrol_bulk_enrolment_oper
                 foreach ($user->enrolments as $enrolment) {
                     $enrolment->courseid  = $enrolment->enrolmentinstance->courseid;
                     $enrolment->enrol     = 'manual';
-                    events_trigger('user_enrol_modified', $enrolment);
+                    // Trigger event.
+                    $event = \core\event\user_enrolment_updated::create(
+                            array(
+                                'objectid' => $enrolment->id,
+                                'courseid' => $enrolment->courseid,
+                                'context' => context_course::instance($enrolment->courseid),
+                                'relateduserid' => $user->id,
+                                'other' => array('enrol' => 'manual')
+                                )
+                            );
+                    $event->trigger();
                 }
             }
             return true;
diff --git a/enrol/meta/classes/observer.php b/enrol/meta/classes/observer.php
new file mode 100644 (file)
index 0000000..625d232
--- /dev/null
@@ -0,0 +1,102 @@
+<?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/>.
+
+/**
+ * Event observer for meta enrolment plugin.
+ *
+ * @package    enrol_meta
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/enrol/meta/locallib.php');
+
+/**
+ * Event observer for enrol_meta.
+ *
+ * @package    enrol_meta
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_meta_observer extends enrol_meta_handler {
+
+    /**
+     * Triggered via user_enrolment_created event.
+     *
+     * @param \core\event\user_enrolment_created $event
+     * @return bool true on success.
+     */
+    public static function user_enrolment_created(\core\event\user_enrolment_created $event) {
+        if (!enrol_is_enabled('meta')) {
+            // No more enrolments for disabled plugins.
+            return true;
+        }
+
+        if ($event->other['enrol'] === 'meta') {
+            // Prevent circular dependencies - we can not sync meta enrolments recursively.
+            return true;
+        }
+
+        self::sync_course_instances($event->courseid, $event->relateduserid);
+        return true;
+    }
+
+    /**
+     * Triggered via user_enrolment_deleted event.
+     *
+     * @param \core\event\user_enrolment_deleted $event
+     * @return bool true on success.
+     */
+    public static function user_enrolment_deleted(\core\event\user_enrolment_deleted $event) {
+        if (!enrol_is_enabled('meta')) {
+            // This is slow, let enrol_meta_sync() deal with disabled plugin.
+            return true;
+        }
+
+        if ($event->other['enrol'] === 'meta') {
+            // Prevent circular dependencies - we can not sync meta enrolments recursively.
+            return true;
+        }
+
+        self::sync_course_instances($event->courseid, $event->relateduserid);
+
+        return true;
+    }
+
+    /**
+     * Triggered via user_enrolment_updated event.
+     *
+     * @param \core\event\user_enrolment_updated $event
+     * @return bool true on success
+     */
+    public static function user_enrolment_updated(\core\event\user_enrolment_updated $event) {
+        if (!enrol_is_enabled('meta')) {
+            // No modifications if plugin disabled.
+            return true;
+        }
+
+        if ($event->other['enrol'] === 'meta') {
+            // Prevent circular dependencies - we can not sync meta enrolments recursively.
+            return true;
+        }
+
+        self::sync_course_instances($event->courseid, $event->relateduserid);
+
+        return true;
+    }
+}
index c1a2d70..08c9625 100644 (file)
@@ -41,31 +41,27 @@ $handlers = array (
         'internal'         => 1,
     ),
 
-    'user_enrolled' => array (
+    'course_deleted' => array (
         'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'user_enrolled'),
+        'handlerfunction'  => array('enrol_meta_handler', 'course_deleted'),
         'schedule'         => 'instant',
         'internal'         => 1,
     ),
+);
 
-    'user_unenrolled' => array (
-        'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'user_unenrolled'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
-    ),
+// List of observers.
+$observers = array(
 
-    'user_enrol_modified' => array (
-        'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'user_enrol_modified'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array(
+        'eventname'   => '\core\event\user_enrolment_created',
+        'callback'    => 'enrol_meta_observer::user_enrolment_created',
     ),
-
-    'course_deleted' => array (
-        'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'course_deleted'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array(
+        'eventname'   => '\core\event\user_enrolment_deleted',
+        'callback'    => 'enrol_meta_observer::user_enrolment_deleted',
+    ),
+    array(
+        'eventname'   => '\core\event\user_enrolment_updated',
+        'callback'    => 'enrol_meta_observer::user_enrolment_updated',
     ),
 );
index 3a3c26b..c20bfaf 100644 (file)
@@ -293,72 +293,6 @@ class enrol_meta_handler {
         return true;
     }
 
-    /**
-     * Triggered via user enrolled event.
-     * @static
-     * @param stdClass $ue
-     * @return bool success
-     */
-    public static function user_enrolled($ue) {
-        if (!enrol_is_enabled('meta')) {
-            // no more enrolments for disabled plugins
-            return true;
-        }
-
-        if ($ue->enrol === 'meta') {
-            // prevent circular dependencies - we can not sync meta enrolments recursively
-            return true;
-        }
-
-        self::sync_course_instances($ue->courseid, $ue->userid);
-
-        return true;
-    }
-
-    /**
-     * Triggered via user unenrolled event.
-     * @static
-     * @param stdClass $ue
-     * @return bool success
-     */
-    public static function user_unenrolled($ue) {
-        if (!enrol_is_enabled('meta')) {
-            // This is slow, let enrol_meta_sync() deal with disabled plugin.
-            return true;
-        }
-
-        if ($ue->enrol === 'meta') {
-            // prevent circular dependencies - we can not sync meta enrolments recursively
-            return true;
-        }
-
-        self::sync_course_instances($ue->courseid, $ue->userid);
-
-        return true;
-    }
-
-    /**
-     * Triggered via user enrolment modification  event.
-     * @static
-     * @param stdClass $ue
-     * @return bool success
-     */
-    public static function user_enrol_modified($ue) {
-        if (!enrol_is_enabled('meta')) {
-            // no modifications if plugin disabled
-            return true;
-        }
-
-        if ($ue->enrol === 'meta') {
-            // prevent circular dependencies - we can not sync meta enrolments recursively
-            return true;
-        }
-
-        self::sync_course_instances($ue->courseid, $ue->userid);
-
-        return true;
-    }
-
     /**
      * Triggered via course_deleted event.
      * @static
index fdfec33..05cb522 100644 (file)
@@ -439,4 +439,112 @@ class enrol_meta_plugin_testcase extends advanced_testcase {
         delete_course($course4, false);
 
     }
+
+    /**
+     * Test user_enrolment_created event.
+     */
+    public function test_user_enrolment_created_observer() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $metaplugin = enrol_get_plugin('meta');
+        $user1 = $this->getDataGenerator()->create_user();
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $student = $DB->get_record('role', array('shortname' => 'student'));
+
+        $e1 = $metaplugin->add_instance($course2, array('customint1' => $course1->id));
+        $enrol1 = $DB->get_record('enrol', array('id' => $e1));
+
+        // Enrol user and capture event.
+        $sink = $this->redirectEvents();
+
+        $metaplugin->enrol_user($enrol1, $user1->id, $student->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_shift($events);
+
+        // Test Event.
+        $dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $user1->id));
+        $this->assertInstanceOf('\core\event\user_enrolment_created', $event);
+        $this->assertEquals($dbuserenrolled->id, $event->objectid);
+        $this->assertEquals('user_enrolled', $event->get_legacy_eventname());
+        $expectedlegacyeventdata = $dbuserenrolled;
+        $expectedlegacyeventdata->enrol = 'meta';
+        $expectedlegacyeventdata->courseid = $course2->id;
+        $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+    }
+
+    /**
+     * Test user_enrolment_deleted observer.
+     */
+    public function test_user_enrolment_deleted_observer() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $metalplugin = enrol_get_plugin('meta');
+        $user1 = $this->getDataGenerator()->create_user();
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $student = $DB->get_record('role', array('shortname'=>'student'));
+
+        $e1 = $metalplugin->add_instance($course2, array('customint1' => $course1->id));
+        $enrol1 = $DB->get_record('enrol', array('id' => $e1));
+
+        // Enrol user.
+        $metalplugin->enrol_user($enrol1, $user1->id, $student->id);
+        $this->assertEquals(1, $DB->count_records('user_enrolments'));
+
+        // Unenrol user and capture event.
+        $sink = $this->redirectEvents();
+        $metalplugin->unenrol_user($enrol1, $user1->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
+        $this->assertEquals(0, $DB->count_records('user_enrolments'));
+        $this->assertInstanceOf('\core\event\user_enrolment_deleted', $event);
+        $this->assertEquals('user_unenrolled', $event->get_legacy_eventname());
+    }
+
+    /**
+     * Test user_enrolment_updated event.
+     */
+    public function test_user_enrolment_updated_observer() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $metalplugin = enrol_get_plugin('meta');
+        $user1 = $this->getDataGenerator()->create_user();
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $student = $DB->get_record('role', array('shortname'=>'student'));
+
+        $e1 = $metalplugin->add_instance($course2, array('customint1' => $course1->id));
+        $enrol1 = $DB->get_record('enrol', array('id' => $e1));
+
+        // Enrol user.
+        $metalplugin->enrol_user($enrol1, $user1->id, $student->id);
+        $this->assertEquals(1, $DB->count_records('user_enrolments'));
+
+        // Updated enrolment for user and capture event.
+        $sink = $this->redirectEvents();
+        $metalplugin->update_user_enrol($enrol1, $user1->id, ENROL_USER_SUSPENDED, null, time());
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_shift($events);
+
+        // Test Event.
+        $dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $user1->id));
+        $this->assertInstanceOf('\core\event\user_enrolment_updated', $event);
+        $this->assertEquals($dbuserenrolled->id, $event->objectid);
+        $this->assertEquals('user_enrol_modified', $event->get_legacy_eventname());
+        $expectedlegacyeventdata = $dbuserenrolled;
+        $expectedlegacyeventdata->enrol = 'meta';
+        $expectedlegacyeventdata->courseid = $course2->id;
+        $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+    }
 }
index cba40a8..bbdc3bc 100644 (file)
@@ -279,4 +279,41 @@ class core_enrollib_testcase extends advanced_testcase {
         // It should be course 1.
         $this->assertEquals($sharedcourse->id, $course1->id);
     }
+
+    /**
+     * Test user enrolment created event.
+     */
+    public function test_user_enrolment_created_event() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+
+        $admin = get_admin();
+
+        $course1 = $this->getDataGenerator()->create_course();
+
+        $maninstance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+
+        $manual = enrol_get_plugin('manual');
+        $this->assertNotEmpty($manual);
+
+        // Enrol user and capture event.
+        $sink = $this->redirectEvents();
+        $manual->enrol_user($maninstance1, $admin->id, $studentrole->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_shift($events);
+
+        $dbuserenrolled = $DB->get_record('user_enrolments', array('userid' => $admin->id));
+        $this->assertInstanceOf('\core\event\user_enrolment_created', $event);
+        $this->assertEquals($dbuserenrolled->id, $event->objectid);
+        $this->assertEquals('user_enrolled', $event->get_legacy_eventname());
+        $expectedlegacyeventdata = $dbuserenrolled;
+        $expectedlegacyeventdata->enrol = $manual->get_name();
+        $expectedlegacyeventdata->courseid = $course1->id;
+        $this->assertEventLegacyData($expectedlegacyeventdata, $event);
+    }
 }
index 199d1eb..8ccb757 100644 (file)
@@ -201,9 +201,9 @@ class core_files_renderer extends plugin_renderer_base {
     <div class="fp-navbar">
         <div class="filemanager-toolbar">
             <div class="fp-toolbar">
-                <div class="{!}fp-btn-add"><a href="#"><img src="'.$this->pix_url('a/add_file').'" /> '.$straddfile.'</a></div>
-                <div class="{!}fp-btn-mkdir"><a href="#"><img src="'.$this->pix_url('a/create_folder').'" /> '.$strmakedir.'</a></div>
-                <div class="{!}fp-btn-download"><a href="#"><img src="'.$this->pix_url('a/download_all').'" /> '.$strdownload.'</a></div>
+                <div class="{!}fp-btn-add"><a role="button" href="#"><img src="'.$this->pix_url('a/add_file').'" /> '.$straddfile.'</a></div>
+                <div class="{!}fp-btn-mkdir"><a role="button" href="#"><img src="'.$this->pix_url('a/create_folder').'" /> '.$strmakedir.'</a></div>
+                <div class="{!}fp-btn-download"><a role="button" href="#"><img src="'.$this->pix_url('a/download_all').'" /> '.$strdownload.'</a></div>
             </div>
             <div class="{!}fp-viewbar">
                 <a title="'. get_string('displayicons', 'repository') .'" class="{!}fp-vb-icons" href="#"></a>
@@ -521,10 +521,10 @@ class core_files_renderer extends plugin_renderer_base {
      */
     private function fp_js_template_generallayout() {
         $rv = '
-<div class="file-picker fp-generallayout">
+<div tabindex="0" class="file-picker fp-generallayout" role="dialog" aria-live="assertive">
     <div class="fp-repo-area">
         <ul class="fp-list">
-            <li class="{!}fp-repo"><a href="#"><img class="{!}fp-repo-icon" alt="'. get_string('repositoryicon', 'repository') .'" width="16" height="16" />&nbsp;<span class="{!}fp-repo-name"></span></a></li>
+            <li class="{!}fp-repo"><a href="#"><img class="{!}fp-repo-icon" alt=" " width="16" height="16" />&nbsp;<span class="{!}fp-repo-name"></span></a></li>
         </ul>
     </div>
     <div class="fp-repo-items" tabindex="0">
index b7fa140..9bd6f21 100644 (file)
@@ -189,7 +189,7 @@ abstract class grade_export {
             $name .= ' ('.get_string('feedback').')';
         }
 
-        return strip_tags($name);
+        return html_to_text($name, 0, false);
     }
 
     /**
index a39aceb..6a5bcd0 100644 (file)
@@ -15,9 +15,9 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * External assign API
+ * External grading API
  *
- * @package    core_grade
+ * @package    core_grading
  * @since      Moodle 2.5
  * @copyright  2013 Paul Charsley
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@@ -29,16 +29,16 @@ require_once("$CFG->libdir/externallib.php");
 require_once("$CFG->dirroot/grade/grading/lib.php");
 
 /**
- * core grade functions
+ * core grading functions
  */
-class core_grade_external extends external_api {
+class core_grading_external extends external_api {
 
     /**
      * Describes the parameters for get_definitions
      * @return external_function_parameters
      * @since Moodle 2.5
      */
-    public static function get_definitions_parameters () {
+    public static function get_definitions_parameters() {
         return new external_function_parameters(
             array(
                 'cmids' => new external_multiple_structure(
@@ -57,7 +57,7 @@ class core_grade_external extends external_api {
      * @return array of areas with definitions for each requested course module id
      * @since Moodle 2.5
      */
-    public static function get_definitions ($cmids, $areaname, $activeonly = false) {
+    public static function get_definitions($cmids, $areaname, $activeonly = false) {
         global $DB, $CFG;
         require_once("$CFG->dirroot/grade/grading/form/lib.php");
         $params = self::validate_parameters(self::get_definitions_parameters(),
@@ -290,4 +290,179 @@ class core_grade_external extends external_api {
         return $methods;
     }
 
+    /**
+     * Describes the parameters for get_gradingform_instances
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.6
+     */
+    public static function get_gradingform_instances_parameters() {
+        return new external_function_parameters(
+            array(
+                'definitionid' => new external_value(PARAM_INT, 'definition id'),
+                'since' => new external_value(PARAM_INT, 'submitted since', VALUE_DEFAULT, 0)
+            )
+        );
+    }
+
+    /**
+     * Returns the instances and fillings for the requested definition id
+     *
+     * @param int $definitionid
+     * @param int $since only return instances with timemodified >= since
+     * @return array of grading instances with fillings for the definition id
+     * @since Moodle 2.6
+     */
+    public static function get_gradingform_instances($definitionid, $since = 0) {
+        global $DB, $CFG;
+        require_once("$CFG->dirroot/grade/grading/form/lib.php");
+        $params = self::validate_parameters(self::get_gradingform_instances_parameters(),
+                      array('definitionid' => $definitionid,
+                            'since' => $since));
+        $instances = array();
+        $warnings = array();
+
+        $definition = $DB->get_record('grading_definitions',
+                                      array('id' => $params['definitionid']),
+                                      'areaid,method', MUST_EXIST);
+        $area = $DB->get_record('grading_areas',
+                                 array('id' => $definition->areaid),
+                                 'contextid,component', MUST_EXIST);
+
+        $context = context::instance_by_id($area->contextid);
+        require_capability('moodle/grade:managegradingforms', $context);
+
+        $gradingmanager = get_grading_manager($definition->areaid);
+        $controller = $gradingmanager->get_controller($definition->method);
+        $activeinstances = $controller->get_all_active_instances ($params['since']);
+        $details = $controller->get_external_instance_filling_details();
+        if ($details == null) {
+            $warnings[] = array(
+                'item' => 'definition',
+                'itemid' => $params['definitionid'],
+                'message' => 'Fillings unavailable because get_external_instance_filling_details is not defined',
+                'warningcode' => '1'
+            );
+        }
+        $getfilling = null;
+        if (method_exists('gradingform_'.$definition->method.'_instance', 'get_'.$definition->method.'_filling')) {
+            $getfilling = 'get_'.$definition->method.'_filling';
+        } else {
+            $warnings[] = array(
+                'item' => 'definition',
+                'itemid' => $params['definitionid'],
+                'message' => 'Fillings unavailable because get_'.$definition->method.'_filling is not defined',
+                'warningcode' => '1'
+            );
+        }
+        foreach ($activeinstances as $activeinstance) {
+            $instance = array();
+            $instance['id'] = $activeinstance->get_id();
+            $instance['raterid'] = $activeinstance->get_data('raterid');
+            $instance['itemid'] = $activeinstance->get_data('itemid');
+            $instance['rawgrade'] = $activeinstance->get_data('rawgrade');
+            $instance['status'] = $activeinstance->get_data('status');
+            $instance['feedback'] = $activeinstance->get_data('feedback');
+            $instance['feedbackformat'] = $activeinstance->get_data('feedbackformat');
+            // Format the feedback text field.
+            $formattedtext = external_format_text($activeinstance->get_data('feedback'),
+                                                  $activeinstance->get_data('feedbackformat'),
+                                                  $context->id,
+                                                  $area->component,
+                                                  'feedback',
+                                                  $params['definitionid']);
+            $instance['feedback'] = $formattedtext[0];
+            $instance['feedbackformat'] = $formattedtext[1];
+            $instance['timemodified'] = $activeinstance->get_data('timemodified');
+
+            if ($details != null && $getfilling != null) {
+                $fillingdata = $activeinstance->$getfilling();
+                $filling = array();
+                foreach ($details as $key => $value) {
+                    $filling[$key] = self::format_text($fillingdata[$key],
+                                                       $context->id,
+                                                       $area->component,
+                                                       $params['definitionid']);
+                }
+                $instance[$definition->method] = $filling;
+            }
+            $instances[] = $instance;
+        }
+        $result = array(
+            'instances' => $instances,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Creates a grading instance
+     *
+     * @return external_single_structure
+     * @since  Moodle 2.6
+     */
+    private static function grading_instance() {
+        global $CFG;
+        $instance = array();
+        $instance['id']                = new external_value(PARAM_INT, 'instance id');
+        $instance['raterid']           = new external_value(PARAM_INT, 'rater id');
+        $instance['itemid']            = new external_value(PARAM_INT, 'item id');
+        $instance['rawgrade']          = new external_value(PARAM_TEXT, 'raw grade', VALUE_OPTIONAL);
+        $instance['status']            = new external_value(PARAM_INT, 'status');
+        $instance['feedback']          = new external_value(PARAM_RAW, 'feedback', VALUE_OPTIONAL);
+        $instance['feedbackformat']    = new external_format_value('feedback', VALUE_OPTIONAL);
+        $instance['timemodified']      = new external_value(PARAM_INT, 'modified time');
+        foreach (self::get_grading_methods() as $method) {
+            require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
+            $details  = call_user_func('gradingform_'.$method.'_controller::get_external_instance_filling_details');
+            if ($details != null) {
+                $items = array();
+                foreach ($details as $key => $value) {
+                    $details[$key]->required = VALUE_OPTIONAL;
+                    $items[$key] = $value;
+                }
+                $instance[$method] = new external_single_structure($items, 'items', VALUE_OPTIONAL);
+            }
+        }
+        return new external_single_structure($instance);
+    }
+
+    /**
+     * Describes the get_gradingform_instances return value
+     *
+     * @return external_single_structure
+     * @since Moodle 2.6
+     */
+    public static function get_gradingform_instances_returns() {
+        return new external_single_structure(
+            array(
+                'instances' => new external_multiple_structure(self::grading_instance(), 'list of grading instances'),
+                'warnings' => new external_warnings()
+            )
+        );
+    }
+
+}
+
+/**
+ * core grading functions. Renamed to core_grading_external
+ *
+ * @since Moodle 2.5
+ * @deprecated since 2.6 See MDL-30085. Please do not use this class any more.
+ * @see core_grading_external
+ */
+class core_grade_external extends external_api {
+
+    public static function get_definitions_parameters() {
+        return core_grading_external::get_definitions_parameters();
+    }
+
+    public static function get_definitions($cmids, $areaname, $activeonly = false) {
+        return core_grading_external::get_definitions($cmids, $areaname, $activeonly = false);
+    }
+
+    public static function get_definitions_returns() {
+        return core_grading_external::get_definitions_returns();
+    }
+
 }
index 942b19b..5c351fa 100644 (file)
@@ -683,6 +683,31 @@ class gradingform_guide_controller extends gradingform_controller {
         );
         return array('guide_criteria' => $guide_criteria, 'guide_comment' => $guide_comment);
     }
+
+    /**
+     * Returns an array that defines the structure of the guide's filling. This function is used by
+     * the web service function core_grading_external::get_gradingform_instances().
+     *
+     * @return An array containing a single key/value pair with the 'criteria' external_multiple_structure
+     * @see gradingform_controller::get_external_instance_filling_details()
+     * @since Moodle 2.6
+     */
+    public static function get_external_instance_filling_details() {
+        $criteria = new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_INT, 'filling id'),
+                    'criterionid' => new external_value(PARAM_INT, 'criterion id'),
+                    'levelid' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL),
+                    'remark' => new external_value(PARAM_RAW, 'remark', VALUE_OPTIONAL),
+                    'remarkformat' => new external_format_value('remark', VALUE_OPTIONAL),
+                    'score' => new external_value(PARAM_FLOAT, 'maximum score')
+                )
+            ), 'filling', VALUE_OPTIONAL
+        );
+        return array ('criteria' => $criteria);
+    }
+
 }
 
 /**
index 666d0d9..6e926d5 100644 (file)
@@ -431,6 +431,27 @@ abstract class gradingform_controller {
         return $rv;
     }
 
+    /**
+     * Returns an array of all active instances for this definition.
+     * (intentionally does not return instances with status NEEDUPDATE)
+     *
+     * @param int since only return instances with timemodified >= since
+     * @return array of gradingform_instance objects
+     */
+    public function get_all_active_instances($since = 0) {
+        global $DB;
+        $conditions = array ($this->definition->id,
+                             gradingform_instance::INSTANCE_STATUS_ACTIVE,
+                             $since);
+        $where = "definitionid = ? AND status = ? AND timemodified >= ?";
+        $records = $DB->get_records_select('grading_instances', $where, $conditions);
+        $rv = array();
+        foreach ($records as $record) {
+            $rv[] = $this->get_instance($record);
+        }
+        return $rv;
+    }
+
     /**
      * Returns true if there are already people who has been graded on this definition.
      * In this case plugins may restrict changes of the grading definition
@@ -680,6 +701,24 @@ abstract class gradingform_controller {
     public static function get_external_definition_details() {
         return null;
     }
+
+    /**
+     * Overridden by sub classes that wish to make instance filling details available to web services.
+     * When not overridden, only instance filling data common to all grading methods is made available.
+     * When overriding, the return value should be an array containing one or more key/value pairs.
+     * These key/value pairs should match the filling data returned by the get_<method>_filling() function
+     * in the gradingform_instance subclass.
+     * For examples, look at:
+     *    $gradingform_rubric_controller->get_external_instance_filling_details()
+     *    $gradingform_guide_controller->get_external_instance_filling_details()
+     *
+     * @return array An array of one or more key/value pairs containing the external_multiple_structure/s
+     * corresponding to the definition returned by $gradingform_<method>_instance->get_<method>_filling()
+     * @since Moodle 2.6
+     */
+    public static function get_external_instance_filling_details() {
+        return null;
+    }
 }
 
 /**
@@ -976,4 +1015,4 @@ abstract class gradingform_instance {
     public function default_validation_error_message() {
         return '';
     }
-}
\ No newline at end of file
+}
index 39e6adc..1cf9b44 100644 (file)
@@ -688,6 +688,29 @@ class gradingform_rubric_controller extends gradingform_controller {
         return array('rubric_criteria' => $rubric_criteria);
     }
 
+    /**
+     * Returns an array that defines the structure of the rubric's filling. This function is used by
+     * the web service function core_grading_external::get_gradingform_instances().
+     *
+     * @return An array containing a single key/value pair with the 'criteria' external_multiple_structure
+     * @see gradingform_controller::get_external_instance_filling_details()
+     * @since Moodle 2.6
+     */
+    public static function get_external_instance_filling_details() {
+        $criteria = new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_INT, 'filling id'),
+                    'criterionid' => new external_value(PARAM_INT, 'criterion id'),
+                    'levelid' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL),
+                    'remark' => new external_value(PARAM_RAW, 'remark', VALUE_OPTIONAL),
+                    'remarkformat' => new external_format_value('remark', VALUE_OPTIONAL)
+                )
+            ), 'filling', VALUE_OPTIONAL
+        );
+        return array ('criteria' => $criteria);
+    }
+
 }
 
 /**
index defbe49..3230236 100644 (file)
@@ -21,14 +21,14 @@ global $CFG;
 require_once($CFG->dirroot . '/webservice/tests/helpers.php');
 
 /**
- * External core grade functions unit tests
+ * Unit tests for the grading API at /grade/externallib.php
  *
- * @package core_grade
+ * @package core_grading
  * @category external
  * @copyright 2013 Paul Charsley
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_grade_externallib_testcase extends externallib_advanced_testcase {
+class core_grading_externallib_testcase extends externallib_advanced_testcase {
 
     /**
      * Tests set up
@@ -41,7 +41,7 @@ class core_grade_externallib_testcase extends externallib_advanced_testcase {
     /**
      * Test get_definitions
      */
-    public function test_get_definitions () {
+    public function test_get_definitions() {
         global $DB, $CFG, $USER;
 
         $this->resetAfterTest(true);
@@ -58,10 +58,10 @@ class core_grade_externallib_testcase extends externallib_advanced_testcase {
         $cm = self::getDataGenerator()->create_module('assign', $assigndata);
 
         // Create manual enrolment record.
-        $manual_enrol_data['enrol'] = 'manual';
-        $manual_enrol_data['status'] = 0;
-        $manual_enrol_data['courseid'] = $course->id;
-        $enrolid = $DB->insert_record('enrol', $manual_enrol_data);
+        $manualenroldata['enrol'] = 'manual';
+        $manualenroldata['status'] = 0;
+        $manualenroldata['courseid'] = $course->id;
+        $enrolid = $DB->insert_record('enrol', $manualenroldata);
 
         // Create a teacher and give them capabilities.
         $coursecontext = context_course::instance($course->id);
@@ -70,10 +70,10 @@ class core_grade_externallib_testcase extends externallib_advanced_testcase {
         $this->assignUserCapability('mod/assign:grade', $modulecontext->id, $roleid);
 
         // Create the teacher's enrolment record.
-        $user_enrolment_data['status'] = 0;
-        $user_enrolment_data['enrolid'] = $enrolid;
-        $user_enrolment_data['userid'] = $USER->id;
-        $DB->insert_record('user_enrolments', $user_enrolment_data);
+        $userenrolmentdata['status'] = 0;
+        $userenrolmentdata['enrolid'] = $enrolid;
+        $userenrolmentdata['userid'] = $USER->id;
+        $DB->insert_record('user_enrolments', $userenrolmentdata);
 
         // Create a grading area.
         $gradingarea = array(
@@ -148,7 +148,7 @@ class core_grade_externallib_testcase extends externallib_advanced_testcase {
         // Call the external function.
         $cmids = array ($cm->id);
         $areaname = 'submissions';
-        $result = core_grade_external::get_definitions($cmids, $areaname);
+        $result = core_grading_external::get_definitions($cmids, $areaname);
 
         $this->assertEquals(1, count($result['areas']));
         $this->assertEquals(1, count($result['areas'][0]['definitions']));
@@ -181,4 +181,129 @@ class core_grade_externallib_testcase extends externallib_advanced_testcase {
         $this->assertTrue($found);
     }
 
+    /**
+     * Test get_gradingform_instances
+     */
+    public function test_get_gradingform_instances() {
+        global $DB, $USER;
+
+        $this->resetAfterTest(true);
+        // Create a course and assignment.
+        $coursedata['idnumber'] = 'idnumbercourse';
+        $coursedata['fullname'] = 'Lightwork Course';
+        $coursedata['summary'] = 'Lightwork Course description';
+        $coursedata['summaryformat'] = FORMAT_MOODLE;
+        $course = self::getDataGenerator()->create_course($coursedata);
+
+        $assigndata['course'] = $course->id;
+        $assigndata['name'] = 'lightwork assignment';
+
+        $assign = self::getDataGenerator()->create_module('assign', $assigndata);
+
+        // Create manual enrolment record.
+        $manualenroldata['enrol'] = 'manual';
+        $manualenroldata['status'] = 0;
+        $manualenroldata['courseid'] = $course->id;
+        $enrolid = $DB->insert_record('enrol', $manualenroldata);
+
+        // Create a teacher and give them capabilities.
+        $coursecontext = context_course::instance($course->id);
+        $roleid = $this->assignUserCapability('moodle/course:viewparticipants', $coursecontext->id, 3);
+        $modulecontext = context_module::instance($assign->id);
+        $this->assignUserCapability('mod/assign:grade', $modulecontext->id, $roleid);
+
+        // Create the teacher's enrolment record.
+        $userenrolmentdata['status'] = 0;
+        $userenrolmentdata['enrolid'] = $enrolid;
+        $userenrolmentdata['userid'] = $USER->id;
+        $DB->insert_record('user_enrolments', $userenrolmentdata);
+
+        // Create a student with an assignment grade.
+        $student = self::getDataGenerator()->create_user();
+        $assigngrade = new stdClass();
+        $assigngrade->assignment = $assign->id;
+        $assigngrade->userid = $student->id;
+        $assigngrade->timecreated = time();
+        $assigngrade->timemodified = $assigngrade->timecreated;
+        $assigngrade->grader = $USER->id;
+        $assigngrade->grade = 50;
+        $assigngrade->attemptnumber = 0;
+        $gid = $DB->insert_record('assign_grades', $assigngrade);
+
+        // Create a grading area.
+        $gradingarea = array(
+            'contextid' => $modulecontext->id,
+            'component' => 'mod_assign',
+            'areaname' => 'submissions',
+            'activemethod' => 'rubric'
+        );
+        $areaid = $DB->insert_record('grading_areas', $gradingarea);
+
+        // Create a rubric grading definition.
+        $rubricdefinition = array (
+            'areaid' => $areaid,
+            'method' => 'rubric',
+            'name' => 'test',
+            'status' => 20,
+            'copiedfromid' => 1,
+            'timecreated' => 1,
+            'usercreated' => $USER->id,
+            'timemodified' => 1,
+            'usermodified' => $USER->id,
+            'timecopied' => 0
+        );
+        $definitionid = $DB->insert_record('grading_definitions', $rubricdefinition);
+
+        // Create a criterion with a level.
+        $rubriccriteria = array (
+            'definitionid' => $definitionid,
+            'sortorder' => 1,
+            'description' => 'Demonstrate an understanding of disease control',
+            'descriptionformat' => 0
+        );
+        $criterionid = $DB->insert_record('gradingform_rubric_criteria', $rubriccriteria);
+        $rubriclevel = array (
+            'criterionid' => $criterionid,
+            'score' => 50,
+            'definition' => 'pass',
+            'definitionformat' => 0
+        );
+        $levelid = $DB->insert_record('gradingform_rubric_levels', $rubriclevel);
+
+        // Create a grading instance.
+        $instance = array (
+            'definitionid' => $definitionid,
+            'raterid' => $USER->id,
+            'itemid' => $gid,
+            'status' => 1,
+            'feedbackformat' => 0,
+            'timemodified' => 1
+        );
+        $instanceid = $DB->insert_record('grading_instances', $instance);
+
+        // Create a filling.
+        $filling = array (
+            'instanceid' => $instanceid,
+            'criterionid' => $criterionid,
+            'levelid' => $levelid,
+            'remark' => 'excellent work',
+            'remarkformat' => 0
+        );
+        $DB->insert_record('gradingform_rubric_fillings', $filling);
+
+        // Call the external function.
+        $result = core_grading_external::get_gradingform_instances($definitionid, 0);
+
+        $this->assertEquals(1, count($result['instances']));
+        $this->assertEquals($USER->id, $result['instances'][0]['raterid']);
+        $this->assertEquals($gid, $result['instances'][0]['itemid']);
+        $this->assertEquals(1, $result['instances'][0]['status']);
+        $this->assertEquals(1, $result['instances'][0]['timemodified']);
+        $this->assertEquals(1, count($result['instances'][0]['rubric']));
+        $this->assertEquals(1, count($result['instances'][0]['rubric']['criteria']));
+        $criteria = $result['instances'][0]['rubric']['criteria'];
+        $this->assertEquals($criterionid, $criteria[$criterionid]['criterionid']);
+        $this->assertEquals($levelid, $criteria[$criterionid]['levelid']);
+        $this->assertEquals('excellent work', $criteria[$criterionid]['remark']);
+    }
 }
index 9e5b147..09cc675 100644 (file)
@@ -101,7 +101,9 @@ $string['badgeprivacysetting_help'] = 'Badges you earn can be displayed on your
 You can still control individual badge privacy settings on your "My badges" page.';
 $string['badgeprivacysetting_str'] = 'Automatically show badges I earn on my profile page';
 $string['badgesalt'] = 'Salt for hashing the recepient\'s email address';
-$string['badgesalt_desc'] = 'Using a hash allows backpack services to confirm the badge earner without having to expose their email address. This setting should only use numbers and letters.';
+$string['badgesalt_desc'] = 'Using a hash allows backpack services to confirm the badge earner without having to expose their email address. This setting should only use numbers and letters.
+
+Note: For recipient verification purposes, please avoid changing this setting once you start issuing badges.';
 $string['badgesdisabled'] = 'Badges are not enabled on this site.';
 $string['badgesearned'] = 'Number of badges earned: {$a}';
 $string['badgesettings'] = 'Badges settings';
@@ -203,6 +205,7 @@ $string['error:cannotact'] = 'Cannot activate the badge. ';
 $string['error:cannotawardbadge'] = 'Cannot award badge to a user.';
 $string['error:clone'] = 'Cannot clone the badge.';
 $string['error:duplicatename'] = 'Badge with such name already exists in the system.';
+$string['error:externalbadgedoesntexist'] = 'Badge not found';
 $string['error:invalidbadgeurl'] = 'Invalid badge issuer URL format.';
 $string['error:invalidcriteriatype'] = 'Invalid criteria type.';
 $string['error:invalidexpiredate'] = 'Expiry date has to be in the future.';
@@ -305,6 +308,9 @@ $string['numawardstat'] = 'This badge has been issued {$a} user(s).';
 $string['overallcrit'] = 'of the selected criteria are complete.';
 $string['potentialrecipients'] = 'Potential badge recipients';
 $string['recipients'] = 'Badge recipients';
+$string['recipientdetails'] = 'Recipient details';
+$string['recipientidentificationproblem'] = 'Cannot find a recipient of this badge among the existing users.';
+$string['recipientvalidationproblem'] = 'Current user cannot be verified as a recipient of this badge.';
 $string['relative'] = 'Relative date';
 $string['requiredcourse'] = 'At least one course should be added to the courseset criterion.';
 $string['reviewbadge'] = 'Review badge criteria';
index a60b503..47403f2 100644 (file)
@@ -44,6 +44,7 @@ $string['cachedef_coursecontacts'] = 'List of course contacts';
 $string['cachedef_coursecattree'] = 'Course categories tree';
 $string['cachedef_databasemeta'] = 'Database meta information';
 $string['cachedef_eventinvalidation'] = 'Event invalidation';
+$string['cachedef_externalbadges'] = 'External badges for particular user';
 $string['cachedef_groupdata'] = 'Course group information';
 $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content';
 $string['cachedef_langmenu'] = 'List of available languages';
index 2beaab1..50c6573 100644 (file)
@@ -69,6 +69,9 @@ $string['errorenrolcohort'] = 'Error creating cohort sync enrolment instance in
 $string['errorenrolcohortusers'] = 'Error enrolling cohort members in this course.';
 $string['errorthresholdlow'] = 'Notification threshold must be at least 1 day.';
 $string['errorwithbulkoperation'] = 'There was an error while processing your bulk enrolment change.';
+$string['eventuserenrolmentcreated'] = 'User enrolled in course';
+$string['eventuserenrolmentdeleted'] = 'User unenrolled from course';
+$string['eventuserenrolmentupdated'] = 'User unenrolment updated';
 $string['expirynotify'] = 'Notify before enrolment expires';
 $string['expirynotify_help'] = 'This setting determines whether enrolment expiry notification messages are sent.';
 $string['expirynotifyall'] = 'Enroller and enrolled user';
index a2508b4..2d15d24 100644 (file)
@@ -177,6 +177,7 @@ $string['coursedoesnotbelongtocategory'] = 'The course doesn\'t belong to this c
 $string['courseformatnotfound'] = 'The course format \'{$a}\' doesn\'t exist or is not recognized';
 $string['coursegroupunknown'] = 'Course corresponding to group {$a} not specified';
 $string['courseidnotfound'] = 'Course id doesn\'t exist';
+$string['courseidnumbertaken'] = 'ID number is already used for another course ({$a})';
 $string['coursemisconf'] = 'Course is misconfigured';
 $string['courserequestdisabled'] = 'Sorry, but course requests have been disabled by the administrator.';
 $string['csvcolumnduplicates'] = 'Duplicate columns detected';
@@ -267,7 +268,7 @@ $string['guestsarenotallowed'] = 'The guest user is not allowed to do this';
 $string['hackdetected'] = 'Hack attack detected!';
 $string['hashpoolproblem'] = 'Incorrect pool file content {$a}.';
 $string['headersent'] = 'Headers already sent';
-$string['idnumbertaken'] = 'ID number is already used for another course';
+$string['idnumbertaken'] = 'This ID number is already in use';
 $string['idnumbertoolong'] = 'ID number is too long';
 $string['importformatnotimplement'] = 'Sorry, importing this format is not yet implemented!';
 $string['incorrectext'] = 'File has an incorrect extension';
@@ -469,7 +470,7 @@ $string['sessionerroruser'] = 'Your session has timed out.  Please login again.'
 $string['sessionerroruser2'] = 'A server error that affects your login session was detected. Please login again or restart your browser.';
 $string['sessionipnomatch'] = 'Sorry, but your IP number seems to have changed from when you first logged in.  This security feature prevents crackers stealing your identity while logged in to this site.  Normal users should not be seeing this message - please ask the site administrator for help.';
 $string['sessionipnomatch2'] = 'Sorry, but your IP number seems to have changed from when you first logged in.  This security feature prevents crackers stealing your identity while logged in to this site. You may see this error if you use wireless networks or if you are roaming between different networks. Please ask the site administrator for more help.<br /><br />If you want to continue please press F5 key to refresh this page.';
-$string['shortnametaken'] = 'Short name is already used for another course';
+$string['shortnametaken'] = 'Short name is already used for another course ({$a})';
 $string['scheduledbackupsdisabled'] = 'Scheduled backups have been disabled by the server admin';
 $string['socksnotsupported'] = 'SOCKS5 proxy is not supported in PHP4';
 $string['spellcheckernotconf'] = 'Spellchecker not configured';
index 10c69f5..ac711e3 100644 (file)
@@ -667,6 +667,10 @@ $string['eventcoursedeleted'] = 'Course deleted';
 $string['eventcourserestored'] = 'Course restored';
 $string['eventcourseupdated'] = 'Course updated';
 $string['eventcoursesectionupdated'] = ' Course section updated';
+$string['eventusercreated'] = 'User created';
+$string['eventuserdeleted'] = 'User deleted';
+$string['eventuserloggedout'] = 'User logged out';
+$string['eventuserupdated'] = 'User updated';
 $string['everybody'] = 'Everybody';
 $string['executeat'] = 'Execute at';
 $string['existing'] = 'Existing';
index 386588a..7d7bb69 100644 (file)
@@ -5763,19 +5763,41 @@ class context_helper extends context {
     /**
      * @var array An array mapping context levels to classes
      */
-    private static $alllevels = array(
+    private static $alllevels;
+
+    /**
+     * Instance does not make sense here, only static use
+     */
+    protected function __construct() {
+    }
+
+    /**
+     * Initialise context levels, call before using self::$alllevels.
+     */
+    private static function init_levels() {
+        global $CFG;
+
+        if (isset(self::$alllevels)) {
+            return;
+        }
+        self::$alllevels = array(
             CONTEXT_SYSTEM    => 'context_system',
             CONTEXT_USER      => 'context_user',
             CONTEXT_COURSECAT => 'context_coursecat',
             CONTEXT_COURSE    => 'context_course',
             CONTEXT_MODULE    => 'context_module',
             CONTEXT_BLOCK     => 'context_block',
-    );
+        );
 
-    /**
-     * Instance does not make sense here, only static use
-     */
-    protected function __construct() {
+        if (empty($CFG->custom_context_classes)) {
+            return;
+        }
+
+        // Unsupported custom levels, use with care!!!
+        foreach ($CFG->custom_context_classes as $level => $classname) {
+            self::$alllevels[$level] = $classname;
+        }
+        ksort(self::$alllevels);
     }
 
     /**
@@ -5786,6 +5808,7 @@ class context_helper extends context {
      * @return string class name of the context class
      */
     public static function get_class_for_level($contextlevel) {
+        self::init_levels();
         if (isset(self::$alllevels[$contextlevel])) {
             return self::$alllevels[$contextlevel];
         } else {
@@ -5800,6 +5823,7 @@ class context_helper extends context {
      * @return array int=>string (level=>level class name)
      */
     public static function get_all_levels() {
+        self::init_levels();
         return self::$alllevels;
     }
 
@@ -5812,6 +5836,8 @@ class context_helper extends context {
      */
     public static function cleanup_instances() {
         global $DB;
+        self::init_levels();
+
         $sqls = array();
         foreach (self::$alllevels as $level=>$classname) {
             $sqls[] = $classname::get_cleanup_sql();
@@ -5841,6 +5867,7 @@ class context_helper extends context {
      * @return void
      */
     public static function create_instances($contextlevel = null, $buildpaths = true) {
+        self::init_levels();
         foreach (self::$alllevels as $level=>$classname) {
             if ($contextlevel and $level > $contextlevel) {
                 // skip potential sub-contexts
@@ -5861,6 +5888,7 @@ class context_helper extends context {
      * @return void
      */
     public static function build_all_paths($force = false) {
+        self::init_levels();
         foreach (self::$alllevels as $classname) {
             $classname::build_paths($force);
         }
index 4901456..061ecd3 100644 (file)
@@ -931,40 +931,6 @@ function badges_add_course_navigation(navigation_node $coursenode, stdClass $cou
     }
 }
 
-/**
- * Triggered when 'user_updated' event happens.
- *
- * @param   object $eventdata Holds all information about a user.
- * @return  boolean
- */
-function badges_award_handle_profile_criteria_review(stdClass $eventdata) {
-    global $DB, $CFG;
-
-    if (!empty($CFG->enablebadges)) {
-        $userid = $eventdata->id;
-
-        if ($rs = $DB->get_records('badge_criteria', array('criteriatype' => BADGE_CRITERIA_TYPE_PROFILE))) {
-            foreach ($rs as $r) {
-                $badge = new badge($r->badgeid);
-                if (!$badge->is_active() || $badge->is_issued($userid)) {
-                    continue;
-                }
-
-                if ($badge->criteria[BADGE_CRITERIA_TYPE_PROFILE]->review($userid)) {
-                    $badge->criteria[BADGE_CRITERIA_TYPE_PROFILE]->mark_complete($userid);
-
-                    if ($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($userid)) {
-                        $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($userid);
-                        $badge->issue($userid);
-                    }
-                }
-            }
-        }
-    }
-
-    return true;
-}
-
 /**
  * Triggered when badge is manually awarded.
  *
@@ -1086,13 +1052,25 @@ function badges_bake($hash, $badgeid, $userid = 0, $pathhash = false) {
 /**
  * Returns external backpack settings and badges from this backpack.
  *
+ * This function first checks if badges for the user are cached and
+ * tries to retrieve them from the cache. Otherwise, badges are obtained
+ * through curl request to the backpack.
+ *
  * @param int $userid Backpack user ID.
+ * @param boolean $refresh Refresh badges collection in cache.
  * @return null|object Returns null is there is no backpack or object with backpack settings.
  */
-function get_backpack_settings($userid) {
+function get_backpack_settings($userid, $refresh = false) {
     global $DB;
     require_once(dirname(dirname(__FILE__)) . '/badges/lib/backpacklib.php');
 
+    // Try to get badges from cache first.
+    $badgescache = cache::make('core', 'externalbadges');
+    $out = $badgescache->get($userid);
+    if ($out !== false && !$refresh) {
+        return $out;
+    }
+    // Get badges through curl request to the backpack.
     $record = $DB->get_record('badge_backpack', array('userid' => $userid));
     if ($record) {
         $backpack = new OpenBadgesBackpackHandler($record);
@@ -1117,6 +1095,7 @@ function get_backpack_settings($userid) {
             $out->totalcollections = 0;
         }
 
+        $badgescache->set($userid, $out);
         return $out;
     }
 
index 2ac98dd..64727eb 100644 (file)
@@ -58,8 +58,8 @@ class behat_util extends testing_util {
      * @return void
      */
     public static function install_site() {
-        global $DB;
-
+        global $DB, $CFG;
+        require_once($CFG->dirroot.'/user/lib.php');
         if (!defined('BEHAT_UTIL')) {
             throw new coding_exception('This method can be only used by Behat CLI tool');
         }
@@ -82,7 +82,7 @@ class behat_util extends testing_util {
         $user->lastname = 'User';
         $user->city = 'Perth';
         $user->country = 'AU';
-        $DB->update_record('user', $user);
+        user_update_user($user, false);
 
         // Disable email message processor.
         $DB->set_field('message_processors', 'enabled', '0', array('name' => 'email'));
index f14b892..9738cd9 100644 (file)
@@ -998,7 +998,7 @@ class block_manager {
      *
      * @param string $region The name of the region to check
      */
-    protected function ensure_content_created($region, $output) {
+    public function ensure_content_created($region, $output) {
         $this->ensure_instances_exist($region);
         if (!array_key_exists($region, $this->visibleblockcontent)) {
             $contents = array();
diff --git a/lib/classes/event/user_created.php b/lib/classes/event/user_created.php
new file mode 100644 (file)
index 0000000..892efd5
--- /dev/null
@@ -0,0 +1,99 @@
+<?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 created event.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when new user profile is created.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_created extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'user';
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventusercreated');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'Profile created for user '.$this->objectid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/user/view.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Return name of the legacy event, which is replaced by this event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'user_created';
+    }
+
+    /**
+     * Return user_created legacy event data.
+     *
+     * @return \stdClass user data.
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('user', $this->objectid);
+    }
+
+    /**
+     * Returns array of parameters to be passed to legacy add_to_log() function.
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'user', 'add', '/view.php?id='.$this->objectid, fullname($this->get_legacy_eventdata()));
+    }
+}
diff --git a/lib/classes/event/user_deleted.php b/lib/classes/event/user_deleted.php
new file mode 100644 (file)
index 0000000..545d691
--- /dev/null
@@ -0,0 +1,105 @@
+<?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 deleted event.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when user profile is deleted.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_deleted extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'user';
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserdeleted');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $user = (object)$this->other['user'];
+        return 'User profile deleted for user '.$user->firstname.' '.$user->lastname.' id ('.$user->id.')';
+    }
+
+    /**
+     * Return name of the legacy event, which is replaced by this event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'user_deleted';
+    }
+
+    /**
+     * Return user_deleted legacy event data.
+     *
+     * @return \stdClass user data.
+     */
+    protected function get_legacy_eventdata() {
+        return (object)$this->other['user'];
+    }
+
+    /**
+     * Returns array of parameters to be passed to legacy add_to_log() function.
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        $user = (object)$this->other['user'];
+        return array(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['user'])) {
+            throw new \coding_exception('user must be set in $other.');
+        }
+    }
+}
diff --git a/lib/classes/event/user_enrolment_created.php b/lib/classes/event/user_enrolment_created.php
new file mode 100644 (file)
index 0000000..d45ca6d
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * User enrolment created event.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when user is enrolled in a course.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_enrolment_created extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'user_enrolments';
+        $this->data['crud'] = 'c';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserenrolmentcreated', 'core_enrol');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'User '.$this->relateduserid. ' is enrolled in course '.$this->courseid.' by user '.$this->userid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/enrol/users.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Return name of the legacy event, which is replaced by this event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'user_enrolled';
+    }
+
+    /**
+     * Return user_enrolled legacy event data.
+     *
+     * @return \stdClass
+     */
+    protected function get_legacy_eventdata() {
+        $legacyeventdata = $this->get_record_snapshot('user_enrolments', $this->objectid);
+        $legacyeventdata->enrol = $this->other['enrol'];
+        $legacyeventdata->courseid = $this->courseid;
+        return $legacyeventdata;
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['enrol'])) {
+            throw new \coding_exception('Enrolment plugin name must be set in $other.');
+        }
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('Related user id must be set.');
+        }
+    }
+}
diff --git a/lib/classes/event/user_enrolment_deleted.php b/lib/classes/event/user_enrolment_deleted.php
new file mode 100644 (file)
index 0000000..82e523f
--- /dev/null
@@ -0,0 +1,110 @@
+<?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 enrolment deleted event.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when user is unenrolled from a course.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_enrolment_deleted extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'user_enrolments';
+        $this->data['crud'] = 'd';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserenrolmentdeleted', 'core_enrol');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'User '.$this->relateduserid. ' is enrolled in course '.$this->courseid.' by user '.$this->userid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/enrol/users.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Return name of the legacy event, which is replaced by this event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'user_unenrolled';
+    }
+
+    /**
+     * Return user_unenrolled legacy event data.
+     *
+     * @return \stdClass
+     */
+    protected function get_legacy_eventdata() {
+        return (object)$this->other['userenrolment'];
+    }
+
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['userenrolment'])) {
+            throw new \coding_exception('User enrolment must be set in $other.');
+        }
+        if (!isset($this->other['enrol'])) {
+            throw new \coding_exception('Enrolment plugin name must be set in $other.');
+        }
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('Related user id must be set.');
+        }
+    }
+}
diff --git a/lib/classes/event/user_enrolment_updated.php b/lib/classes/event/user_enrolment_updated.php
new file mode 100644 (file)
index 0000000..786f519
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * User enrolment updated event.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when user enrolment is updated.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_enrolment_updated extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'user_enrolments';
+        $this->data['crud'] = 'u';
+        $this->data['level'] = self::LEVEL_TEACHING;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserenrolmentupdated', 'core_enrol');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'User '.$this->relateduserid. ' has updated enrolment for user '.$this->userid.' in course '.$this->courseid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/enrol/users.php', array('id' => $this->courseid));
+    }
+
+    /**
+     * Return name of the legacy event, which is replaced by this event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'user_enrol_modified';
+    }
+
+    /**
+     * Return user_enrol_modified legacy event data.
+     *
+     * @return \stdClass
+     */
+    protected function get_legacy_eventdata() {
+        $legacyeventdata = $this->get_record_snapshot('user_enrolments', $this->objectid);
+        $legacyeventdata->enrol = $this->other['enrol'];
+        $legacyeventdata->courseid = $this->courseid;
+        return $legacyeventdata;
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+        if (!isset($this->other['enrol'])) {
+            throw new \coding_exception('Enrolment plugin name must be set in $other.');
+        }
+        if (!isset($this->relateduserid)) {
+            throw new \coding_exception('Related user id must be set.');
+        }
+    }
+}
diff --git a/lib/classes/event/user_loggedout.php b/lib/classes/event/user_loggedout.php
new file mode 100644 (file)
index 0000000..f881b47
--- /dev/null
@@ -0,0 +1,100 @@
+<?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 logout event.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when user logout.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_loggedout extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'user';
+        $this->data['crud'] = 'r';
+        $this->data['level'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserloggedout');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'User '.$this->objectid.' logged out.';
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/user/view.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Return name of the legacy event, which is replaced by this event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'user_logout';
+    }
+
+    /**
+     * Return user_logout legacy event data.
+     *
+     * @return \stdClass user data.
+     */
+    protected function get_legacy_eventdata() {
+        return $this->get_record_snapshot('user', $this->objectid);
+    }
+
+    /**
+     * Returns array of parameters to be passed to legacy add_to_log() function.
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'user', 'logout', 'view.php?id='.$this->objectid.'&course='.SITEID, $this->objectid, 0,
+            $this->objectid);
+    }
+}
diff --git a/lib/classes/event/user_updated.php b/lib/classes/event/user_updated.php
new file mode 100644 (file)
index 0000000..005ee40
--- /dev/null
@@ -0,0 +1,99 @@
+<?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 updated event.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event when user profile is updated.
+ *
+ * @package    core
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_updated extends base {
+
+    /**
+     * Initialise required event data properties.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'user';
+        $this->data['crud'] = 'u';
+        $this->data['level'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventuserupdated');
+    }
+
+    /**
+     * Returns non-localised event description with id's for admin use only.
+     *
+     * @return string
+     */
+    public function get_description() {
+        return 'User profile updated for userid '.$this->objectid;
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        return new \moodle_url('/user/view.php', array('id' => $this->objectid));
+    }
+
+    /**
+     * Return name of the legacy event, which is replaced by this event.
+     *
+     * @return string legacy event name
+     */
+    public static function get_legacy_eventname() {
+        return 'user_updated';
+    }
+
+    /**
+     * Return user_updated legacy event data.
+     *
+     * @return \stdClass user data.
+     */
+    protected function get_legacy_eventdata () {
+        return $this->get_record_snapshot('user', $this->objectid);
+    }
+
+    /**
+     * Returns array of parameters to be passed to legacy add_to_log() function.
+     *
+     * @return array
+     */
+    protected function get_legacy_logdata() {
+        return array(SITEID, 'user', 'update', 'view.php?id='.$this->objectid, '');
+    }
+}
index 031ac61..8192704 100644 (file)
@@ -95,6 +95,9 @@ function min_fix_utf8($value) {
         error_reporting($olderror ^ E_NOTICE);
     }
 
+    // No null bytes expected in our data, so let's remove it.
+    $value = str_replace("\0", '', $value);
+
     static $buggyiconv = null;
     if ($buggyiconv === null) {
         $buggyiconv = (!function_exists('iconv') or iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
index 9f36f54..d03f1c3 100644 (file)
@@ -233,4 +233,10 @@ $definitions = array(
         'mode' => cache_store::MODE_REQUEST,
         'persistent' => true,
     ),
+    // Used to store external badges.
+    'externalbadges' => array(
+        'mode' => cache_store::MODE_APPLICATION,
+        'simplekeys' => true,
+        'ttl' => 3600,
+    ),
 );
index b3ba54a..99e5f32 100644 (file)
@@ -36,14 +36,6 @@ defined('MOODLE_INTERNAL') || die();
 /* List of legacy event handlers */
 
 $handlers = array(
-
-    'user_updated' => array (
-        'handlerfile'      => '/lib/badgeslib.php',
-        'handlerfunction'  => 'badges_award_handle_profile_criteria_review',
-        'schedule'         => 'instant',
-        'internal'         => 1,
-    ),
-
 /*
  * portfolio queued event - for non interactive file transfers
  * NOTE: this is a HACK, please do not add any more things like this here
@@ -70,6 +62,10 @@ $observers = array(
     array(
         'eventname'   => '\core\event\course_completed',
         'callback'    => 'core_badges_observer::course_criteria_review',
+    ),
+    array(
+        'eventname'   => '\core\event\user_updated',
+        'callback'    => 'core_badges_observer::profile_criteria_review',
     )
 
 );
index 311c0dd..9781298 100644 (file)
@@ -782,13 +782,29 @@ $functions = array(
         'capabilities'=> 'moodle/notes:manage',
     ),
 
-    // === grade related functions ===
+    // === grading related functions ===
+
+    'core_grading_get_definitions' => array(
+        'classname'   => 'core_grading_external',
+        'methodname'  => 'get_definitions',
+        'classpath'   => 'grade/externallib.php',
+        'description' => 'Get grading definitions',
+        'type'        => 'read'
+    ),
 
     'core_grade_get_definitions' => array(
         'classname'   => 'core_grade_external',
         'methodname'  => 'get_definitions',
         'classpath'   => 'grade/externallib.php',
-        'description' => 'Get grading definitions',
+        'description' => 'DEPRECATED: this deprecated function will be removed in a future version. This function has been renamed as core_grading_get_definitions()',
+        'type'        => 'read'
+    ),
+
+    'core_grading_get_gradingform_instances' => array(
+        'classname'   => 'core_grading_external',
+        'methodname'  => 'get_gradingform_instances',
+        'classpath'   => 'grade/externallib.php',
+        'description' => 'Get grading form instances',
         'type'        => 'read'
     ),
 
index 4000e8e..b4def7a 100644 (file)
@@ -641,6 +641,7 @@ class mssql_native_moodle_database extends moodle_database {
 
             } else {
                 $param = str_replace("'", "''", $param);
+                $param = str_replace("\0", "", $param);
                 $return .= "N'$param'";
             }
 
index 5761c7b..b939e9f 100644 (file)
@@ -719,6 +719,7 @@ class sqlsrv_native_moodle_database extends moodle_database {
                 $return .= $param;
             } else {
                 $param = str_replace("'", "''", $param);
+                $param = str_replace("\0", "", $param);
                 $return .= "N'$param'";
             }
 
index 7ee56f4..b86da9d 100644 (file)
@@ -68,6 +68,13 @@ M.editor_tinymce.init_editor = function(Y, editorid, options) {
                 ed.contentDocument.addEventListener('keydown', function() {
                     ed.contentWindow.focus();
                 });
+
+                // Whenever a touch event is registered against the content document,
+                // reapply focus. This works around an issue with the location caret not
+                // being focusable without use of the Loupe.
+                ed.contentDocument.addEventListener('touchend', function() {
+                    ed.contentWindow.focus();
+                });
             });
         };
     }
index a9741f9..1793749 100644 (file)
@@ -1276,16 +1276,7 @@ abstract class enrol_plugin {
         if ($ue = $DB->get_record('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
             //only update if timestart or timeend or status are different.
             if ($ue->timestart != $timestart or $ue->timeend != $timeend or (!is_null($status) and $ue->status != $status)) {
-                $ue->timestart    = $timestart;
-                $ue->timeend      = $timeend;
-                if (!is_null($status)) {
-                    $ue->status   = $status;
-                }
-                $ue->modifierid   = $USER->id;
-                $ue->timemodified = time();
-                $DB->update_record('user_enrolments', $ue);
-
-                $updated = true;
+                $this->update_user_enrol($instance, $userid, $status, $timestart, $timeend);
             }
         } else {
             $ue = new stdClass();
@@ -1303,16 +1294,17 @@ abstract class enrol_plugin {
         }
 
         if ($inserted) {
-            // add extra info and trigger event
-            $ue->courseid  = $courseid;
-            $ue->enrol     = $name;
-            events_trigger('user_enrolled', $ue);
-        } else if ($updated) {
-            $ue->courseid  = $courseid;
-            $ue->enrol     = $name;
-            events_trigger('user_enrol_modified', $ue);
-            // resets current enrolment caches
-            $context->mark_dirty();
+            // Trigger event.
+            $event = \core\event\user_enrolment_created::create(
+                    array(
+                        'objectid' => $ue->id,
+                        'courseid' => $courseid,
+                        'context' => $context,
+                        'relateduserid' => $ue->userid,
+                        'other' => array('enrol' => $name)
+                        )
+                    );
+            $event->trigger();
         }
 
         if ($roleid) {
@@ -1389,10 +1381,17 @@ abstract class enrol_plugin {
         $DB->update_record('user_enrolments', $ue);
         context_course::instance($instance->courseid)->mark_dirty(); // reset enrol caches
 
-        // trigger event
-        $ue->courseid  = $instance->courseid;
-        $ue->enrol     = $instance->name;
-        events_trigger('user_enrol_modified', $ue);
+        // Trigger event.
+        $event = \core\event\user_enrolment_updated::create(
+                array(
+                    'objectid' => $ue->id,
+                    'courseid' => $instance->courseid,
+                    'context' => context_course::instance($instance->courseid),
+                    'relateduserid' => $ue->userid,
+                    'other' => array('enrol' => $name)
+                    )
+                );
+        $event->trigger();
     }
 
     /**
@@ -1440,8 +1439,6 @@ abstract class enrol_plugin {
                  WHERE ue.userid = :userid AND e.courseid = :courseid";
         if ($DB->record_exists_sql($sql, array('userid'=>$userid, 'courseid'=>$courseid))) {
             $ue->lastenrol = false;
-            events_trigger('user_unenrolled', $ue);
-            // user still has some enrolments, no big cleanup yet
 
         } else {
             // the big cleanup IS necessary!
@@ -1458,9 +1455,20 @@ abstract class enrol_plugin {
             $DB->delete_records('user_lastaccess', array('userid'=>$userid, 'courseid'=>$courseid));
 
             $ue->lastenrol = true; // means user not enrolled any more
-            events_trigger('user_unenrolled', $ue);
         }
-
+        // Trigger event.
+        $event = \core\event\user_enrolment_deleted::create(
+                array(
+                    'courseid' => $courseid,
+                    'context' => $context,
+                    'relateduserid' => $ue->userid,
+                    'other' => array(
+                        'userenrolment' => (array)$ue,
+                        'enrol' => $name
+                        )
+                    )
+                );
+        $event->trigger();
         // reset all enrol caches
         $context->mark_dirty();
 
index ad04ace..a0fae10 100644 (file)
@@ -1220,6 +1220,8 @@ function fix_utf8($value) {
             // Shortcut.
             return $value;
         }
+        // No null bytes expected in our data, so let's remove it.
+        $value = str_replace("\0", '', $value);
 
         // Lower error reporting because glibc throws bogus notices.
         $olderror = error_reporting();
@@ -3213,11 +3215,7 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
 function require_logout() {
     global $USER;
 
-    $params = $USER;
-
     if (isloggedin()) {
-        add_to_log(SITEID, "user", "logout", "view.php?id=$USER->id&course=".SITEID, $USER->id, 0, $USER->id);
-
         $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
         foreach ($authsequence as $authname) {
             $authplugin = get_auth_plugin($authname);
@@ -3225,9 +3223,16 @@ function require_logout() {
         }
     }
 
-    events_trigger('user_logout', $params);
+    $event = \core\event\user_loggedout::create(
+            array(
+                'objectid' => $USER->id,
+                'context' => context_user::instance($USER->id)
+                )
+            );
+    $event->trigger();
+
     session_get_instance()->terminate_current();
-    unset($params);
+    unset($GLOBALS['USER']);
 }
 
 /**
@@ -3440,7 +3445,9 @@ function get_user_key($script, $userid, $instance=null, $iprestriction=null, $va
  * @return bool Always returns true
  */
 function update_user_login_times() {
-    global $USER, $DB;
+    global $USER, $DB, $CFG;
+
+    require_once($CFG->dirroot.'/user/lib.php');
 
     if (isguestuser()) {
         // Do not update guest access times/ips for performance.
@@ -3466,7 +3473,7 @@ function update_user_login_times() {
     $USER->lastaccess = $user->lastaccess = $now;
     $USER->lastip = $user->lastip = getremoteaddr();
 
-    $DB->update_record('user', $user);
+    user_update_user($user, false);
     return true;
 }
 
@@ -3965,7 +3972,9 @@ function get_user_fieldnames() {
  */
 function create_user_record($username, $password, $auth = 'manual') {
     global $CFG, $DB;
-    require_once($CFG->dirroot."/user/profile/lib.php");
+    require_once($CFG->dirroot.'/user/profile/lib.php');
+    require_once($CFG->dirroot.'/user/lib.php');
+
     // Just in case check text case.
     $username = trim(core_text::strtolower($username));
 
@@ -4006,7 +4015,7 @@ function create_user_record($username, $password, $auth = 'manual') {
     $newuser->timemodified = $newuser->timecreated;
     $newuser->mnethostid = $CFG->mnet_localhost_id;
 
-    $newuser->id = $DB->insert_record('user', $newuser);
+    $newuser->id = user_create_user($newuser, false);
 
     // Save user profile data.
     profile_save_data($newuser);
@@ -4018,10 +4027,6 @@ function create_user_record($username, $password, $auth = 'manual') {
     // Set the password.
     update_internal_user_password($user, $password);
 
-    // Fetch full user record for the event, the complete user data contains too much info
-    // and we want to be consistent with other places that trigger this event.
-    events_trigger('user_created', $DB->get_record('user', array('id' => $user->id)));
-
     return $user;
 }
 
@@ -4034,6 +4039,7 @@ function create_user_record($username, $password, $auth = 'manual') {
 function update_user_record($username) {
     global $DB, $CFG;
     require_once($CFG->dirroot."/user/profile/lib.php");
+    require_once($CFG->dirroot.'/user/lib.php');
     // Just in case check text case.
     $username = trim(core_text::strtolower($username));
 
@@ -4076,14 +4082,10 @@ function update_user_record($username) {
         if ($newuser) {
             $newuser['id'] = $oldinfo->id;
             $newuser['timemodified'] = time();
-            $DB->update_record('user', $newuser);
+            user_update_user((object) $newuser, false);
 
             // Save user profile data.
             profile_save_data((object) $newuser);
-
-            // Fetch full user record for the event, the complete user data contains too much info
-            // and we want to be consistent with other places that trigger this event.
-            events_trigger('user_updated', $DB->get_record('user', array('id' => $oldinfo->id)));
         }
     }
 
@@ -4141,6 +4143,7 @@ function delete_user(stdClass $user) {
     require_once($CFG->libdir.'/gradelib.php');
     require_once($CFG->dirroot.'/message/lib.php');
     require_once($CFG->dirroot.'/tag/lib.php');
+    require_once($CFG->dirroot.'/user/lib.php');
 
     // Make sure nobody sends bogus record type as parameter.
     if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
@@ -4167,6 +4170,9 @@ function delete_user(stdClass $user) {
         return false;
     }
 
+    // Keep a copy of user context, we need it for event.
+    $usercontext = context_user::instance($user->id);
+
     // Delete all grades - backup is kept in grade_grades_history table.
     grade_user_delete($user->id);
 
@@ -4216,9 +4222,6 @@ function delete_user(stdClass $user) {
     // Force logout - may fail if file based sessions used, sorry.
     session_kill_user($user->id);
 
-    // Now do a final accesslib cleanup - removes all role assignments in user context and context itself.
-    context_helper::delete_instance(CONTEXT_USER, $user->id);
-
     // Workaround for bulk deletes of users with the same email address.
     $delname = "$user->email.".time();
     while ($DB->record_exists('user', array('username' => $delname))) { // No need to use mnethostid here.
@@ -4235,9 +4238,22 @@ function delete_user(stdClass $user) {
     $updateuser->picture      = 0;
     $updateuser->timemodified = time();
 
-    $DB->update_record('user', $updateuser);
-    // Add this action to log.
-    add_to_log(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
+    user_update_user($updateuser, false);
+
+    // Now do a final accesslib cleanup - removes all role assignments in user context and context itself.
+    context_helper::delete_instance(CONTEXT_USER, $user->id);
+
+    // Any plugin that needs to cleanup should register this event.
+    // Trigger event.
+    $event = \core\event\user_deleted::create(
+            array(
+                'objectid' => $user->id,
+                'context' => $usercontext,
+                'other' => array('user' => (array)clone $user)
+                )
+            );
+    $event->add_record_snapshot('user', $updateuser);
+    $event->trigger();
 
     // We will update the user's timemodified, as it will be passed to the user_deleted event, which
     // should know about this updated property persisted to the user's table.
@@ -4247,9 +4263,6 @@ function delete_user(stdClass $user) {
     $authplugin = get_auth_plugin($user->auth);
     $authplugin->user_delete($user);
 
-    // Any plugin that needs to cleanup should register this event.
-    events_trigger('user_deleted', $user);
-
     return true;
 }
 
index 5b6af5c..03f7d22 100644 (file)
@@ -3101,6 +3101,18 @@ class action_menu implements renderable {
      */
     public $attributessecondary = array();
 
+    /**
+     * The string to use next to the icon for the action icon relating to the secondary (dropdown) menu.
+     * @var array
+     */
+    public $actiontext = null;
+
+    /**
+     * An icon to use for the toggling the secondary menu (dropdown).
+     * @var pix_icon
+     */
+    public $actionicon;
+
     /**
      * Constructs the action menu with the given items.
      *
@@ -3128,6 +3140,12 @@ class action_menu implements renderable {
             'aria-labelledby' => 'action-menu-toggle-'.$this->instance,
             'role' => 'menu'
         );
+        $this->actionicon = new pix_icon(
+            't/contextmenu',
+            new lang_string('actions', 'moodle'),
+            'moodle',
+            array('class' => 'iconsmall', 'title' => '')
+        );
         $this->set_alignment(self::TR, self::BR);
         foreach ($actions as $action) {
             $this->add($action);
@@ -3208,14 +3226,18 @@ class action_menu implements renderable {
         if ($output === null) {
             $output = $OUTPUT;
         }
-        $title = get_string('actions', 'moodle');
-        $pixicon = $output->pix_icon(
-            't/contextmenu',
-            $title,
-            'moodle',
-            array('class' => 'iconsmall', 'title' => '')
-        );
-
+        $pixicon = $this->actionicon;
+        $title = new lang_string('actions', 'moodle');
+        if ($pixicon instanceof renderable) {
+            $pixicon = $output->render($pixicon);
+            if ($pixicon instanceof pix_icon && isset($pixicon->attributes['alt'])) {
+                $title = $pixicon->attributes['alt'];
+            }
+        }
+        $string = '';
+        if ($this->actiontext) {
+            $string = $this->actiontext;
+        }
         $actions = $this->primaryactions;
         $attributes = array(
             'class' => 'toggle-display',
@@ -3223,7 +3245,7 @@ class action_menu implements renderable {
             'id' => 'action-menu-toggle-'.$this->instance,
             'role' => 'menuitem'
         );
-        $actions[] = html_writer::link('#', $pixicon, $attributes);
+        $actions[] = html_writer::link('#', $string.$pixicon, $attributes);
         return $actions;
     }
 
@@ -3344,7 +3366,7 @@ class action_menu_link extends action_link implements renderable {
      * @param bool $primary Whether this is a primary action or not.
      * @param array $attributes Any attribtues associated with the action.
      */
-    public function __construct(moodle_url $url, pix_icon $icon, $text, $primary = true, array $attributes = array()) {
+    public function __construct(moodle_url $url, pix_icon $icon = null, $text, $primary = true, array $attributes = array()) {
         parent::__construct($url, $text, null, $attributes, $icon);
         $this->primary = (bool)$primary;
         $this->add_class('menu-action');
@@ -3369,7 +3391,7 @@ class action_menu_link_primary extends action_menu_link {
      * @param string $text
      * @param array $attributes
      */
-    public function __construct(moodle_url $url, pix_icon $icon, $text, array $attributes = array()) {
+    public function __construct(moodle_url $url, pix_icon $icon = null, $text, array $attributes = array()) {
         parent::__construct($url, $icon, $text, true, $attributes);
     }
 }
@@ -3391,7 +3413,7 @@ class action_menu_link_secondary extends action_menu_link {
      * @param string $text
      * @param array $attributes
      */
-    public function __construct(moodle_url $url, pix_icon $icon, $text, array $attributes = array()) {
+    public function __construct(moodle_url $url, pix_icon $icon = null, $text, array $attributes = array()) {
         parent::__construct($url, $icon, $text, false, $attributes);
     }
 }
index e133403..77dbfb8 100644 (file)
@@ -345,6 +345,16 @@ class core_renderer extends renderer_base {
      */
     public function standard_head_html() {
         global $CFG, $SESSION;
+
+        // Before we output any content, we need to ensure that certain
+        // page components are set up.
+
+        // Blocks must be set up early as they may require javascript which
+        // has to be included in the page header before output is created.
+        foreach ($this->page->blocks->get_regions() as $region) {
+            $this->page->blocks->ensure_content_created($region, $this);
+        }
+
         $output = '';
         $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
         $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
index 310fc0c..ea46e3a 100644 (file)
@@ -44,7 +44,7 @@ class core_configonlylib_testcase extends advanced_testcase {
      */
     public function test_min_fix_utf8() {
         $this->assertSame('abc', min_fix_utf8('abc'));
-        $this->assertSame("žlutý koníček přeskočil potůček \n\t\r\0", min_fix_utf8("žlutý koníček přeskočil potůček \n\t\r\0"));
+        $this->assertSame("žlutý koníček přeskočil potůček \n\t\r", min_fix_utf8("žlutý koníček přeskočil potůček \n\t\r\0"));
         $this->assertSame('aš', min_fix_utf8('a'.chr(130).'š'), 'This fails with buggy iconv() when mbstring extenstion is not available as fallback.');
     }
 
index 1c9fe7e..2a70acc 100644 (file)
@@ -169,8 +169,8 @@ class core_moodlelib_testcase extends advanced_testcase {
         $object->b = 'bb';
         $this->assertEquals($object, fix_utf8($object));
 
-        // Valid utf8 string.
-        $this->assertSame("žlutý koníček přeskočil potůček \n\t\r\0", fix_utf8("žlutý koníček přeskočil potůček \n\t\r\0"));
+        // valid utf8 string
+        $this->assertSame("žlutý koníček přeskočil potůček \n\t\r", fix_utf8("žlutý koníček přeskočil potůček \n\t\r\0"));
 
         // Invalid utf8 string.
         $this->assertSame('aš', fix_utf8('a'.chr(130).'š'), 'This fails with buggy iconv() when mbstring extenstion is not available as fallback.');
@@ -1851,7 +1851,14 @@ class core_moodlelib_testcase extends advanced_testcase {
         $user = $this->getDataGenerator()->create_user(array('idnumber'=>'abc'));
         $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'xyz'));
 
+        // Delete user and capture event.
+        $sink = $this->redirectEvents();
         $result = delete_user($user);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
+        // Test user is deleted in DB.
         $this->assertTrue($result);
         $deluser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
         $this->assertEquals(1, $deluser->deleted);
@@ -1862,8 +1869,15 @@ class core_moodlelib_testcase extends advanced_testcase {
 
         $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
 
-        // Try invalid params.
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_deleted', $event);
+        $this->assertSame($user->id, $event->objectid);
+        $this->assertSame('user_deleted', $event->get_legacy_eventname());
+        $this->assertEventLegacyData($user, $event);
+        $expectedlogdata = array(SITEID, 'user', 'delete', "view.php?id=$user->id", $user->firstname.' '.$user->lastname);
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
 
+        // Try invalid params.
         $record = new stdClass();
         $record->grrr = 1;
         try {
@@ -2407,4 +2421,35 @@ class core_moodlelib_testcase extends advanced_testcase {
         $expectedarray = array('19' => 'second', '38' => 'first', '44' => 'firsthalf');
         $this->assertEquals($expectedarray, order_in_string($valuearray, $formatstring));
     }
+
+    /**
+     * Test require_logout.
+     */
+    public function test_require_logout() {
+        $this->resetAfterTest();
+        $user = $this->getDataGenerator()->create_user();
+        $this->setUser($user);
+        $course = $this->getDataGenerator()->create_course();
+
+        $this->assertTrue(isloggedin());
+
+        // Logout user and capture event.
+        $sink = $this->redirectEvents();
+        require_logout();
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
+        // Check if user is logged out.
+        $this->assertFalse(isloggedin());
+
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_loggedout', $event);
+        $this->assertSame($user->id, $event->objectid);
+        $this->assertSame('user_logout', $event->get_legacy_eventname());
+        $this->assertEventLegacyData($user, $event);
+        $expectedlogdata = array(SITEID, 'user', 'logout', 'view.php?id='.$event->objectid.'&course='.SITEID, $event->objectid, 0,
+            $event->objectid);
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+    }
 }
index 9581c3e..74479fe 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js differ
index 17126d6..692eebf 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js differ
index 9581c3e..74479fe 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js and b/lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js differ
index bc2a5e7..6624784 100644 (file)
@@ -7,16 +7,12 @@
 
 var DIALOGUE_NAME = 'Moodle dialogue',
     DIALOGUE,
-    DIALOGUE_FULLSCREEN_CLASS,
-    DIALOGUE_HIDDEN_CLASS,
-    EXISTING_WINDOW_SELECTOR,
-    NOSCROLLING_CLASS;
-
-DIALOGUE_MODAL_CLASS = 'yui3-widget-modal';
-DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX+'-fullscreen';
-DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX+'-hidden';
-EXISTING_WINDOW_SELECTOR = '[role=dialog]';
-NOSCROLLING_CLASS = 'no-scrolling';
+    DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen',
+    DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden',
+    DIALOGUE_MODAL_CLASS = 'yui3-widget-modal',
+    DIALOGUE_SELECTOR =' [role=dialog]',
+    MENUBAR_SELECTOR = '[role=menubar]',
+    NOSCROLLING_CLASS = 'no-scrolling';
 
 /**
  * A re-usable dialogue box with Moodle classes applied.
@@ -126,39 +122,42 @@ Y.extend(DIALOGUE, Y.Panel, {
      */
     applyZIndex : function() {
         var highestzindex = 0,
-            zindex,
-            bb;
-
-        bb = this.get('boundingBox');
-        if (this.get('zIndex')) {
+            bb = this.get('boundingBox'),
+            zindex = this.get('zIndex');
+        if (zindex) {
             // The zindex was specified so we should use that.
-            bb.setStyle('zIndex', this.get('zIndex'));
+            bb.setStyle('zIndex', zindex);
         } else {
-            // Determine the correct zindex by looking at all existing dialogs in the page.
-            // Get the zindex of the parent of each wrapper node.
-            Y.all(EXISTING_WINDOW_SELECTOR).each(function (node) {
-                zindex = node.getStyle('zIndex');
-
-                // In most cases the zindex is set on the parent of the dialog.
-                if (!zindex) {
-                    zindex = node.get('parentNode').getStyle('zIndex');
-                }
-
-                if (zindex) {
-                    zindex = parseInt(zindex, 10);
-
-                    if (zindex > highestzindex) {
-                        highestzindex = zindex;
-                    }
+            // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
+            Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR).each(function (node) {
+                var zindex = this.findZIndex(node);
+                if (zindex > highestzindex) {
+                    highestzindex = zindex;
                 }
-            });
+            }, this);
             // Only set the zindex if we found a wrapper.
             if (highestzindex > 0) {
-                bb.setStyle('zIndex', highestzindex + 1);
+                bb.setStyle('zIndex', (highestzindex + 1).toString());
             }
         }
     },
 
+    /**
+     * Finds the zIndex of the given node or its parent.
+     *
+     * @method findZIndex
+     * @param Node node
+     * @returns int Return either the zIndex of 0 if one was not found.
+     */
+    findZIndex : function(node) {
+        // In most cases the zindex is set on the parent of the dialog.
+        var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
+        if (zindex) {
+            return parseInt(zindex, 10);
+        }
+        return 0;
+    },
+
     /**
      * Enable or disable document scrolling (see if there are any modal or fullscreen popups).
      *
@@ -303,6 +302,26 @@ Y.extend(DIALOGUE, Y.Panel, {
     shouldResizeFullscreen : function() {
         return (window === window.parent) && this.get('responsive') &&
                Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
+    },
+
+    /**
+     * Override the show method to set keyboard focus on the dialogue.
+     *
+     * @method show
+     * @return void
+     */
+    show : function() {
+        var result = null,
+            header = this.headerNode,
+            content = this.bodyNode;
+
+        result = DIALOGUE.superclass.show.call(this);
+        if (header && header !== '') {
+            header.focus();
+        } else if (content && content !== '') {
+            content.focus();
+        }
+        return result;
     }
 }, {
     NAME : DIALOGUE_NAME,
index 3af9fab..63fd2e5 100644 (file)
@@ -100,6 +100,10 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
             $data->cutoffdate = $this->apply_date_offset($data->cutoffdate);
         }
 
+        if ($data->grade < 0) { // Scale found, get mapping.
+            $data->grade = -($this->get_mappingid('scale', abs($data->grade)));
+        }
+
         $newitemid = $DB->insert_record('assign', $data);
 
         $this->apply_activity_instance($newitemid);
index 51274f4..210994d 100644 (file)
@@ -3914,9 +3914,10 @@ class assign {
                 $team = groups_get_members($submission->groupid, 'u.id');
 
                 foreach ($team as $member) {
-                    $submission->groupid = 0;
-                    $submission->userid = $member->id;
-                    $this->gradebook_item_update($submission, null);
+                    $membersubmission = clone $submission;
+                    $membersubmission->groupid = 0;
+                    $membersubmission->userid = $member->id;
+                    $this->gradebook_item_update($membersubmission, null);
                 }
                 return;
             }
index 7812c26..742b679 100644 (file)
@@ -792,6 +792,16 @@ if ($showactivity) {
         $records = array();
     }
 
+    if ($mode != 'single' && $canmanageentries) {
+        echo html_writer::empty_tag('input', array('type' => 'button', 'id' => 'checkall', 'value' => get_string('selectall')));
+        echo html_writer::empty_tag('input', array('type' => 'button', 'id' => 'checknone', 'value' => get_string('deselectall')));
+        echo html_writer::empty_tag('input', array('class' => 'form-submit', 'type' => 'submit', 'value' => get_string('deleteselected')));
+
+        $module = array('name'=>'mod_data', 'fullpath'=>'/mod/data/module.js');
+        $PAGE->requires->js_init_call('M.mod_data.init_view', null, false, $module);
+    }
+    echo html_writer::end_tag('form');
+
     if ($mode == '' && !empty($CFG->enableportfolios) && !empty($records)) {
         require_once($CFG->libdir . '/portfoliolib.php');
         $button = new portfolio_add_button();
@@ -802,20 +812,8 @@ if ($showactivity) {
         echo $button->to_html(PORTFOLIO_ADD_FULL_FORM);
     }
 
-
     //Advanced search form doesn't make sense for single (redirects list view)
     if (($maxcount || $mode == 'asearch') && $mode != 'single') {
-        if ($canmanageentries) {
-            echo html_writer::start_tag('div', array('class' => 'form-buttons'));
-            echo html_writer::empty_tag('input', array('type' => 'button', 'id' => 'checkall', 'value' => get_string('selectall')));
-            echo html_writer::empty_tag('input', array('type' => 'button', 'id' => 'checknone', 'value' => get_string('deselectall')));
-            echo html_writer::empty_tag('input', array('class' => 'form-submit', 'type' => 'submit', 'value' => get_string('deleteselected')));
-            echo html_writer::end_tag('div');
-
-            $module = array('name'=>'mod_data', 'fullpath'=>'/mod/data/module.js');
-            $PAGE->requires->js_init_call('M.mod_data.init_view', null, false, $module);
-        }
-        echo html_writer::end_tag('form');
         data_print_preference_form($data, $perpage, $search, $sort, $order, $search_array, $advanced, $mode);
     }
 }
diff --git a/mod/forum/classes/observer.php b/mod/forum/classes/observer.php
new file mode 100644 (file)
index 0000000..a41efff
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Event observers used in forum.
+ *
+ * @package    mod_forum
+ * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Event observer for mod_forum.
+ */
+class mod_forum_observer {
+    /**
+     * Triggered via user_enrolment_deleted event.
+     *
+     * @param \core\event\user_enrolment_deleted $event
+     */
+    public static function user_enrolment_deleted(\core\event\user_enrolment_deleted $event) {
+        global $DB;
+
+        // NOTE: this has to be as fast as possible.
+        // Get user enrolment info from event.
+        $cp = (object)$event->other['userenrolment'];
+        if ($cp->lastenrol) {
+            $params = array('userid' => $cp->userid, 'courseid' => $cp->courseid);
+            $forumselect = "IN (SELECT f.id FROM {forum} f WHERE f.course = :courseid)";
+
+            $DB->delete_records_select('forum_digests', 'userid = :userid AND forum '.$forumselect, $params);
+            $DB->delete_records_select('forum_subscriptions', 'userid = :userid AND forum '.$forumselect, $params);
+            $DB->delete_records_select('forum_track_prefs', 'userid = :userid AND forumid '.$forumselect, $params);
+            $DB->delete_records_select('forum_read', 'userid = :userid AND forumid '.$forumselect, $params);
+        }
+    }
+}
index ded6583..5421b5a 100644 (file)
@@ -31,11 +31,13 @@ $handlers = array (
         'schedule'         => 'instant',
         'internal'         => 1,
     ),
+);
 
-    'user_unenrolled' => array (
-        'handlerfile'      => '/mod/forum/lib.php',
-        'handlerfunction'  => 'forum_user_unenrolled',
-        'schedule'         => 'instant',
-        'internal'         => 1,
+// List of observers.
+$observers = array(
+
+    array(
+        'eventname'   => '\core\event\user_enrolment_deleted',
+        'callback'    => 'mod_forum_observer::user_enrolment_deleted',
     ),
 );
index 917bb8a..be0a2ea 100644 (file)
@@ -6289,28 +6289,6 @@ function forum_user_role_assigned($cp) {
     }
 }
 
-/**
- * This function gets run whenever user is unenrolled from course
- *
- * @param stdClass $cp
- * @return void
- */
-function forum_user_unenrolled($cp) {
-    global $DB;
-
-    // NOTE: this has to be as fast as possible!
-
-    if ($cp->lastenrol) {
-        $params = array('userid'=>$cp->userid, 'courseid'=>$cp->courseid);
-        $forumselect = "IN (SELECT f.id FROM {forum} f WHERE f.course = :courseid)";
-
-        $DB->delete_records_select('forum_digests',       "userid = :userid AND forum $forumselect", $params);
-        $DB->delete_records_select('forum_subscriptions', "userid = :userid AND forum $forumselect", $params);
-        $DB->delete_records_select('forum_track_prefs',   "userid = :userid AND forumid $forumselect", $params);
-        $DB->delete_records_select('forum_read',          "userid = :userid AND forumid $forumselect", $params);
-    }
-}
-
 // Functions to do with read tracking.
 
 /**
index 90de875..d5bfb7a 100644 (file)
@@ -168,4 +168,37 @@ class mod_forum_lib_testcase extends advanced_testcase {
             $this->assertContains($course->shortname, array($course1->shortname, $course2->shortname));
         }
     }
+
+    /**
+     * Test user_enrolment_deleted observer.
+     */
+    public function test_user_enrolment_deleted_observer() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $metaplugin = enrol_get_plugin('meta');
+        $user1 = $this->getDataGenerator()->create_user();
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $student = $DB->get_record('role', array('shortname'=>'student'));
+
+        $e1 = $metaplugin->add_instance($course2, array('customint1' => $course1->id));
+        $enrol1 = $DB->get_record('enrol', array('id' => $e1));
+
+        // Enrol user.
+        $metaplugin->enrol_user($enrol1, $user1->id, $student->id);
+        $this->assertEquals(1, $DB->count_records('user_enrolments'));
+
+        // Unenrol user and capture event.
+        $sink = $this->redirectEvents();
+        $metaplugin->unenrol_user($enrol1, $user1->id);
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
+        $this->assertEquals(0, $DB->count_records('user_enrolments'));
+        $this->assertInstanceOf('\core\event\user_enrolment_deleted', $event);
+        $this->assertEquals('user_unenrolled', $event->get_legacy_eventname());
+    }
 }
index a8d6db6..0d6b9dd 100644 (file)
@@ -1296,13 +1296,15 @@ M.core_filepicker.init = function(Y, options) {
         },
         render: function() {
             var client_id = this.options.client_id;
+            var fpid = "filepicker-"+ client_id;
+            var labelid = 'fp-dialog-label_'+ client_id;
             this.fpnode = Y.Node.createWithFilesSkin(M.core_filepicker.templates.generallayout).
-                set('id', 'filepicker-'+client_id);
+                set('id', 'filepicker-'+client_id).set('aria-labelledby', labelid);
             this.mainui = new M.core.dialogue({
                 extraClasses : ['filepicker'],
                 draggable    : true,
                 bodyContent  : this.fpnode,
-                headerContent: M.str.repository.filepicker,
+                headerContent: '<span id="'+ labelid +'">'+ M.str.repository.filepicker +'</span>',
                 centered     : true,
                 modal        : true,
                 visible      : false,
index 566c122..0aed990 100644 (file)
@@ -349,6 +349,7 @@ input#id_externalurl {direction:ltr;}
 .groupmanagementtable #potentialcell {width: 42%;}
 .groupmanagementtable #buttonscell {width: 16%;}
 .groupmanagementtable #buttonscell input {width: 80%;}
+.groupmanagementtable #buttonscell p.arrow_button input {width: auto;min-width: 80%;margin: 0 auto;}
 .groupmanagementtable #removeselect_wrapper,
 .groupmanagementtable #addselect_wrapper {width: 100%;}
 .groupmanagementtable #removeselect_wrapper label,