Merge branch 'master_MDL-28579' of git://github.com/danmarsden/moodle
authorDamyon Wiese <damyon@moodle.com>
Wed, 4 Sep 2013 01:00:48 +0000 (09:00 +0800)
committerDamyon Wiese <damyon@moodle.com>
Wed, 4 Sep 2013 01:00:48 +0000 (09:00 +0800)
168 files changed:
admin/renderer.php
admin/repository.php
admin/roles/classes/define_role_table_advanced.php
admin/tool/behat/renderer.php
admin/tool/behat/tests/behat/basic_actions.feature
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/langimport/lang/en/tool_langimport.php
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
admin/tool/uploaduser/index.php
admin/tool/xmldb/lang/en/tool_xmldb.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/dbops/backup_controller_dbops.class.php
backup/util/includes/backup_includes.php
backup/util/includes/restore_includes.php
backup/util/plan/backup_plan.class.php
backup/util/plan/base_plan.class.php
backup/util/plan/base_task.class.php
backup/util/plan/restore_plan.class.php
backup/util/plan/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]
backup/util/ui/restore_ui_components.php
backup/util/ui/tests/behat/backup_courses.feature
badges/classes/observer.php
badges/external.php
badges/mybackpack.php
badges/renderer.php
badges/tests/badgeslib_test.php
blocks/course_list/lang/en/block_course_list.php
blocks/navigation/tests/behat/view_my_courses.feature
course/externallib.php
course/lib.php
course/tests/behat/course_controls.feature
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/admin.php
lang/en/badges.php
lang/en/cache.php
lang/en/completion.php
lang/en/enrol.php
lang/en/error.php
lang/en/install.php
lang/en/mathslib.php
lang/en/moodle.php
lang/en/repository.php
lib/accesslib.php
lib/badgeslib.php
lib/behat/behat_base.php
lib/behat/classes/behat_command.php
lib/behat/classes/behat_selectors.php [new file with mode: 0644]
lib/behat/classes/util.php
lib/blocklib.php
lib/classes/component.php
lib/classes/event/course_completed.php
lib/classes/event/course_completion_updated.php
lib/classes/event/course_module_completion_updated.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/ldaplib.php
lib/modinfolib.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/tests/behat/behat_hooks.php
lib/tests/component_test.php
lib/tests/configonlylib_test.php
lib/tests/modinfolib_test.php
lib/tests/moodlelib_test.php
lib/upgrade.txt
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
login/change_password.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/locallib.php
mod/assignment/backup/moodle1/lib.php
mod/assignment/backup/moodle2/restore_assignment_stepslib.php
mod/assignment/lang/en/assignment.php
mod/assignment/lib.php
mod/assignment/mod_form.php
mod/assignment/view.php
mod/data/view.php
mod/feedback/lang/en/feedback.php
mod/forum/classes/observer.php [new file with mode: 0644]
mod/forum/db/events.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/tests/lib_test.php
mod/lesson/lang/en/lesson.php
mod/lti/lang/en/lti.php
portfolio/googledocs/db/upgrade.php
portfolio/googledocs/db/upgradelib.php [new file with mode: 0644]
portfolio/picasa/db/upgrade.php
portfolio/picasa/db/upgradelib.php [new file with mode: 0644]
question/engine/lib.php
report/security/lang/en/report_security.php
repository/coursefiles/lib.php
repository/filepicker.js
repository/filesystem/lib.php
repository/googledocs/db/upgrade.php
repository/googledocs/db/upgradelib.php [new file with mode: 0644]
repository/lib.php
repository/picasa/db/upgrade.php
repository/picasa/db/upgradelib.php [new file with mode: 0644]
repository/repository_ajax.php
repository/tests/behat/behat_filepicker.php
repository/upgrade.txt
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/profile/field/menu/lang/en/profilefield_menu.php
user/tests/userlib_test.php [new file with mode: 0644]
version.php

index fad8eff..350254c 100644 (file)
@@ -1011,9 +1011,11 @@ class core_admin_renderer extends plugin_renderer_base {
             } else {
                 $str = 'otherplugin';
             }
+            $componenturl = new moodle_url('https://moodle.org/plugins/view.php?plugin='.$component);
+            $componenturl = html_writer::tag('a', $component, array('href' => $componenturl->out()));
             $requires[] = html_writer::tag('li',
                     get_string($str, 'core_plugin',
-                            array('component' => $component, 'version' => $requiredversion)),
+                            array('component' => $componenturl, 'version' => $requiredversion)),
                     array('class' => $class));
         }
 
index 50a08ee..8f976cf 100644 (file)
@@ -143,8 +143,10 @@ if (($action == 'edit') || ($action == 'new')) {
             $success = $repositorytype->update_options($settings);
         } else {
             $type = new repository_type($plugin, (array)$fromform, $visible);
-            $type->create();
             $success = true;
+            if (!$repoid = $type->create()) {
+                $success = false;
+            }
             $data = data_submitted();
         }
         if ($success) {
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 db94c85..0f5ac5e 100644 (file)
@@ -25,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
-require_once($CFG->libdir . '/behat/classes/behat_command.php');
+require_once($CFG->libdir . '/behat/classes/behat_selectors.php');
 
 /**
  * Renderer for behat tool web features
@@ -92,7 +92,7 @@ class tool_behat_renderer extends plugin_renderer_base {
             // Replace text selector type arguments with a user-friendly select.
             $stepsdefinitions = preg_replace_callback('/(TEXT_SELECTOR\d?_STRING)/',
                 function ($matches) {
-                    return html_writer::select(behat_command::$allowedtextselectors, uniqid());
+                    return html_writer::select(behat_selectors::get_allowed_text_selectors(), uniqid());
                 },
                 $stepsdefinitions
             );
@@ -100,7 +100,7 @@ class tool_behat_renderer extends plugin_renderer_base {
             // Replace selector type arguments with a user-friendly select.
             $stepsdefinitions = preg_replace_callback('/(SELECTOR\d?_STRING)/',
                 function ($matches) {
-                    return html_writer::select(behat_command::$allowedselectors, uniqid());
+                    return html_writer::select(behat_selectors::get_allowed_selectors(), uniqid());
                 },
                 $stepsdefinitions
             );
index 637086f..4ec803d 100644 (file)
@@ -35,7 +35,7 @@ Feature: Page contents assertions
       | Course 1 | C1 | 0 |
     And I log in as "admin"
     And I follow "Course 1"
-    When I click on "Move this to the dock" "button" in the ".block_settings" "css_element"
+    When I click on "Move this to the dock" "button" in the "Administration" "block"
     Then I should not see "Question bank"
     And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
 
@@ -45,5 +45,5 @@ Feature: Page contents assertions
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And I log in as "admin"
-    When I click on "Move this to the dock" "button" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
+    When I click on "Move this to the dock" "button" in the "Administration" "block"
     Then I should not see "Turn editing on"
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 e194219..87dee88 100644 (file)
@@ -23,7 +23,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['install'] = 'Install selected language pack';
+$string['install'] = 'Install selected language pack(s)';
 $string['installedlangs'] = 'Installed language packs';
 $string['langimport'] = 'Language import utility';
 $string['langimportdisabled'] = 'Language import feature has been disabled. You have to update your language packs manually at the file-system level. Do not forget to purge string caches after you do so.';
@@ -38,7 +38,7 @@ $string['nolangupdateneeded'] = 'All your language packs are up to date, no upda
 $string['pluginname'] = 'Language packs';
 $string['purgestringcaches'] = 'Purge string caches';
 $string['remotelangnotavailable'] = 'Because Moodle cannot connect to download.moodle.org, it is not possible for language packs to be installed automatically. Please download the appropriate ZIP file(s) from <a href="http://download.moodle.org/langpack/">download.moodle.org/langpack</a>, copy them to your {$a} directory and unzip them manually.';
-$string['uninstall'] = 'Uninstall selected language pack';
+$string['uninstall'] = 'Uninstall selected language pack(s)';
 $string['uninstallconfirm'] = 'You are about to completely uninstall language pack {$a}, are you sure?';
 $string['updatelangs'] = 'Update all installed language packs';
 
index 3be7ec4..999ca70 100644 (file)
@@ -99,7 +99,7 @@ $string['shortnametemplate_help'] = 'The short name of the course is displayed i
 $string['templatefile'] = 'Restore from this file after upload';
 $string['templatefile_help'] = 'Select a file to use as a template for the creation of all courses.';
 $string['unknownimportmode'] = 'Unknown import mode';
-$string['updatemissing'] = 'Fill in missing from CSV data and defaults';
+$string['updatemissing'] = 'Fill in missing items from CSV data and defaults';
 $string['updatemode'] = 'Update mode';
 $string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated';
 $string['updateonly'] = 'Only update existing courses';
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 4dfc221..040cf3a 100644 (file)
@@ -33,24 +33,38 @@ $string['confirmdeleteindex'] = 'Are you absolutely sure that you want to delete
 $string['confirmdeletekey'] = 'Are you absolutely sure that you want to delete the key:';
 $string['confirmdeletetable'] = 'Are you absolutely sure that you want to delete the table:';
 $string['confirmdeletexmlfile'] = 'Are you absolutely sure that you want to delete the file:';
-$string['confirmcheckbigints'] = 'This functionality will search for <a href="http://tracker.moodle.org/browse/MDL-11038">potential wrong integer fields</a> in your Moodle server, generating (but not executing!) automatically the needed SQL statements to have all the integers in your DB properly defined.<br /><br />
-Once generated you can copy such statements and execute them safely with your favourite SQL interface (don\'t forget to backup your data before doing that).<br /><br />
-It\'s highly recommended to be running the latest (+ version) available of your Moodle release (1.8, 1.9, 2.x ...) before executing the search of wrong integers.<br /><br />
+$string['confirmcheckbigints'] = 'This functionality will search for <a href="http://tracker.moodle.org/browse/MDL-11038">potential wrong integer fields</a> in your Moodle server, generating (but not executing!) automatically the needed SQL statements to have all the integers in your DB properly defined.
+
+Once generated you can copy such statements and execute them safely with your favourite SQL interface (don\'t forget to backup your data before doing that).
+
+It\'s highly recommended to be running the latest (+ version) available of your Moodle release before executing the search of wrong integers.
+
 This functionality doesn\'t perform any action against the DB (just reads from it), so can be safely executed at any moment.';
-$string['confirmcheckdefaults'] = 'This functionality will search for inconsistent default values in your Moodle server, generating (but not executing!) the needed SQL statements to have all the default values properly defined.<br /><br />
-Once generated you can copy such statements and execute them safely with your favourite SQL interface (don\'t forget to backup your data before doing that).<br /><br />
-It\'s highly recommended to be running the latest (+ version) available of your Moodle release (1.8, 1.9, 2.x ...) before executing the search of inconsistent default values.<br /><br />
+$string['confirmcheckdefaults'] = 'This functionality will search for inconsistent default values in your Moodle server, generating (but not executing!) the needed SQL statements to have all the default values properly defined.
+
+Once generated you can copy such statements and execute them safely with your favourite SQL interface (don\'t forget to backup your data before doing that).
+
+It\'s highly recommended to be running the latest (+ version) available of your Moodle release before executing the search of inconsistent default values.
+
 This functionality doesn\'t perform any action against the DB (just reads from it), so can be safely executed at any moment.';
-$string['confirmcheckforeignkeys'] = 'This functionality will search for potential violations of the foreign keys defined in the install.xml definitions. (Moodle does not currently generate actual foreign key constraints in the database, which is why invalid data may be present.)<br /><br />
-It\'s highly recommended to be running the latest (+ version) available of your Moodle release (1.8, 1.9, 2.x ...) before executing the search of missing indexes.<br /><br />
+$string['confirmcheckforeignkeys'] = 'This functionality will search for potential violations of the foreign keys defined in the install.xml definitions. (Moodle does not currently generate actual foreign key constraints in the database, which is why invalid data may be present.)
+
+It\'s highly recommended to be running the latest (+ version) available of your Moodle release before executing the search of missing indexes.
+
 This functionality doesn\'t perform any action against the DB (just reads from it), so can be safely executed at any moment.';
-$string['confirmcheckindexes'] = 'This functionality will search for potential missing indexes in your Moodle server, generating (but not executing!) automatically the needed SQL statements to keep everything updated.<br /><br />
-Once generated you can copy such statements and execute them safely with your favourite SQL interface (don\'t forget to backup your data before doing that).<br /><br />
-It\'s highly recommended to be running the latest (+ version) available of your Moodle release (1.8, 1.9, 2.x ...) before executing the search of missing indexes.<br /><br />
+$string['confirmcheckindexes'] = 'This functionality will search for potential missing indexes in your Moodle server, generating (but not executing!) automatically the needed SQL statements to keep everything updated.
+
+Once generated you can copy such statements and execute them safely with your favourite SQL interface (don\'t forget to backup your data before doing that).
+
+It\'s highly recommended to be running the latest (+ version) available of your Moodle release before executing the search of missing indexes.
+
 This functionality doesn\'t perform any action against the DB (just reads from it), so can be safely executed at any moment.';
-$string['confirmcheckoraclesemantics'] = 'This functionality will search for <a href="http://tracker.moodle.org/browse/MDL-29322">Oracle varchar2 columns using BYTE semantics</a> in your Moodle server, generating (but not executing!) automatically the needed SQL statements to have all the columns converted to use CHAR semantics instead (better for cross-db compatibility and increased contents max. length).<br /><br />
-Once generated you can copy such statements and execute them safely with your favourite SQL interface (don\'t forget to backup your data before doing that).<br /><br />
-It\'s highly recommended to be running the latest (+ version) available of your Moodle release (2.2, 2.3, 2.x ...) before executing the search of BYTE semantics.<br /><br />
+$string['confirmcheckoraclesemantics'] = 'This functionality will search for <a href="http://tracker.moodle.org/browse/MDL-29322">Oracle varchar2 columns using BYTE semantics</a> in your Moodle server, generating (but not executing!) automatically the needed SQL statements to have all the columns converted to use CHAR semantics instead (better for cross-db compatibility and increased contents max. length).
+
+Once generated you can copy such statements and execute them safely with your favourite SQL interface (don\'t forget to backup your data before doing that).
+
+It\'s highly recommended to be running the latest (+ version) available of your Moodle release before executing the search of BYTE semantics.
+
 This functionality doesn\'t perform any action against the DB (just reads from it), so can be safely executed at any moment.';
 $string['confirmrevertchanges'] = 'Are you absolutely sure that you want to revert changes performed over:';
 $string['create'] = 'Create';
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 668ff61..409be32 100644 (file)
  */
 abstract class backup_controller_dbops extends backup_dbops {
 
+    /**
+     * @var string Backup id for cached backup_includes_files result.
+     */
+    protected static $includesfilescachebackupid;
+
+    /**
+     * @var int Cached backup_includes_files result
+     */
+    protected static $includesfilescache;
+
     /**
      * Send one backup controller to DB
      *
@@ -441,9 +451,20 @@ abstract class backup_controller_dbops extends backup_dbops {
      * @return int Indicates whether files should be included in backups.
      */
     public static function backup_includes_files($backupid) {
-        // Load controller
+        // This function is called repeatedly in a backup with many files.
+        // Loading the controller is a nontrivial operation (in a large test
+        // backup it took 0.3 seconds), so we do a temporary cache of it within
+        // this request.
+        if (self::$includesfilescachebackupid === $backupid) {
+            return self::$includesfilescache;
+        }
+
+        // Load controller, get value, then destroy controller and return result.
+        self::$includesfilescachebackupid = $backupid;
         $bc = self::load_controller($backupid);
-        return $bc->get_include_files();
+        self::$includesfilescache = $bc->get_include_files();
+        $bc->destroy();
+        return self::$includesfilescache;
     }
 
     /**
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 6f65533..f892b8c 100644 (file)
@@ -174,22 +174,20 @@ abstract class restore_search_base implements renderable {
         $this->totalcount = 0;
         $contextlevel = $this->get_itemcontextlevel();
         list($sql, $params) = $this->get_searchsql();
-        $blocksz = 5000;
-        $offs = 0;
-        // Get total number, to avoid some incorrect iterations
+        // Get total number, to avoid some incorrect iterations.
         $countsql = preg_replace('/ORDER BY.*/', '', $sql);
         $totalcourses = $DB->count_records_sql("SELECT COUNT(*) FROM ($countsql) sel", $params);
-        // User to be checked is always the same (usually null, get it form first element)
-        $firstcap = reset($this->requiredcapabilities);
-        $userid = isset($firstcap['user']) ? $firstcap['user'] : null;
-        // Extract caps to check, this saves us a bunch of iterations
-        $requiredcaps = array();
-        foreach ($this->requiredcapabilities as $cap) {
-            $requiredcaps[] = $cap['capability'];
-        }
-        // Iterate while we have records and haven't reached $this->maxresults.
-        while ($totalcourses > $offs and $this->totalcount < $this->maxresults) {
-            $resultset = $DB->get_records_sql($sql, $params, $offs, $blocksz);
+        if ($totalcourses > 0) {
+            // User to be checked is always the same (usually null, get it from first element).
+            $firstcap = reset($this->requiredcapabilities);
+            $userid = isset($firstcap['user']) ? $firstcap['user'] : null;
+            // Extract caps to check, this saves us a bunch of iterations.
+            $requiredcaps = array();
+            foreach ($this->requiredcapabilities as $cap) {
+                $requiredcaps[] = $cap['capability'];
+            }
+            // Iterate while we have records and haven't reached $this->maxresults.
+            $resultset = $DB->get_recordset_sql($sql, $params);
             foreach ($resultset as $result) {
                 context_helper::preload_from_record($result);
                 $classname = context_helper::get_class_for_level($contextlevel);
@@ -208,7 +206,7 @@ abstract class restore_search_base implements renderable {
                 $this->totalcount++;
                 $this->results[$result->id] = $result;
             }
-            $offs += $blocksz;
+            $resultset->close();
         }
 
         return $this->totalcount;
index f8b85d7..6732194 100644 (file)
@@ -13,8 +13,9 @@ Feature: Backup Moodle courses
   @javascript
   Scenario: Backup a course providing options
     When I backup "Course 1" course using this options:
+      | Filename | test_backup.mbz |
     Then I should see "Restore"
-    And I click on "Restore" "link" in the ".backup-files-table" "css_element"
+    And I click on "Restore" "link" in the "test_backup.mbz" "table_row"
     And I should see "URL of backup"
     And I should see "Anonymize user information"
 
@@ -27,11 +28,11 @@ Feature: Backup Moodle courses
       | setting_section_section_5_userinfo | 0 |
       | setting_section_section_5_included | 0 |
     Then I should see "Restore"
-    And I click on "Restore" "link" in the ".backup-files-table" "css_element"
+    And I click on "Restore" "link" in the "test_backup.mbz" "table_row"
     And I should not see "Section 3"
     And I press "Continue"
     And I click on "Continue" "button" in the ".bcs-current-course" "css_element"
     And "//div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element" should exists
     And I check "Include course logs"
     And I press "Cancel"
-    And I click on "Cancel" "button" in the ".confirmation-dialogue" "css_element"
+    And I click on "Cancel" "button" in the "Cancel backup" "dialogue"
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 a9e8dc9..6d8589a 100644 (file)
@@ -26,9 +26,9 @@
 $string['adminview'] = 'Admin view';
 $string['allcourses'] = 'Admin user sees all courses';
 $string['configadminview'] = 'What should the admin see in the course list block?';
-$string['confighideallcourseslink'] = 'Hide "All courses" link at the bottom of the block. Link hiding does not affects Admin\'s view';
+$string['confighideallcourseslink'] = 'Remove the \'All courses\' link under the list of courses. (This setting does not affect the admin view.)';
 $string['course_list:addinstance'] = 'Add a new courses block';
 $string['course_list:myaddinstance'] = 'Add a new courses block to My home';
-$string['hideallcourseslink'] = 'Hide All courses link';
+$string['hideallcourseslink'] = 'Hide \'All courses\' link';
 $string['owncourses'] = 'Admin user sees own courses';
 $string['pluginname'] = 'Courses';
index 567250b..6287bad 100644 (file)
@@ -38,14 +38,14 @@ Feature: View my courses in navigation block
     And I log out
     And I log in as "student1"
     When I follow "My home"
-    Then I should not see "cat1" in the "div.block_navigation .type_system" "css_element"
-    And I should not see "cat2" in the "div.block_navigation .type_system" "css_element"
-    And I should see "c1" in the "div.block_navigation .type_system" "css_element"
-    And I should see "c31" in the "div.block_navigation .type_system" "css_element"
-    And I should see "c331" in the "div.block_navigation .type_system" "css_element"
-    And I should not see "c2" in the "div.block_navigation .type_system" "css_element"
-    And I should not see "c32" in the "div.block_navigation .type_system" "css_element"
-    And I should not see "c332" in the "div.block_navigation .type_system" "css_element"
+    Then I should not see "cat1" in the "Navigation" "block"
+    And I should not see "cat2" in the "Navigation" "block"
+    And I should see "c1" in the "Navigation" "block"
+    And I should see "c31" in the "Navigation" "block"
+    And I should see "c331" in the "Navigation" "block"
+    And I should not see "c2" in the "Navigation" "block"
+    And I should not see "c32" in the "Navigation" "block"
+    And I should not see "c332" in the "Navigation" "block"
 
   @javascript
   Scenario: The nested list of enrolled courses is shown
@@ -54,18 +54,18 @@ Feature: View my courses in navigation block
     And I log out
     And I log in as "student1"
     When I follow "My home"
-    Then I should see "cat1" in the "div.block_navigation .type_system" "css_element"
-    And I should see "cat3" in the "div.block_navigation .type_system" "css_element"
-    And I should not see "cat2" in the "div.block_navigation .type_system" "css_element"
+    Then I should see "cat1" in the "Navigation" "block"
+    And I should see "cat3" in the "Navigation" "block"
+    And I should not see "cat2" in the "Navigation" "block"
     And I expand "cat3" node
     And I wait "2" seconds
-    And I should see "cat31" in the "div.block_navigation .type_system" "css_element"
-    And I should see "cat33" in the "div.block_navigation .type_system" "css_element"
-    And I should not see "cat32" in the "div.block_navigation .type_system" "css_element"
+    And I should see "cat31" in the "Navigation" "block"
+    And I should see "cat33" in the "Navigation" "block"
+    And I should not see "cat32" in the "Navigation" "block"
     And I expand "cat31" node
     And I wait "2" seconds
-    And I should see "c31" in the "div.block_navigation .type_system" "css_element"
+    And I should see "c31" in the "Navigation" "block"
     And I expand "cat33" node
     And I wait "2" seconds
-    And I should see "c331" in the "div.block_navigation .type_system" "css_element"
-    And I should not see "c332" in the "div.block_navigation .type_system" "css_element"
+    And I should see "c331" in the "Navigation" "block"
+    And I should not see "c332" in the "Navigation" "block"
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 8b2ef82..98f41c0 100644 (file)
@@ -40,7 +40,7 @@ Feature: Course activity controls works as expected
     And I should see "Turn editing on"
     And "Turn editing on" "button" should exists
     And I turn editing mode on
-    And I click on "Actions" "link" in the ".block_recent_activity" "css_element"
+    And I click on "Actions" "link" in the "Recent activity" "block"
     And I click on "Delete Recent activity block" "link"
     And I press "Yes"
     And "#section-2" "css_element" <should_see_other_sections> exists
@@ -77,8 +77,8 @@ Feature: Course activity controls works as expected
     And section "1" should be visible
     And I add the "Section links" block
     And "#section-2" "css_element" <should_see_other_sections> exists
-    And I should see "1 2 3 4 5" in the ".block_section_links" "css_element"
-    And I click on "2" "link" in the ".block_section_links" "css_element"
+    And I should see "1 2 3 4 5" in the "Section links" "block"
+    And I click on "2" "link" in the "Section links" "block"
     And I <should_see_other_sections_following_block_sections_links> see "Test forum name 2"
 
     Examples:
@@ -114,7 +114,7 @@ Feature: Course activity controls works as expected
     And I should see "Turn editing on"
     And "Turn editing on" "button" should exists
     And I turn editing mode on
-    And I click on "Actions" "link" in the ".block_recent_activity" "css_element"
+    And I click on "Actions" "link" in the "Recent activity" "block"
     And I click on "Delete Recent activity block" "link"
     And I press "Yes"
     And "#section-2" "css_element" <should_see_other_sections> exists
@@ -154,8 +154,8 @@ Feature: Course activity controls works as expected
     And section "1" should be visible
     And I add the "Section links" block
     And "#section-2" "css_element" <should_see_other_sections> exists
-    And I should see "1 2 3 4 5" in the ".block_section_links" "css_element"
-    And I click on "2" "link" in the ".block_section_links" "css_element"
+    And I should see "1 2 3 4 5" in the "Section links" "block"
+    And I click on "2" "link" in the "Section links" "block"
     And I <should_see_other_sections_following_block_sections_links> see "Test forum name 2"
 
     Examples:
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..7aab283
--- /dev/null
@@ -0,0 +1,210 @@
+<?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;
+    }
+
+    /**
+     * Triggered via role_assigned event.
+     *
+     * @param \core\event\role_assigned $event
+     * @return bool true on success.
+     */
+    public static function role_assigned(\core\event\role_assigned $event) {
+        if (!enrol_is_enabled('meta')) {
+            return true;
+        }
+
+        // Prevent circular dependencies - we can not sync meta roles recursively.
+        if ($event->other['component'] === 'enrol_meta') {
+            return true;
+        }
+
+        // Only course level roles are interesting.
+        if (!$parentcontext = context::instance_by_id($event->contextid, IGNORE_MISSING)) {
+            return true;
+        }
+        if ($parentcontext->contextlevel != CONTEXT_COURSE) {
+            return true;
+        }
+
+        self::sync_course_instances($parentcontext->instanceid, $event->relateduserid);
+
+        return true;
+    }
+
+    /**
+     * Triggered via role_unassigned event.
+     *
+     * @param \core\event\role_unassigned $event
+     * @return bool true on success
+     */
+    public static function role_unassigned(\core\event\role_unassigned $event) {
+        if (!enrol_is_enabled('meta')) {
+            // All roles are removed via cron automatically.
+            return true;
+        }
+
+        // Prevent circular dependencies - we can not sync meta roles recursively.
+        if ($event->other['component'] === 'enrol_meta') {
+            return true;
+        }
+
+        // Only course level roles are interesting.
+        if (!$parentcontext = context::instance_by_id($event->contextid, IGNORE_MISSING)) {
+            return true;
+        }
+        if ($parentcontext->contextlevel != CONTEXT_COURSE) {
+            return true;
+        }
+
+        self::sync_course_instances($parentcontext->instanceid, $event->relateduserid);
+
+        return true;
+    }
+
+    /**
+     * Triggered via course_deleted event.
+     *
+     * @param \core\event\course_deleted $event
+     * @return bool true on success
+     */
+    public static function course_deleted(\core\event\course_deleted $event) {
+        global $DB;
+
+        if (!enrol_is_enabled('meta')) {
+            // This is slow, let enrol_meta_sync() deal with disabled plugin.
+            return true;
+        }
+
+        // Does anything want to sync with this parent?
+        if (!$enrols = $DB->get_records('enrol', array('customint1' => $event->objectid, 'enrol' => 'meta'),
+                'courseid ASC, id ASC')) {
+            return true;
+        }
+
+        $plugin = enrol_get_plugin('meta');
+        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
+
+        if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
+            // Simple, just delete this instance which purges all enrolments,
+            // admins were warned that this is risky setting!
+            foreach ($enrols as $enrol) {
+                $plugin->delete_instance($enrol);
+            }
+            return true;
+        }
+
+        foreach ($enrols as $enrol) {
+            $enrol->customint = 0;
+            $DB->update_record('enrol', $enrol);
+
+            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+                // This makes all enrolments suspended very quickly.
+                $plugin->update_status($enrol, ENROL_INSTANCE_DISABLED);
+            }
+            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+                $context = context_course::instance($enrol->courseid);
+                role_unassign_all(array('contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id));
+            }
+        }
+
+        return true;
+    }
+}
index c1a2d70..8d2a590 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-/* List of handlers */
-$handlers = array (
-    'role_assigned' => array (
-        'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'role_assigned'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
-    ),
+// List of observers.
+$observers = array(
 
-    'role_unassigned' => array (
-        'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'role_unassigned'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array(
+        'eventname'   => '\core\event\user_enrolment_created',
+        'callback'    => 'enrol_meta_observer::user_enrolment_created',
     ),
-
-    'user_enrolled' => array (
-        'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'user_enrolled'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array(
+        'eventname'   => '\core\event\user_enrolment_deleted',
+        'callback'    => 'enrol_meta_observer::user_enrolment_deleted',
     ),
-
-    'user_unenrolled' => array (
-        'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'user_unenrolled'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array(
+        'eventname'   => '\core\event\user_enrolment_updated',
+        'callback'    => 'enrol_meta_observer::user_enrolment_updated',
     ),
-
-    '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\role_assigned',
+        'callback'    => 'enrol_meta_observer::role_assigned',
     ),
-
-    'course_deleted' => array (
-        'handlerfile'      => '/enrol/meta/locallib.php',
-        'handlerfunction'  => array('enrol_meta_handler', 'course_deleted'),
-        'schedule'         => 'instant',
-        'internal'         => 1,
+    array(
+        'eventname'   => '\core\event\role_unassigned',
+        'callback'    => 'enrol_meta_observer::role_unassigned',
+    ),
+    array(
+        'eventname'   => '\core\event\course_deleted',
+        'callback'    => 'enrol_meta_observer::course_deleted',
     ),
 );
index 3a3c26b..5c10d79 100644 (file)
@@ -233,182 +233,8 @@ class enrol_meta_handler {
             debugging('Unknown unenrol action '.$unenrolaction);
         }
     }
-
-    /**
-     * Triggered via role assigned event.
-     * @static
-     * @param stdClass $ra
-     * @return bool success
-     */
-    public static function role_assigned($ra) {
-        if (!enrol_is_enabled('meta')) {
-            return true;
-        }
-
-        // prevent circular dependencies - we can not sync meta roles recursively
-        if ($ra->component === 'enrol_meta') {
-            return true;
-        }
-
-        // only course level roles are interesting
-        if (!$parentcontext = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
-            return true;
-        }
-        if ($parentcontext->contextlevel != CONTEXT_COURSE) {
-            return true;
-        }
-
-        self::sync_course_instances($parentcontext->instanceid, $ra->userid);
-
-        return true;
-    }
-
-    /**
-     * Triggered via role unassigned event.
-     * @static
-     * @param stdClass $ra
-     * @return bool success
-     */
-    public static function role_unassigned($ra) {
-        if (!enrol_is_enabled('meta')) {
-            // all roles are removed via cron automatically
-            return true;
-        }
-
-        // prevent circular dependencies - we can not sync meta roles recursively
-        if ($ra->component === 'enrol_meta') {
-            return true;
-        }
-
-        // only course level roles are interesting
-        if (!$parentcontext = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
-            return true;
-        }
-        if ($parentcontext->contextlevel != CONTEXT_COURSE) {
-            return true;
-        }
-
-        self::sync_course_instances($parentcontext->instanceid, $ra->userid);
-
-        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
-     * @param stdClass $course
-     * @return bool success
-     */
-    public static function course_deleted($course) {
-        global $DB;
-
-        if (!enrol_is_enabled('meta')) {
-            // This is slow, let enrol_meta_sync() deal with disabled plugin.
-            return true;
-        }
-
-        // does anything want to sync with this parent?
-        if (!$enrols = $DB->get_records('enrol', array('customint1'=>$course->id, 'enrol'=>'meta'), 'courseid ASC, id ASC')) {
-            return true;
-        }
-
-        $plugin = enrol_get_plugin('meta');
-        $unenrolaction = $plugin->get_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
-
-        if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) {
-            // Simple, just delete this instance which purges all enrolments,
-            // admins were warned that this is risky setting!
-            foreach ($enrols as $enrol) {
-                $plugin->delete_instance($enrol);
-            }
-            return true;
-        }
-
-        foreach ($enrols as $enrol) {
-            $enrol->customint = 0;
-            $DB->update_record('enrol', $enrol);
-
-            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
-                // This makes all enrolments suspended very quickly.
-                $plugin->update_status($enrol, ENROL_INSTANCE_DISABLED);
-            }
-            if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
-                $context = context_course::instance($enrol->courseid);
-                role_unassign_all(array('contextid'=>$context->id, 'component'=>'enrol_meta', 'itemid'=>$enrol->id));
-            }
-        }
-
-        return true;
-    }
 }
 
-
 /**
  * Sync all meta course links.
  *
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 2eb56b1..d230f67 100644 (file)
@@ -121,7 +121,7 @@ $string['commonfiltersettings'] = 'Common filter settings';
 $string['commonsettings'] = 'Common settings';
 $string['componentinstalled'] = 'Component installed';
 $string['computedfromlogs'] = 'Computed from logs since {$a}.';
-$string['condifmodeditdefaults'] = 'The values you set here define the default values that are used in the activity settings form when you create a new activity. You can also configure which activity settings are considered advanced.';
+$string['condifmodeditdefaults'] = 'Default values are used in the settings form when creating a new activity or resource.';
 $string['confeditorhidebuttons'] = 'Select the buttons that should be hidden in the HTML editor.';
 $string['configallcountrycodes'] = 'This is the list of countries that may be selected in various places, for example in a user\'s profile. If blank (the default) the list in countries.php in the standard English language pack is used. That is the list from ISO 3166-1. Otherwise, you can specify a comma-separated list of codes, for example \'GB,FR,ES\'. If you add new, non-standard codes here, you will need to add them to countries.php in \'en\' and your language pack.';
 $string['configallowassign'] = 'You can allow people who have the roles on the left side to assign some of the column roles to other people';
@@ -165,15 +165,15 @@ $string['configcronremotepassword'] = 'This means that the cron.php script canno
     http://site.example.com/admin/cron.php?password=opensesame
 </pre>If this is left empty, no password is required.';
 $string['configcurlcache'] = 'Time-to-live for cURL cache, in seconds.';
-$string['configcustommenuitems'] = 'You can configure a custom menu here to be shown by themes. Each line consists of some menu text, a link URL (optional) and a tooltip title (optional), separated by pipe characters. You can specify a structure using hyphens. For example:
+$string['configcustommenuitems'] = 'You can configure a custom menu here to be shown by themes. Each line consists of some menu text, a link URL (optional), a tooltip title (optional) and a language code or comma-separated list of codes (optional, for displaying the line to users of the specified language only), separated by pipe characters. You can specify a structure using hyphens. For example:
 <pre>
 Moodle community|http://moodle.org
 -Moodle free support|http://moodle.org/support
 -Moodle development|http://moodle.org/development
 --Moodle Tracker|http://tracker.moodle.org
---Moodle Docs|http://docs.moodle.org
--Moodle News|http://moodle.org/news
-Moodle company
+--Moodle Docs|http://docs.moodle.org|Moodle Docs in German
+--German Moodle Docs|http://docs.moodle.org/de|Documentation in German|de
+-Moodle News|http://moodle.org/news Moodle company
 -Moodle commercial hosting|http://moodle.com/hosting
 -Moodle commercial support|http://moodle.com/support
 </pre>';
@@ -533,10 +533,10 @@ $string['filters'] = 'Filters';
 $string['filtersettings'] = 'Manage filters';
 $string['filtersettingsgeneral'] = 'General filter settings';
 $string['filteruploadedfiles'] = 'Filter uploaded files';
-$string['forcelogin'] = 'Force users to login';
-$string['forceloginforprofileimage'] = 'Force users to login to view user pictures';
+$string['forcelogin'] = 'Force users to log in';
+$string['forceloginforprofileimage'] = 'Force users to log in to view user pictures';
 $string['forceloginforprofileimage_help'] = 'If enabled, users must login in order to view user profile pictures and the default user picture will be used in all notification emails.';
-$string['forceloginforprofiles'] = 'Force users to login for profiles';
+$string['forceloginforprofiles'] = 'Force users to log in for profiles';
 $string['forcetimezone'] = 'Force default timezone';
 $string['formatuninstallwithcourses'] = 'There are {$a->count} courses using {$a->format}. Their format will be changed to {$a->defaultformat} (default format for this site). Some format-specific data may be lost. Are you sure you want to proceed?';
 $string['frontpage'] = 'Front page';
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 56cae77..dfb5c06 100644 (file)
@@ -70,7 +70,7 @@ $string['completionenabled'] = 'Enabled, control via completion and activity set
 $string['completionexpected'] = 'Expect completed on';
 $string['completionexpected_help'] = 'This setting specifies the date when the activity is expected to be completed. The date is not shown to students and is only displayed in the activity completion report.';
 $string['completionicons'] = 'Completion tick boxes';
-$string['completionicons_help'] = 'A tick next an activity name may be used to indicate when the activity is complete.
+$string['completionicons_help'] = 'A tick next to an activity name may be used to indicate when the activity is complete.
 
 If a box with a dotted border is shown, a tick will appear automatically when you have completed the activity according to conditions set by the teacher.
 
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..2f18aa6 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';
@@ -219,7 +220,7 @@ $string['duplicaterolename'] = 'There is already a role with this name!';
 $string['duplicateroleshortname'] = 'There is already a role with this short name!';
 $string['duplicateusername'] = 'Duplicate username - skipping record';
 $string['emailfail'] = 'Emailing failed';
-$string['error'] = 'Error occured';
+$string['error'] = 'Error occurred';
 $string['errorcleaningdirectory'] = 'Error cleaning directory "{$a}"';
 $string['errorcopyingfiles'] = 'Error copying files';
 $string['errorcreatingdirectory'] = 'Error creating directory "{$a}"';
@@ -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 3b52fc0..882655a 100644 (file)
@@ -38,7 +38,7 @@ $string['admindirsettingsub'] = 'A very few webhosts use /admin as a special URL
     renaming the admin directory in your installation, and putting that 
     new name here.  For example: <br /> <br /><b>moodleadmin</b><br /> <br />
     This will fix admin links in Moodle.';
-$string['availablelangs'] = 'List of available languages';
+$string['availablelangs'] = 'Available language packs';
 $string['caution'] = 'Caution';
 $string['cliadminpassword'] = 'New admin user password';
 $string['cliadminusername'] = 'Admin account username';
index ab55cf0..488cbf8 100644 (file)
@@ -23,7 +23,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['anunexpectederroroccured'] = 'an unexpected error occured';
+$string['anunexpectederroroccured'] = 'an unexpected error occurred';
 $string['cannotassigntoconstant'] = 'cannot assign to constant \'{$a}\'';
 $string['cannotredefinebuiltinfunction'] = 'cannot redefine built-in function \'{$a}()\'';
 $string['divisionbyzero'] = 'division by zero';
index 10c69f5..488667d 100644 (file)
@@ -333,6 +333,7 @@ $string['courselegacyfiles_help'] = 'The course files area provides some backwar
 
 If you use this area to store course files, you can expose yourself to a number of privacy and security issues, as well as experiencing missing files in backups, course imports and any time content is shared or re-used.  It is therefore recommended that you do not use this area unless you really know what you are doing.';
 $string['courselegacyfiles_link'] = 'coursefiles2';
+$string['courselegacyfilesofcourse'] = 'Legacy course files: {$a}';
 $string['courseoverview'] = 'Course overview';
 $string['courseoverviewgraph'] = 'Course overview graph';
 $string['courseprofiles'] = 'Course profiles';
@@ -667,6 +668,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 f14ef53..a6734bf 100644 (file)
@@ -106,7 +106,7 @@ $string['errornotyourfile'] = 'You cannot pick file which is not added by your';
 $string['erroruniquename'] = 'Repository instance name should be unique';
 $string['errorpostmaxsize'] = 'The uploaded file may exceed the post_max_size directive in php.ini.';
 $string['errorwhilecommunicatingwith'] = 'Error while communicating with the repository \'{$a}\'.';
-$string['errorwhiledownload'] = 'An error occured while downloading the file: {$a}';
+$string['errorwhiledownload'] = 'An error occurred while downloading the file: {$a}';
 $string['existingrepository'] = 'This repository already exists';
 $string['federatedsearch'] = 'Federated search';
 $string['fileexists'] = 'File name already being used, please use another name';
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 02b968f..667273f 100644 (file)
@@ -367,22 +367,13 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      */
     protected function transform_selector($selectortype, $element) {
 
-        // Here we don't know if a $allowedtextselector is used.
-        if (!isset(behat_command::$allowedselectors[$selectortype])) {
+        // Here we don't know if an allowed text selector is being used.
+        $selectors = behat_selectors::get_allowed_selectors();
+        if (!isset($selectors[$selectortype])) {
             throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession());
         }
 
-        // CSS and XPath selectors locator is one single argument.
-        if ($selectortype == 'css_element' || $selectortype == 'xpath_element') {
-            $selector = str_replace('_element', '', $selectortype);
-            $locator = $element;
-        } else {
-            // Named selectors uses arrays as locators including the type of named selector.
-            $locator = array($selectortype, $this->getSession()->getSelectorsHandler()->xpathLiteral($element));
-            $selector = 'named';
-        }
-
-        return array($selector, $locator);
+        return behat_selectors::get_behat_selector($selectortype, $element, $this->getSession());
     }
 
     /**
@@ -398,7 +389,8 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
      */
     protected function transform_text_selector($selectortype, $element) {
 
-        if ($selectortype != 'css_element' && $selectortype != 'xpath_element') {
+        $selectors = behat_selectors::get_allowed_text_selectors();
+        if (empty($selectors[$selectortype])) {
             throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession());
         }
 
index 67a3812..fb149ad 100644 (file)
@@ -42,34 +42,6 @@ class behat_command {
      */
     const DOCS_URL = 'http://docs.moodle.org/dev/Acceptance_testing';
 
-    /**
-     * @var Allowed types when using text selectors arguments.
-     */
-    public static $allowedtextselectors = array(
-        'css_element' => 'css_element',
-        'xpath_element' => 'xpath_element'
-    );
-
-    /**
-     * @var Allowed types when using selector arguments.
-     */
-    public static $allowedselectors = array(
-        'link' => 'link',
-        'button' => 'button',
-        'link_or_button' => 'link_or_button',
-        'select' => 'select',
-        'checkbox' => 'checkbox',
-        'radio' => 'radio',
-        'file' => 'file',
-        'optgroup' => 'optgroup',
-        'option' => 'option',
-        'table' => 'table',
-        'field' => 'field',
-        'fieldset' => 'fieldset',
-        'css_element' => 'css_element',
-        'xpath_element' => 'xpath_element'
-    );
-
     /**
      * Ensures the behat dir exists in moodledata
      * @return string Full path
diff --git a/lib/behat/classes/behat_selectors.php b/lib/behat/classes/behat_selectors.php
new file mode 100644 (file)
index 0000000..e92131d
--- /dev/null
@@ -0,0 +1,161 @@
+<?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/>.
+
+/**
+ * Moodle-specific selectors.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Moodle selectors manager.
+ *
+ * @package    core
+ * @category   test
+ * @copyright  2013 David Monllaó
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_selectors {
+
+    /**
+     * @var Allowed types when using text selectors arguments.
+     */
+    protected static $allowedtextselectors = array(
+        'dialogue' => 'dialogue',
+        'block' => 'block',
+        'region' => 'region',
+        'table_row' => 'table_row',
+        'table' => 'table',
+        'fieldset' => 'fieldset',
+        'css_element' => 'css_element',
+        'xpath_element' => 'xpath_element'
+    );
+
+    /**
+     * @var Allowed types when using selector arguments.
+     */
+    protected static $allowedselectors = array(
+        'dialogue' => 'dialogue',
+        'block' => 'block',
+        'region' => 'region',
+        'table_row' => 'table_row',
+        'link' => 'link',
+        'button' => 'button',
+        'link_or_button' => 'link_or_button',
+        'select' => 'select',
+        'checkbox' => 'checkbox',
+        'radio' => 'radio',
+        'file' => 'file',
+        'optgroup' => 'optgroup',
+        'option' => 'option',
+        'table' => 'table',
+        'field' => 'field',
+        'fieldset' => 'fieldset',
+        'css_element' => 'css_element',
+        'xpath_element' => 'xpath_element'
+    );
+
+    /**
+     * Behat by default comes with XPath, CSS and named selectors,
+     * named selectors are a mapping between names (like button) and
+     * xpaths that represents that names and includes a placeholder that
+     * will be replaced by the locator. These are Moodle's own xpaths.
+     *
+     * @var XPaths for moodle elements.
+     */
+    protected static $moodleselectors = array(
+        'dialogue' => <<<XPATH
+.//div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ')]/descendant::h1[normalize-space(.) = %locator%]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' moodle-dialogue ')]
+XPATH
+        , 'block' => <<<XPATH
+.//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' '))] | .//div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]/descendant::h2[normalize-space(.) = %locator%]/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]
+XPATH
+        , 'region' => <<<XPATH
+.//div[./@id = %locator%]
+XPATH
+        , 'table_row' => <<<XPATH
+.//tr[contains(normalize-space(.), %locator%)]
+XPATH
+    );
+
+    /**
+     * Returns the behat selector and locator for a given moodle selector and locator
+     *
+     * @param string $selectortype The moodle selector type, which includes moodle selectors
+     * @param string $element The locator we look for in that kind of selector
+     * @param Session $session The Mink opened session
+     * @return array Contains the selector and the locator expected by Mink.
+     */
+    public static function get_behat_selector($selectortype, $element, Behat\Mink\Session $session) {
+
+        // CSS and XPath selectors locator is one single argument.
+        if ($selectortype == 'css_element' || $selectortype == 'xpath_element') {
+            $selector = str_replace('_element', '', $selectortype);
+            $locator = $element;
+        } else {
+            // Named selectors uses arrays as locators including the type of named selector.
+            $locator = array($selectortype, $session->getSelectorsHandler()->xpathLiteral($element));
+            $selector = 'named';
+        }
+
+        return array($selector, $locator);
+    }
+
+    /**
+     * Adds moodle selectors as behat named selectors.
+     *
+     * @param Session $session The mink session
+     * @return void
+     */
+    public static function register_moodle_selectors(Behat\Mink\Session $session) {
+
+        foreach (self::get_moodle_selectors() as $name => $xpath) {
+            $session->getSelectorsHandler()->getSelector('named')->registerNamedXpath($name, $xpath);
+        }
+    }
+
+    /**
+     * Allowed selectors getter.
+     *
+     * @return array
+     */
+    public static function get_allowed_selectors() {
+        return self::$allowedselectors;
+    }
+
+    /**
+     * Allowed text selectors getter.
+     *
+     * @return array
+     */
+    public static function get_allowed_text_selectors() {
+        return self::$allowedtextselectors;
+    }
+
+    /**
+     * Moodle selectors attribute accessor.
+     *
+     * @return array
+     */
+    protected static function get_moodle_selectors() {
+        return self::$moodleselectors;
+    }
+}
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();
index 0fa7c30..d5ec8ac 100644 (file)
@@ -31,7 +31,7 @@ class core_component {
     /** @var array list of ignored directories - watch out for auth/db exception */
     protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true, 'fonts'=>true);
     /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
-    protected static $supportsubplugins = array('mod', 'editor', 'local');
+    protected static $supportsubplugins = array('mod', 'editor', 'tool', 'local');
 
     /** @var null cache of plugin types */
     protected static $plugintypes = null;
@@ -429,6 +429,9 @@ $cache = '.var_export($cache, true).';
                         error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
                         continue;
                     }
+                    if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
+                        $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
+                    }
                     if (!is_dir("$CFG->dirroot/$dir")) {
                         error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
                         continue;
index 3c7389a..78bc557 100644 (file)
@@ -40,7 +40,7 @@ class course_completed extends base {
      * @return string
      */
     public static function get_name() {
-        return new get_string('eventcoursecompleted', 'core_completion');
+        return get_string('eventcoursecompleted', 'core_completion');
     }
 
     /**
index bd1dcbd..1637054 100644 (file)
@@ -49,7 +49,7 @@ class course_completion_updated extends base {
      * @return string
      */
     public static function get_name() {
-        return new get_string('eventcoursecompletionupdated', 'core_completion');
+        return get_string('eventcoursecompletionupdated', 'core_completion');
     }
 
     /**
index fbc8c7b..af292a4 100644 (file)
@@ -40,7 +40,7 @@ class course_module_completion_updated extends base {
      * @return string
      */
     public static function get_name() {
-        return new get_string('eventcoursemodulecompletionupdated', 'core_completion');
+        return get_string('eventcoursemodulecompletionupdated', 'core_completion');
     }
 
     /**
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&nb