Merge branch 'MDL-28561_M26' of git://github.com/lazydaisy/moodle
authorDan Poltawski <dan@moodle.com>
Tue, 10 Sep 2013 04:04:51 +0000 (12:04 +0800)
committerDan Poltawski <dan@moodle.com>
Tue, 10 Sep 2013 04:04:51 +0000 (12:04 +0800)
357 files changed:
admin/index.php
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/cas/auth.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_display_progress_if_slow.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.class.php
backup/util/ui/restore_ui_components.php
backup/util/ui/restore_ui_stage.class.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
blocks/rss_client/viewfeed.php
blog/edit.php
blog/renderer.php
cache/classes/definition.php
cache/classes/factory.php
calendar/classes/type_base.php [new file with mode: 0644]
calendar/classes/type_factory.php [new file with mode: 0644]
calendar/lib.php
calendar/managesubscriptions.php
calendar/renderer.php
calendar/tests/calendartype_test.php [new file with mode: 0644]
calendar/tests/calendartype_test_example.php [new file with mode: 0644]
calendar/type/gregorian/classes/structure.php [new file with mode: 0644]
calendar/type/gregorian/lang/en/calendartype_gregorian.php [new file with mode: 0644]
calendar/type/gregorian/version.php [new file with mode: 0644]
course/edit.php
course/edit_form.php
course/editcategory.php
course/externallib.php
course/lib.php
course/modedit.php
course/moodleform_mod.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
error/index.php
files/renderer.php
grade/edit/outcome/edit.php
grade/edit/scale/edit.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/report/grader/lib.php
grade/report/grader/module.js
grade/report/grader/styles.css
grade/tests/externallib_test.php
group/group.php
install/lang/bs/admin.php [new file with mode: 0644]
install/lang/bs/error.php [new file with mode: 0644]
install/lang/bs/install.php
install/lang/bs/moodle.php [new file with mode: 0644]
install/lang/ckb/langconfig.php
install/lang/cy/error.php
install/lang/cy/install.php
install/lang/en/install.php
install/lang/es_mx/install.php
install/lang/hi/admin.php
install/lang/ja/install.php
install/lang/nl/error.php
install/lang/no/error.php
lang/en/admin.php
lang/en/badges.php
lang/en/cache.php
lang/en/calendar.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/plugin.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/coursecatlib.php
lib/cronlib.php
lib/db/caches.php
lib/db/events.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/dml/mssql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/editor/tinymce/module.js
lib/enrollib.php
lib/filelib.php
lib/filestorage/file_packer.php
lib/filestorage/file_progress.php [new file with mode: 0644]
lib/filestorage/stored_file.php
lib/filestorage/tests/zip_packer_test.php
lib/filestorage/zip_archive.php
lib/filestorage/zip_packer.php
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/editor.php
lib/formslib.php
lib/ldaplib.php
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/pluginlib.php
lib/setuplib.php
lib/simplepie/moodle_simplepie.php
lib/tests/behat/behat_hooks.php
lib/tests/component_test.php
lib/tests/configonlylib_test.php
lib/tests/cronlib_test.php [new file with mode: 0644]
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
message/index.php
message/lib.php
message/send_form.php
message/tests/behat/manage_contacts.feature
message/tests/behat/search_history.feature
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/classes/event/all_submissions_downloaded.php
mod/assign/classes/event/assessable_submitted.php
mod/assign/classes/event/extension_granted.php
mod/assign/classes/event/identities_revealed.php
mod/assign/classes/event/marker_updated.php
mod/assign/classes/event/statement_accepted.php
mod/assign/classes/event/submission_duplicated.php
mod/assign/classes/event/submission_graded.php
mod/assign/classes/event/submission_locked.php
mod/assign/classes/event/submission_status_updated.php
mod/assign/classes/event/submission_unlocked.php
mod/assign/classes/event/submission_updated.php
mod/assign/classes/event/workflow_state_updated.php
mod/assign/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/type/online/classes/event/assessable_uploaded.php
mod/assignment/type/upload/classes/event/assessable_submitted.php
mod/assignment/type/upload/classes/event/assessable_uploaded.php
mod/assignment/view.php
mod/book/view.php
mod/chat/gui_basic/index.php
mod/chat/index.php
mod/chat/report.php
mod/chat/styles.css
mod/chat/view.php
mod/data/edit.php
mod/data/export.php
mod/data/index.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/locallib.php
mod/data/preset.php
mod/data/renderer.php
mod/data/styles.css
mod/data/templates.php
mod/data/view.php
mod/feedback/lang/en/feedback.php
mod/forum/classes/event/assessable_uploaded.php
mod/forum/classes/observer.php [new file with mode: 0644]
mod/forum/classes/post_form.php
mod/forum/db/events.php
mod/forum/discuss.php
mod/forum/index.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/search.php
mod/forum/tests/behat/track_read_posts.feature
mod/forum/tests/lib_test.php
mod/forum/view.php
mod/glossary/approve.php
mod/glossary/backup/moodle2/restore_glossary_activity_task.class.php
mod/glossary/db/log.php
mod/glossary/edit.php
mod/glossary/editcategories.php
mod/glossary/index.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/tabs.php
mod/glossary/view.php
mod/lesson/import.php
mod/lesson/index.php
mod/lesson/lang/en/lesson.php
mod/lesson/lesson.php
mod/lesson/report.php
mod/lti/lang/en/lti.php
mod/resource/backup/moodle2/backup_resource_activity_task.class.php
mod/scorm/lang/en/scorm.php
mod/scorm/lib.php
mod/scorm/locallib.php
mod/scorm/mod_form.php
mod/scorm/renderer.php [new file with mode: 0644]
mod/scorm/report/basic/report.php
mod/scorm/report/default.php
mod/scorm/report/interactions/report.php
mod/scorm/report/objectives/report.php
mod/scorm/report/reportlib.php
mod/scorm/report/userreport.php [new file with mode: 0644]
mod/scorm/report/userreportinteractions.php [new file with mode: 0644]
mod/scorm/report/userreporttabs.php [new file with mode: 0644]
mod/scorm/report/userreporttracks.php [new file with mode: 0644]
mod/scorm/styles.css
mod/scorm/tests/validatepackage_test.php
mod/scorm/userreport.php [deleted file]
mod/survey/lang/en/survey.php
mod/survey/lib.php
mod/workshop/classes/event/assessable_uploaded.php
mod/workshop/submission.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
question/format/blackboard_six/tests/blackboardformatpool_test.php
question/format/blackboard_six/tests/blackboardsixformatqti_test.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
tag/edit.php
theme/anomaly/style/base.css
theme/base/style/core.css
theme/base/style/course.css
theme/base/style/grade.css
theme/base/style/user.css
theme/bootstrapbase/config.php
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/backup-restore.less
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/grade.less [deleted file]
theme/bootstrapbase/less/moodle/undo.less
theme/bootstrapbase/less/moodle/user.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/upgrade.txt [new file with mode: 0644]
theme/boxxie/style/core.css
theme/brick/style/colors.css
theme/brick/style/core.css
theme/canvas/style/admin.css
theme/canvas/style/blocks.css
theme/canvas/style/mods.css
theme/canvas/style/text.css
theme/clean/config.php
theme/formal_white/style/course.css
theme/formal_white/style/formal_white.css
theme/magazine/style/colors.css
theme/magazine/style/core.css
theme/mymobile/jquery/custom131.js
theme/mymobile/style/core.css
theme/splash/style/core.css
theme/splash/style/pagelayout.css
theme/standard/style/core.css
theme/standard/style/course.css
theme/upgrade.txt
user/edit.php
user/editadvanced.php
user/editlib.php
user/emailupdate.php
user/lib.php
user/messageselect.php
user/profile/field/datetime/define.class.php
user/profile/field/datetime/field.class.php
user/profile/field/datetime/lang/en/profilefield_datetime.php
user/profile/field/datetime/version.php
user/profile/field/menu/lang/en/profilefield_menu.php
user/tests/userlib_test.php [new file with mode: 0644]
user/view.php
version.php

index 466832c..1f186b6 100644 (file)
@@ -66,6 +66,16 @@ if (empty($_GET['cache']) and empty($_POST['cache']) and empty($_GET['sesskey'])
 }
 
 require('../config.php');
+
+// Invalidate the cache of version.php in any circumstances to help core_component
+// detecting if the version has changed and component cache should be reset.
+if (function_exists('opcache_invalidate')) {
+    opcache_invalidate($CFG->dirroot . '/version.php', true);
+}
+// Make sure the component cache gets rebuilt if necessary, any method that
+// indirectly calls the protected init() method is good here.
+core_component::get_core_subsystems();
+
 require_once($CFG->libdir.'/adminlib.php');    // various admin-only functions
 require_once($CFG->libdir.'/upgradelib.php');  // general upgrade/install related functions
 require_once($CFG->libdir.'/pluginlib.php');   // available updates notifications
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 6d1d582..634e32b 100644 (file)
@@ -137,7 +137,7 @@ class auth_plugin_cas extends auth_plugin_ldap {
             // test pgtIou parameter for proxy mode (https connection
             // in background from CAS server to the php server)
             if ($authCAS != 'CAS' && !isset($_GET['pgtIou'])) {
-                $PAGE->set_url('/auth/cas/auth.php');
+                $PAGE->set_url('/login/index.php');
                 $PAGE->navbar->add($CASform);
                 $PAGE->set_title("$site->fullname: $CASform");
                 $PAGE->set_heading($site->fullname);
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..00b36e0 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
@@ -335,6 +363,9 @@ class restore_controller extends backup implements loggable {
         if ($this->status != backup::STATUS_NEED_PRECHECK) {
             throw new restore_controller_exception('cannot_precheck_wrong_status', $this->status);
         }
+        // Basic/initial prevention against time/memory limits
+        set_time_limit(1 * 60 * 60); // 1 hour for 1 course initially granted
+        raise_memory_limit(MEMORY_EXTRA);
         $this->precheck = restore_prechecks_helper::execute_prechecks($this, $droptemptablesafter);
         if (!array_key_exists('errors', $this->precheck)) { // No errors, can be executed
             $this->set_status(backup::STATUS_AWAITING);
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..6040d4b 100644 (file)
@@ -43,11 +43,37 @@ if ($stage & restore_ui::STAGE_CONFIRM + restore_ui::STAGE_DESTINATION) {
     }
 }
 
+$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();
+
+// Prepare a progress bar which can display optionally during long-running
+// operations while setting up the UI.
+$slowprogress = new core_backup_display_progress_if_slow();
+// Depending on the code branch above, $restore may be a restore_ui or it may
+// be a restore_ui_independent_stage. Either way, this function exists.
+$restore->set_progress_reporter($slowprogress);
 $outcome = $restore->process();
+
+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 +82,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..c22faef 100644 (file)
@@ -60,6 +60,10 @@ 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/progress/core_backup_display_progress_if_slow.class.php');
 require_once($CFG->dirroot . '/backup/util/factories/backup_factory.class.php');
 require_once($CFG->dirroot . '/backup/util/factories/restore_factory.class.php');
 require_once($CFG->dirroot . '/backup/util/helper/backup_helper.class.php');
index 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..f893b43
--- /dev/null
@@ -0,0 +1,141 @@
+<?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();
+    }
+
+    /**
+     * When progress is updated, updates the bar.
+     *
+     * @see core_backup_progress::update_progress()
+     */
+    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_display_progress_if_slow.class.php b/backup/util/progress/core_backup_display_progress_if_slow.class.php
new file mode 100644 (file)
index 0000000..ee9511e
--- /dev/null
@@ -0,0 +1,107 @@
+<?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. Same as core_backup_display_progress, but the bar does not
+ * appear until a certain time has elapsed, and disappears automatically
+ * after it finishes.
+ *
+ * The bar can be re-used, i.e. if you end all sections it will disappear,
+ * but if you start all sections, a new bar will be output.
+ *
+ * @package core_backup
+ * @copyright 2013 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class core_backup_display_progress_if_slow extends core_backup_display_progress {
+    /**
+     * @var int Waits this many seconds before displaying progress bar
+     */
+    const DEFAULT_DISPLAY_DELAY = 5;
+
+    /**
+     * @var int Number in the next id to use
+     */
+    private static $nextid = 1;
+
+    /**
+     * @var string HTML id for containing div
+     */
+    protected $id;
+
+    /**
+     * @var int Time at which the progress bar should display (if it isn't yet)
+     */
+    protected $starttime;
+
+    /**
+     * Constructs the progress reporter. This will not output HTML just yet,
+     * until the required delay time expires.
+     *
+     * @param int $delay Delay time (default 5 seconds)
+     */
+    public function __construct($delay = self::DEFAULT_DISPLAY_DELAY) {
+        // Set start time based on delay.
+        $this->starttime = time() + $delay;
+        parent::__construct(false);
+    }
+
+    /**
+     * Adds a div around the parent display so it can be hidden.
+     *
+     * @see core_backup_display_progress::start_html()
+     */
+    public function start_html() {
+        $this->id = 'core_backup_display_progress_if_slow' . self::$nextid;
+        self::$nextid++;
+        echo html_writer::start_div('', array('id' => $this->id));
+        parent::start_html();
+    }
+
+    /**
+     * When progress is updated, after a certain time, starts actually displaying
+     * the progress bar.
+     *
+     * @see core_backup_progress::update_progress()
+     */
+    public function update_progress() {
+        // If we haven't started yet, consider starting.
+        if ($this->starttime) {
+            if (time() > $this->starttime) {
+                $this->starttime = 0;
+            } else {
+                // Do nothing until start time.
+                return;
+            }
+        }
+
+        // We have started, so handle as default.
+        parent::update_progress();
+    }
+
+    /**
+     * Finishes parent display then closes div and hides it.
+     *
+     * @see core_backup_display_progress::end_html()
+     */
+    public function end_html() {
+        parent::end_html();
+        echo html_writer::end_div();
+        echo html_writer::script('document.getElementById("' . $this->id .
+                '").style.display = "none"');
+    }
+}
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 bf80a96..251cbc1 100644 (file)
@@ -50,6 +50,11 @@ class restore_ui extends base_ui {
      */
     protected $stage = null;
 
+    /**
+     * @var core_backup_progress Progress indicator (where there is no controller)
+     */
+    protected $progressreporter = null;
+
     /**
      * String mappings to the above stages
      * @var array
@@ -127,6 +132,38 @@ class restore_ui extends base_ui {
     public function get_restoreid() {
         return $this->controller->get_restoreid();
     }
+
+    /**
+     * Gets the progress reporter object in use for this restore UI.
+     *
+     * IMPORTANT: This progress reporter is used only for UI progress that is
+     * outside the restore controller. The restore controller has its own
+     * progress reporter which is used for progress during the main restore.
+     * Use the restore controller's progress reporter to report progress during
+     * a restore operation, not this one.
+     *
+     * This extra reporter is necessary because on some restore UI screens,
+     * there are long-running tasks even though there is no restore controller
+     * in use.
+     *
+     * @return core_backup_null_progress
+     */
+    public function get_progress_reporter() {
+        if (!$this->progressreporter) {
+            $this->progressreporter = new core_backup_null_progress();
+        }
+        return $this->progressreporter;
+    }
+
+    /**
+     * Sets the progress reporter that will be returned by get_progress_reporter.
+     *
+     * @param core_backup_progress $progressreporter Progress reporter
+     */
+    public function set_progress_reporter(core_backup_progress $progressreporter) {
+        $this->progressreporter = $progressreporter;
+    }
+
     /**
      * Executes the restore plan
      * @return bool
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 a7464bf..327722e 100644 (file)
@@ -93,10 +93,48 @@ abstract class restore_ui_stage extends base_ui_stage {
  * no use for the restore controller.
  */
 abstract class restore_ui_independent_stage {
+    /**
+     * @var core_backup_progress Optional progress reporter
+     */
+    private $progressreporter;
+
     abstract public function __construct($contextid);
     abstract public function process();
     abstract public function display(core_backup_renderer $renderer);
     abstract public function get_stage();
+
+    /**
+     * Gets the progress reporter object in use for this restore UI stage.
+     *
+     * IMPORTANT: This progress reporter is used only for UI progress that is
+     * outside the restore controller. The restore controller has its own
+     * progress reporter which is used for progress during the main restore.
+     * Use the restore controller's progress reporter to report progress during
+     * a restore operation, not this one.
+     *
+     * This extra reporter is necessary because on some restore UI screens,
+     * there are long-running tasks even though there is no restore controller
+     * in use. There is a similar function in restore_ui. but that class is not
+     * used on some stages.
+     *
+     * @return core_backup_null_progress
+     */
+    public function get_progress_reporter() {
+        if (!$this->progressreporter) {
+            $this->progressreporter = new core_backup_null_progress();
+        }
+        return $this->progressreporter;
+    }
+
+    /**
+     * Sets the progress reporter that will be returned by get_progress_reporter.
+     *
+     * @param core_backup_progress $progressreporter Progress reporter
+     */
+    public function set_progress_reporter(core_backup_progress $progressreporter) {
+        $this->progressreporter = $progressreporter;
+    }
+
     /**
      * Gets an array of progress bar items that can be displayed through the restore renderer.
      * @return array Array of items for the progress bar
@@ -142,11 +180,18 @@ abstract class restore_ui_independent_stage {
  *
  * This is the first stage, it is independent.
  */
-class restore_ui_stage_confirm extends restore_ui_independent_stage {
+class restore_ui_stage_confirm extends restore_ui_independent_stage implements file_progress {
+
     protected $contextid;
     protected $filename = null;
     protected $filepath = null;
     protected $details;
+
+    /**
+     * @var bool True if we have started reporting progress
+     */
+    protected $startedprogress = false;
+
     public function __construct($contextid) {
         $this->contextid = $contextid;
         $this->filename = required_param('filename', PARAM_FILE);
@@ -168,7 +213,35 @@ class restore_ui_stage_confirm extends restore_ui_independent_stage {
         $this->filepath = restore_controller::get_tempdir_name($this->contextid, $USER->id);
 
         $fb = get_file_packer();
-        return ($fb->extract_to_pathname("$CFG->tempdir/backup/".$this->filename, "$CFG->tempdir/backup/$this->filepath/"));
+        $result = $fb->extract_to_pathname("$CFG->tempdir/backup/".$this->filename,
+                "$CFG->tempdir/backup/$this->filepath/", null, $this);
+
+        // If any progress happened, end it.
+        if ($this->startedprogress) {
+            $this->get_progress_reporter()->end_progress();
+        }
+        return $result;
+    }
+
+    /**
+     * Implementation for file_progress interface to display unzip progress.
+     *
+     * @param int $progress Current progress
+     * @param int $max Max value
+     */
+    public function progress($progress = file_progress::INDETERMINATE, $max = file_progress::INDETERMINATE) {
+        $reporter = $this->get_progress_reporter();
+
+        // Start tracking progress if necessary.
+        if (!$this->startedprogress) {
+            $reporter->start_progress('extract_file_to_dir',
+                    ($max == file_progress::INDETERMINATE) ? core_backup_progress::INDETERMINATE : $max);
+            $this->startedprogress = true;
+        }
+
+        // Pass progress through to whatever handles it.
+        $reporter->progress(
+                ($progress == file_progress::INDETERMINATE) ? core_backup_progress::INDETERMINATE : $progress);
     }
 
     /**
@@ -475,8 +548,9 @@ class restore_ui_stage_schema extends restore_ui_stage {
         if ($this->stageform === null) {
             $form = new restore_schema_form($this, $PAGE->url);
             $tasks = $this->ui->get_tasks();
-            $content = '';
             $courseheading = false;
+
+            $allsettings = array();
             foreach ($tasks as $task) {
                 if (!($task instanceof restore_root_task)) {
                     if (!$courseheading) {
@@ -484,13 +558,11 @@ class restore_ui_stage_schema extends restore_ui_stage {
                         $form->add_heading('coursesettings', get_string('coursesettings', 'backup'));
                         $courseheading = true;
                     }
-                    // First add each setting
-                    foreach ($task->get_settings() as $setting) {
-                        $form->add_setting($setting, $task);
-                    }
-                    // The add all the dependencies
+                    // Put each setting into an array of settings to add. Adding
+                    // a setting individually is a very slow operation, so we add
+                    // them all in a batch later on.
                     foreach ($task->get_settings() as $setting) {
-                        $form->add_dependencies($setting);
+                        $allsettings[] = array($setting, $task);
                     }
                 } else if ($this->ui->enforce_changed_dependencies()) {
                     // Only show these settings if dependencies changed them.
@@ -505,6 +577,15 @@ class restore_ui_stage_schema extends restore_ui_stage {
                     }
                 }
             }
+
+            // Actually add all the settings that we put in the array.
+            $form->add_settings($allsettings);
+
+            // Add the dependencies for all the settings.
+            foreach ($allsettings as $settingtask) {
+                $form->add_dependencies($settingtask[0]);
+            }
+
             $this->stageform = $form;
         }
         return $this->stageform;
@@ -523,7 +604,7 @@ class restore_ui_stage_schema extends restore_ui_stage {
 class restore_ui_stage_review extends restore_ui_stage {
     /**
      * Constructs the stage
-     * @param backup_ui $ui
+     * @param restore_ui $ui
      */
     public function __construct($ui, array $params=null) {
         $this->stage = restore_ui::STAGE_REVIEW;
@@ -562,7 +643,11 @@ class restore_ui_stage_review extends restore_ui_stage {
             $content = '';
             $courseheading = false;
 
-            foreach ($this->ui->get_tasks() as $task) {
+            $progress = $this->ui->get_progress_reporter();
+            $tasks = $this->ui->get_tasks();
+            $progress->start_progress('initialise_stage_form', count($tasks));
+            $done = 1;
+            foreach ($tasks as $task) {
                 if ($task instanceof restore_root_task) {
                     // If its a backup root add a root settings heading to group nicely
                     $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
@@ -575,7 +660,10 @@ class restore_ui_stage_review extends restore_ui_stage {
                 foreach ($task->get_settings() as $setting) {
                     $form->add_fixed_setting($setting, $task);
                 }
+                // Update progress.
+                $progress->progress($done++);
             }
+            $progress->end_progress();
             $this->stageform = $form;
         }
         return $this->stageform;
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..f18883b 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);
+
+// If we didn't find any badges then print an error.
+if (is_null($out)) {
+    print_error('error:externalbadgedoesntexist', 'badges');
+}
+
+$badges = $out->badges;
+
+// The variable to store the badge we want.
+$badge = '';
+
+// 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 the userid parameter.
+if (empty($badge)) {
+    print_error('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 1c287d7..26451d6 100644 (file)
@@ -84,15 +84,16 @@ if (!empty($rssrecord->preferredtitle)) {
     $feedtitle =  $rss->get_title();
 }
 echo '<table align="center" width="50%" cellspacing="1">'."\n";
-echo '<tr><td colspan="2"><strong>'. $feedtitle .'</strong></td></tr>'."\n";
+echo '<tr><td colspan="2"><strong>'. s($feedtitle) .'</strong></td></tr>'."\n";
 foreach ($rss->get_items() as $item) {
     echo '<tr><td valign="middle">'."\n";
-    echo '<a href="'. $item->get_link() .'" target="_blank"><strong>'. $item->get_title();
+    echo '<a href="'.$item->get_link().'" target="_blank"><strong>';
+    echo s($item->get_title());
     echo '</strong></a>'."\n";
     echo '</td>'."\n";
     echo '</tr>'."\n";
     echo '<tr><td colspan="2"><small>';
-    echo $item->get_description() .'</small></td></tr>'."\n";
+    echo format_text($item->get_description(), FORMAT_HTML) .'</small></td></tr>'."\n";
 }
 echo '</table>'."\n";
 
index 2c36763..5c06993 100644 (file)
@@ -162,7 +162,8 @@ if (!empty($entry->id)) {
 }
 
 require_once('edit_form.php');
-$summaryoptions = array('subdirs'=>false, 'maxfiles'=> 99, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>true, 'context'=>$sitecontext);
+$summaryoptions = array('maxfiles'=> 99, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>true, 'context'=>$sitecontext,
+    'subdirs'=>file_area_contains_subdirs($sitecontext, 'blog', 'post', $entry->id));
 $attachmentoptions = array('subdirs'=>false, 'maxfiles'=> 99, 'maxbytes'=>$CFG->maxbytes);
 
 $blogeditform = new blog_edit_form(null, compact('entry', 'summaryoptions', 'attachmentoptions', 'sitecontext', 'courseid', 'modid'));
index b605016..af1724d 100644 (file)
@@ -119,11 +119,14 @@ class core_blog_renderer extends plugin_renderer_base {
         // Body.
         $o .= format_text($entry->summary, $entry->summaryformat, array('overflowdiv' => true));
 
-        // Uniquehash is used as a link to an external blog.
         if (!empty($entry->uniquehash)) {
-            $o .= $this->output->container_start('externalblog');
-            $o .= html_writer::link($entry->uniquehash, get_string('linktooriginalentry', 'blog'));
-            $o .= $this->output->container_end();
+            // Uniquehash is used as a link to an external blog.
+            $url = clean_param($entry->uniquehash, PARAM_URL);
+            if (!empty($url)) {
+                $o .= $this->output->container_start('externalblog');
+                $o .= html_writer::link($url, get_string('linktooriginalentry', 'blog'));
+                $o .= $this->output->container_end();
+            }
         }
 
         // Links to tags.
index 1b19fb2..da5cd4c 100644 (file)
@@ -756,7 +756,7 @@ class cache_definition {
      */
     public function set_identifiers(array $identifiers = array()) {
         foreach ($this->requireidentifiers as $identifier) {
-            if (!array_key_exists($identifier, $identifiers)) {
+            if (!isset($identifiers[$identifier])) {
                 throw new coding_exception('Identifier required for cache has not been provided: '.$identifier);
             }
         }
index e57d6ef..476ec0b 100644 (file)
@@ -173,7 +173,7 @@ class cache_factory {
      */
     public function create_cache_from_definition($component, $area, array $identifiers = array(), $aggregate = null) {
         $definitionname = $component.'/'.$area;
-        if (array_key_exists($definitionname, $this->cachesfromdefinitions)) {
+        if (isset($this->cachesfromdefinitions[$definitionname])) {
             $cache = $this->cachesfromdefinitions[$definitionname];
             $cache->set_identifiers($identifiers);
             return $cache;
@@ -368,7 +368,7 @@ class cache_factory {
         if ($aggregate) {
             $id .= '::'.$aggregate;
         }
-        if (!array_key_exists($id, $this->definitions)) {
+        if (!isset($this->definitions[$id])) {
             // This is the first time this definition has been requested.
             if ($this->is_initialising()) {
                 // We're initialising the cache right now. Don't try to create another config instance.
diff --git a/calendar/classes/type_base.php b/calendar/classes/type_base.php
new file mode 100644 (file)
index 0000000..6458220
--- /dev/null
@@ -0,0 +1,116 @@
+<?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/>.
+
+namespace core_calendar;
+
+/**
+ * Defines functions used by calendar type plugins.
+ *
+ * This library provides a unified interface for calendar types.
+ *
+ * @package core_calendar
+ * @copyright 2008 onwards Foodle Group {@link http://foodle.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class type_base {
+
+    /**
+     * Returns a list of all the possible days for all months.
+     *
+     * This is used to generate the select box for the days
+     * in the date selector elements. Some months contain more days
+     * than others so this function should return all possible days as
+     * we can not predict what month will be chosen (the user
+     * may have JS turned off and we need to support this situation in
+     * Moodle).
+     *
+     * @return array the days
+     */
+    public abstract function get_days();
+
+    /**
+     * Returns a list of all the names of the months.
+     *
+     * @return array the month names
+     */
+    public abstract function get_months();
+
+    /**
+     * Returns the minimum year of the calendar.
+     *
+     * @return int the minumum year
+     */
+    public abstract function get_min_year();
+
+    /**
+     * Returns the maximum year of the calendar.
+     *
+     * @return int the max year
+     */
+    public abstract function get_max_year();
+
+    /**
+     * Returns a formatted string that represents a date in user time.
+     *
+     * @param int $date the timestamp in UTC, as obtained from the database
+     * @param string $format strftime format
+     * @param int|float|string $timezone the timezone to use
+     *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+     * @param bool $fixday if true then the leading zero from %d is removed,
+     *        if false then the leading zero is maintained
+     * @param bool $fixhour if true then the leading zero from %I is removed,
+     *        if false then the leading zero is maintained
+     * @return string the formatted date/time
+     */
+    public abstract function timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour);
+
+    /**
+     * Given a $time timestamp in GMT (seconds since epoch), returns an array that represents
+     * the date in user time.
+     *
+     * @param int $time timestamp in GMT
+     * @param float|int|string $timezone the timezone to use to calculate the time
+     *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+     * @return array an array that represents the date in user time
+     */
+    public abstract function timestamp_to_date_array($time, $timezone);
+
+    /**
+     * Provided with a day, month, year, hour and minute in the specific
+     * calendar type convert it into the equivalent Gregorian date.
+     *
+     * @param int $year
+     * @param int $month
+     * @param int $day
+     * @param int $hour
+     * @param int $minute
+     * @return array the converted day, month and year.
+     */
+    public abstract function convert_to_gregorian($year, $month, $day, $hour = 0, $minute = 0);
+
+    /**
+     * Provided with a day, month, year, hour and minute in a Gregorian date
+     * convert it into the specific calendar type date.
+     *
+     * @param int $year
+     * @param int $month
+     * @param int $day
+     * @param int $hour
+     * @param int $minute
+     * @return array the converted day, month and year.
+     */
+    public abstract function convert_from_gregorian($year, $month, $day, $hour = 0, $minute = 0);
+}
diff --git a/calendar/classes/type_factory.php b/calendar/classes/type_factory.php
new file mode 100644 (file)
index 0000000..535dd2b
--- /dev/null
@@ -0,0 +1,91 @@
+<?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/>.
+
+namespace core_calendar;
+
+/**
+ * Class \core_calendar\type_factory.
+ *
+ * Factory class producing required subclasses of {@link \core_calendar\type_base}.
+ *
+ * @package core_calendar
+ * @copyright 2008 onwards Foodle Group {@link http://foodle.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class type_factory {
+
+    /**
+     * Returns an instance of the currently used calendar type.
+     *
+     * @param string|null $type the calendar type to use, if none provided use logic to determine
+     * @return calendartype_* the created calendar_type class
+     * @throws coding_exception if the calendar type file could not be loaded
+     */
+    public static function get_calendar_instance($type = null) {
+        if (is_null($type)) {
+            $type = self::get_calendar_type();
+        }
+
+        $class = "\\calendartype_$type\\structure";
+
+        // Ensure the calendar type exists. It may occur that a user has selected a calendar type, which was then
+        // deleted. If this happens we want to fall back on the Gregorian calendar type.
+        if (!class_exists($class)) {
+            $class = "\\calendartype_gregorian\\structure";
+        }
+
+        return new $class();
+    }
+
+    /**
+     * Returns a list of calendar typess available for use.
+     *
+     * @return array the list of calendar types
+     */
+    public static function get_list_of_calendar_types() {
+        $calendars = array();
+        $calendardirs = \core_component::get_plugin_list('calendartype');
+
+        foreach ($calendardirs as $name => $location) {
+            $calendars[$name] = get_string('name', "calendartype_{$name}");
+        }
+
+        return $calendars;
+    }
+
+    /**
+     * Returns the current calendar type in use.
+     *
+     * @return string the current calendar type being used
+     */
+    public static function get_calendar_type() {
+        global $CFG, $USER, $SESSION, $COURSE;
+
+        if (!empty($COURSE->id) and $COURSE->id != SITEID and !empty($COURSE->calendartype)) { // Course calendartype can override all other settings for this page.
+            $return = $COURSE->calendartype;
+        } else if (!empty($SESSION->calendartype)) { // Session calendartype can override other settings.
+            $return = $SESSION->calendartype;
+        } else if (!empty($USER->calendartype)) {
+            $return = $USER->calendartype;
+        } else if (!empty($CFG->calendartype)) {
+            $return = $CFG->calendartype;
+        } else {
+            $return = 'gregorian';
+        }
+
+        return $return;
+    }
+}
index 7fed0c0..ede8280 100644 (file)
@@ -110,6 +110,16 @@ define('CALENDAR_IMPORT_EVENT_UPDATED',  1);
  */
 define('CALENDAR_IMPORT_EVENT_INSERTED', 2);
 
+/**
+ * CALENDAR_SUBSCRIPTION_UPDATE - Used to represent update action for subscriptions in various forms.
+ */
+define('CALENDAR_SUBSCRIPTION_UPDATE', 1);
+
+/**
+ * CALENDAR_SUBSCRIPTION_REMOVE - Used to represent remove action for subscriptions in various forms.
+ */
+define('CALENDAR_SUBSCRIPTION_REMOVE', 2);
+
 /**
  * Return the days of the week
  *
@@ -2914,17 +2924,13 @@ function calendar_add_icalendar_event($event, $courseid, $subscriptionid) {
  * @return string A log of the import progress, including errors
  */
 function calendar_process_subscription_row($subscriptionid, $pollinterval, $action) {
-    global $DB;
 
     // Fetch the subscription from the database making sure it exists.
     $sub = calendar_get_subscription($subscriptionid);
 
-    $strupdate = get_string('update');
-    $strremove = get_string('remove');
-
     // Update or remove the subscription, based on action.
     switch ($action) {
-        case $strupdate:
+        case CALENDAR_SUBSCRIPTION_UPDATE:
             // Skip updating file subscriptions.
             if (empty($sub->url)) {
                 break;
@@ -2935,7 +2941,7 @@ function calendar_process_subscription_row($subscriptionid, $pollinterval, $acti
             // Update the events.
             return "<p>".get_string('subscriptionupdated', 'calendar', $sub->name)."</p>" . calendar_update_subscription_events($subscriptionid);
 
-        case $strremove:
+        case CALENDAR_SUBSCRIPTION_REMOVE:
             calendar_delete_subscription($subscriptionid);
             return get_string('subscriptionremoved', 'calendar', $sub->name);
             break;
index 21782aa..06d60b4 100644 (file)
@@ -33,7 +33,7 @@ $courseid = optional_param('course', SITEID, PARAM_INT);
 // Used for processing subscription actions.
 $subscriptionid = optional_param('id', 0, PARAM_INT);
 $pollinterval  = optional_param('pollinterval', 0, PARAM_INT);
-$action = optional_param('action', '', PARAM_ALPHA);
+$action = optional_param('action', '', PARAM_INT);
 
 $url = new moodle_url('/calendar/managesubscriptions.php');
 if ($courseid != SITEID) {
index fbbc848..4fc9a05 100644 (file)
@@ -836,9 +836,11 @@ class core_calendar_renderer extends plugin_renderer_base {
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'course', 'value' => $courseid));
         $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'id', 'value' => $subscription->id));
         if (!empty($subscription->url)) {
-            $html .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'action', 'value' => get_string('update')));
+            $html .= html_writer::tag('button', get_string('update'), array('type'  => 'submit', 'name' => 'action',
+                                                                            'value' => CALENDAR_SUBSCRIPTION_UPDATE));
         }
-        $html .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'action', 'value' => get_string('remove')));
+        $html .= html_writer::tag('button', get_string('remove'), array('type'  => 'submit', 'name' => 'action',
+                                                                        'value' => CALENDAR_SUBSCRIPTION_REMOVE));
         $html .= html_writer::end_tag('div');
         $html .= html_writer::end_tag('form');
         return $html;
diff --git a/calendar/tests/calendartype_test.php b/calendar/tests/calendartype_test.php
new file mode 100644 (file)
index 0000000..10e72ea
--- /dev/null
@@ -0,0 +1,282 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the class that handles testing the calendar type system.
+ *
+ * @package core_calendar
+ * @copyright 2013 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+// The test calendar type.
+require_once($CFG->dirroot . '/calendar/tests/calendartype_test_example.php');
+
+// Used to test the dateselector elements.
+require_once($CFG->libdir . '/form/dateselector.php');
+require_once($CFG->libdir . '/form/datetimeselector.php');
+
+// Used to test the user datetime profile field.
+require_once($CFG->dirroot . '/user/profile/lib.php');
+require_once($CFG->dirroot . '/user/profile/definelib.php');
+require_once($CFG->dirroot . '/user/profile/index_field_form.php');
+
+/**
+ * Unit tests for the calendar type system.
+ *
+ * @package core_calendar
+ * @copyright 2013 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.6
+ */
+class core_calendar_type_testcase extends advanced_testcase {
+
+    /**
+     * The test user.
+     */
+    private $user;
+
+    /**
+     * Test set up.
+     */
+    protected function setUp() {
+        // The user we are going to test this on.
+        $this->user = self::getDataGenerator()->create_user();
+        self::setUser($this->user);
+    }
+
+    /**
+     * Test that setting the calendar type works.
+     */
+    public function test_calendar_type_set() {
+        // We want to reset the test data after this run.
+        $this->resetAfterTest();
+
+        // Test setting it as the 'Test' calendar type.
+        $this->set_calendar_type('test');
+        $this->assertEquals('test', \core_calendar\type_factory::get_calendar_type());
+
+        // Test setting it as the 'Gregorian' calendar type.
+        $this->set_calendar_type('gregorian');
+        $this->assertEquals('gregorian', \core_calendar\type_factory::get_calendar_type());
+    }
+
+    /**
+     * Test that calling core Moodle functions responsible for displaying the date
+     * have the same results as directly calling the same function in the calendar type.
+     */
+    public function test_calendar_type_core_functions() {
+        // We want to reset the test data after this run.
+        $this->resetAfterTest();
+
+        // Test that the core functions reproduce the same results as the Gregorian calendar.
+        $this->core_functions_test('gregorian');
+
+        // Test that the core functions reproduce the same results as the test calendar.
+        $this->core_functions_test('test');
+    }
+
+    /**
+     * Test that dates selected using the date selector elements are being saved as unixtime, and that the
+     * unixtime is being converted back to a valid date to display in the date selector elements for
+     * different calendar types.
+     */
+    public function test_calendar_type_dateselector_elements() {
+        // We want to reset the test data after this run.
+        $this->resetAfterTest();
+
+        // Check converting dates to Gregorian when submitting a date selector element works. Note: the test
+        // calendar is 2 years, 2 months, 2 days, 2 hours and 2 minutes ahead of the Gregorian calendar.
+        $date1 = array();
+        $date1['day'] = 4;
+        $date1['month'] = 7;
+        $date1['year'] = 2013;
+        $date1['hour'] = 0;
+        $date1['minute'] = 0;
+        $date1['timestamp'] = 1372896000;
+        $this->convert_dateselector_to_unixtime_test('dateselector', 'gregorian', $date1);
+
+        $date2 = array();
+        $date2['day'] = 7;
+        $date2['month'] = 9;
+        $date2['year'] = 2015;
+        $date2['hour'] = 0; // The dateselector element does not have hours.
+        $date2['minute'] = 0; // The dateselector element does not have minutes.
+        $date2['timestamp'] = 1372896000;
+        $this->convert_dateselector_to_unixtime_test('dateselector', 'test', $date2);
+
+        $date3 = array();
+        $date3['day'] = 4;
+        $date3['month'] = 7;
+        $date3['year'] = 2013;
+        $date3['hour'] = 23;
+        $date3['minute'] = 15;
+        $date3['timestamp'] = 1372979700;
+        $this->convert_dateselector_to_unixtime_test('datetimeselector', 'gregorian', $date3);
+
+        $date4 = array();
+        $date4['day'] = 7;
+        $date4['month'] = 9;
+        $date4['year'] = 2015;
+        $date4['hour'] = 1;
+        $date4['minute'] = 17;
+        $date4['timestamp'] = 1372979700;
+        $this->convert_dateselector_to_unixtime_test('datetimeselector', 'test', $date4);
+
+        // The date selector element values are set by using the function usergetdate, here we want to check that
+        // the unixtime passed is being successfully converted to the correct values for the calendar type.
+        $this->convert_unixtime_to_dateselector_test('gregorian', $date3);
+        $this->convert_unixtime_to_dateselector_test('test', $date4);
+    }
+
+    /**
+     * Test that the user profile field datetime minimum and maximum year settings are saved as the
+     * equivalent Gregorian years.
+     */
+    public function test_calendar_type_datetime_field_submission() {
+        // We want to reset the test data after this run.
+        $this->resetAfterTest();
+
+        // Create an array with the input values and expected values once submitted.
+        $date = array();
+        $date['inputminyear'] = '1970';
+        $date['inputmaxyear'] = '2013';
+        $date['expectedminyear'] = '1970';
+        $date['expectedmaxyear'] = '2013';
+        $this->datetime_field_submission_test('gregorian', $date);
+
+        // The test calendar is 2 years, 2 months, 2 days in the future, so when the year 1970 is submitted,
+        // the year 1967 should be saved in the DB, as 1/1/1970 converts to 30/10/1967 in Gregorian.
+        $date['expectedminyear'] = '1967';
+        $date['expectedmaxyear'] = '2010';
+        $this->datetime_field_submission_test('test', $date);
+    }
+
+    /**
+     * Test all the core functions that use the calendar type system.
+     *
+     * @param string $type the calendar type we want to test
+     */
+    private function core_functions_test($type) {
+        $this->set_calendar_type($type);
+
+        // Get the calendar.
+        $calendar = \core_calendar\type_factory::get_calendar_instance();
+
+        // Test the userdate function.
+        $this->assertEquals($calendar->timestamp_to_date_string($this->user->timecreated, '', 99, true, true),
+            userdate($this->user->timecreated));
+    }
+
+    /**
+     * Simulates submitting a form with a date selector element and tests that the chosen dates
+     * are converted into unixtime before being saved in DB.
+     *
+     * @param string $element the form element we are testing
+     * @param string $type the calendar type we want to test
+     * @param array $date the date variables
+     */
+    private function convert_dateselector_to_unixtime_test($element, $type, $date) {
+        $this->set_calendar_type($type);
+
+        if ($element == 'dateselector') {
+            $el = new MoodleQuickForm_date_selector('dateselector', null, array('timezone' => 0.0, 'step' => 1));
+        } else {
+            $el = new MoodleQuickForm_date_time_selector('dateselector', null, array('timezone' => 0.0, 'step' => 1));
+        }
+        $el->_createElements();
+        $submitvalues = array('dateselector' => $date);
+
+        $this->assertSame($el->exportValue($submitvalues), array('dateselector' => $date['timestamp']));
+    }
+
+    /**
+     * Test converting dates from unixtime to a date for the calendar type specified.
+     *
+     * @param string $type the calendar type we want to test
+     * @param array $date the date variables
+     */
+    private function convert_unixtime_to_dateselector_test($type, $date) {
+        $this->set_calendar_type($type);
+
+        // Get the calendar.
+        $calendar = \core_calendar\type_factory::get_calendar_instance();
+
+        $usergetdate = $calendar->timestamp_to_date_array($date['timestamp'], 0.0);
+        $comparedate = array(
+            'minute' => $usergetdate['minutes'],
+            'hour' => $usergetdate['hours'],
+            'day' => $usergetdate['mday'],
+            'month' => $usergetdate['mon'],
+            'year' => $usergetdate['year'],
+            'timestamp' => $date['timestamp']
+        );
+
+        $this->assertEquals($comparedate, $date);
+    }
+
+    /**
+     * Test saving the minimum and max year settings for the user datetime field.
+     *
+     * @param string $type the calendar type we want to test
+     * @param array $date the date variables
+     */
+    private function datetime_field_submission_test($type, $date) {
+        $this->set_calendar_type($type);
+
+        // Get the data we are submitting for the form.
+        $formdata = array();
+        $formdata['id'] = 0;
+        $formdata['shortname'] = 'Shortname';
+        $formdata['name'] = 'Name';
+        $formdata['param1'] = $date['inputminyear'];
+        $formdata['param2'] = $date['inputmaxyear'];
+
+        // Mock submitting this.
+        field_form::mock_submit($formdata);
+
+        // Create the user datetime form.
+        $form = new field_form(null, 'datetime');
+
+        // Get the data from the submission.
+        $submissiondata = $form->get_data();
+        // On the user profile field page after get_data, the function define_save is called
+        // in the field base class, which then calls the field's function define_save_preprocess.
+        $field = new profile_define_datetime();
+        $submissiondata = $field->define_save_preprocess($submissiondata);
+
+        // Create an array we want to compare with the date passed.
+        $comparedate = $date;
+        $comparedate['expectedminyear'] = $submissiondata->param1;
+        $comparedate['expectedmaxyear'] = $submissiondata->param2;
+
+        $this->assertEquals($comparedate, $date);
+    }
+
+    /**
+     * Set the calendar type for this user.
+     *
+     * @param string $type the calendar type we want to set
+     */
+    private function set_calendar_type($type) {
+        $this->user->calendartype = $type;
+        session_set_user($this->user);
+    }
+}
diff --git a/calendar/tests/calendartype_test_example.php b/calendar/tests/calendartype_test_example.php
new file mode 100644 (file)
index 0000000..679422e
--- /dev/null
@@ -0,0 +1,176 @@
+<?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/>.
+
+namespace calendartype_test;
+use \core_calendar\type_base;
+
+/**
+ * Handles calendar functions for the test calendar.
+ *
+ * The test calendar is going to be 2 years, 2 days, 2 hours and 2 minutes
+ * in the future of the Gregorian calendar.
+ *
+ * @package core_calendar
+ * @copyright 2013 Mark Nelson <markn@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class structure extends type_base {
+
+    /**
+     * Returns a list of all the possible days for all months.
+     *
+     * This is used to generate the select box for the days
+     * in the date selector elements. Some months contain more days
+     * than others so this function should return all possible days as
+     * we can not predict what month will be chosen (the user
+     * may have JS turned off and we need to support this situation in
+     * Moodle).
+     *
+     * @return array the days
+     */
+    public function get_days() {
+        $days = array();
+
+        for ($i = 1; $i <= 31; $i++) {
+            $days[$i] = $i;
+        }
+
+        return $days;
+    }
+
+    /**
+     * Returns a list of all the names of the months.
+     *
+     * @return array the month names
+     */
+    public function get_months() {
+        $months = array();
+
+        for ($i = 1; $i <= 12; $i++) {
+            $months[$i] = $i;
+        }
+
+        return $months;
+    }
+
+    /**
+     * Returns the minimum year of the calendar.
+     *
+     * @return int the minumum year
+     */
+    public function get_min_year() {
+        return 1970;
+    }
+
+    /**
+     * Returns the maximum year of the calendar.
+     *
+     * @return int the max year
+     */
+    public function get_max_year() {
+        return 2050;
+    }
+
+    /**
+     * Returns a formatted string that represents a date in user time.
+     *
+     * @param int $date the timestamp in UTC, as obtained from the database
+     * @param string $format strftime format
+     * @param int|float|string $timezone the timezone to use
+     *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+     * @param bool $fixday if true then the leading zero from %d is removed,
+     *        if false then the leading zero is maintained
+     * @param bool $fixhour if true then the leading zero from %I is removed,
+     *        if false then the leading zero is maintained
+     * @return string the formatted date/time
+     */
+    public function timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour) {
+        return '';
+    }
+
+    /**
+     * Given a $time timestamp in GMT (seconds since epoch), returns an array that represents
+     * the date in user time.
+     *
+     * @param int $time timestamp in GMT
+     * @param float|int|string $timezone the timezone to use to calculate the time
+     *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+     * @return array an array that represents the date in user time
+     */
+    public function timestamp_to_date_array($time, $timezone) {
+        $gregoriancalendar = \core_calendar\type_factory::get_calendar_instance('gregorian');
+        $date = $gregoriancalendar->timestamp_to_date_array($time, $timezone);
+        $newdate = $this->convert_from_gregorian($date['year'], $date['mon'], $date['mday'],
+            $date['hours'], $date['minutes']);
+
+        $date['year'] = $newdate['year'];
+        $date['mon'] = $newdate['month'];
+        $date['mday'] = $newdate['day'];
+        $date['hours'] = $newdate['hour'];
+        $date['minutes']  = $newdate['minute'];
+
+        return $date;
+    }
+
+    /**
+     * Provided with a day, month, year, hour and minute
+     * convert it into the equivalent Gregorian date.
+     *
+     * @param int $year
+     * @param int $month
+     * @param int $day
+     * @param int $hour
+     * @param int $minute
+     * @return array the converted day, month, year, hour and minute.
+     */
+    public function convert_to_gregorian($year, $month, $day, $hour = 0, $minute = 0) {
+        $timestamp = make_timestamp($year, $month, $day, $hour, $minute);
+        $date = date('Y/n/j/H/i', strtotime('-2 year, -2 months, -2 days, -2 hours, -2 minutes', $timestamp));
+
+        list($year, $month, $day, $hour, $minute) = explode('/', $date);
+
+        return array('year' => (int) $year,
+                     'month' => (int) $month,
+                     'day' => (int) $day,
+                     'hour' => (int) $hour,
+                     'minute' => (int) $minute);
+
+    }
+
+    /**
+     * Provided with a day, month, year, hour and minute in a Gregorian date
+     * convert it into the specific calendar type date.
+     *
+     * @param int $year
+     * @param int $month
+     * @param int $day
+     * @param int $hour
+     * @param int $minute
+     * @return array the converted day, month, year, hour and minute.
+     */
+    public function convert_from_gregorian($year, $month, $day, $hour = 0, $minute = 0) {
+        $timestamp = make_timestamp($year, $month, $day, $hour, $minute);
+        $date = date('Y/n/j/H/i', strtotime('+2 year, +2 months, +2 days, +2 hours, +2 minutes', $timestamp));
+
+        list($year, $month, $day, $hour, $minute) = explode('/', $date);
+
+        return array('year' => (int) $year,
+                     'month' => (int) $month,
+                     'day' => (int) $day,
+                     'hour' => (int) $hour,
+                     'minute' => (int) $minute);
+    }
+}
diff --git a/calendar/type/gregorian/classes/structure.php b/calendar/type/gregorian/classes/structure.php
new file mode 100644 (file)
index 0000000..d209864
--- /dev/null
@@ -0,0 +1,232 @@
+<?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/>.
+
+namespace calendartype_gregorian;
+use core_calendar\type_base;
+
+/**
+ * Handles calendar functions for the gregorian calendar.
+ *
+ * @package calendartype_gregorian
+ * @copyright 2008 onwards Foodle Group {@link http://foodle.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class structure extends type_base {
+
+    /**
+     * Returns a list of all the possible days for all months.
+     *
+     * This is used to generate the select box for the days
+     * in the date selector elements. Some months contain more days
+     * than others so this function should return all possible days as
+     * we can not predict what month will be chosen (the user
+     * may have JS turned off and we need to support this situation in
+     * Moodle).
+     *
+     * @return array the days
+     */
+    public function get_days() {
+        $days = array();
+
+        for ($i = 1; $i <= 31; $i++) {
+            $days[$i] = $i;
+        }
+
+        return $days;
+    }
+
+    /**
+     * Returns a list of all the names of the months.
+     *
+     * @return array the month names
+     */
+    public function get_months() {
+        $months = array();
+
+        for ($i = 1; $i <= 12; $i++) {
+            $months[$i] = userdate(gmmktime(12, 0, 0, $i, 15, 2000), '%B');
+        }
+
+        return $months;
+    }
+
+    /**
+     * Returns the minimum year of the calendar.
+     *
+     * @return int the minumum year
+     */
+    public function get_min_year() {
+        return 1900;
+    }
+
+    /**
+     * Returns the maximum year of the calendar.
+     *
+     * @return int the max year
+     */
+    public function get_max_year() {
+        return 2050;
+    }
+
+    /**
+     * Returns a formatted string that represents a date in user time.
+     *
+     * Returns a formatted string that represents a date in user time
+     * <b>WARNING: note that the format is for strftime(), not date().</b>
+     * Because of a bug in most Windows time libraries, we can't use
+     * the nicer %e, so we have to use %d which has leading zeroes.
+     * A lot of the fuss in the function is just getting rid of these leading
+     * zeroes as efficiently as possible.
+     *
+     * If parameter fixday = true (default), then take off leading
+     * zero from %d, else maintain it.
+     *
+     * @param int $date the timestamp in UTC, as obtained from the database
+     * @param string $format strftime format
+     * @param int|float|string $timezone the timezone to use
+     *        {@link http://docs.moodle.org/dev/Time_API#Timezone}
+     * @param bool $fixday if true then the leading zero from %d is removed,
+     *        if false then the leading zero is maintained
+     * @param bool $fixhour if true then the leading zero from %I is removed,
+     *        if false then the leading zero is maintained
+     * @return string the formatted date/time
+     */
+    public function timestamp_to_date_string($date, $format, $timezone, $fixday, $fixhour) {
+        global $CFG;
+
+        if (empty($format)) {
+            $format = get_string('strftimedaydatetime', 'langconfig');
+        }
+
+        if (!empty($CFG->nofixday)) { // Config.php can force %d not to be fixed.
+            $fixday = false;
+        } else if ($fixday) {
+            $formatnoday = str_replace('%d', 'DD', $format);
+            $fixday = ($formatnoday != $format);
+            $format = $formatnoday;
+        }
+
+        // Note: This logic about fixing 12-hour time to remove unnecessary leading
+        // zero is required because on Windows,