Merge branch 'MDL-30022_message_duplicate_23' of git://github.com/andyjdavis/moodle...
authorSam Hemelryk <sam@moodle.com>
Tue, 4 Sep 2012 04:50:23 +0000 (16:50 +1200)
committerSam Hemelryk <sam@moodle.com>
Tue, 4 Sep 2012 04:50:23 +0000 (16:50 +1200)
215 files changed:
admin/environment.xml
admin/roles/lib.php
admin/settings/courses.php
admin/settings/location.php
admin/tool/assignmentupgrade/module.js
admin/tool/phpunit/cli/util.php
auth/email/auth.php
auth/ldap/auth.php
auth/manual/auth.php
backup/moodle2/backup_custom_fields.php
backup/moodle2/restore_stepslib.php
backup/upgrade.txt [new file with mode: 0644]
backup/util/dbops/backup_plan_dbops.class.php
backup/util/dbops/restore_dbops.class.php
backup/util/helper/backup_cron_helper.class.php
backup/util/helper/tests/cronhelper_test.php [new file with mode: 0644]
backup/util/plan/backup_structure_step.class.php
backup/util/plan/restore_structure_step.class.php
backup/util/structure/backup_nested_element.class.php
backup/util/ui/backup_ui_stage.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/restore_ui_stage.class.php
backup/util/ui/yui/backupselectall/backupselectall.js [new file with mode: 0644]
blocks/completionstatus/block_completionstatus.php
blocks/completionstatus/details.php
blocks/completionstatus/lang/en/block_completionstatus.php
blocks/tags/block_tags.php
blog/locallib.php
course/edit_form.php
course/editsection_form.php
course/format/renderer.php
course/lib.php
course/recent_form.php
course/reset_form.php
course/tests/externallib_test.php
enrol/category/cli/sync.php
enrol/category/db/access.php
enrol/category/db/events.php
enrol/category/db/install.php
enrol/category/lang/en/enrol_category.php
enrol/category/lib.php
enrol/category/locallib.php
enrol/category/settings.php
enrol/category/tests/sync_test.php [new file with mode: 0644]
enrol/category/version.php
enrol/cohort/lib.php
enrol/locallib.php
enrol/manual/locallib.php
enrol/paypal/ipn.php
filter/algebra/filter.php
filter/algebra/tests/filter_test.php [new file with mode: 0644]
grade/lib.php
install/lang/es_mx/langconfig.php
install/lang/ko/install.php
install/lang/lt/admin.php
install/lang/pl/admin.php
install/lang/zh_cn/install.php
iplookup/index.php
iplookup/module.js
lang/en/admin.php
lang/en/backup.php
lang/en/completion.php
lang/en/moodle.php
lang/en/repository.php
lib/accesslib.php
lib/blocklib.php
lib/boxlib.php
lib/completion/completion_completion.php
lib/completionlib.php
lib/cronlib.php
lib/db/upgradelib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/stored_file.php
lib/form/tests/dateselector_test.php
lib/form/tests/datetimeselector_test.php
lib/googleapi.php
lib/grade/grade_item.php
lib/messagelib.php
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/oauthlib.php
lib/phpunit/classes/data_generator.php
lib/phpunit/classes/hint_resultprinter.php
lib/phpunit/classes/util.php
lib/phpunit/tests/generator_test.php
lib/pluginlib.php
lib/questionlib.php
lib/tests/backup_test.php [deleted file]
lib/tests/completionlib_test.php
lib/tests/moodlelib_test.php
lib/tests/pluginlib_test.php
lib/tests/questionlib_test.php
login/index.php
message/edit.php
mod/assign/gradingtable.php
mod/assign/index.php
mod/assign/lang/en/assign.php
mod/assign/mod_form.php
mod/assign/module.js
mod/assign/renderer.php
mod/assignment/assignment.js
mod/assignment/lang/en/assignment.php
mod/assignment/mod_form.php
mod/assignment/type/upload/assignment.class.php
mod/book/backup/moodle2/restore_book_activity_task.class.php
mod/book/version.php
mod/chat/lib.php
mod/choice/backup/moodle2/restore_choice_stepslib.php
mod/data/field/checkbox/mod.html
mod/data/field/latlong/field.class.php
mod/data/field/menu/mod.html
mod/data/field/multimenu/mod.html
mod/data/field/picture/field.class.php
mod/data/field/picture/mod.html
mod/data/field/radiobutton/mod.html
mod/data/field/textarea/mod.html
mod/data/lib.php
mod/data/styles.css
mod/data/templates.php
mod/feedback/item/multichoice/lib.php
mod/feedback/item/multichoicerated/lib.php
mod/feedback/lang/en/feedback.php
mod/forum/lib.php
mod/lesson/format.php
mod/lesson/lib.php
mod/quiz/backup/moodle1/lib.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/quiz/lib.php
mod/quiz/renderer.php
mod/quiz/report/responses/responses_table.php
mod/quiz/report/upgrade.txt
mod/quiz/styles.css
mod/quiz/version.php
mod/wiki/editors/wikieditor.php
mod/wiki/lib.php
mod/workshop/form/comments/backup/moodle1/lib.php
mod/workshop/form/numerrors/backup/moodle1/lib.php
mod/workshop/form/rubric/backup/moodle1/lib.php
phpunit.xml.dist
question/category_class.php
question/editlib.php
question/engine/bank.php
question/engine/datalib.php
question/format.php
question/format/blackboard/format.php
question/format/blackboard/lang/en/qformat_blackboard.php
question/format/blackboard/tests/blackboardformat_test.php [new file with mode: 0644]
question/format/blackboard/tests/fixtures/sample_blackboard.dat [new file with mode: 0644]
question/format/blackboard/version.php
question/format/blackboard_six/format.php
question/format/blackboard_six/formatbase.php [new file with mode: 0644]
question/format/blackboard_six/formatpool.php [new file with mode: 0644]
question/format/blackboard_six/formatqti.php [new file with mode: 0644]
question/format/blackboard_six/lang/en/qformat_blackboard_six.php
question/format/blackboard_six/tests/blackboardformatpool_test.php [new file with mode: 0644]
question/format/blackboard_six/tests/blackboardsixformatqti_test.php [new file with mode: 0644]
question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat [new file with mode: 0644]
question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat [new file with mode: 0644]
question/format/blackboard_six/version.php
question/format/examview/format.php
question/format/examview/lang/en/qformat_examview.php
question/format/examview/tests/examviewformat_test.php [new file with mode: 0644]
question/format/examview/tests/fixtures/examview_sample.xml [new file with mode: 0644]
question/format/examview/version.php
question/format/gift/format.php
question/format/multianswer/format.php
question/format/multianswer/tests/fixtures/questions.multianswer.txt [new file with mode: 0644]
question/format/multianswer/tests/multianswerformat_test.php [new file with mode: 0644]
question/previewlib.php
question/type/calculated/questiontype.php
question/type/edit_question_form.php
question/type/essay/db/upgrade.php
question/type/match/lang/en/qtype_match.php
question/type/multianswer/lang/en/qtype_multianswer.php
question/type/multianswer/questiontype.php
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/multichoice/styles.css
question/type/numerical/question.php
question/type/numerical/tests/answer_test.php
question/type/shortanswer/lang/en/qtype_shortanswer.php
report/backups/index.php
report/stats/lib.php
report/stats/locallib.php
repository/alfresco/lib.php
repository/boxnet/lib.php
repository/dropbox/db/upgrade.php [new file with mode: 0644]
repository/dropbox/lang/en/repository_dropbox.php
repository/dropbox/lib.php
repository/dropbox/locallib.php
repository/dropbox/thumbnail.php [new file with mode: 0644]
repository/dropbox/version.php
repository/equella/lib.php
repository/filepicker.php
repository/filesystem/lib.php
repository/flickr/lib.php
repository/flickr_public/lib.php
repository/googledocs/lib.php
repository/lib.php
repository/repository_ajax.php
tag/edit.php
tag/lib.php
tag/locallib.php
theme/afterburner/style/afterburner_styles.css
theme/anomaly/style/general.css
theme/base/style/admin.css
theme/base/style/core.css
theme/formal_white/style/formal_white.css
theme/yui_combo.php
user/profile.php
user/selector/lib.php
user/selector/module.js
version.php

index 004cab0..584a9b0 100644 (file)
         </FEEDBACK>
       </PHP_SETTING>
     </PHP_SETTINGS>
-</MOODLE>
+  </MOODLE>
+  <MOODLE version="2.4" requires="2.2">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mysql" version="5.1.33" />
+      <VENDOR name="postgres" version="8.3" />
+      <VENDOR name="mssql" version="9.0" />
+      <VENDOR name="odbc_mssql" version="9.0" />
+      <VENDOR name="mssql_n" version="9.0" />
+      <VENDOR name="oracle" version="10.2" />
+      <VENDOR name="sqlite" version="2.0" />
+    </DATABASE>
+    <PHP version="5.3.2" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_CHECK message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_CHECK message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opensslrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="gdrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_CHECK message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_CHECK message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="intlrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="40M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="safe_mode" value="0" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingsafemode" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index d57233f..93703c8 100644 (file)
@@ -1042,12 +1042,10 @@ class potential_assignees_below_course extends role_assign_user_selector_base {
         $countfields = 'SELECT COUNT(u.id)';
 
         $sql   = " FROM {user} u
-                  WHERE u.id IN ($enrolsql) $wherecondition
-                        AND u.id NOT IN (
-                           SELECT r.userid
-                             FROM {role_assignments} r
-                            WHERE r.contextid = :contextid
-                                  AND r.roleid = :roleid)";
+              LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.roleid = :roleid AND ra.contextid = :contextid)
+                  WHERE u.id IN ($enrolsql)
+                        $wherecondition
+                        AND ra.id IS NULL";
         $order = ' ORDER BY lastname ASC, firstname ASC';
 
         $params['contextid'] = $this->context->id;
index 92722eb..f07107c 100644 (file)
@@ -47,6 +47,11 @@ if ($hassiteconfig
         $temp->add(new admin_setting_configselect('moodlecourse/legacyfiles', new lang_string('courselegacyfiles'), new lang_string('courselegacyfiles_help'), key($choices), $choices));
     }
 
+    $choices = array();
+    $choices[COURSE_DISPLAY_SINGLEPAGE] = new lang_string('coursedisplay_single');
+    $choices[COURSE_DISPLAY_MULTIPAGE] = new lang_string('coursedisplay_multi');
+    $temp->add(new admin_setting_configselect('moodlecourse/coursedisplay', new lang_string('coursedisplay'), new lang_string('coursedisplay_help'), COURSE_DISPLAY_SINGLEPAGE, $choices));
+
     $temp->add(new admin_setting_heading('groups', new lang_string('groups', 'group'), ''));
     $choices = array();
     $choices[NOGROUPS] = new lang_string('groupsnone', 'group');
index 3d3ccb8..9f74d51 100644 (file)
@@ -14,7 +14,8 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
 
     $temp->add(new admin_setting_heading('iplookup', new lang_string('iplookup', 'admin'), new lang_string('iplookupinfo', 'admin')));
     $temp->add(new admin_setting_configfile('geoipfile', new lang_string('geoipfile', 'admin'), new lang_string('configgeoipfile', 'admin', $CFG->dataroot.'/geoip/'), $CFG->dataroot.'/geoip/GeoLiteCity.dat'));
-    $temp->add(new admin_setting_configtext('googlemapkey', new lang_string('googlemapkey', 'admin'), new lang_string('configgooglemapkey', 'admin', $CFG->wwwroot), ''));
+    $temp->add(new admin_setting_configtext('googlemapkey', new lang_string('googlemapkey', 'admin'), new lang_string('configgooglemapkey', 'admin', $CFG->wwwroot), '', PARAM_RAW, 60));
+    $temp->add(new admin_setting_configtext('googlemapkey3', new lang_string('googlemapkey3', 'admin'), new lang_string('googlemapkey3_help', 'admin'), '', PARAM_RAW, 60));
 
     $temp->add(new admin_setting_configtext('allcountrycodes', new lang_string('allcountrycodes', 'admin'), new lang_string('configallcountrycodes', 'admin'), '', '/^(?:\w+(?:,\w+)*)?$/'));
 
index edee839..59df8c5 100644 (file)
@@ -42,8 +42,8 @@ M.tool_assignmentupgrade = {
             }
         });
 
-        var batchform = Y.one('.tool_assignmentupgrade_batchform form');
-        batchform.on('submit', function(e) {
+        var upgradeselectedbutton = Y.one('#id_upgradeselected');
+        upgradeselectedbutton.on('click', function(e) {
             checkboxes = Y.all('td.c0 input');
             var selectedassignments = [];
             checkboxes.each(function(node) {
index 2986d64..5afd3ba 100644 (file)
@@ -150,7 +150,7 @@ if ($diag) {
 } else if ($drop) {
     // make sure tests do not run in parallel
     phpunit_util::acquire_test_lock();
-    phpunit_util::drop_site();
+    phpunit_util::drop_site(true);
     // note: we must stop here because $CFG is messed up and we can not reinstall, sorry
     exit(0);
 
index 0051aaf..e50c09e 100644 (file)
@@ -132,7 +132,9 @@ class auth_plugin_email extends auth_plugin_base {
 
             } else if ($user->secret == $confirmsecret) {   // They have provided the secret key to get in
                 $DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
-                $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+                if ($user->firstaccess == 0) {
+                    $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+                }
                 return AUTH_CONFIRM_OK;
             }
         } else {
index bc90ebc..88bb10d 100644 (file)
@@ -546,7 +546,9 @@ class auth_plugin_ldap extends auth_plugin_base {
                     return AUTH_CONFIRM_FAIL;
                 }
                 $DB->set_field('user', 'confirmed', 1, array('id'=>$user->id));
-                $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id));
+                if ($user->firstaccess == 0) {
+                    $DB->set_field('user', 'firstaccess', time(), array('id'=>$user->id));
+                }
                 return AUTH_CONFIRM_OK;
             }
         } else {
index e3df78a..29cb59a 100644 (file)
@@ -170,7 +170,9 @@ class auth_plugin_manual extends auth_plugin_base {
                 return AUTH_CONFIRM_ALREADY;
             } else {
                 $DB->set_field("user", "confirmed", 1, array("id"=>$user->id));
-                $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+                if ($user->firstaccess == 0) {
+                    $DB->set_field("user", "firstaccess", time(), array("id"=>$user->id));
+                }
                 return AUTH_CONFIRM_OK;
             }
         } else  {
index e0ceb33..12929de 100644 (file)
@@ -96,14 +96,19 @@ class file_nested_element extends backup_nested_element {
         if (is_null($this->backupid)) {
             $this->backupid = $processor->get_var(backup::VAR_BACKUPID);
         }
-        parent::process($processor);
+        return parent::process($processor);
     }
 
     public function fill_values($values) {
         // Fill values
         parent::fill_values($values);
         // Do our own tasks (copy file from moodle to backup)
-        backup_file_manager::copy_file_moodle2backup($this->backupid, $values);
+        try {
+            backup_file_manager::copy_file_moodle2backup($this->backupid, $values);
+        } catch (file_exception $e) {
+            $this->add_result(array('missing_files_in_pool' => true));
+            $this->add_log('missing file in pool: ' . $e->debuginfo, backup::LOG_WARNING);
+        }
     }
 }
 
index 19d183a..dbd2c14 100644 (file)
@@ -270,7 +270,14 @@ class restore_gradebook_structure_step extends restore_structure_step {
 
         $data->contextid = get_context_instance(CONTEXT_COURSE, $this->get_courseid())->id;
 
-        $newitemid = $DB->insert_record('grade_letters', $data);
+        $gradeletter = (array)$data;
+        unset($gradeletter['id']);
+        if (!$DB->record_exists('grade_letters', $gradeletter)) {
+            $newitemid = $DB->insert_record('grade_letters', $data);
+        } else {
+            $newitemid = $data->id;
+        }
+
         $this->set_mapping('grade_letter', $oldid, $newitemid);
     }
     protected function process_grade_setting($data) {
@@ -2401,17 +2408,21 @@ class restore_activity_grades_structure_step extends restore_structure_step {
 
     /**
      * process activity grade_letters. Note that, while these are possible,
-     * because grade_letters are contextid based, in proctice, only course
+     * because grade_letters are contextid based, in practice, only course
      * context letters can be defined. So we keep here this method knowing
      * it won't be executed ever. gradebook restore will restore course letters.
      */
     protected function process_grade_letter($data) {
         global $DB;
 
-        $data = (object)$data;
+        $data['contextid'] = $this->task->get_contextid();
+        $gradeletter = (object)$data;
 
-        $data->contextid = $this->task->get_contextid();
-        $newitemid = $DB->insert_record('grade_letters', $data);
+        // Check if it exists before adding it
+        unset($data['id']);
+        if (!$DB->record_exists('grade_letters', $data)) {
+            $newitemid = $DB->insert_record('grade_letters', $gradeletter);
+        }
         // no need to save any grade_letter mapping
     }
 }
diff --git a/backup/upgrade.txt b/backup/upgrade.txt
new file mode 100644 (file)
index 0000000..cb02095
--- /dev/null
@@ -0,0 +1,17 @@
+This files describes API changes in /backup/*,
+information provided here is intended especially for developers.
+
+=== 2.4 ===
+
+* Since 2.3.1+ the backup file name schema has changed. The ID of the course will always be part of
+    the filename regardless of the setting 'backup_shortname'. See MDL-33812.
+
+=== 2.3 ===
+
+* Since 2.3.1+ the backup file name schema has changed. The ID of the course will always be part of
+    the filename regardless of the setting 'backup_shortname'. See MDL-33812.
+
+=== 2.2 ===
+
+* Since 2.2.4+ the backup file name schema has changed. The ID of the course will always be part of
+    the filename regardless of the setting 'backup_shortname'. See MDL-33812.
\ No newline at end of file
index 2ce9434..0752f0d 100644 (file)
@@ -112,7 +112,7 @@ abstract class backup_plan_dbops extends backup_dbops {
 
         // Get all sections belonging to requested course
         $sectionsarr = array();
-        $sections = $DB->get_records('course_sections', array('course' => $courseid));
+        $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section');
         foreach ($sections as $section) {
             $sectionsarr[] = $section->id;
         }
@@ -197,19 +197,19 @@ abstract class backup_plan_dbops extends backup_dbops {
     * @param int $courseid/$sectionid/$cmid
     * @param bool $users Should be true is users were included in the backup
     * @param bool $anonymised Should be true is user information was anonymized.
-    * @param bool $useidasname true to use id, false to use strings (default)
+    * @param bool $useidonly only use the ID in the file name
     * @return string The filename to use
     */
-    public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $useidasname = false) {
+    public static function get_default_backup_filename($format, $type, $id, $users, $anonymised, $useidonly = false) {
         global $DB;
 
         // Calculate backup word
         $backupword = str_replace(' ', '_', textlib::strtolower(get_string('backupfilename')));
         $backupword = trim(clean_filename($backupword), '_');
 
+        // Not $useidonly, lets fetch the name
         $shortname = '';
-        // Not $useidasname, lets calculate it, else $id will be used
-        if (!$useidasname) {
+        if (!$useidonly) {
             // Calculate proper name element (based on type)
             switch ($type) {
                 case backup::TYPE_1COURSE:
@@ -231,7 +231,11 @@ abstract class backup_plan_dbops extends backup_dbops {
             $shortname = textlib::strtolower(trim(clean_filename($shortname), '_'));
         }
 
-        $name = empty($shortname) ? $id : $shortname;
+        // The name will always contain the ID, but we append the course short name if requested.
+        $name = $id;
+        if (!$useidonly && $shortname != '') {
+            $name .= '-' . $shortname;
+        }
 
         // Calculate date
         $backupdateformat = str_replace(' ', '_', get_string('backupnameformat', 'langconfig'));
index 2e73e8f..fb7beb3 100644 (file)
@@ -819,10 +819,13 @@ abstract class restore_dbops {
      * @param int|null $olditemid
      * @param int|null $forcenewcontextid explicit value for the new contextid (skip mapping)
      * @param bool $skipparentitemidctxmatch
+     * @return array of result object
      */
     public static function send_files_to_pool($basepath, $restoreid, $component, $filearea, $oldcontextid, $dfltuserid, $itemname = null, $olditemid = null, $forcenewcontextid = null, $skipparentitemidctxmatch = false) {
         global $DB;
 
+        $results = array();
+
         if ($forcenewcontextid) {
             // Some components can have "forced" new contexts (example: questions can end belonging to non-standard context mappings,
             // with questions originally at system/coursecat context in source being restored to course context in target). So we need
@@ -902,8 +905,14 @@ abstract class restore_dbops {
                 // this is a regular file, it must be present in the backup pool
                 $backuppath = $basepath . backup_file_manager::get_backup_content_file_location($file->contenthash);
 
+                // The file is not found in the backup.
                 if (!file_exists($backuppath)) {
-                    throw new restore_dbops_exception('file_not_found_in_pool', $file);
+                    $result = new stdClass();
+                    $result->code = 'file_missing_in_backup';
+                    $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
+                    $result->level = backup::LOG_WARNING;
+                    $results[] = $result;
+                    continue;
                 }
 
                 // create the file in the filepool if it does not exist yet
@@ -960,6 +969,7 @@ abstract class restore_dbops {
             }
         }
         $rs->close();
+        return $results;
     }
 
     /**
index 9715a5d..f7c6bc1 100644 (file)
@@ -46,6 +46,8 @@ abstract class backup_cron_automated_helper {
     const BACKUP_STATUS_UNFINISHED = 2;
     /** Course automated backup was skipped */
     const BACKUP_STATUS_SKIPPED = 3;
+    /** Course automated backup had warnings */
+    const BACKUP_STATUS_WARNING = 4;
 
     /** Run if required by the schedule set in config. Default. **/
     const RUN_ON_SCHEDULE = 0;
@@ -110,7 +112,7 @@ abstract class backup_cron_automated_helper {
             $nextstarttime = backup_cron_automated_helper::calculate_next_automated_backup($admin->timezone, $now);
             $showtime = "undefined";
             if ($nextstarttime > 0) {
-                $showtime = userdate($nextstarttime,"",$admin->timezone);
+                $showtime = date('r', $nextstarttime);
             }
 
             $rs = $DB->get_recordset('course');
@@ -124,7 +126,14 @@ abstract class backup_cron_automated_helper {
                 }
 
                 // Skip courses that do not yet need backup
-                $skipped = !(($backupcourse->nextstarttime >= 0 && $backupcourse->nextstarttime < $now) || $rundirective == self::RUN_IMMEDIATELY);
+                $skipped = !(($backupcourse->nextstarttime > 0 && $backupcourse->nextstarttime < $now) || $rundirective == self::RUN_IMMEDIATELY);
+                if ($skipped && $backupcourse->nextstarttime != $nextstarttime) {
+                    $backupcourse->nextstarttime = $nextstarttime;
+                    $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED;
+                    $DB->update_record('backup_courses', $backupcourse);
+                    mtrace('Backup of \'' . $course->fullname . '\' is scheduled on ' . $showtime);
+                }
+
                 // Skip backup of unavailable courses that have remained unmodified in a month
                 if (!$skipped && empty($course->visible) && ($now - $course->timemodified) > 31*24*60*60) {  //Hidden + settings were unmodified last month
                     //Check log if there were any modifications to the course content
@@ -132,16 +141,17 @@ abstract class backup_cron_automated_helper {
                     $params = array('courseid' => $course->id, 'time' => $now-31*24*60*60, 'action' => '%view%');
                     $logexists = $DB->record_exists_select('log', $sqlwhere, $params);
                     if (!$logexists) {
-                        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_SKIPPED;
+                        $backupcourse->laststatus = self::BACKUP_STATUS_SKIPPED;
                         $backupcourse->nextstarttime = $nextstarttime;
                         $DB->update_record('backup_courses', $backupcourse);
                         mtrace('Skipping unchanged course '.$course->fullname);
                         $skipped = true;
                     }
                 }
+
                 //Now we backup every non-skipped course
                 if (!$skipped) {
-                    mtrace('Backing up '.$course->fullname'...');
+                    mtrace('Backing up '.$course->fullname.'...');
 
                     //We have to send a email because we have included at least one backup
                     $emailpending = true;
@@ -152,7 +162,7 @@ abstract class backup_cron_automated_helper {
                         $starttime = time();
 
                         $backupcourse->laststarttime = time();
-                        $backupcourse->laststatus = backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED;
+                        $backupcourse->laststatus = self::BACKUP_STATUS_UNFINISHED;
                         $DB->update_record('backup_courses', $backupcourse);
 
                         $backupcourse->laststatus = backup_cron_automated_helper::launch_automated_backup($course, $backupcourse->laststarttime, $admin->id);
@@ -161,7 +171,7 @@ abstract class backup_cron_automated_helper {
 
                         $DB->update_record('backup_courses', $backupcourse);
 
-                        if ($backupcourse->laststatus) {
+                        if ($backupcourse->laststatus === self::BACKUP_STATUS_OK) {
                             // Clean up any excess course backups now that we have
                             // taken a successful backup.
                             $removedcount = backup_cron_automated_helper::remove_excess_backups($course);
@@ -180,17 +190,18 @@ abstract class backup_cron_automated_helper {
             $message = "";
 
             $count = backup_cron_automated_helper::get_backup_status_array();
-            $haserrors = ($count[backup_cron_automated_helper::BACKUP_STATUS_ERROR] != 0 || $count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED] != 0);
+            $haserrors = ($count[self::BACKUP_STATUS_ERROR] != 0 || $count[self::BACKUP_STATUS_UNFINISHED] != 0);
 
             //Build the message text
             //Summary
             $message .= get_string('summary')."\n";
             $message .= "==================================================\n";
             $message .= "  ".get_string('courses').": ".array_sum($count)."\n";
-            $message .= "  ".get_string('ok').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_OK]."\n";
-            $message .= "  ".get_string('skipped').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_SKIPPED]."\n";
-            $message .= "  ".get_string('error').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_ERROR]."\n";
-            $message .= "  ".get_string('unfinished').": ".$count[backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED]."\n\n";
+            $message .= "  ".get_string('ok').": ".$count[self::BACKUP_STATUS_OK]."\n";
+            $message .= "  ".get_string('skipped').": ".$count[self::BACKUP_STATUS_SKIPPED]."\n";
+            $message .= "  ".get_string('error').": ".$count[self::BACKUP_STATUS_ERROR]."\n";
+            $message .= "  ".get_string('unfinished').": ".$count[self::BACKUP_STATUS_UNFINISHED]."\n";
+            $message .= "  ".get_string('warning').": ".$count[self::BACKUP_STATUS_WARNING]."\n\n";
 
             //Reference
             if ($haserrors) {
@@ -253,9 +264,10 @@ abstract class backup_cron_automated_helper {
             self::BACKUP_STATUS_OK => 0,
             self::BACKUP_STATUS_UNFINISHED => 0,
             self::BACKUP_STATUS_SKIPPED => 0,
+            self::BACKUP_STATUS_WARNING => 0
         );
 
-        $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) statuscount FROM {backup_courses} bc GROUP BY bc.laststatus');
+        $statuses = $DB->get_records_sql('SELECT DISTINCT bc.laststatus, COUNT(bc.courseid) AS statuscount FROM {backup_courses} bc GROUP BY bc.laststatus');
 
         foreach ($statuses as $status) {
             if (empty($status->statuscount)) {
@@ -270,38 +282,47 @@ abstract class backup_cron_automated_helper {
     /**
      * Works out the next time the automated backup should be run.
      *
-     * @param mixed $timezone
-     * @param int $now
-     * @return int
+     * @param mixed $timezone user timezone
+     * @param int $now timestamp, should not be in the past, most likely time()
+     * @return int timestamp of the next execution at server time
      */
     public static function calculate_next_automated_backup($timezone, $now) {
 
-        $result = -1;
+        $result = 0;
         $config = get_config('backup');
-        $midnight = usergetmidnight($now, $timezone);
+        $autohour = $config->backup_auto_hour;
+        $automin = $config->backup_auto_minute;
+
+        // Gets the user time relatively to the server time.
         $date = usergetdate($now, $timezone);
+        $usertime = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']);
+        $diff = $now - $usertime;
 
-        // Get number of days (from today) to execute backups
+        // Get number of days (from user's today) to execute backups.
         $automateddays = substr($config->backup_auto_weekdays, $date['wday']) . $config->backup_auto_weekdays;
-        $daysfromtoday = strpos($automateddays, "1", 1);
+        $daysfromnow = strpos($automateddays, "1");
 
-        // If we can't find the next day, we set it to tomorrow
-        if (empty($daysfromtoday)) {
-            $daysfromtoday = 1;
+        // Error, there are no days to schedule the backup for.
+        if ($daysfromnow === false) {
+            return 0;
         }
 
-        // If some day has been found
-        if ($daysfromtoday !== false) {
-            // Calculate distance
-            $dist = ($daysfromtoday * 86400) +                // Days distance
-                    ($config->backup_auto_hour * 3600) +      // Hours distance
-                    ($config->backup_auto_minute * 60);       // Minutes distance
-            $result = $midnight + $dist;
+        // Checks if the date would happen in the future (of the user).
+        $userresult = mktime($autohour, $automin, 0, $date['mon'], $date['mday'] + $daysfromnow, $date['year']);
+        if ($userresult <= $usertime) {
+            // If not, we skip the first scheduled day, that should fix it.
+            $daysfromnow = strpos($automateddays, "1", 1);
+            $userresult = mktime($autohour, $automin, 0, $date['mon'], $date['mday'] + $daysfromnow, $date['year']);
         }
 
-        // If that time is past, call the function recursively to obtain the next valid day
-        if ($result > 0 && $result < time()) {
-            $result = self::calculate_next_automated_backup($timezone, $result);
+        // Now we generate the time relative to the server.
+        $result = $userresult + $diff;
+
+        // If that time is past, call the function recursively to obtain the next valid day.
+        if ($result <= $now) {
+            // Checking time() in here works, but makes PHPUnit Tests extremely hard to predict.
+            // $now should never be earlier than time() anyway...
+            $result = self::calculate_next_automated_backup($timezone, $now + DAYSECS);
         }
 
         return $result;
@@ -317,7 +338,7 @@ abstract class backup_cron_automated_helper {
      */
     public static function launch_automated_backup($course, $starttime, $userid) {
 
-        $outcome = true;
+        $outcome = self::BACKUP_STATUS_OK;
         $config = get_config('backup');
         $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_AUTOMATED, $userid);
 
@@ -352,6 +373,7 @@ abstract class backup_cron_automated_helper {
 
             $bc->execute_plan();
             $results = $bc->get_results();
+            $outcome = self::outcome_from_results($results);
             $file = $results['backup_destination']; // may be empty if file already moved to target location
             $dir = $config->backup_auto_destination;
             $storage = (int)$config->backup_auto_storage;
@@ -360,8 +382,10 @@ abstract class backup_cron_automated_helper {
             }
             if ($file && !empty($dir) && $storage !== 0) {
                 $filename = backup_plan_dbops::get_default_backup_filename($format, $type, $course->id, $users, $anonymised, !$config->backup_shortname);
-                $outcome = $file->copy_content_to($dir.'/'.$filename);
-                if ($outcome && $storage === 1) {
+                if (!$file->copy_content_to($dir.'/'.$filename)) {
+                    $outcome = self::BACKUP_STATUS_ERROR;
+                }
+                if ($outcome != self::BACKUP_STATUS_ERROR && $storage === 1) {
                     $file->delete();
                 }
             }
@@ -370,7 +394,7 @@ abstract class backup_cron_automated_helper {
             $bc->log('backup_auto_failed_on_course', backup::LOG_ERROR, $course->shortname); // Log error header.
             $bc->log('Exception: ' . $e->errorcode, backup::LOG_ERROR, $e->a, 1); // Log original exception problem.
             $bc->log('Debug: ' . $e->debuginfo, backup::LOG_DEBUG, null, 1); // Log original debug information.
-            $outcome = false;
+            $outcome = self::BACKUP_STATUS_ERROR;
         }
 
         $bc->destroy();
@@ -379,6 +403,30 @@ abstract class backup_cron_automated_helper {
         return $outcome;
     }
 
+    /**
+     * Returns the backup outcome by analysing its results.
+     *
+     * @param array $results returned by a backup
+     * @return int {@link self::BACKUP_STATUS_OK} and other constants
+     */
+    public static function outcome_from_results($results) {
+        $outcome = self::BACKUP_STATUS_OK;
+        foreach ($results as $code => $value) {
+            // Each possible error and warning code has to be specified in this switch
+            // which basically analyses the results to return the correct backup status.
+            switch ($code) {
+                case 'missing_files_in_pool':
+                    $outcome = self::BACKUP_STATUS_WARNING;
+                    break;
+            }
+            // If we found the highest error level, we exit the loop.
+            if ($outcome == self::BACKUP_STATUS_ERROR) {
+                break;
+            }
+        }
+        return $outcome;
+    }
+
     /**
      * Removes deleted courses fromn the backup_courses table so that we don't
      * waste time backing them up.
@@ -411,7 +459,12 @@ abstract class backup_cron_automated_helper {
 
         $config = get_config('backup');
         $active = (int)$config->backup_auto_active;
-        if ($active === self::AUTO_BACKUP_DISABLED || ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL)) {
+        $weekdays = (string)$config->backup_auto_weekdays;
+
+        // In case of automated backup also check that it is scheduled for at least one weekday.
+        if ($active === self::AUTO_BACKUP_DISABLED ||
+                ($rundirective == self::RUN_ON_SCHEDULE && $active === self::AUTO_BACKUP_MANUAL) ||
+                ($rundirective == self::RUN_ON_SCHEDULE && strpos($weekdays, '1') === false)) {
             return self::STATE_DISABLED;
         } else if (!empty($config->backup_auto_running)) {
             // Detect if the backup_auto_running semaphore is a valid one
@@ -508,18 +561,7 @@ abstract class backup_cron_automated_helper {
         if (!empty($dir) && ($storage == 1 || $storage == 2)) {
             // Calculate backup filename regex, ignoring the date/time/info parts that can be
             // variable, depending of languages, formats and automated backup settings
-
-
-            // MDL-33531: use different filenames depending on backup_shortname option
-            if ( !empty($config->backup_shortname) ) {
-                $context = get_context_instance(CONTEXT_COURSE, $course->id);
-                $courseref = format_string($course->shortname, true, array('context' => $context));
-                $courseref = str_replace(' ', '_', $courseref);
-                $courseref = textlib::strtolower(trim(clean_filename($courseref), '_'));
-            } else {
-                $courseref = $course->id;
-            }
-            $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' .$courseref . '-';
+            $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-' .$course->id . '-';
             $regex = '#^'.preg_quote($filename, '#').'.*\.mbz$#';
 
             // Store all the matching files into fullpath => timemodified array
diff --git a/backup/util/helper/tests/cronhelper_test.php b/backup/util/helper/tests/cronhelper_test.php
new file mode 100644 (file)
index 0000000..7afd1be
--- /dev/null
@@ -0,0 +1,442 @@
+<?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 backups cron helper.
+ *
+ * @package   core_backup
+ * @category  phpunit
+ * @copyright 2012 Frédéric Massart <fred@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php');
+
+/**
+ * Unit tests for backup cron helper
+ */
+class backup_cron_helper_testcase extends advanced_testcase {
+
+    /**
+     * Test {@link backup_cron_automated_helper::calculate_next_automated_backup}.
+     */
+    public function test_next_automated_backup() {
+        $this->resetAfterTest();
+        set_config('backup_auto_active', '1', 'backup');
+
+        // Notes
+        // - backup_auto_weekdays starts on Sunday
+        // - Tests cannot be done in the past
+        // - Only the DST on the server side is handled.
+
+        // Every Tue and Fri at 11pm.
+        set_config('backup_auto_weekdays', '0010010', 'backup');
+        set_config('backup_auto_hour', '23', 'backup');
+        set_config('backup_auto_minute', '0', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 18:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('5-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('5-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('5-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-23:00', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-23:00', date('w-H:i', $next));
+
+        // Every Sun and Sat at 12pm.
+        set_config('backup_auto_weekdays', '1000001', 'backup');
+        set_config('backup_auto_hour', '0', 'backup');
+        set_config('backup_auto_minute', '0', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        // Every Sun at 4am.
+        set_config('backup_auto_weekdays', '1000000', 'backup');
+        set_config('backup_auto_hour', '4', 'backup');
+        set_config('backup_auto_minute', '0', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-04:00', date('w-H:i', $next));
+
+        // Every day but Wed at 8:30pm.
+        set_config('backup_auto_weekdays', '1110111', 'backup');
+        set_config('backup_auto_hour', '20', 'backup');
+        set_config('backup_auto_minute', '30', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('1-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('4-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('4-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('5-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-20:30', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 17:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-20:30', date('w-H:i', $next));
+
+        // Sun, Tue, Thu, Sat at 12pm.
+        set_config('backup_auto_weekdays', '1010101', 'backup');
+        set_config('backup_auto_hour', '0', 'backup');
+        set_config('backup_auto_minute', '0', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Monday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Tuesday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('4-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Wednesday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('4-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Thursday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Friday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('6-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Saturday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0-00:00', date('w-H:i', $next));
+
+        $now = strtotime('next Sunday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('2-00:00', date('w-H:i', $next));
+
+        // None.
+        set_config('backup_auto_weekdays', '0000000', 'backup');
+        set_config('backup_auto_hour', '15', 'backup');
+        set_config('backup_auto_minute', '30', 'backup');
+        $timezone = 99;
+
+        $now = strtotime('next Sunday 13:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals('0', $next);
+
+        // Playing with timezones.
+        set_config('backup_auto_weekdays', '1111111', 'backup');
+        set_config('backup_auto_hour', '20', 'backup');
+        set_config('backup_auto_minute', '00', 'backup');
+
+        $timezone = 99;
+        date_default_timezone_set('Australia/Perth');
+        $now = strtotime('18:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
+
+        $timezone = 99;
+        date_default_timezone_set('Europe/Brussels');
+        $now = strtotime('18:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
+
+        $timezone = 99;
+        date_default_timezone_set('America/New_York');
+        $now = strtotime('18:00:00');
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
+
+        // Viva Australia! (UTC+8).
+        date_default_timezone_set('Australia/Perth');
+        $now = strtotime('18:00:00');
+
+        $timezone = -10.0; // 12am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-14:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        $timezone = -5.0; // 5am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-09:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        $timezone = 0.0;  // 10am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-04:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        $timezone = 3.0; // 1pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-01:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        $timezone = 8.0; // 6pm for the user (same than the server).
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-20:00'), date('w-H:i', $next));
+
+        $timezone = 9.0; // 7pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-19:00'), date('w-H:i', $next));
+
+        $timezone = 13.0; // 12am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $this->assertEquals(date('w-15:00', strtotime('tomorrow')), date('w-H:i', $next));
+
+        // Let's have a Belgian beer! (UTC+1 / UTC+2 DST).
+        date_default_timezone_set('Europe/Brussels');
+        $now = strtotime('18:00:00');
+        $dst = date('I');
+
+        $timezone = -10.0; // 7am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-07:00', strtotime('tomorrow')) : date('w-08:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = -5.0; // 12pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-02:00', strtotime('tomorrow')) : date('w-03:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 0.0;  // 5pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-21:00') : date('w-22:00');
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 3.0; // 8pm for the user (note the expected time is today while in DST).
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-18:00', strtotime('tomorrow')) : date('w-19:00');
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 8.0; // 1am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-13:00', strtotime('tomorrow')) : date('w-14:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 9.0; // 2am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-12:00', strtotime('tomorrow')) : date('w-13:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 13.0; // 6am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-08:00', strtotime('tomorrow')) : date('w-09:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        // The big apple! (UTC-5 / UTC-4 DST).
+        date_default_timezone_set('America/New_York');
+        $now = strtotime('18:00:00');
+        $dst = date('I');
+
+        $timezone = -10.0; // 1pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-01:00', strtotime('tomorrow')) : date('w-02:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = -5.0; // 6pm for the user (server time).
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-20:00') : date('w-21:00');
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 0.0;  // 11pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-15:00', strtotime('tomorrow')) : date('w-16:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 3.0; // 2am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-12:00', strtotime('tomorrow')) : date('w-13:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 8.0; // 7am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-07:00', strtotime('tomorrow')) : date('w-08:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 9.0; // 8am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-06:00', strtotime('tomorrow')) : date('w-07:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 13.0; // 6am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? date('w-02:00', strtotime('tomorrow')) : date('w-03:00', strtotime('tomorrow'));
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        // Some more timezone tests
+        set_config('backup_auto_weekdays', '0100001', 'backup');
+        set_config('backup_auto_hour', '20', 'backup');
+        set_config('backup_auto_minute', '00', 'backup');
+
+        date_default_timezone_set('Europe/Brussels');
+        $now = strtotime('next Monday 18:00:00');
+        $dst = date('I');
+
+        $timezone = -12.0;  // 1pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '2-09:00' : '2-10:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = -4.0;  // 1pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '2-01:00' : '2-02:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 0.0;  // 5pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '1-21:00' : '1-22:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 2.0;  // 7pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '1-19:00' : '1-20:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 4.0;  // 9pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '6-17:00' : '6-18:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 12.0;  // 6am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '6-09:00' : '6-10:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        // Some more timezone tests
+        set_config('backup_auto_weekdays', '0100001', 'backup');
+        set_config('backup_auto_hour', '02', 'backup');
+        set_config('backup_auto_minute', '00', 'backup');
+
+        date_default_timezone_set('America/New_York');
+        $now = strtotime('next Monday 04:00:00');
+        $dst = date('I');
+
+        $timezone = -12.0;  // 8pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '1-09:00' : '1-10:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = -4.0;  // 4am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '6-01:00' : '6-02:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 0.0;  // 8am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '5-21:00' : '5-22:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 2.0;  // 10am for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '5-19:00' : '5-20:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 4.0;  // 12pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '5-17:00' : '5-18:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+        $timezone = 12.0;  // 8pm for the user.
+        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
+        $expected = !$dst ? '5-09:00' : '5-10:00';
+        $this->assertEquals($expected, date('w-H:i', $next));
+
+    }
+}
index f62fee9..964dd3f 100644 (file)
@@ -94,11 +94,22 @@ abstract class backup_structure_step extends backup_step {
         // Process structure definition
         $structure->process($pr);
 
+        // Get the results from the nested elements
+        $results = $structure->get_results();
+
+        // Get the log messages to append to the log
+        $logs = $structure->get_logs();
+        foreach ($logs as $log) {
+            $this->log($log->message, $log->level, $log->a, $log->depth, $log->display);
+        }
+
         // Close everything
         $xw->stop();
 
         // Destroy the structure. It helps PHP 5.2 memory a lot!
         $structure->destroy();
+
+        return $results;
     }
 
     /**
index 4b0ff58..b6c0ead 100644 (file)
@@ -218,8 +218,14 @@ abstract class restore_structure_step extends restore_step {
      */
     public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) {
         $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid;
-        restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
-                                          $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid);
+        $results = restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
+                $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid);
+        $resultstoadd = array();
+        foreach ($results as $result) {
+            $this->log($result->message, $result->level);
+            $resultstoadd[$result->code] = true;
+        }
+        $this->task->add_result($resultstoadd);
     }
 
     /**
index 9a47901..8557ec8 100644 (file)
@@ -37,6 +37,8 @@ class backup_nested_element extends base_nested_element implements processable {
     protected $aliases;   // Define DB->final element aliases
     protected $fileannotations;   // array of file areas to be searched by file annotations
     protected $counter;   // Number of instances of this element that have been processed
+    protected $results;  // Logs the results we encounter during the process.
+    protected $logs;     // Some log messages that could be retrieved later.
 
     /**
      * Constructor - instantiates one backup_nested_element, specifying its basic info.
@@ -55,8 +57,16 @@ class backup_nested_element extends base_nested_element implements processable {
         $this->aliases   = array();
         $this->fileannotations = array();
         $this->counter   = 0;
+        $this->results  = array();
+        $this->logs     = array();
     }
 
+    /**
+     * Process the nested element
+     *
+     * @param object $processor the processor
+     * @return void
+     */
     public function process($processor) {
         if (!$processor instanceof base_processor) { // No correct processor, throw exception
             throw new base_element_struct_exception('incorrect_processor');
@@ -113,6 +123,69 @@ class backup_nested_element extends base_nested_element implements processable {
         $iterator->close();
     }
 
+    /**
+     * Saves a log message to an array
+     *
+     * @see backup_helper::log()
+     * @param string $message to add to the logs
+     * @param int $level level of importance {@link backup::LOG_DEBUG} and other constants
+     * @param mixed $a to be included in $message
+     * @param int $depth of the message
+     * @param display $bool supporting translation via get_string() if true
+     * @return void
+     */
+    protected function add_log($message, $level, $a = null, $depth = null, $display = false) {
+        // Adding the result to the oldest parent.
+        if ($this->get_parent()) {
+            $parent = $this->get_grandparent();
+            $parent->add_log($message, $level, $a, $depth, $display);
+        } else {
+            $log = new stdClass();
+            $log->message = $message;
+            $log->level = $level;
+            $log->a = $a;
+            $log->depth = $depth;
+            $log->display = $display;
+            $this->logs[] = $log;
+        }
+    }
+
+    /**
+     * Saves the results to an array
+     *
+     * @param array $result associative array
+     * @return void
+     */
+    protected function add_result($result) {
+        if (is_array($result)) {
+            // Adding the result to the oldest parent.
+            if ($this->get_parent()) {
+                $parent = $this->get_grandparent();
+                $parent->add_result($result);
+            } else {
+                $this->results = array_merge($this->results, $result);
+            }
+        }
+    }
+
+    /**
+     * Returns the logs
+     *
+     * @return array of log objects
+     */
+    public function get_logs() {
+        return $this->logs;
+    }
+
+    /**
+     * Returns the results
+     *
+     * @return associative array of results
+     */
+    public function get_results() {
+        return $this->results;
+    }
+
     public function set_source_array($arr) {
         // TODO: Only elements having final elements can set source
         $this->var_array = $arr;
index 7995561..0b90e15 100644 (file)
@@ -487,6 +487,9 @@ class backup_ui_stage_complete extends backup_ui_stage_final {
         if (!empty($this->results['include_file_references_to_external_content'])) {
             $output .= $renderer->notification(get_string('filereferencesincluded', 'backup'), 'notifyproblem');
         }
+        if (!empty($this->results['missing_files_in_pool'])) {
+            $output .= $renderer->notification(get_string('missingfilesinpool', 'backup'), 'notifyproblem');
+        }
         $output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess');
         $output .= $renderer->continue_button($restorerul);
         $output .= $renderer->box_end();
index 3a72edc..ce95e7a 100644 (file)
@@ -324,6 +324,9 @@ abstract class base_moodleform extends moodleform {
         $config->noLabel = get_string('confirmcancelno', 'backup');
         $PAGE->requires->yui_module('moodle-backup-confirmcancel', 'M.core_backup.watch_cancel_buttons', array($config));
 
+        $PAGE->requires->yui_module('moodle-backup-backupselectall', 'M.core_backup.select_all_init',
+                array(array('select' => get_string('select'), 'all' => get_string('all'), 'none' => get_string('none'))));
+
         parent::display();
     }
 
index 7e92069..34bff92 100644 (file)
@@ -772,6 +772,9 @@ class restore_ui_stage_complete extends restore_ui_stage_process {
             $html .= $renderer->box_end();
         }
         $html .= $renderer->box_start();
+        if (array_key_exists('file_missing_in_backup', $this->results)) {
+            $html .= $renderer->notification(get_string('restorefileweremissing', 'backup'), 'notifyproblem');
+        }
         $html .= $renderer->notification(get_string('restoreexecutionsuccess', 'backup'), 'notifysuccess');
         $html .= $renderer->continue_button(new moodle_url('/course/view.php', array(
             'id' => $this->get_ui()->get_controller()->get_courseid())), 'get');
diff --git a/backup/util/ui/yui/backupselectall/backupselectall.js b/backup/util/ui/yui/backupselectall/backupselectall.js
new file mode 100644 (file)
index 0000000..9f05b7f
--- /dev/null
@@ -0,0 +1,80 @@
+YUI.add('moodle-backup-backupselectall', function(Y) {
+
+// Namespace for the backup
+M.core_backup = M.core_backup || {};
+
+/**
+ * Adds select all/none links to the top of the backup/restore/import schema page.
+ */
+M.core_backup.select_all_init = function(str) {
+    var formid = null;
+
+    var helper = function(e, check, type) {
+        e.preventDefault();
+
+        var len = type.length;
+        Y.all('input[type="checkbox"]').each(function(checkbox) {
+            var name = checkbox.get('name');
+            if (name.substring(name.length - len) == type) {
+                checkbox.set('checked', check);
+            }
+        });
+
+        // At this point, we really need to persuade the form we are part of to
+        // update all of its disabledIf rules. However, as far as I can see,
+        // given the way that lib/form/form.js is written, that is impossible.
+        if (formid && M.form) {
+            M.form.updateFormState(formid);
+        }
+    };
+
+    var html_generator = function(classname, idtype) {
+        return '<div class="' + classname + '">' +
+                    '<div class="fitem fitem_fcheckbox">' +
+                        '<div class="fitemtitle">' + str.select + '</div>' +
+                        '<div class="felement">' +
+                            '<a id="backup-all-' + idtype + '" href="#">' + str.all + '</a> / ' +
+                            '<a id="backup-none-' + idtype + '" href="#">' + str.none + '</a>' +
+                        '</div>' +
+                    '</div>' +
+                '</div>';
+    };
+
+    var firstsection = Y.one('fieldset#coursesettings .fcontainer.clearfix .grouped_settings.section_level');
+    if (!firstsection) {
+        // This is not a relevant page.
+        return;
+    }
+    if (!firstsection.one('.felement.fcheckbox')) {
+        // No checkboxes.
+        return;
+    }
+
+    formid = firstsection.ancestor('form').getAttribute('id');
+
+    var withuserdata = false;
+    Y.all('input[type="checkbox"]').each(function(checkbox) {
+        var name = checkbox.get('name');
+        if (name.substring(name.length - 9) == '_userdata') {
+            withuserdata = '_userdata';
+        } else if (name.substring(name.length - 9) == '_userinfo') {
+            withuserdata = '_userinfo';
+        }
+    });
+
+    var html = html_generator('include_setting section_level', 'included');
+    if (withuserdata) {
+        html += html_generator('normal_setting', 'userdata');
+    }
+    var links = Y.Node.create('<div class="grouped_settings section_level">' + html + '</div>');
+    firstsection.insert(links, 'before');
+
+    Y.one('#backup-all-included').on('click',  function(e) { helper(e, true,  '_included'); });
+    Y.one('#backup-none-included').on('click', function(e) { helper(e, false, '_included'); });
+    if (withuserdata) {
+        Y.one('#backup-all-userdata').on('click',  function(e) { helper(e, true,  withuserdata); });
+        Y.one('#backup-none-userdata').on('click', function(e) { helper(e, false, withuserdata); });
+    }
+}
+
+}, '@VERSION@', {'requires':['base','node','event', 'node-event-simulate']});
index 0daec7c..449e632 100644 (file)
  *
  * @package    block
  * @subpackage completion
- * @copyright  2009 Catalyst IT Ltd
+ * @copyright  2009-2012 Catalyst IT Ltd
  * @author     Aaron Barnes <aaronb@catalyst.net.nz>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-
-require_once($CFG->libdir.'/completionlib.php');
+require_once("{$CFG->libdir}/completionlib.php");
 
 /**
  * Course completion status
@@ -36,25 +35,27 @@ require_once($CFG->libdir.'/completionlib.php');
 class block_completionstatus extends block_base {
 
     public function init() {
-        $this->title   = get_string('pluginname', 'block_completionstatus');
+        $this->title = get_string('pluginname', 'block_completionstatus');
     }
 
     public function get_content() {
-        global $USER, $CFG, $DB, $COURSE;
+        global $USER;
 
         // If content is cached
         if ($this->content !== NULL) {
             return $this->content;
         }
 
+        $course  = $this->page->course;
+
         // Create empty content
-        $this->content = new stdClass;
+        $this->content = new stdClass();
 
         // Can edit settings?
-        $can_edit = has_capability('moodle/course:update', get_context_instance(CONTEXT_COURSE, $this->page->course->id));
+        $can_edit = has_capability('moodle/course:update', get_context_instance(CONTEXT_COURSE, $course->id));
 
         // Get course completion data
-        $info = new completion_info($this->page->course);
+        $info = new completion_info($course);
 
         // Don't display if completion isn't enabled!
         if (!completion_info::is_enabled_for_site()) {
@@ -84,9 +85,9 @@ class block_completionstatus extends block_base {
         // Check this user is enroled
         if (!$info->is_tracked_user($USER->id)) {
             // If not enrolled, but are can view the report:
-            if (has_capability('report/completion:view', get_context_instance(CONTEXT_COURSE, $COURSE->id))) {
-                $this->content->text = '<a href="'.$CFG->wwwroot.'/report/completion/index.php?course='.$COURSE->id.
-                                       '">'.get_string('viewcoursereport', 'completion').'</a>';
+            if (has_capability('report/completion:view', get_context_instance(CONTEXT_COURSE, $course->id))) {
+                $report = new moodle_url('/report/completion/index.php', array('course' => $course->id));
+                $this->content->text = '<a href="'.$report->out().'">'.get_string('viewcoursereport', 'completion').'</a>';
                 return $this->content;
             }
 
@@ -187,7 +188,7 @@ class block_completionstatus extends block_base {
         // Load course completion
         $params = array(
             'userid' => $USER->id,
-            'course' => $COURSE->id
+            'course' => $course->id
         );
         $ccompletion = new completion_completion($params);
 
@@ -221,7 +222,8 @@ class block_completionstatus extends block_base {
         $this->content->text .= $shtml.'</tbody></table>';
 
         // Display link to detailed view
-        $this->content->footer = '<br><a href="'.$CFG->wwwroot.'/blocks/completionstatus/details.php?course='.$COURSE->id.'">'.get_string('moredetails', 'completion').'</a>';
+        $details = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id));
+        $this->content->footer = '<br><a href="'.$details->out().'">'.get_string('moredetails', 'completion').'</a>';
 
         return $this->content;
     }
index a1ac771..84406e3 100644 (file)
  *
  * @package    block
  * @subpackage completion
- * @copyright  2009 Catalyst IT Ltd
+ * @copyright  2009-2012 Catalyst IT Ltd
  * @author     Aaron Barnes <aaronb@catalyst.net.nz>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require_once('../../config.php');
-require_once($CFG->libdir.'/completionlib.php');
-
-
-// TODO:  Make this page Moodle 2.0 compliant
+require_once(dirname(__FILE__).'/../../config.php');
+require_once("{$CFG->libdir}/completionlib.php");
 
 
 ///
 /// Load data
 ///
 $id = required_param('course', PARAM_INT);
-// User id
 $userid = optional_param('user', 0, PARAM_INT);
 
 // Load course
-$course = $DB->get_record('course', array('id' => $id));
+$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST);
 
 // Load user
 if ($userid) {
@@ -76,21 +72,13 @@ if (!$can_view) {
 // Load completion data
 $info = new completion_info($course);
 
-$returnurl = "{$CFG->wwwroot}/course/view.php?id={$id}";
+$returnurl = new moodle_url('/course/view.php', array('id' => $id));
 
 // Don't display if completion isn't enabled!
 if (!$info->is_enabled()) {
     print_error('completionnotenabled', 'completion', $returnurl);
 }
 
-// Load criteria to display
-$completions = $info->get_completions($user->id);
-
-// Check if this course has any criteria
-if (empty($completions)) {
-    print_error('nocriteriaset', 'completion', $returnurl);
-}
-
 // Check this user is enroled
 if (!$info->is_tracked_user($user->id)) {
     if ($USER->id == $user->id) {
@@ -104,6 +92,7 @@ if (!$info->is_tracked_user($user->id)) {
 ///
 /// Display page
 ///
+$PAGE->set_context(context_course::instance($course->id));
 
 // Print header
 $page = get_string('completionprogressdetails', 'block_completionstatus');
@@ -111,7 +100,7 @@ $title = format_string($course->fullname) . ': ' . $page;
 
 $PAGE->navbar->add($page);
 $PAGE->set_pagelayout('standard');
-$PAGE->set_url('/blocks/completionstatus/details.php', array('course' => $course->id));
+$PAGE->set_url('/blocks/completionstatus/details.php', array('course' => $course->id, 'user' => $user->id));
 $PAGE->set_title(get_string('course') . ': ' . $course->fullname);
 $PAGE->set_heading($title);
 echo $OUTPUT->header();
@@ -135,122 +124,148 @@ $coursecomplete = $info->is_course_complete($user->id);
 // Has this user completed any criteria?
 $criteriacomplete = $info->count_course_user_data($user->id);
 
+// Load course completion
+$params = array(
+    'userid' => $user->id,
+    'course' => $course->id,
+);
+$ccompletion = new completion_completion($params);
+
 if ($coursecomplete) {
     echo get_string('complete');
-} else if (!$criteriacomplete) {
+} else if (!$criteriacomplete && !$ccompletion->timestarted) {
     echo '<i>'.get_string('notyetstarted', 'completion').'</i>';
 } else {
     echo '<i>'.get_string('inprogress','completion').'</i>';
 }
 
 echo '</td></tr>';
-echo '<tr><td colspan="2"><b>'.get_string('required').':</b> ';
 
-// Get overall aggregation method
-$overall = $info->get_aggregation_method();
+// Load criteria to display
+$completions = $info->get_completions($user->id);
 
-if ($overall == COMPLETION_AGGREGATION_ALL) {
-    echo get_string('criteriarequiredall', 'completion');
+// Check if this course has any criteria
+if (empty($completions)) {
+    echo '<tr><td colspan="2"><br />';
+    echo $OUTPUT->box(get_string('err_nocriteria', 'completion'), 'noticebox');
+    echo '</td></tr></tbody></table>';
 } else {
-    echo get_string('criteriarequiredany', 'completion');
-}
+    echo '<tr><td colspan="2"><b>'.get_string('required').':</b> ';
 
-echo '</td></tr></tbody></table>';
-
-// Generate markup for criteria statuses
-echo '<table class="generalbox boxaligncenter" cellpadding="3"><tbody>';
-echo '<tr class="ccheader">';
-echo '<th class="c0 header" scope="col">'.get_string('criteriagroup', 'block_completionstatus').'</th>';
-echo '<th class="c1 header" scope="col">'.get_string('criteria', 'completion').'</th>';
-echo '<th class="c2 header" scope="col">'.get_string('requirement', 'block_completionstatus').'</th>';
-echo '<th class="c3 header" scope="col">'.get_string('status').'</th>';
-echo '<th class="c4 header" scope="col">'.get_string('complete').'</th>';
-echo '<th class="c5 header" scope="col">'.get_string('completiondate', 'report_completion').'</th>';
-echo '</tr>';
-
-// Save row data
-$rows = array();
-
-global $COMPLETION_CRITERIA_TYPES;
-
-// Loop through course criteria
-foreach ($completions as $completion) {
-    $criteria = $completion->get_criteria();
-    $complete = $completion->is_complete();
-
-    $row = array();
-    $row['type'] = $criteria->criteriatype;
-    $row['title'] = $criteria->get_title();
-    $row['status'] = $completion->get_status();
-    $row['timecompleted'] = $completion->timecompleted;
-    $row['details'] = $criteria->get_details($completion);
-    $rows[] = $row;
-}
+    // Get overall aggregation method
+    $overall = $info->get_aggregation_method();
 
-// Print table
-$last_type = '';
-$agg_type = false;
+    if ($overall == COMPLETION_AGGREGATION_ALL) {
+        echo get_string('criteriarequiredall', 'completion');
+    } else {
+        echo get_string('criteriarequiredany', 'completion');
+    }
+
+    echo '</td></tr></tbody></table>';
+
+    // Generate markup for criteria statuses
+    echo '<table class="generalbox logtable boxaligncenter" id="criteriastatus" width="100%"><tbody>';
+    echo '<tr class="ccheader">';
+    echo '<th class="c0 header" scope="col">'.get_string('criteriagroup', 'block_completionstatus').'</th>';
+    echo '<th class="c1 header" scope="col">'.get_string('criteria', 'completion').'</th>';
+    echo '<th class="c2 header" scope="col">'.get_string('requirement', 'block_completionstatus').'</th>';
+    echo '<th class="c3 header" scope="col">'.get_string('status').'</th>';
+    echo '<th class="c4 header" scope="col">'.get_string('complete').'</th>';
+    echo '<th class="c5 header" scope="col">'.get_string('completiondate', 'report_completion').'</th>';
+    echo '</tr>';
 
-foreach ($rows as $row) {
+    // Save row data
+    $rows = array();
+
+    // Loop through course criteria
+    foreach ($completions as $completion) {
+        $criteria = $completion->get_criteria();
+
+        $row = array();
+        $row['type'] = $criteria->criteriatype;
+        $row['title'] = $criteria->get_title();
+        $row['status'] = $completion->get_status();
+        $row['complete'] = $completion->is_complete();
+        $row['timecompleted'] = $completion->timecompleted;
+        $row['details'] = $criteria->get_details($completion);
+        $rows[] = $row;
+    }
 
-    // Criteria group
-    echo '<td class="c0">';
-    if ($last_type !== $row['details']['type']) {
-        $last_type = $row['details']['type'];
-        echo $last_type;
+    // Print table
+    $last_type = '';
+    $agg_type = false;
+    $oddeven = 0;
 
-        // Reset agg type
-        $agg_type = true;
-    } else {
-        // Display aggregation type
-        if ($agg_type) {
-            $agg = $info->get_aggregation_method($row['type']);
+    foreach ($rows as $row) {
 
-            echo '(<i>';
+        echo '<tr class="r' . $oddeven . '">';
 
-            if ($agg == COMPLETION_AGGREGATION_ALL) {
-                echo strtolower(get_string('all', 'completion'));
-            } else {
-                echo strtolower(get_string('any', 'completion'));
-            }
+        // Criteria group
+        echo '<td class="cell c0">';
+        if ($last_type !== $row['details']['type']) {
+            $last_type = $row['details']['type'];
+            echo $last_type;
+
+            // Reset agg type
+            $agg_type = true;
+        } else {
+            // Display aggregation type
+            if ($agg_type) {
+                $agg = $info->get_aggregation_method($row['type']);
 
-            echo '</i> '.strtolower(get_string('required')).')';
-            $agg_type = false;
+                echo '(<i>';
+
+                if ($agg == COMPLETION_AGGREGATION_ALL) {
+                    echo strtolower(get_string('aggregateall', 'completion'));
+                } else {
+                    echo strtolower(get_string('aggregateany', 'completion'));
+                }
+
+                echo '</i> '.strtolower(get_string('required')).')';
+                $agg_type = false;
+            }
         }
+        echo '</td>';
+
+        // Criteria title
+        echo '<td class="cell c1">';
+        echo $row['details']['criteria'];
+        echo '</td>';
+
+        // Requirement
+        echo '<td class="cell c2">';
+        echo $row['details']['requirement'];
+        echo '</td>';
+
+        // Status
+        echo '<td class="cell c3">';
+        echo $row['details']['status'];
+        echo '</td>';
+
+        // Is complete
+        echo '<td class="cell c4">';
+        echo $row['complete'] ? get_string('yes') : get_string('no');
+        echo '</td>';
+
+        // Completion data
+        echo '<td class="cell c5">';
+        if ($row['timecompleted']) {
+            echo userdate($row['timecompleted'], get_string('strftimedate', 'langconfig'));
+        } else {
+            echo '-';
+        }
+        echo '</td>';
+        echo '</tr>';
+        // for row striping
+        $oddeven = $oddeven ? 0 : 1;
     }
-    echo '</td>';
-
-    // Criteria title
-    echo '<td class="c1">';
-    echo $row['details']['criteria'];
-    echo '</td>';
-
-    // Requirement
-    echo '<td class="c2">';
-    echo $row['details']['requirement'];
-    echo '</td>';
-
-    // Status
-    echo '<td class="c3">';
-    echo $row['details']['status'];
-    echo '</td>';
-
-    // Is complete
-    echo '<td class="c4">';
-    echo ($row['status'] === get_string('yes')) ? get_string('yes') : get_string('no');
-    echo '</td>';
-
-    // Completion data
-    echo '<td class="c5">';
-    if ($row['timecompleted']) {
-        echo userdate($row['timecompleted'], '%e %B %G');
-    } else {
-        echo '-';
-    }
-    echo '</td>';
-    echo '</tr>';
+
+    echo '</tbody></table>';
 }
 
-echo '</tbody></table>';
+echo '<div class="buttons">';
+$courseurl = new moodle_url("/course/view.php", array('id' => $course->id));
+echo $OUTPUT->single_button($courseurl, get_string('returntocourse', 'block_completionstatus'), 'get');
+echo '</div>';
 
 echo $OUTPUT->footer();
index fcc965a..6658c17 100644 (file)
@@ -5,3 +5,4 @@ $string['criteriagroup'] = 'Criteria group';
 $string['firstofsecond'] = '{$a->first} of {$a->second}';
 $string['pluginname'] = 'Course completion status';
 $string['requirement'] = 'Requirement';
+$string['returntocourse'] = 'Return to course';
index 7ce4a4a..6183a9b 100644 (file)
@@ -40,6 +40,7 @@ class block_tags extends block_base {
         global $CFG, $COURSE, $SITE, $USER, $SCRIPT, $OUTPUT;
 
         if (empty($CFG->usetags)) {
+            $this->content = new stdClass();
             $this->content->text = '';
             if ($this->page->user_is_editing()) {
                 $this->content->text = get_string('disabledtags', 'block_tags');
index 6ac8944..a0d394e 100644 (file)
@@ -405,11 +405,10 @@ class blog_entry {
      * @return void
      */
     public function delete() {
-        global $DB, $USER;
-
-        $returnurl = '';
+        global $DB;
 
         $this->delete_attachments();
+        $this->remove_associations();
 
         $DB->delete_records('post', array('id' => $this->id));
         tag_set('post', $this->id, array());
index dd3d4bd..75380f3 100644 (file)
@@ -124,7 +124,7 @@ class course_edit_form extends moodleform {
             array(COURSE_DISPLAY_SINGLEPAGE => get_string('coursedisplay_single'),
                 COURSE_DISPLAY_MULTIPAGE => get_string('coursedisplay_multi')));
         $mform->addHelpButton('coursedisplay', 'coursedisplay');
-        $mform->setDefault('coursedisplay', COURSE_DISPLAY_SINGLEPAGE);
+        $mform->setDefault('coursedisplay', $courseconfig->coursedisplay);
 
         for ($i = 0; $i <= $courseconfig->maxsections; $i++) {
             $sectionmenu[$i] = "$i";
index 9f33f83..73723af 100644 (file)
@@ -38,6 +38,7 @@ class editsection_form extends moodleform {
         $course = $this->_customdata['course'];
 
         if (!empty($CFG->enableavailability)) {
+            $mform->addElement('header', '', get_string('availabilityconditions', 'condition'));
             // Grouping conditions - only if grouping is enabled at site level
             if (!empty($CFG->enablegroupmembersonly)) {
                 $options = array();
@@ -49,7 +50,6 @@ class editsection_form extends moodleform {
                                 $grouping->name, true, array('context' => $context));
                     }
                 }
-                $mform->addElement('header', '', get_string('availabilityconditions', 'condition'));
                 $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options);
                 $mform->addHelpButton('groupingid', 'groupingsection', 'group');
             }
index ea88504..c758cac 100644 (file)
@@ -291,8 +291,11 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o .= html_writer::tag('div', '', array('class' => 'right side'));
         $o .= html_writer::start_tag('div', array('class' => 'content'));
 
-        $title = html_writer::tag('a', get_section_name($course, $section),
-                array('href' => course_get_url($course, $section->section), 'class' => $linkclasses));
+        $title = get_section_name($course, $section);
+        if ($section->uservisible) {
+            $title = html_writer::tag('a', $title,
+                    array('href' => course_get_url($course, $section->section), 'class' => $linkclasses));
+        }
         $o .= $this->output->heading($title, 3, 'section-title');
 
         $o.= html_writer::start_tag('div', array('class' => 'summarytext'));
@@ -448,7 +451,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $links = array('previous' => '', 'next' => '');
         $back = $sectionno - 1;
         while ($back > 0 and empty($links['previous'])) {
-            if ($canviewhidden || $sections[$back]->visible) {
+            if ($canviewhidden || $sections[$back]->uservisible) {
                 $params = array();
                 if (!$sections[$back]->visible) {
                     $params = array('class' => 'dimmed_text');
@@ -462,7 +465,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
 
         $forward = $sectionno + 1;
         while ($forward <= $course->numsections and empty($links['next'])) {
-            if ($canviewhidden || $sections[$forward]->visible) {
+            if ($canviewhidden || $sections[$forward]->uservisible) {
                 $params = array();
                 if (!$sections[$forward]->visible) {
                     $params = array('class' => 'dimmed_text');
index f799b0d..ac7a957 100644 (file)
@@ -47,9 +47,6 @@ define('FIRSTUSEDEXCELROW', 3);
 define('MOD_CLASS_ACTIVITY', 0);
 define('MOD_CLASS_RESOURCE', 1);
 
-define('COURSE_DISPLAY_SINGLEPAGE', 0); // display all sections on one page
-define('COURSE_DISPLAY_MULTIPAGE', 1); // split pages into a page per section
-
 function make_log_url($module, $url) {
     switch ($module) {
         case 'course':
index b79e443..dc7fa19 100644 (file)
@@ -101,8 +101,6 @@ class recent_form extends moodleform {
             $mform->setAdvanced('user');
         }
 
-        $sectiontitle = get_string('sectionname', 'format_'.$COURSE->format);
-
         $options = array(''=>get_string('allactivities'));
         $modsused = array();
 
index cb899f0..b4a86fe 100644 (file)
@@ -19,7 +19,7 @@ class course_reset_form extends moodleform {
         $mform->addElement('checkbox', 'reset_logs', get_string('deletelogs'));
         $mform->addElement('checkbox', 'reset_notes', get_string('deletenotes', 'notes'));
         $mform->addElement('checkbox', 'reset_comments', get_string('deleteallcomments', 'moodle'));
-        $mform->addElement('checkbox', 'reset_course_completion', get_string('deletecoursecompletiondata', 'completion'));
+        $mform->addElement('checkbox', 'reset_completion', get_string('deletecompletiondata', 'completion'));
         $mform->addElement('checkbox', 'delete_blog_associations', get_string('deleteblogassociations', 'blog'));
         $mform->addHelpButton('delete_blog_associations', 'deleteblogassociations', 'blog');
 
@@ -61,13 +61,13 @@ class course_reset_form extends moodleform {
         if ($allmods = $DB->get_records('modules') ) {
             foreach ($allmods as $mod) {
                 $modname = $mod->name;
-                if (!$DB->count_records($modname, array('course'=>$COURSE->id))) {
-                    continue; // skip mods with no instances
-                }
                 $modfile = $CFG->dirroot."/mod/$modname/lib.php";
                 $mod_reset_course_form_definition = $modname.'_reset_course_form_definition';
                 $mod_reset__userdata = $modname.'_reset_userdata';
                 if (file_exists($modfile)) {
+                    if (!$DB->count_records($modname, array('course'=>$COURSE->id))) {
+                        continue; // Skip mods with no instances
+                    }
                     include_once($modfile);
                     if (function_exists($mod_reset_course_form_definition)) {
                         $mod_reset_course_form_definition($mform);
index ac3c4d9..08f8b4a 100644 (file)
@@ -315,7 +315,7 @@ class core_course_external_testcase extends externallib_advanced_testcase {
         $course2['format'] = 'weeks';
         $course2['showgrades'] = 1;
         $course2['newsitems'] = 3;
-        $course2['startdate'] = 32882306400; // 01/01/3012
+        $course2['startdate'] = 1420092000; // 01/01/2015
         $course2['numsections'] = 4;
         $course2['maxbytes'] = 100000;
         $course2['showreports'] = 1;
index 47b1e37..7db80b8 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
  * CLI sync for full category enrol synchronisation.
  *
  * Sample execution:
- * $sudo -u www-data /usr/bin/php /var/www/moodle/enrol/category/cli/sync.php
+ * $ sudo -u www-data /usr/bin/php /var/www/moodle/enrol/category/cli/sync.php
  *
  * Notes:
  *   - it is required to use the web server account when executing PHP CLI scripts
  *   - you need to change the "www-data" to match the apache user account
  *   - use "su" if "sudo" not available
  *
- * @package    enrol
- * @subpackage category
+ * @package    enrol_category
  * @copyright  2010 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -36,9 +34,35 @@ define('CLI_SCRIPT', true);
 
 require(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php');
 require_once("$CFG->dirroot/enrol/category/locallib.php");
+require_once("$CFG->libdir/clilib.php");
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(array('verbose'=>false, 'help'=>false), array('v'=>'verbose', 'h'=>'help'));
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+if ($options['help']) {
+    $help =
+        "Execute course category enrolment sync.
+
+Options:
+-v, --verbose         Print verbose progess information
+-h, --help            Print out this help
+
+Example:
+\$ sudo -u www-data /usr/bin/php enrol/category/cli/sync.php
+";
+    echo $help;
+    die;
+}
+
 
 if (!enrol_is_enabled('category')) {
-     die('enrol_category plugin is disabled, sync is disabled');
+    cli_error('enrol_category plugin is disabled, synchronisation stopped', 2);
 }
 
-enrol_category_sync_full();
+$verbose = !empty($options['verbose']);
+return enrol_category_sync_full($verbose);
index 1a9ead0..7feea4d 100644 (file)
@@ -25,9 +25,9 @@
 defined('MOODLE_INTERNAL') || die();
 
 $capabilities = array(
-    // marks roles that have category role assignments synchronised to course enrolments
+    // Marks roles that have category role assignments synchronised to course enrolments
     // overrides below system context are ignored (for performance reasons).
-    // by default his is not allowed in new installs, admins have to explicitly allow category enrolments
+    // By default his is not allowed in new installs, admins have to explicitly allow category enrolments.
     'enrol/category:synchronised' => array(
         'captype' => 'write',
         'contextlevel' => CONTEXT_SYSTEM,
index 66194df..3a67866 100644 (file)
 /**
  * Category enrolment plugin event handler definition.
  *
- * @package enrol_category
- * @category event
+ * @package   enrol_category
+ * @category  event
  * @copyright 2010 Petr Skoda {@link http://skodak.org}
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
index 0de987f..834aa67 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -18,8 +17,7 @@
 /**
  * category enrolment plugin installation.
  *
- * @package    enrol
- * @subpackage category
+ * @package    enrol_category
  * @copyright  2010 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
index 5a8bee3..417b28d 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Strings for component 'enrol_category', language 'en', branch 'MOODLE_20_STABLE'
+ * Strings for component 'enrol_category', language 'en'.
  *
- * @package    enrol
- * @subpackage category
+ * @package    enrol_category
  * @copyright  2010 Petr Skoda  {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
index ba60ba0..e6525e3 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 /**
  * Category enrolment plugin.
  *
- * @package    enrol
- * @subpackage category
+ * @package    enrol_category
  * @copyright  2010 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
+
 /**
  * category enrolment plugin implementation.
- * @author Petr Skoda
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @author  Petr Skoda
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class enrol_category_plugin extends enrol_plugin {
 
    /**
      * Is it possible to delete enrol instance via standard UI?
      *
-     * @param object $instance
+     * @param stdClass $instance
      * @return bool
      */
     public function instance_deleteable($instance) {
@@ -45,7 +44,7 @@ class enrol_category_plugin extends enrol_plugin {
         if (!enrol_is_enabled('category')) {
             return true;
         }
-        // allow delete only when no synced users here
+        // Allow delete only when no synced users here.
         return !$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id));
     }
 
@@ -55,8 +54,8 @@ class enrol_category_plugin extends enrol_plugin {
      * @return moodle_url page url
      */
     public function get_newinstance_link($courseid) {
-        // instances are added automatically as necessary
-        return NULL;
+        // Instances are added automatically as necessary.
+        return null;
     }
 
     /**
@@ -78,8 +77,8 @@ class enrol_category_plugin extends enrol_plugin {
      * Called after updating/inserting course.
      *
      * @param bool $inserted true if course just inserted
-     * @param object $course
-     * @param object $data form data
+     * @param stdClass $course
+     * @param stdClass $data form data
      * @return void
      */
     public function course_updated($inserted, $course, $data) {
@@ -89,10 +88,8 @@ class enrol_category_plugin extends enrol_plugin {
             return;
         }
 
-        // sync category enrols
+        // Sync category enrols.
         require_once("$CFG->dirroot/enrol/category/locallib.php");
         enrol_category_sync_course($course);
     }
 }
-
-
index 6a7671b..c9687fa 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 /**
  * Local stuff for category enrolment plugin.
  *
- * @package    enrol
- * @subpackage category
+ * @package    enrol_category
  * @copyright  2010 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
+
 /**
  * Event handler for category enrolment plugin.
  *
@@ -33,6 +32,12 @@ defined('MOODLE_INTERNAL') || die();
  * it may fail sometimes, so we always do a full sync in cron too.
  */
 class enrol_category_handler {
+    /**
+     * Triggered when user is assigned a new role.
+     * @static
+     * @param stdClass $ra
+     * @return bool
+     */
     public static function role_assigned($ra) {
         global $DB;
 
@@ -40,20 +45,20 @@ class enrol_category_handler {
             return true;
         }
 
-        //only category level roles are interesting
+        // Only category level roles are interesting.
         $parentcontext = get_context_instance_by_id($ra->contextid);
         if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
             return true;
         }
 
-        // make sure the role is to be actually synchronised
-        // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync)
-        $syscontext = get_context_instance(CONTEXT_SYSTEM);
+        // Make sure the role is to be actually synchronised,
+        // please note we are ignoring overrides of the synchronised capability (for performance reasons in full sync).
+        $syscontext = context_system::instance();
         if (!$DB->record_exists('role_capabilities', array('contextid'=>$syscontext->id, 'roleid'=>$ra->roleid, 'capability'=>'enrol/category:synchronised', 'permission'=>CAP_ALLOW))) {
             return true;
         }
 
-        // add necessary enrol instances
+        // Add necessary enrol instances.
         $plugin = enrol_get_plugin('category');
         $sql = "SELECT c.*
                   FROM {course} c
@@ -67,7 +72,7 @@ class enrol_category_handler {
         }
         $rs->close();
 
-        // now look for missing enrols
+        // Now look for missing enrolments.
         $sql = "SELECT e.*
                   FROM {course} c
                   JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel AND ctx.path LIKE :match)
@@ -84,6 +89,12 @@ class enrol_category_handler {
         return true;
     }
 
+    /**
+     * Triggered when user role is unassigned.
+     * @static
+     * @param stdClass $ra
+     * @return bool
+     */
     public static function role_unassigned($ra) {
         global $DB;
 
@@ -91,14 +102,14 @@ class enrol_category_handler {
             return true;
         }
 
-        // only category level roles are interesting
+        // Only category level roles are interesting.
         $parentcontext = get_context_instance_by_id($ra->contextid);
         if ($parentcontext->contextlevel != CONTEXT_COURSECAT) {
             return true;
         }
 
-        // now this is going to be a bit slow, take all enrolments in child courses and verify each separately
-        $syscontext = get_context_instance(CONTEXT_SYSTEM);
+        // Now this is going to be a bit slow, take all enrolments in child courses and verify each separately.
+        $syscontext = context_system::instance();
         if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
             return true;
         }
@@ -117,9 +128,9 @@ class enrol_category_handler {
         $params['userid'] = $ra->userid;
 
         foreach ($rs as $instance) {
-            $coursecontext = get_context_instance(CONTEXT_COURSE, $instance->courseid);
+            $coursecontext = context_course::instance($instance->courseid);
             $contextids = get_parent_contexts($coursecontext);
-            array_pop($contextids); // remove system context, we are interested in categories only
+            array_pop($contextids); // Remove system context, we are interested in categories only.
 
             list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
             $params = array_merge($params, $contextparams);
@@ -128,7 +139,7 @@ class enrol_category_handler {
                       FROM {role_assignments} ra
                      WHERE ra.userid = :userid AND ra.contextid $contextids AND ra.roleid $roleids";
             if (!$DB->record_exists_sql($sql, $params)) {
-                // user does not have any interesting role in any parent context, let's unenrol
+                // User does not have any interesting role in any parent context, let's unenrol.
                 $plugin->unenrol_user($instance, $ra->userid);
             }
         }
@@ -140,7 +151,7 @@ class enrol_category_handler {
 
 /**
  * Sync all category enrolments in one course
- * @param int $courseid course id
+ * @param stdClass $course
  * @return void
  */
 function enrol_category_sync_course($course) {
@@ -152,11 +163,11 @@ function enrol_category_sync_course($course) {
 
     $plugin = enrol_get_plugin('category');
 
-    $syscontext = get_context_instance(CONTEXT_SYSTEM);
+    $syscontext = context_system::instance();
     $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
 
     if (!$roles) {
-        //nothing to sync, so remove the instance completely if exists
+        // Nothing to sync, so remove the instance completely if exists.
         if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
             foreach ($instances as $instance) {
                 $plugin->delete_instance($instance);
@@ -165,10 +176,10 @@ function enrol_category_sync_course($course) {
         return;
     }
 
-    // first find out if any parent category context contains interesting role assignments
-    $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
+    // First find out if any parent category context contains interesting role assignments.
+    $coursecontext = context_course::instance($course->id);
     $contextids = get_parent_contexts($coursecontext);
-    array_pop($contextids); // remove system context, we are interested in categories only
+    array_pop($contextids); // Remove system context, we are interested in categories only.
 
     list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
     list($contextids, $contextparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'c');
@@ -180,7 +191,7 @@ function enrol_category_sync_course($course) {
              WHERE roleid $roleids AND contextid $contextids";
     if (!$DB->record_exists_sql($sql, $params)) {
         if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
-            // should be max one instance, but anyway
+            // Should be max one instance, but anyway.
             foreach ($instances as $instance) {
                 $plugin->delete_instance($instance);
             }
@@ -188,7 +199,7 @@ function enrol_category_sync_course($course) {
         return;
     }
 
-    // make sure the enrol instance exists - there should be always only one instance
+    // Make sure the enrol instance exists - there should be always only one instance.
     $delinstances = array();
     if ($instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'enrol'=>'category'))) {
         $instance = array_shift($instances);
@@ -198,7 +209,7 @@ function enrol_category_sync_course($course) {
         $instance = $DB->get_record('enrol', array('id'=>$i));
     }
 
-    // add new enrolments
+    // Add new enrolments.
     $sql = "SELECT ra.userid, ra.estart
               FROM (SELECT xra.userid, MIN(xra.timemodified) AS estart
                       FROM {role_assignments} xra
@@ -214,7 +225,7 @@ function enrol_category_sync_course($course) {
     }
     $rs->close();
 
-    // remove unwanted enrolments
+    // Remove unwanted enrolments.
     $sql = "SELECT DISTINCT ue.userid
               FROM {user_enrolments} ue
          LEFT JOIN {role_assignments} ra ON (ra.roleid $roleids AND ra.contextid $contextids AND ra.userid = ue.userid)
@@ -226,46 +237,71 @@ function enrol_category_sync_course($course) {
     $rs->close();
 
     if ($delinstances) {
-        // we have to do this as the last step in order to prevent temporary unenrolment
+        // We have to do this as the last step in order to prevent temporary unenrolment.
         foreach ($delinstances as $delinstance) {
             $plugin->delete_instance($delinstance);
         }
     }
 }
 
-function enrol_category_sync_full() {
+/**
+ * Synchronise courses in all categories.
+ *
+ * It gets out-of-sync if:
+ * - you move course to different category
+ * - reorder categories
+ * - disable enrol_category and enable it again
+ *
+ * @param bool $verbose
+ * @return int exit code - 0 is ok, 1 means error, 2 if plugin disabled
+ */
+function enrol_category_sync_full($verbose = false) {
     global $DB;
 
 
     if (!enrol_is_enabled('category')) {
-        return;
+        return 2;
     }
 
-    // we may need a lot of time here
+    // We may need a lot of time here.
     @set_time_limit(0);
 
     $plugin = enrol_get_plugin('category');
 
-    $syscontext = get_context_instance(CONTEXT_SYSTEM);
+    $syscontext = context_system::instance();
 
-    // any interesting roles worth synchronising?
+    // Any interesting roles worth synchronising?
     if (!$roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext)) {
         // yay, nothing to do, so let's remove all leftovers
+        if ($verbose) {
+            mtrace("No roles with 'enrol/category:synchronised' capability found.");
+        }
         if ($instances = $DB->get_records('enrol', array('enrol'=>'category'))) {
             foreach ($instances as $instance) {
+                if ($verbose) {
+                    mtrace("  deleting category enrol instance from course {$instance->courseid}");
+                }
                 $plugin->delete_instance($instance);
             }
         }
-        return;
+        return 0;
+    }
+    $rolenames = array();
+    foreach($roles as $role) {
+        $rolenames[$role->id] = $role->shortname;
+    }
+    if ($verbose) {
+        mtrace('Synchronising category enrolments for roles: '.implode(', ', $rolenames).'...');
     }
 
     list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r');
     $params['courselevel'] = CONTEXT_COURSE;
     $params['catlevel'] = CONTEXT_COURSECAT;
 
-    // first of all add necessary enrol instances to all courses
+    // First of all add necessary enrol instances to all courses.
     $parentcat = $DB->sql_concat("cat.path", "'/%'");
-    // need whole course records to be used by add_instance(), use inner view (ci) to
+    $parentcctx = $DB->sql_concat("cctx.path", "'/%'");
+    // Need whole course records to be used by add_instance(), use inner view (ci) to
     // get distinct records only.
     // TODO: Moodle 2.1. Improve enrol API to accept courseid / courserec
     $sql = "SELECT c.*
@@ -288,17 +324,16 @@ function enrol_category_sync_full() {
     }
     $rs->close();
 
-    // now look for courses that do not have any interesting roles in parent contexts,
-    // but still have the instance and delete them
+    // Now look for courses that do not have any interesting roles in parent contexts,
+    // but still have the instance and delete them.
     $sql = "SELECT e.*
               FROM {enrol} e
               JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
-         LEFT JOIN (SELECT DISTINCT cctx.path
-                      FROM {course_categories} cc
+         LEFT JOIN ({course_categories} cc
                       JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
                       JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
-                   ) cat ON (ctx.path LIKE $parentcat)
-             WHERE e.enrol = 'category' AND cat.path IS NULL";
+                   ) ON (ctx.path LIKE $parentcctx)
+             WHERE e.enrol = 'category' AND cc.id IS NULL";
 
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $instance) {
@@ -306,7 +341,7 @@ function enrol_category_sync_full() {
     }
     $rs->close();
 
-    // add missing enrolments
+    // Add missing enrolments.
     $sql = "SELECT e.*, cat.userid, cat.estart
               FROM {enrol} e
               JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
@@ -325,25 +360,36 @@ function enrol_category_sync_full() {
         unset($instance->userid);
         unset($instance->estart);
         $plugin->enrol_user($instance, $userid, null, $estart);
+        if ($verbose) {
+            mtrace("  enrolling: user $userid ==> course $instance->courseid");
+        }
     }
     $rs->close();
 
-    // remove stale enrolments
+    // Remove stale enrolments.
     $sql = "SELECT e.*, ue.userid
               FROM {enrol} e
               JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)
               JOIN {user_enrolments} ue ON (ue.enrolid = e.id)
-         LEFT JOIN (SELECT DISTINCT cctx.path, ra.userid
-                      FROM {course_categories} cc
+         LEFT JOIN ({course_categories} cc
                       JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)
                       JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid $roleids)
-                   ) cat ON (ctx.path LIKE $parentcat AND cat.userid = ue.userid)
-             WHERE e.enrol = 'category' AND cat.userid IS NULL";
+                   ) ON (ctx.path LIKE $parentcctx AND ra.userid = ue.userid)
+             WHERE e.enrol = 'category' AND cc.id IS NULL";
     $rs = $DB->get_recordset_sql($sql, $params);
     foreach($rs as $instance) {
         $userid = $instance->userid;
         unset($instance->userid);
         $plugin->unenrol_user($instance, $userid);
+        if ($verbose) {
+            mtrace("  unenrolling: user $userid ==> course $instance->courseid");
+        }
     }
     $rs->close();
+
+    if ($verbose) {
+        mtrace('...user enrolment synchronisation finished.');
+    }
+
+    return 0;
 }
index a854267..484c23b 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * category enrolment plugin settings and presets.
+ * Category enrolment plugin settings and presets.
  *
- * @package    enrol
- * @subpackage category
+ * @package    enrol_category
  * @copyright  2010 Petr Skoda {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -35,4 +33,3 @@ if ($ADMIN->fulltree) {
     //--- enrol instance defaults ----------------------------------------------------------------------------
 
 }
-
diff --git a/enrol/category/tests/sync_test.php b/enrol/category/tests/sync_test.php
new file mode 100644 (file)
index 0000000..3649764
--- /dev/null
@@ -0,0 +1,365 @@
+<?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/>.
+
+/**
+ * Category enrolment sync functional test.
+ *
+ * @package    enrol_category
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/enrol/category/locallib.php');
+
+class enrol_category_testcase extends advanced_testcase {
+
+    protected function enable_plugin() {
+        $enabled = enrol_get_plugins(true);
+        $enabled['category'] = true;
+        $enabled = array_keys($enabled);
+        set_config('enrol_plugins_enabled', implode(',', $enabled));
+    }
+
+    protected function disable_plugin() {
+        $enabled = enrol_get_plugins(true);
+        unset($enabled['category']);
+        $enabled = array_keys($enabled);
+        set_config('enrol_plugins_enabled', implode(',', $enabled));
+    }
+
+    protected function enable_role_sync($roleid) {
+        global $DB;
+
+        $syscontext = context_system::instance();
+
+        if ($rc = $DB->record_exists('role_capabilities', array('capability'=>'enrol/category:synchronised', 'roleid'=>$roleid, 'contextid'=>$syscontext->id))) {
+            if ($rc->permission != CAP_ALLOW) {
+                $rc->permission = CAP_ALLOW;
+                $DB->update_record('role_capabilities', $rc);
+            }
+        } else {
+            $rc = new stdClass();
+            $rc->capability = 'enrol/category:synchronised';
+            $rc->roleid = $roleid;
+            $rc->contextid = $syscontext->id;
+            $rc->permission = CAP_ALLOW;
+            $rc->timemodified = time();
+            $rc->modifierid = 0;
+            $DB->insert_record('role_capabilities', $rc);
+        }
+    }
+
+    protected function disable_role_sync($roleid) {
+        global $DB;
+
+        $syscontext = context_system::instance();
+
+        $DB->delete_records('role_capabilities', array('capability'=>'enrol/category:synchronised', 'roleid'=>$roleid, 'contextid'=>$syscontext->id));
+    }
+
+    /**
+     * Test utility methods used in syn test, fail here means something
+     * in core accesslib was changed, but it is possible that only this test
+     * is affected, nto the plugin itself...
+     */
+    public function test_utils() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $syscontext = context_system::instance();
+
+        $this->assertFalse(enrol_is_enabled('category'));
+        $this->enable_plugin();
+        $this->assertTrue(enrol_is_enabled('category'));
+
+        $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
+        $this->assertEmpty($roles);
+
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+
+        $this->enable_role_sync($studentrole->id);
+        $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
+        $this->assertEquals(1, count($roles));
+        $this->assertEquals($studentrole, reset($roles));
+
+        $this->disable_role_sync($studentrole->id);
+        $roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext);
+        $this->assertEmpty($roles);
+    }
+
+    public function test_handler_sync() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Setup a few courses and categories.
+
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+        $teacherrole = $DB->get_record('role', array('shortname'=>'teacher'));
+        $this->assertNotEmpty($teacherrole);
+        $managerrole = $DB->get_record('role', array('shortname'=>'manager'));
+        $this->assertNotEmpty($managerrole);
+
+        $cat1 = $this->getDataGenerator()->create_category();
+        $cat2 = $this->getDataGenerator()->create_category();
+        $cat3 = $this->getDataGenerator()->create_category(array('parent'=>$cat2->id));
+
+        $course1 = $this->getDataGenerator()->create_course(array('category'=>$cat1->id));
+        $course2 = $this->getDataGenerator()->create_course(array('category'=>$cat2->id));
+        $course3 = $this->getDataGenerator()->create_course(array('category'=>$cat3->id));
+        $course4 = $this->getDataGenerator()->create_course(array('category'=>$cat3->id));
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        $this->enable_role_sync($studentrole->id);
+        $this->enable_role_sync($teacherrole->id);
+        $this->enable_plugin();
+
+        $this->assertEquals(0, $DB->count_records('role_assignments', array()));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array()));
+
+        // Test assign event.
+
+        role_assign($managerrole->id, $user1->id, context_coursecat::instance($cat1->id));
+        role_assign($managerrole->id, $user3->id, context_course::instance($course1->id));
+        role_assign($managerrole->id, $user3->id, context_course::instance($course2->id));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array()));
+
+        role_assign($studentrole->id, $user1->id, context_coursecat::instance($cat2->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course2->id), $user1->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course3->id), $user1->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course4->id), $user1->id));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array()));
+
+        role_assign($managerrole->id, $user2->id, context_coursecat::instance($cat3->id));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array()));
+
+        role_assign($teacherrole->id, $user4->id, context_coursecat::instance($cat1->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course1->id), $user4->id));
+        $this->assertEquals(4, $DB->count_records('user_enrolments', array()));
+
+        // Test role unassigned event.
+
+        role_unassign($teacherrole->id, $user4->id, context_coursecat::instance($cat1->id)->id);
+        $this->assertFalse(is_enrolled(context_course::instance($course1->id), $user4->id));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array()));
+
+        // Make sure handlers are disabled when plugin disabled.
+
+        $this->disable_plugin();
+        role_unassign($studentrole->id, $user1->id, context_coursecat::instance($cat2->id)->id);
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array()));
+
+        role_assign($studentrole->id, $user3->id, context_coursecat::instance($cat1->id));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array()));
+
+    }
+
+    public function test_sync_course() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Setup a few courses and categories.
+
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+        $teacherrole = $DB->get_record('role', array('shortname'=>'teacher'));
+        $this->assertNotEmpty($teacherrole);
+        $managerrole = $DB->get_record('role', array('shortname'=>'manager'));
+        $this->assertNotEmpty($managerrole);
+
+        $cat1 = $this->getDataGenerator()->create_category();
+        $cat2 = $this->getDataGenerator()->create_category();
+        $cat3 = $this->getDataGenerator()->create_category(array('parent'=>$cat2->id));
+
+        $course1 = $this->getDataGenerator()->create_course(array('category'=>$cat1->id));
+        $course2 = $this->getDataGenerator()->create_course(array('category'=>$cat2->id));
+        $course3 = $this->getDataGenerator()->create_course(array('category'=>$cat3->id));
+        $course4 = $this->getDataGenerator()->create_course(array('category'=>$cat3->id));
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        $this->enable_role_sync($studentrole->id);
+        $this->enable_role_sync($teacherrole->id);
+        $this->enable_plugin();
+
+        $this->assertEquals(0, $DB->count_records('role_assignments', array()));
+        role_assign($managerrole->id, $user1->id, context_coursecat::instance($cat1->id));
+        role_assign($managerrole->id, $user3->id, context_course::instance($course1->id));
+        role_assign($managerrole->id, $user3->id, context_course::instance($course2->id));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array()));
+
+
+        $this->disable_plugin(); // Stops the event handlers.
+        role_assign($studentrole->id, $user1->id, context_coursecat::instance($cat2->id));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array()));
+        $this->enable_plugin();
+        enrol_category_sync_course($course2);
+        $this->assertTrue(is_enrolled(context_course::instance($course2->id), $user1->id));
+        $this->assertFalse(is_enrolled(context_course::instance($course3->id), $user1->id));
+        $this->assertFalse(is_enrolled(context_course::instance($course4->id), $user1->id));
+        $this->assertEquals(1, $DB->count_records('user_enrolments', array()));
+
+        enrol_category_sync_course($course2);
+        enrol_category_sync_course($course3);
+        enrol_category_sync_course($course4);
+        $this->assertFalse(is_enrolled(context_course::instance($course1->id), $user1->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course2->id), $user1->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course3->id), $user1->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course4->id), $user1->id));
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array()));
+
+        $this->disable_plugin(); // Stops the event handlers.
+        role_assign($studentrole->id, $user2->id, context_coursecat::instance($cat1->id));
+        role_assign($teacherrole->id, $user4->id, context_coursecat::instance($cat1->id));
+        role_unassign($studentrole->id, $user1->id, context_coursecat::instance($cat2->id)->id);
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array()));
+        $this->enable_plugin();
+        enrol_category_sync_course($course2);
+        $this->assertFalse(is_enrolled(context_course::instance($course2->id), $user1->id));
+        $this->assertFalse(is_enrolled(context_course::instance($course2->id), $user2->id));
+        $this->assertFalse(is_enrolled(context_course::instance($course2->id), $user4->id));
+        enrol_category_sync_course($course1);
+        enrol_category_sync_course($course3);
+        enrol_category_sync_course($course4);
+        $this->assertEquals(2, $DB->count_records('user_enrolments', array()));
+        $this->assertTrue(is_enrolled(context_course::instance($course1->id), $user2->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course1->id), $user4->id));
+
+        $this->disable_role_sync($studentrole->id);
+        enrol_category_sync_course($course1);
+        enrol_category_sync_course($course2);
+        enrol_category_sync_course($course3);
+        enrol_category_sync_course($course4);
+        $this->assertEquals(1, $DB->count_records('user_enrolments', array()));
+        $this->assertTrue(is_enrolled(context_course::instance($course1->id), $user4->id));
+
+        $this->assertEquals(1, $DB->count_records('enrol', array('enrol'=>'category')));
+        $this->disable_role_sync($teacherrole->id);
+        enrol_category_sync_course($course1);
+        enrol_category_sync_course($course2);
+        enrol_category_sync_course($course3);
+        enrol_category_sync_course($course4);
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array()));
+        $this->assertEquals(0, $DB->count_records('enrol', array('enrol'=>'category')));
+    }
+
+    public function test_sync_full() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Setup a few courses and categories.
+
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+        $teacherrole = $DB->get_record('role', array('shortname'=>'teacher'));
+        $this->assertNotEmpty($teacherrole);
+        $managerrole = $DB->get_record('role', array('shortname'=>'manager'));
+        $this->assertNotEmpty($managerrole);
+
+        $cat1 = $this->getDataGenerator()->create_category();
+        $cat2 = $this->getDataGenerator()->create_category();
+        $cat3 = $this->getDataGenerator()->create_category(array('parent'=>$cat2->id));
+
+        $course1 = $this->getDataGenerator()->create_course(array('category'=>$cat1->id));
+        $course2 = $this->getDataGenerator()->create_course(array('category'=>$cat2->id));
+        $course3 = $this->getDataGenerator()->create_course(array('category'=>$cat3->id));
+        $course4 = $this->getDataGenerator()->create_course(array('category'=>$cat3->id));
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        $this->enable_role_sync($studentrole->id);
+        $this->enable_role_sync($teacherrole->id);
+        $this->enable_plugin();
+
+        $this->assertEquals(0, $DB->count_records('role_assignments', array()));
+        role_assign($managerrole->id, $user1->id, context_coursecat::instance($cat1->id));
+        role_assign($managerrole->id, $user3->id, context_course::instance($course1->id));
+        role_assign($managerrole->id, $user3->id, context_course::instance($course2->id));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array()));
+
+        $result = enrol_category_sync_full();
+        $this->assertSame(0, $result);
+
+        $this->disable_plugin();
+        role_assign($studentrole->id, $user1->id, context_coursecat::instance($cat2->id));
+        $this->enable_plugin();
+        $result = enrol_category_sync_full();
+        $this->assertSame(0, $result);
+        $this->assertEquals(3, $DB->count_records('user_enrolments', array()));
+        $this->assertTrue(is_enrolled(context_course::instance($course2->id), $user1->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course3->id), $user1->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course4->id), $user1->id));
+
+        $this->disable_plugin();
+        role_unassign($studentrole->id, $user1->id, context_coursecat::instance($cat2->id)->id);
+        role_assign($studentrole->id, $user2->id, context_coursecat::instance($cat1->id));
+        role_assign($teacherrole->id, $user4->id, context_coursecat::instance($cat1->id));
+        role_assign($teacherrole->id, $user3->id, context_coursecat::instance($cat2->id));
+        role_assign($managerrole->id, $user3->id, context_course::instance($course3->id));
+        $this->enable_plugin();
+        $result = enrol_category_sync_full();
+        $this->assertSame(0, $result);
+        $this->assertEquals(5, $DB->count_records('user_enrolments', array()));
+        $this->assertTrue(is_enrolled(context_course::instance($course1->id), $user2->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course1->id), $user4->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course2->id), $user3->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course3->id), $user3->id));
+        $this->assertTrue(is_enrolled(context_course::instance($course4->id), $user3->id));
+
+        // Cleanup everything.
+
+        $this->assertNotEmpty($DB->count_records('role_assignments', array()));
+        $this->assertNotEmpty($DB->count_records('user_enrolments', array()));
+
+        $this->disable_plugin();
+        role_unassign_all(array('roleid'=>$studentrole->id));
+        role_unassign_all(array('roleid'=>$managerrole->id));
+        role_unassign_all(array('roleid'=>$teacherrole->id));
+
+        $result = enrol_category_sync_full();
+        $this->assertSame(2, $result);
+        $this->assertEquals(0, $DB->count_records('role_assignments', array()));
+        $this->assertNotEmpty($DB->count_records('user_enrolments', array()));
+        $this->disable_role_sync($studentrole->id);
+        $this->disable_role_sync($teacherrole->id);
+
+        $this->enable_plugin();
+        $result = enrol_category_sync_full();
+        $this->assertSame(0, $result);
+        $this->assertEquals(0, $DB->count_records('role_assignments', array()));
+        $this->assertEquals(0, $DB->count_records('user_enrolments', array()));
+        $this->assertEquals(0, $DB->count_records('enrol', array('enrol'=>'category')));
+    }
+}
index 371a7f0..23c33cf 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012061700;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2012061700;        // Requires this Moodle version
+$plugin->version   = 2012081800;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2012062501;        // Requires this Moodle version
 $plugin->component = 'enrol_category';  // Full name of the plugin (used for diagnostics)
-$plugin->cron      = 60;
\ No newline at end of file
+$plugin->cron      = 60;
index 2009867..cfa352c 100644 (file)
@@ -121,16 +121,7 @@ class enrol_cohort_plugin extends enrol_plugin {
      * @return void
      */
     public function course_updated($inserted, $course, $data) {
-        global $CFG;
-
-        if (!$inserted) {
-            // sync cohort enrols
-            require_once("$CFG->dirroot/enrol/cohort/locallib.php");
-            enrol_cohort_sync($course->id);
-        } else {
-            // cohorts are never inserted automatically
-        }
-
+        // It turns out there is no need for cohorts to deal with this hook, see MDL-34870.
     }
 
     /**
index f7d5030..41b7337 100644 (file)
@@ -278,7 +278,7 @@ class course_enrolment_manager {
         global $DB, $CFG;
 
         // Add some additional sensible conditions
-        $tests = array("id <> :guestid", 'u.deleted = 0', 'u.confirmed = 1');
+        $tests = array("u.id <> :guestid", 'u.deleted = 0', 'u.confirmed = 1');
         $params = array('guestid' => $CFG->siteguest);
         if (!empty($search)) {
             $conditions = get_extra_user_fields($this->get_context());
@@ -306,10 +306,9 @@ class course_enrolment_manager {
         $fields      = 'SELECT '.$ufields;
         $countfields = 'SELECT COUNT(1)';
         $sql = " FROM {user} u
+            LEFT JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = :enrolid)
                 WHERE $wherecondition
-                      AND u.id NOT IN (SELECT ue.userid
-                                         FROM {user_enrolments} ue
-                                         JOIN {enrol} e ON (e.id = ue.enrolid AND e.id = :enrolid))";
+                      AND ue.id IS NULL";
         $order = ' ORDER BY u.lastname ASC, u.firstname ASC';
         $params['enrolid'] = $enrolid;
         $totalusers = $DB->count_records_sql($countfields . $sql, $params);
@@ -353,12 +352,9 @@ class course_enrolment_manager {
         $fields      = 'SELECT '.user_picture::fields('u', array('username','lastaccess'));
         $countfields = 'SELECT COUNT(u.id)';
         $sql   = " FROM {user} u
+              LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid = :contextid)
                   WHERE $wherecondition
-                    AND u.id NOT IN (
-                           SELECT u.id
-                             FROM {role_assignments} r, {user} u
-                            WHERE r.contextid = :contextid AND
-                                  u.id = r.userid)";
+                    AND ra.id IS NULL";
         $order = ' ORDER BY lastname ASC, firstname ASC';
 
         $params['contextid'] = $this->context->id;
index 3255882..1d09681 100644 (file)
@@ -55,11 +55,9 @@ class enrol_manual_potential_participant extends user_selector_base {
         $countfields = 'SELECT COUNT(1)';
 
         $sql = " FROM {user} u
-                WHERE $wherecondition AND
-                      u.id NOT IN (
-                          SELECT ue.userid
-                            FROM {user_enrolments} ue
-                            JOIN {enrol} e ON (e.id = ue.enrolid AND e.id = :enrolid))";
+            LEFT JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = :enrolid)
+                WHERE $wherecondition
+                      AND ue.id IS NULL";
         $order = ' ORDER BY u.lastname ASC, u.firstname ASC';
 
         if (!$this->is_validating()) {
index b4a6bf4..e5ec7dd 100644 (file)
@@ -34,6 +34,7 @@ require("../../config.php");
 require_once("lib.php");
 require_once($CFG->libdir.'/eventslib.php');
 require_once($CFG->libdir.'/enrollib.php');
+require_once($CFG->libdir . '/filelib.php');
 
 
 /// Keep out casual intruders
@@ -89,14 +90,17 @@ if (! $plugin_instance = $DB->get_record("enrol", array("id"=>$data->instanceid,
 $plugin = enrol_get_plugin('paypal');
 
 /// Open a connection back to PayPal to validate the data
-$header = '';
-$header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
-$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
-$header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
+$c = new curl();
+$options = array(
+    'returntransfer' => true,
+    'httpheader' => array('application/x-www-form-urlencoded'),
+    'timeout' => 30,
+);
 $paypaladdr = empty($CFG->usepaypalsandbox) ? 'www.paypal.com' : 'www.sandbox.paypal.com';
-$fp = fsockopen ($paypaladdr, 80, $errno, $errstr, 30);
+$location = "https://$paypaladdr/cgi-bin/webscr";
+$result = $c->post($location, $req, $options);
 
-if (!$fp) {  /// Could not open a socket to PayPal - FAIL
+if (!$result) {  /// Could not connect to PayPal - FAIL
     echo "<p>Error: could not access paypal.com</p>";
     message_paypal_error_to_admin("Could not access paypal.com to verify payment", $data);
     die;
@@ -104,12 +108,9 @@ if (!$fp) {  /// Could not open a socket to PayPal - FAIL
 
 /// Connection is OK, so now we post the data to validate it
 
-fputs ($fp, $header.$req);
-
 /// Now read the response and check if everything is OK.
 
-while (!feof($fp)) {
-    $result = fgets($fp, 1024);
+if (strlen($result) > 0) {
     if (strcmp($result, "VERIFIED") == 0) {          // VALID PAYMENT!
 
 
@@ -296,7 +297,6 @@ while (!feof($fp)) {
     }
 }
 
-fclose($fp);
 exit;
 
 
index 54ffaff..def6b44 100644 (file)
@@ -89,7 +89,7 @@ function filter_algebra_image($imagefile, $tex= "", $height="", $width="", $alig
 }
 
 class filter_algebra extends moodle_text_filter {
-    function filter($text, array $options = array()){
+    public function filter($text, array $options = array()){
         global $CFG, $DB;
 
         /// Do a quick check using stripos to avoid unnecessary wor
@@ -114,9 +114,6 @@ class filter_algebra extends moodle_text_filter {
 #        return $text;
 #    }
 
-
-        $text .= ' ';
-
         preg_match_all('/@(@@+)([^@])/',$text,$matches);
         for ($i=0;$i<count($matches[0]);$i++) {
             $replacement = str_replace('@','&#x00040;',$matches[1][$i]).$matches[2][$i];
@@ -129,6 +126,17 @@ class filter_algebra extends moodle_text_filter {
         preg_match_all('/<algebra>(.+?)<\/algebra>|@@(.+?)@@/is', $text, $matches);
         for ($i=0; $i<count($matches[0]); $i++) {
             $algebra = $matches[1][$i] . $matches[2][$i];
+
+            // Look for some common false positives, and skip processing them.
+            if ($algebra == 'PLUGINFILE' || $algebra == 'DRAFTFILE') {
+                // Raw pluginfile URL.
+                continue;
+            }
+            if (preg_match('/^ -\d+(,\d+)? \+\d+(,\d+)? $/', $algebra)) {
+                // Part of a unified diff.
+                continue;
+            }
+
             $algebra = str_replace('<nolink>','',$algebra);
             $algebra = str_replace('</nolink>','',$algebra);
             $algebra = str_replace('<span class="nolink">','',$algebra);
@@ -159,7 +167,7 @@ class filter_algebra extends moodle_text_filter {
                $algebra = str_replace('upsilon','zupslon',$algebra);
                $algebra = preg_replace('!\r\n?!',' ',$algebra);
                $algebra = escapeshellarg($algebra);
-               if ( (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows") ) {
+               if ( (PHP_OS == "WINNT") || (PHP_OS == "WIN32") || (PHP_OS == "Windows")) {
                   $cmd  = "cd $CFG->dirroot\\filter\\algebra & algebra2tex.pl $algebra";
                } else {
                   $cmd  = "cd $CFG->dirroot/filter/algebra; ./algebra2tex.pl $algebra";
@@ -220,6 +228,7 @@ class filter_algebra extends moodle_text_filter {
                   $texexp = preg_replace('/\\\int\\\left\((.+?d[a-z])\\\right\)/s','\int '. "\$1 ",$texexp);
                   $texexp = preg_replace('/\\\lim\\\left\((.+?),(.+?),(.+?)\\\right\)/s','\lim_'. "{\$2\\to \$3}\$1 ",$texexp);
                   $texexp = str_replace('\mbox', '', $texexp); // now blacklisted in tex, sorry
+                  $texcache = new stdClass();
                   $texcache->filter = 'algebra';
                   $texcache->version = 1;
                   $texcache->md5key = $md5;
diff --git a/filter/algebra/tests/filter_test.php b/filter/algebra/tests/filter_test.php
new file mode 100644 (file)
index 0000000..6c7db8f
--- /dev/null
@@ -0,0 +1,90 @@
+<?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 test for the filter_algebra
+ *
+ * @package    filter_algebra
+ * @category   phpunit
+ * @copyright  2012 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/filter/algebra/filter.php');
+
+
+/**
+ * Unit tests for filter_algebra.
+ *
+ * Note that this only tests some of the filter logic. It does not acutally test
+ * the normal case of the filter working, because I cannot make it work on my
+ * test server, and if it does not work here, it probably does not also work
+ * for other people. A failing test will be irritating noise.
+ *
+ * @copyright  2012 Tim Hunt
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class filter_algebra_testcase extends basic_testcase {
+
+    protected $filter;
+
+    protected function setUp() {
+        parent::setUp();
+        $this->filter = new filter_algebra(context_system::instance(), array());
+    }
+
+    function test_algebra_filter_no_algebra() {
+        $this->assertEquals('<p>Look no algebra!</p>',
+                $this->filter->filter('<p>Look no algebra!</p>'));
+    }
+
+
+    function test_algebra_filter_pluginfile() {
+        $this->assertEquals('<img src="@@PLUGINFILE@@/photo.jpg">',
+                $this->filter->filter('<img src="@@PLUGINFILE@@/photo.jpg">'));
+    }
+
+    function test_algebra_filter_draftfile() {
+        $this->assertEquals('<img src="@@DRAFTFILE@@/photo.jpg">',
+                $this->filter->filter('<img src="@@DRAFTFILE@@/photo.jpg">'));
+    }
+
+    function test_algebra_filter_unified_diff() {
+        $diff = '
+diff -u -r1.1 Worksheet.php
+--- Worksheet.php   26 Sep 2003 04:18:02 -0000  1.1
++++ Worksheet.php   18 Nov 2009 03:58:50 -0000
+@@ -1264,10 +1264,10 @@
+         }
+
+         // Strip the = or @ sign at the beginning of the formula string
+-        if (ereg("^=",$formula)) {
++        if (preg_match("/^=/",$formula)) {
+             $formula = preg_replace("/(^=)/","",$formula);
+         }
+-        elseif(ereg("^@",$formula)) {
++        elseif(preg_match("/^@/",$formula)) {
+             $formula = preg_replace("/(^@)/","",$formula);
+         }
+         else {
+';
+        $this->assertEquals('<pre>' . $diff . '</pre>',
+                $this->filter->filter('<pre>' . $diff . '</pre>'));
+    }
+}
index b166e54..d7d3b5b 100644 (file)
@@ -2574,6 +2574,7 @@ abstract class grade_helper {
      * @return array
      */
     public static function get_info_letters($courseid) {
+        global $SITE;
         if (self::$letterinfo !== null) {
             return self::$letterinfo;
         }
@@ -2581,9 +2582,15 @@ abstract class grade_helper {
         $canmanage = has_capability('moodle/grade:manage', $context);
         $canmanageletters = has_capability('moodle/grade:manageletters', $context);
         if ($canmanage || $canmanageletters) {
+            // Redirect to system context when report is accessed from admin settings MDL-31633
+            if ($context->instanceid == $SITE->id) {
+                $param = array('edit' => 1);
+            } else {
+                $param = array('edit' => 1,'id' => $context->id);
+            }
             self::$letterinfo = array(
                 'view' => new grade_plugin_info('view', new moodle_url('/grade/edit/letter/index.php', array('id'=>$context->id)), get_string('view')),
-                'edit' => new grade_plugin_info('edit', new moodle_url('/grade/edit/letter/index.php', array('edit'=>1,'id'=>$context->id)), get_string('edit'))
+                'edit' => new grade_plugin_info('edit', new moodle_url('/grade/edit/letter/index.php', $param), get_string('edit'))
             );
         } else {
             self::$letterinfo = false;
index 1dfd27c..f7c639b 100644 (file)
@@ -30,6 +30,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['parentlanguage'] = 'es';
+$string['parentlanguage'] = '';
 $string['thisdirection'] = 'ltr';
 $string['thislanguage'] = 'Español - Mexico';
index c8574b5..a395a57 100644 (file)
@@ -40,6 +40,7 @@ $string['databasehost'] = '데이터베이스 호스트';
 $string['databasename'] = '데이터베이스 명칭';
 $string['databasetypehead'] = '데이터베이스 드라이버 선택';
 $string['dataroot'] = '데이타 경로';
+$string['datarootpermission'] = '데이터 디렉토리 권한';
 $string['dbprefix'] = '테이블 접두어';
 $string['dirroot'] = '무들 디렉토리';
 $string['environmenthead'] = '구동환경을 점검합니다...';
index 3c005d8..9977e2b 100644 (file)
@@ -34,9 +34,11 @@ $string['clianswerno'] = 'n';
 $string['cliansweryes'] = 't';
 $string['cliincorrectvalueerror'] = 'Klaida, klaidinga "{$a->option}" reikšmė "{$a->value}"';
 $string['cliincorrectvalueretry'] = 'Klaidinga reikšmė, bandykite dar kartą';
-$string['clitypevalue'] = 'tipo reikšmė';
-$string['clitypevaluedefault'] = 'tipo reikšmė, paspauskite „Enter“, jei norite naudoti numatytąją reikšmę ({$a})';
-$string['cliunknowoption'] = 'Neatpažintos parinktys: {$a} naudokite --žinyno parinktį.';
+$string['clitypevalue'] = 'įveskite reikšmę';
+$string['clitypevaluedefault'] = 'įveskite reikšmę, paspauskite „Enter“, jei norite naudoti numatytąją reikšmę ({$a})';
+$string['cliunknowoption'] = 'Neatpažintos parinktys:
+{$a}
+Naudokite --help parinktį.';
 $string['cliyesnoprompt'] = 'įveskite t (taip) arba n (ne)';
 $string['environmentrequireinstall'] = 'turi būti įdiegta ir įgalinta';
 $string['environmentrequireversion'] = 'reikalinga {$a->needed} versija, o Jūs turite {$a->current}';
index 61786be..31980bd 100644 (file)
@@ -34,11 +34,11 @@ $string['clianswerno'] = 'n';
 $string['cliansweryes'] = 't';
 $string['cliincorrectvalueerror'] = 'Błąd, niepoprawna wartość "{$a->value}" dla "{$a->option}"';
 $string['cliincorrectvalueretry'] = 'Nieprawidłowa wartość, spróbuj ponownie';
-$string['clitypevalue'] = 'typ wartości';
-$string['clitypevaluedefault'] = 'typ wartości, naciśnij Enter, aby użyć wartości domyślnej ({$a})';
-$string['cliunknowoption'] = 'Nieznana opcja:
+$string['clitypevalue'] = 'wartość typu';
+$string['clitypevaluedefault'] = 'Typ wartości, naciśnij Enter, aby użyć wartości domyślnej ({$a})';
+$string['cliunknowoption'] = 'Nieznane opcje:
   {$a}
-Proszę użyć pomocy.';
+Proszę użyć opcji --help.';
 $string['cliyesnoprompt'] = 'wpisz y (czyli tak) lub n (czyli nie)';
 $string['environmentrequireinstall'] = 'jest niezbędnę, żeby było zainstalowane/włączone';
 $string['environmentrequireversion'] = 'wersja {$a->needed} jest niezbędna a ty używasz wersji {$a->current}';
index c338c8b..93647de 100644 (file)
@@ -81,7 +81,7 @@ $string['phpversionhelp'] = '<p>Moodle需要PHP 4.3.0或5.1.0(5.0.x有若干
 (如果正使用5.0.x,您也可以降级到4.4.x版)</p>
 ';
 $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
-$string['welcomep20'] = '您看到这个页面表明您已经成功地在您的计算机上安装了<strong>{$a->packname} {$a->packversion}</strong>。恭喜您!';
+$string['welcomep20'] = '您看到这个页面表明您已经成功地在您的计算机上安装并启用了<strong>{$a->packname} {$a->packversion}</strong>软件包。恭喜您!';
 $string['welcomep30'] = '<strong>{$a->installername}</strong>的此发行版包含了可以创建<strong>Moodle</strong>运行环境的应用程序:';
 $string['welcomep40'] = '这个软件包还包含了<strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>。';
 $string['welcomep50'] = '使用本软件包中包含的应用程序时应遵循它们各自的授权协议。整个<strong>{$a->installername}</strong>软件包都是<a href="http://www.opensource.org/docs/definition_plain.html">开源</a>的,并且遵循<a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>授权协议发布。';
index afb5383..5eff878 100644 (file)
@@ -80,7 +80,7 @@ $PAGE->set_title(get_string('iplookup', 'admin').': '.$title);
 $PAGE->set_heading($title);
 echo $OUTPUT->header();
 
-if (empty($CFG->googlemapkey)) {
+if (empty($CFG->googlemapkey) and empty($CFG->googlemapkey3)) {
     $imgwidth  = 620;
     $imgheight = 310;
     $dotwidth  = 18;
@@ -95,11 +95,23 @@ if (empty($CFG->googlemapkey)) {
     echo '</div>';
     echo '<div id="note">'.$info['note'].'</div>';
 
-} else {
+} else if (empty($CFG->googlemapkey3)) {
     $PAGE->requires->js(new moodle_url("http://maps.google.com/maps?file=api&v=2&key=$CFG->googlemapkey"));
     $module = array('name'=>'core_iplookup', 'fullpath'=>'/iplookup/module.js');
     $PAGE->requires->js_init_call('M.core_iplookup.init', array($info['latitude'], $info['longitude']), true, $module);
 
+    echo '<div id="map" style="width: 650px; height: 360px"></div>';
+    echo '<div id="note">'.$info['note'].'</div>';
+
+} else {
+    if (strpos($CFG->wwwroot, 'https:') === 0) {
+        $PAGE->requires->js(new moodle_url('https://maps.googleapis.com/maps/api/js', array('key'=>$CFG->googlemapkey3, 'sensor'=>'false')));
+    } else {
+        $PAGE->requires->js(new moodle_url('http://maps.googleapis.com/maps/api/js', array('key'=>$CFG->googlemapkey3, 'sensor'=>'false')));
+    }
+    $module = array('name'=>'core_iplookup', 'fullpath'=>'/iplookup/module.js');
+    $PAGE->requires->js_init_call('M.core_iplookup.init3', array($info['latitude'], $info['longitude'], $ip), true, $module);
+
     echo '<div id="map" style="width: 650px; height: 360px"></div>';
     echo '<div id="note">'.$info['note'].'</div>';
 }
index dba71cc..004b7d2 100644 (file)
@@ -41,3 +41,21 @@ M.core_iplookup.init = function(Y, latitude, longitude) {
         }, document.body);
     }
 };
+
+M.core_iplookup.init3 = function(Y, latitude, longitude, ip) {
+    var ipLatlng = new google.maps.LatLng(latitude, longitude);
+
+    var mapOptions = {
+        center: ipLatlng,
+        zoom: 6,
+        mapTypeId: google.maps.MapTypeId.ROADMAP
+    };
+
+    var map = new google.maps.Map(document.getElementById("map"), mapOptions);
+
+    var marker = new google.maps.Marker({
+        position: ipLatlng,
+        map: map,
+        title: ip
+    });
+};
index 6b571c4..5d26680 100644 (file)
@@ -68,7 +68,7 @@ $string['availablelicenses'] = 'Available licences';
 $string['backgroundcolour'] = 'Transparent colour';
 $string['backups'] = 'Backups';
 $string['backup_shortname'] = 'Use course name in backup filename';
-$string['backup_shortnamehelp'] = 'Use the course name as part of the backup filename instead of the course id number.';
+$string['backup_shortnamehelp'] = 'Use the course name as part of the backup filename.';
 $string['badwordsconfig'] = 'Enter your list of bad words separated by commas.';
 $string['badwordsdefault'] = 'If the custom list is empty, a default list from the language pack will be used.';
 $string['badwordslist'] = 'Custom bad words list';
@@ -214,11 +214,11 @@ $string['configforcelogin'] = 'Normally, the front page of the site and the cour
 $string['configforceloginforprofiles'] = 'This setting forces people to login as a real (non-guest) account before viewing any user\'s profile. If you disabled this setting, you may find that some users post advertising (spam) or other inappropriate content in their profiles, which is then visible to the whole world.';
 $string['configfrontpage'] = 'The items selected above will be displayed on the site\'s front page.';
 $string['configfrontpageloggedin'] = 'The items selected above will be displayed on the site\'s front page when a user is logged in.';
-$string['configfullnamedisplay'] = 'This defines how names are shown when they are displayed in full. For most mono-lingual sites the most efficient setting is the default "First name + Surname", but you may choose to hide surnames altogether, or to leave it up to the current language pack to decide (some languages have different conventions).';
+$string['configfullnamedisplay'] = 'This defines how names are shown when they are displayed in full. For most mono-lingual sites the most efficient setting is "First name + Surname", but you may choose to hide surnames altogether, or to leave it up to the current language pack to decide (some languages have different conventions).';
 $string['configgdversion'] = 'Indicate the version of GD that is installed.  The version shown by default is the one that has been auto-detected.  Don\'t change this unless you really know what you\'re doing.';
 $string['configgeoipfile'] = 'Location of GeoIP City binary data file. This file is not part of Moodle distribution and must be obtained separately from <a href="http://www.maxmind.com/">MaxMind</a>. You can either buy a commercial version or use the free version.<br />Simply download <a href="http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz" >http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz</a> and extract it into "{$a}" directory on your server.';
 $string['configgetremoteaddrconf'] = 'If your server is behind a reverse proxy, you can use this setting to specify which HTTP headers can be trusted to contain the remote IP address. The headers are read in order, using the first one that is available.';
-$string['configgooglemapkey'] = 'You need to enter a special key to use Google Maps for IP address lookup visualization. You can obtain the key free of charge at <a href="http://code.google.com/apis/maps/signup.html" >http://code.google.com/apis/maps/signup.html</a>.<br />Your web site URL is: {$a}';
+$string['configgooglemapkey'] = 'You need to enter a special key to use Google Maps for IP address lookup visualization. You can obtain the key free of charge at <a href="https://developers.google.com/maps/documentation/javascript/v2/introduction#Obtaining_Key">https://developers.google.com/maps/documentation/javascript/v2/introduction#Obtaining_Key</a>.<br />Your web site URL is: {$a}';
 $string['configgradebookroles'] = 'This setting allows you to control who appears on the gradebook.  Users need to have at least one of these roles in a course to be shown in the gradebook for that course.';
 $string['configgradeexport'] = 'Choose which gradebook export formats are your primary methods for exporting grades.  Chosen plugins will then set and use a "last exported" field for every grade.  For example, this might result in exported records being identified as being "new" or "updated".  If you are not sure about this then leave everything unchecked.';
 $string['confighiddenuserfields'] = 'Select which user information fields you wish to hide from other users other than course teachers/admins. This will increase student privacy. Hold CTRL key to select multiple fields.';
@@ -545,7 +545,9 @@ $string['globalsquoteswarning'] = '<p><strong>Security Warning</strong>: to oper
 $string['globalswarning'] = '<p><strong>SECURITY WARNING!</strong></p><p> To operate properly, Moodle requires <br />that you make certain changes to your current PHP settings.</p><p>You <em>must</em> set <code>register_globals=off</code>.</p><p>This setting is controlled by editing your <code>php.ini</code>, Apache/IIS <br />configuration or <code>.htaccess</code> file.</p>';
 $string['groupenrolmentkeypolicy'] = 'Group enrolment key policy';
 $string['groupenrolmentkeypolicy_desc'] = 'Turning this on will make Moodle check group enrolment keys against a valid password policy.';
-$string['googlemapkey'] = 'Google Maps API key';
+$string['googlemapkey'] = 'Google Maps API V2 key';
+$string['googlemapkey3'] = 'Google Maps API V3 key';
+$string['googlemapkey3_help'] = 'You need to enter a special key to use Google Maps for IP address lookup visualization. You can obtain the key free of charge at <a href="https://developers.google.com/maps/documentation/javascript/tutorial#api_key" target="_blank">https://developers.google.com/maps/documentation/javascript/tutorial#api_key</a>';
 $string['gotofirst'] = 'Go to first missing string';
 $string['gradebook'] = 'Gradebook';
 $string['gradebookroles'] = 'Graded roles';
index 5fde19e..e053c96 100644 (file)
@@ -163,6 +163,7 @@ $string['lockedbypermission'] = 'You don\'t have sufficient permissions to chang
 $string['lockedbyconfig'] = 'This setting has been locked by the default backup settings';
 $string['lockedbyhierarchy'] = 'Locked by dependencies';
 $string['managefiles'] = 'Manage backup files';
+$string['missingfilesinpool'] = 'Some files could not be saved during the backup, it won\'t be possible to restore them.';
 $string['moodleversion'] = 'Moodle version';
 $string['moreresults'] = 'There are too many results, enter a more specific search.';
 $string['nomatchingcourses'] = 'There are no courses to display';
@@ -177,6 +178,7 @@ $string['restoreactivity'] = 'Restore activity';
 $string['restorecourse'] = 'Restore course';
 $string['restorecoursesettings'] = 'Course settings';
 $string['restoreexecutionsuccess'] = 'The course was restored successfully, clicking the continue button below will take you to view the course you restored.';
+$string['restorefileweremissing'] = 'Some files could not be restored because they were missing in the backup.';
 $string['restorenewcoursefullname'] = 'New course name';
 $string['restorenewcourseshortname'] = 'New course short name';
 $string['restorenewcoursestartdate'] = 'New start date';
index 9a9d10c..f7e0829 100644 (file)
@@ -71,6 +71,7 @@ $string['completionview_desc'] = 'Student must view this activity to complete it
 $string['configenablecompletion'] = 'When enabled, this lets you turn on completion tracking (progress) features at course level.';
 $string['csvdownload'] = 'Download in spreadsheet format (UTF-8 .csv)';
 $string['deletecoursecompletiondata'] = 'Delete course completion data';
+$string['deletecompletiondata'] = 'Delete completion data';
 $string['enablecompletion'] = 'Enable completion tracking';
 $string['err_noactivities'] = 'Completion information is not enabled for any activity, so none can be displayed. You can enable completion information by editing the settings for an activity.';
 $string['err_nousers'] = 'There are no students on this course or group for whom completion information is displayed. (By default, completion information is displayed only for students, so if there are no students, you will see this error. Administrators can alter this option via the admin screens.)';
index 377b840..a3d28ab 100644 (file)
@@ -1803,6 +1803,7 @@ $string['virusfounduser'] = 'The file you have uploaded, {$a->filename}, has bee
 $string['virusplaceholder'] = 'This file that has been uploaded was found to contain a virus and has been moved or deleted and the user notified.';
 $string['visible'] = 'Visible';
 $string['visibletostudents'] = 'Visible to {$a}';
+$string['warning'] = 'Warning';
 $string['warningdeleteresource'] = 'Warning: {$a} is referred in a resource. Would you like to update the resource?';
 $string['webpage'] = 'Web page';
 $string['week'] = 'Week';
index 1245426..8682378 100644 (file)
@@ -100,6 +100,7 @@ $string['error'] = 'An unknown error occurred!';
 $string['errornotyourfile'] = 'You cannot pick file which is not added by your';
 $string['erroruniquename'] = 'Repository instance name should be unique';
 $string['errorpostmaxsize'] = 'The uploaded file may exceed max_post_size directive in php.ini.';
+$string['errorwhiledownload'] = 'An error occured while downloading the file: {$a}';
 $string['existingrepository'] = 'This repository already exists';
 $string['federatedsearch'] = 'Federated search';
 $string['fileexists'] = 'File name already being used, please use another name';
index c62dae4..50a9ee4 100644 (file)
@@ -6537,7 +6537,7 @@ class context_module extends context {
                     if ($withprefix){
                         $name = get_string('modulename', $cm->modname).': ';
                     }
-                    $name .= $mod->name;
+                    $name .= format_string($mod->name, true, array('context' => $this));
                 }
             }
         return $name;
index 1c7097e..b41b67c 100644 (file)
@@ -457,6 +457,12 @@ class block_manager {
         if (!$this->page->theme->enable_dock) {
             return false;
         }
+
+        // Do not dock the region when the user attemps to move a block.
+        if ($this->movingblock) {
+            return false;
+        }
+
         $this->check_is_loaded();
         $this->ensure_content_created($region, $output);
         foreach($this->visibleblockcontent[$region] as $instance) {
index 0cee0da..974124f 100644 (file)
@@ -176,7 +176,7 @@ class boxclient {
         $params['action']     = 'get_account_tree';
         $params['onelevel']   = 1;
         $params['params[]']   = 'nozip';
-        $c = new curl(array('debug'=>$this->debug, 'cache'=>true, 'module_cache'=>'repository'));
+        $c = new curl(array('debug'=>$this->debug));
         $c->setopt(array('CURLOPT_FOLLOWLOCATION'=>1));
         try {
             $args = array();
@@ -196,23 +196,25 @@ class boxclient {
      * Get box.net file info
      *
      * @param string $fileid
-     * @return string|null
+     * @param int $timeout request timeout in seconds
+     * @return stdClass|null
      */
-    function get_file_info($fileid) {
+    function get_file_info($fileid, $timeout = 0) {
         $this->_clearErrors();
         $params = array();
         $params['action']     = 'get_file_info';
         $params['file_id']    = $fileid;
         $params['auth_token'] = $this->auth_token;
         $params['api_key']    = $this->api_key;
-        $http = new curl(array('debug'=>$this->debug, 'cache'=>true, 'module_cache'=>'repository'));
-        $xml = $http->get($this->_box_api_url, $params);
-        $o = simplexml_load_string(trim($xml));
-        if ($o->status == 's_get_file_info') {
-            return $o->info;
-        } else {
-            return null;
+        $http = new curl(array('debug'=>$this->debug));
+        $xml = $http->get($this->_box_api_url, $params, array('timeout' => $timeout));
+        if (!$http->get_errno()) {
+            $o = simplexml_load_string(trim($xml));
+            if ($o->status == 's_get_file_info') {
+                return $o->info;
+            }
         }
+        return null;
     }
 
     /**
index e6523ea..abfe789 100644 (file)
@@ -165,7 +165,11 @@ class completion_completion extends data_object {
         $this->timecompleted = $timecomplete;
 
         // Save record
-        return $this->_save();
+        if ($result = $this->_save()) {
+            events_trigger('course_completed', $this->get_record_data());
+        }
+
+        return $result;
     }
 
     /**
index 4df3bc0..fcd5910 100644 (file)
@@ -621,16 +621,20 @@ class completion_info {
             debugging('set_module_viewed must be called before header is printed',
                     DEBUG_DEVELOPER);
         }
+
         // Don't do anything if view condition is not turned on
         if ($cm->completionview == COMPLETION_VIEW_NOT_REQUIRED || !$this->is_enabled($cm)) {
             return;
         }
+
         // Get current completion state
-        $data = $this->get_data($cm, $userid);
+        $data = $this->get_data($cm, false, $userid);
+
         // If we already viewed it, don't do anything
         if ($data->viewed == COMPLETION_VIEWED) {
             return;
         }
+
         // OK, change state, save it, and update completion
         $data->viewed = COMPLETION_VIEWED;
         $this->internal_set_data($cm, $data);
@@ -711,6 +715,31 @@ class completion_info {
         $DB->delete_records('course_completion_crit_compl', array('course' => $this->course_id));
     }
 
+    /**
+     * Deletes all activity and course completion data for an entire course
+     * (the below delete_all_state function does this for a single activity).
+     *
+     * Used by course reset page.
+     */
+    public function delete_all_completion_data() {
+        global $DB, $SESSION;
+
+        // Delete from database.
+        $DB->delete_records_select('course_modules_completion',
+                'coursemoduleid IN (SELECT id FROM {course_modules} WHERE course=?)',
+                array($this->course_id));
+
+        // Reset cache for current user.
+        if (isset($SESSION->completioncache) &&
+            array_key_exists($this->course_id, $SESSION->completioncache)) {
+
+            unset($SESSION->completioncache[$this->course_id]);
+        }
+
+        // Wipe course completion data too.
+        $this->delete_course_completion_data();
+    }
+
     /**
      * Deletes completion state related to an activity for all users.
      *
index 7155373..6562052 100644 (file)
@@ -373,6 +373,13 @@ function cron_run() {
     }
 
 
+    // Run question bank clean-up.
+    mtrace("Starting the question bank cron...", '');
+    require_once($CFG->libdir . '/questionlib.php');
+    question_bank::cron();
+    mtrace('done.');
+
+
     //Run registration updated cron
     mtrace(get_string('siteupdatesstart', 'hub'));
     require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php');
@@ -459,10 +466,6 @@ function cron_run() {
     $fs = get_file_storage();
     $fs->cron();
 
-    mtrace("Clean up cached external files");
-    // 1 week
-    cache_file::cleanup(array(), 60 * 60 * 24 * 7);
-
     mtrace("Cron script completed correctly");
 
     $difftime = microtime_diff($starttime, microtime());
index 5103e0b..5247e49 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+/**
+ * Returns all non-view and non-temp tables with sane names.
+ * Prints list of non-supported tables using $OUTPUT->notification()
+ *
+ * @return array
+ */
+function upgrade_mysql_get_supported_tables() {
+    global $OUTPUT, $DB;
+
+    $tables = array();
+    $patprefix = str_replace('_', '\\_', $DB->get_prefix());
+    $pregprefix = preg_quote($DB->get_prefix(), '/');
+
+    $sql = "SHOW FULL TABLES LIKE '$patprefix%'";
+    $rs = $DB->get_recordset_sql($sql);
+    foreach ($rs as $record) {
+        $record = array_change_key_case((array)$record, CASE_LOWER);
+        $type = $record['table_type'];
+        unset($record['table_type']);
+        $fullname = array_shift($record);
+
+        if ($pregprefix === '') {
+            $name = $fullname;
+        } else {
+            $count = null;
+            $name = preg_replace("/^$pregprefix/", '', $fullname, -1, $count);
+            if ($count !== 1) {
+                continue;
+            }
+        }
+
+        if (!preg_match("/^[a-z][a-z0-9_]*$/", $name)) {
+            echo $OUTPUT->notification("Database table with invalid name '$fullname' detected, skipping.", 'notifyproblem');
+            continue;
+        }
+        if ($type === 'VIEW') {
+            echo $OUTPUT->notification("Unsupported database table view '$fullname' detected, skipping.", 'notifyproblem');
+            continue;
+        }
+        $tables[$name] = $name;
+    }
+    $rs->close();
+
+    return $tables;
+}
 
 /**
  * Remove all signed numbers from current database - mysql only.
@@ -50,7 +95,7 @@ function upgrade_mysql_fix_unsigned_columns() {
     $pbar = new progress_bar('mysqlconvertunsigned', 500, true);
 
     $prefix = $DB->get_prefix();
-    $tables = $DB->get_tables();
+    $tables = upgrade_mysql_get_supported_tables();
 
     $tablecount = count($tables);
     $i = 0;
@@ -115,7 +160,7 @@ function upgrade_mysql_fix_lob_columns() {
     $pbar = new progress_bar('mysqlconvertlobs', 500, true);
 
     $prefix = $DB->get_prefix();
-    $tables = $DB->get_tables();
+    $tables = upgrade_mysql_get_supported_tables();
     asort($tables);
 
     $tablecount = count($tables);
index 4f60f3c..df23d91 100644 (file)
@@ -804,10 +804,6 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
                 continue;
             }
 
-            // Replaced file content
-            if ($oldfile->get_contenthash() != $newfile->get_contenthash()) {
-                $oldfile->replace_content_with($newfile);
-            }
             // Updated author
             if ($oldfile->get_author() != $newfile->get_author()) {
                 $oldfile->set_author($newfile->get_author());
@@ -827,16 +823,18 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
                 $oldfile->set_sortorder($newfile->get_sortorder());
             }
 
-            // Update file size
-            if ($oldfile->get_filesize() != $newfile->get_filesize()) {
-                $oldfile->set_filesize($newfile->get_filesize());
-            }
-
             // Update file timemodified
             if ($oldfile->get_timemodified() != $newfile->get_timemodified()) {
                 $oldfile->set_timemodified($newfile->get_timemodified());
             }
 
+            // Replaced file content
+            if ($oldfile->get_contenthash() != $newfile->get_contenthash() || $oldfile->get_filesize() != $newfile->get_filesize()) {
+                $oldfile->replace_content_with($newfile);
+                // push changes to all local files that are referencing this file
+                $fs->update_references_to_storedfile($this);
+            }
+
             // unchanged file or directory - we keep it as is
             unset($newhashes[$oldhash]);
             if (!$oldfile->is_directory()) {
@@ -2328,7 +2326,7 @@ function send_stored_file($stored_file, $lifetime=86400 , $filter=0, $forcedownl
     }
 
     // handle external resource
-    if ($stored_file && $stored_file->is_external_file()) {
+    if ($stored_file && $stored_file->is_external_file() && !isset($options['sendcachedexternalfile'])) {
         $stored_file->send_file($lifetime, $filter, $forcedownload, $options);
         die;
     }
@@ -2752,6 +2750,8 @@ class curl {
     public  $info;
     /** @var string error */
     public  $error;
+    /** @var int error code */
+    public  $errno;
 
     /** @var array cURL options */
     private $options;
@@ -2895,6 +2895,14 @@ class curl {
         unset($this->options['CURLOPT_INFILE']);
         unset($this->options['CURLOPT_INFILESIZE']);
         unset($this->options['CURLOPT_CUSTOMREQUEST']);
+        unset($this->options['CURLOPT_FILE']);
+    }
+
+    /**
+     * Resets the HTTP Request headers (to prepare for the new request)
+     */
+    public function resetHeader() {
+        $this->header = array();
     }
 
     /**
@@ -3119,6 +3127,7 @@ class curl {
 
         $this->info  = curl_getinfo($curl);
         $this->error = curl_error($curl);
+        $this->errno = curl_errno($curl);
 
         if ($this->debug){
             echo '<h1>Return Data</h1>';
@@ -3202,6 +3211,62 @@ class curl {
         return $this->request($url, $options);
     }
 
+    /**
+     * Downloads one file and writes it to the specified file handler
+     *
+     * <code>
+     * $c = new curl();
+     * $file = fopen('savepath', 'w');
+     * $result = $c->download_one('http://localhost/', null,
+     *   array('file' => $file, 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));
+     * fclose($file);
+     * $download_info = $c->get_info();
+     * if ($result === true) {
+     *   // file downloaded successfully
+     * } else {
+     *   $error_text = $result;
+     *   $error_code = $c->get_errno();
+     * }
+     * </code>
+     *
+     * <code>
+     * $c = new curl();
+     * $result = $c->download_one('http://localhost/', null,
+     *   array('filepath' => 'savepath', 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));
+     * // ... see above, no need to close handle and remove file if unsuccessful
+     * </code>
+     *
+     * @param string $url
+     * @param array|null $params key-value pairs to be added to $url as query string
+     * @param array $options request options. Must include either 'file' or 'filepath'
+     * @return bool|string true on success or error string on failure
+     */
+    public function download_one($url, $params, $options = array()) {
+        $options['CURLOPT_HTTPGET'] = 1;
+        $options['CURLOPT_BINARYTRANSFER'] = true;
+        if (!empty($params)){
+            $url .= (stripos($url, '?') !== false) ? '&' : '?';
+            $url .= http_build_query($params, '', '&');
+        }
+        if (!empty($options['filepath']) && empty($options['file'])) {
+            // open file
+            if (!($options['file'] = fopen($options['filepath'], 'w'))) {
+                $this->errno = 100;
+                return get_string('cannotwritefile', 'error', $options['filepath']);
+            }
+            $filepath = $options['filepath'];
+        }
+        unset($options['filepath']);
+        $result = $this->request($url, $options);
+        if (isset($filepath)) {
+            fclose($options['file']);
+            if ($result !== true) {
+                unlink($filepath);
+            }
+        }
+        return $result;
+    }
+
     /**
      * HTTP PUT method
      *
@@ -3279,6 +3344,15 @@ class curl {
     public function get_info() {
         return $this->info;
     }
+
+    /**
+     * Get curl error code
+     *
+     * @return int
+     */
+    public function get_errno() {
+        return $this->errno;
+    }
 }
 
 /**
@@ -4197,170 +4271,3 @@ function file_pluginfile($relativepath, $forcedownload, $preview = null) {
     }
 
 }
-
-/**
- * Universe file cacheing class
- *
- * @package    core_files
- * @category   files
- * @copyright  2012 Dongsheng Cai {@link http://dongsheng.org}
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class cache_file {
-    /** @var string */
-    public $cachedir = '';
-
-    /**
-     * static method to create cache_file class instance
-     *
-     * @param array $options caching ooptions
-     */
-    public static function get_instance($options = array()) {
-        return new cache_file($options);
-    }
-
-    /**
-     * Constructor
-     *
-     * @param array $options
-     */
-    private function __construct($options = array()) {
-        global $CFG;
-
-        // Path to file caches.
-        if (isset($options['cachedir'])) {
-            $this->cachedir = $options['cachedir'];
-        } else {
-            $this->cachedir = $CFG->cachedir . '/filedir';
-        }
-
-        // Create cache directory.
-        if (!file_exists($this->cachedir)) {
-            mkdir($this->cachedir, $CFG->directorypermissions, true);
-        }
-
-        // When use cache_file::get, it will check ttl.
-        if (isset($options['ttl']) && is_numeric($options['ttl'])) {
-            $this->ttl = $options['ttl'];
-        } else {
-            // One day.
-            $this->ttl = 60 * 60 * 24;
-        }
-    }
-
-    /**
-     * Get cached file, false if file expires
-     *
-     * @param mixed $param
-     * @param array $options caching options
-     * @return bool|string
-     */
-    public static function get($param, $options = array()) {
-        $instance = self::get_instance($options);
-        $filepath = $instance->generate_filepath($param);
-        if (file_exists($filepath)) {
-            $lasttime = filemtime($filepath);
-            if (time() - $lasttime > $instance->ttl) {
-                // Remove cache file.
-                unlink($filepath);
-                return false;
-            } else {
-                return $filepath;
-            }
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Static method to create cache from a file
-     *
-     * @param mixed $ref
-     * @param string $srcfile
-     * @param array $options
-     * @return string cached file path
-     */
-    public static function create_from_file($ref, $srcfile, $options = array()) {
-        $instance = self::get_instance($options);
-        $cachedfilepath = $instance->generate_filepath($ref);
-        copy($srcfile, $cachedfilepath);
-        return $cachedfilepath;
-    }
-
-    /**
-     * Static method to create cache from url
-     *
-     * @param mixed $ref file reference
-     * @param string $url file url
-     * @param array $options options
-     * @return string cached file path
-     */
-    public static function create_from_url($ref, $url, $options = array()) {
-        $instance = self::get_instance($options);
-        $cachedfilepath = $instance->generate_filepath($ref);
-        $fp = fopen($cachedfilepath, 'w');
-        $curl = new curl;
-        $curl->download(array(array('url'=>$url, 'file'=>$fp)));
-        // Must close file handler.
-        fclose($fp);
-        return $cachedfilepath;
-    }
-
-    /**
-     * Static method to create cache from string
-     *
-     * @param mixed $ref file reference
-     * @param string $url file url
-     * @param array $options options
-     * @return string cached file path
-     */
-    public static function create_from_string($ref, $string, $options = array()) {
-        $instance = self::get_instance($options);
-        $cachedfilepath = $instance->generate_filepath($ref);
-        $fp = fopen($cachedfilepath, 'w');
-        fwrite($fp, $string);
-        // Must close file handler.
-        fclose($fp);
-        return $cachedfilepath;
-    }
-
-    /**
-     * Build path to cache file
-     *
-     * @param mixed $ref
-     * @return string
-     */
-    private function generate_filepath($ref) {
-        global $CFG;
-        $hash = sha1(serialize($ref));
-        $l1 = $hash[0].$hash[1];
-        $l2 = $hash[2].$hash[3];
-        $dir = $this->cachedir . "/$l1/$l2";
-        if (!file_exists($dir)) {
-            mkdir($dir, $CFG->directorypermissions, true);
-        }
-        return "$dir/$hash";
-    }
-
-    /**
-     * Remove cache files
-     *
-     * @param array $options options
-     * @param int $expire The number of seconds before expiry
-     */
-    public static function cleanup($options = array(), $expire) {
-        global $CFG;
-        $instance = self::get_instance($options);
-        if ($dir = opendir($instance->cachedir)) {
-            while (($file = readdir($dir)) !== false) {
-                if (!is_dir($file) && $file != '.' && $file != '..') {
-                    $lasttime = @filemtime($instance->cachedir . $file);
-                    if(time() - $lasttime > $expire){
-                        @unlink($instance->cachedir . $file);
-                    }
-                }
-            }
-            closedir($dir);
-        }
-    }
-}
index 2b248d1..ae8df9c 100644 (file)
@@ -976,10 +976,6 @@ class file_storage {
             $filerecord->sortorder = 0;
         }
 
-        $filerecord->referencefileid   = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
-        $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
-        $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
-
         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
             // path must start and end with '/'
@@ -1094,9 +1090,6 @@ class file_storage {
         } else {
             $filerecord->sortorder = 0;
         }
-        $filerecord->referencefileid   = !isset($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
-        $filerecord->referencelastsync = !isset($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
-        $filerecord->referencelifetime = !isset($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
 
         $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
         if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
@@ -1217,9 +1210,10 @@ class file_storage {
             $filerecord->sortorder = 0;
         }
 
-        $filerecord->referencefileid   = empty($filerecord->referencefileid) ? 0 : $filerecord->referencefileid;
-        $filerecord->referencelastsync = empty($filerecord->referencelastsync) ? 0 : $filerecord->referencelastsync;
-        $filerecord->referencelifetime = empty($filerecord->referencelifetime) ? 0 : $filerecord->referencelifetime;
+        // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
+        unset($filerecord->referencelastsync);
+        unset($filerecord->referencelifetime);
+
         $filerecord->mimetype          = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
         $filerecord->userid            = empty($filerecord->userid) ? null : $filerecord->userid;
         $filerecord->source            = empty($filerecord->source) ? null : $filerecord->source;
@@ -1266,22 +1260,39 @@ class file_storage {
         $transaction = $DB->start_delegated_transaction();
 
         try {
-            $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference,
-                $filerecord->referencelastsync, $filerecord->referencelifetime);
+            $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
         } catch (Exception $e) {
             throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
         }
 
-        // External file doesn't have content in moodle.
-        // So we create an empty file for it.
-        list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
+        if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
+            // there was specified the contenthash for a file already stored in moodle filepool
+            if (empty($filerecord->filesize)) {
+                $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
+                $filerecord->filesize = filesize($filepathname);
+            } else {
+                $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
+            }
+        } else {
+            // atempt to get the result of last synchronisation for this reference
+            $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
+                    'id, contenthash, filesize', IGNORE_MULTIPLE);
+            if ($lastcontent) {
+                $filerecord->contenthash = $lastcontent->contenthash;
+                $filerecord->filesize = $lastcontent->filesize;
+            } else {
+                // External file doesn't have content in moodle.
+                // So we create an empty file for it.
+                list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
+            }
+        }
 
         $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
 
         try {
             $filerecord->id = $DB->insert_record('files', $filerecord);
         } catch (dml_exception $e) {
-            if ($newfile) {
+            if (!empty($newfile)) {
                 $this->deleted_file_cleanup($filerecord->contenthash);
             }
             throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
@@ -1292,10 +1303,8 @@ class file_storage {
 
         $transaction->allow_commit();
 
-        // Adding repositoryid and reference to file record to create stored_file instance
-        $filerecord->repositoryid = $repositoryid;
-        $filerecord->reference = $reference;
-        return $this->get_file_instance($filerecord);
+        // this will retrieve all reference information from DB as well
+        return $this->get_file_by_id($filerecord->id);
     }
 
     /**
@@ -1805,18 +1814,51 @@ class file_storage {
         return $this->search_references_count(self::pack_reference($params));
     }
 
+    /**
+     * Updates all files that are referencing this file with the new contenthash
+     * and filesize
+     *
+     * @param stored_file $storedfile
+     */
+    public function update_references_to_storedfile(stored_file $storedfile) {
+        global $CFG;
+        $params = array();
+        $params['contextid'] = $storedfile->get_contextid();
+        $params['component'] = $storedfile->get_component();
+        $params['filearea']  = $storedfile->get_filearea();
+        $params['itemid']    = $storedfile->get_itemid();
+        $params['filename']  = $storedfile->get_filename();
+        $params['filepath']  = $storedfile->get_filepath();
+        $reference = self::pack_reference($params);
+        $referencehash = sha1($reference);
+
+        $sql = "SELECT repositoryid, id FROM {files_reference}
+                 WHERE referencehash = ? and reference = ?";
+        $rs = $DB->get_recordset_sql($sql, array($referencehash, $reference));
+
+        $now = time();
+        foreach ($rs as $record) {
+            require_once($CFG->dirroot.'/repository/lib.php');
+            $repo = repository::get_instance($record->repositoryid);
+            $lifetime = $repo->get_reference_file_lifetime($reference);
+            $this->update_references($record->id, $now, $lifetime,
+                    $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
+        }
+        $rs->close();
+    }
+
     /**
      * Convert file alias to local file
      *
+     * @throws moodle_exception if file could not be downloaded
+     *
      * @param stored_file $storedfile a stored_file instances
+     * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
      * @return stored_file stored_file
      */
-    public function import_external_file(stored_file $storedfile) {
+    public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
         global $CFG;
-        require_once($CFG->dirroot.'/repository/lib.php');
-        // sync external file
-        repository::sync_external_file($storedfile);
-        // Remove file references
+        $storedfile->import_external_file_contents($maxbytes);
         $storedfile->delete_reference();
         return $storedfile;
     }
@@ -1923,10 +1965,12 @@ class file_storage {
         // else problems like MDL-33172 occur.
         $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
             'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
-            'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid',
-            'referencelastsync', 'referencelifetime');
+            'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
 
-        $referencefields = array('repositoryid', 'reference');
+        $referencefields = array('repositoryid' => 'repositoryid',
+            'reference' => 'reference',
+            'lastsync' => 'referencelastsync',
+            'lifetime' => 'referencelifetime');
 
         // id is specifically named to prevent overlaping between the two tables.
         $fields = array();
@@ -1935,8 +1979,8 @@ class file_storage {
             $fields[] = "{$filesprefix}.{$field}";
         }
 
-        foreach ($referencefields as $field) {
-            $fields[] = "{$filesreferenceprefix}.{$field}";
+        foreach ($referencefields as $field => $alias) {
+            $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
         }
 
         return implode(', ', $fields);
@@ -1997,4 +2041,40 @@ class file_storage {
         return $DB->get_field('files_reference', 'id',
             array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
     }
+
+    /**
+     * Updates a reference to the external resource and all files that use it
+     *
+     * This function is called after synchronisation of an external file and updates the
+     * contenthash, filesize and status of all files that reference this external file
+     * as well as time last synchronised and sync lifetime (how long we don't need to call
+     * synchronisation for this reference).
+     *
+     * @param int $referencefileid
+     * @param int $lastsync
+     * @param int $lifetime
+     * @param string $contenthash
+     * @param int $filesize
+     * @param int $status 0 if ok or 666 if source is missing
+     */
+    public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
+        global $DB;
+        $referencefileid = clean_param($referencefileid, PARAM_INT);
+        $lastsync = clean_param($lastsync, PARAM_INT);
+        $lifetime = clean_param($lifetime, PARAM_INT);
+        validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
+        $filesize = clean_param($filesize, PARAM_INT);
+        $status = clean_param($status, PARAM_INT);
+        $params = array('contenthash' => $contenthash,
+                    'filesize' => $filesize,
+                    'status' => $status,
+                    'referencefileid' => $referencefileid,
+                    'lastsync' => $lastsync,
+                    'lifetime' => $lifetime);
+        $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
+            status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
+            WHERE referencefileid = :referencefileid', $params);
+        $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
+        $DB->update_record('files_reference', (object)$data);
+    }
 }
index e9410bd..fee7bfa 100644 (file)
@@ -70,6 +70,12 @@ class stored_file {
         } else {
             $this->repository = null;
         }
+        // make sure all reference fields exist in file_record even when it is not a reference
+        foreach (array('referencelastsync', 'referencelifetime', 'referencefileid', 'reference', 'repositoryid') as $key) {
+            if (empty($this->file_record->$key)) {
+                $this->file_record->$key = null;
+            }
+        }
     }
 
     /**
@@ -142,12 +148,18 @@ class stored_file {
                     }
                 }
 
-                if ($field === 'referencefileid' or $field === 'referencelastsync' or $field === 'referencelifetime') {
+                if ($field === 'referencefileid') {
                     if (!is_null($value) and !is_number($value)) {
                         throw new file_exception('storedfileproblem', 'Invalid reference info');
                     }
                 }
 
+                if ($field === 'referencelastsync' or $field === 'referencelifetime') {
+                    // do not update those fields
+                    // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
+                    continue;
+                }
+
                 // adding the field
                 $this->file_record->$field = $value;
             } else {
@@ -195,6 +207,7 @@ class stored_file {
     public function replace_content_with(stored_file $storedfile) {
         $contenthash = $storedfile->get_contenthash();
         $this->set_contenthash($contenthash);
+        $this->set_filesize($storedfile->get_filesize());
     }
 
     /**
@@ -225,8 +238,6 @@ class stored_file {
         // Update the underlying record in the database.
         $update = new stdClass();
         $update->referencefileid = null;
-        $update->referencelastsync = null;
-        $update->referencelifetime = null;
         $this->update($update);
 
         $transaction->allow_commit();
@@ -877,36 +888,43 @@ class stored_file {
      * We update contenthash, filesize and status in files table if changed
      * and we always update lastsync in files_reference table
      *
-     * @param type $contenthash
-     * @param type $filesize
+     * @param string $contenthash
+     * @param int $filesize
+     * @param int $status
+     * @param int $lifetime the life time of this synchronisation results
      */
-    public function set_synchronized($contenthash, $filesize, $status = 0) {
+    public function set_synchronized($contenthash, $filesize, $status = 0, $lifetime = null) {
         global $DB;
         if (!$this->is_external_file()) {
             return;
         }
         $now = time();
-        $filerecord = new stdClass();
-        if ($this->get_contenthash() !== $contenthash) {
-            $filerecord->contenthash = $contenthash;
-        }
-        if ($this->get_filesize() != $filesize) {
-            $filerecord->filesize = $filesize;
+        if ($contenthash != $this->file_record->contenthash) {
+            $oldcontenthash = $this->file_record->contenthash;
         }
-        if ($this->get_status() != $status) {
-            $filerecord->status = $status;
+        if ($lifetime === null) {
+            $lifetime = $this->file_record->referencelifetime;
         }
-        $filerecord->referencelastsync = $now; // TODO MDL-33416 remove this
-        if (!empty($filerecord)) {
-            $this->update($filerecord);
+        // this will update all entries in {files} that have the same filereference id
+        $this->fs->update_references($this->file_record->referencefileid, $now, $lifetime, $contenthash, $filesize, $status);
+        // we don't need to call update() for this object, just set the values of changed fields
+        $this->file_record->contenthash = $contenthash;
+        $this->file_record->filesize = $filesize;
+        $this->file_record->status = $status;
+        $this->file_record->referencelastsync = $now;
+        $this->file_record->referencelifetime = $lifetime;
+        if (isset($oldcontenthash)) {
+            $this->fs->deleted_file_cleanup($oldcontenthash);
         }
-
-        $DB->set_field('files_reference', 'lastsync', $now, array('id'=>$this->get_referencefileid()));
-        // $this->file_record->lastsync = $now; // TODO MDL-33416 uncomment or remove
     }
 
-    public function set_missingsource() {
-        $this->set_synchronized($this->get_contenthash(), 0, 666);
+    /**
+     * Sets the error status for a file that could not be synchronised
+     *
+     * @param int $lifetime the life time of this synchronisation results
+     */
+    public function set_missingsource($lifetime = null) {
+        $this->set_synchronized($this->get_contenthash(), $this->get_filesize(), 666, $lifetime);
     }
 
     /**
@@ -920,4 +938,16 @@ class stored_file {
     public function send_file($lifetime, $filter, $forcedownload, $options) {
         $this->repository->send_file($this, $lifetime, $filter, $forcedownload, $options);
     }
+
+    /**
+     * Imports the contents of an external file into moodle filepool.
+     *
+     * @throws moodle_exception if file could not be downloaded or is too big
+     * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
+     */
+    public function import_external_file_contents($maxbytes = 0) {
+        if ($this->repository) {
+            $this->repository->import_external_file_contents($this, $maxbytes);
+        }
+    }
 }
index ab8cb9a..8db9155 100644 (file)
@@ -128,7 +128,7 @@ class dateselector_form_element_testcase extends basic_testcase {
                 'year' => 2011,
                 'usertimezone' => 0.0,
                 'timezone' => 0.0,
-                'timestamp' => 1309449600
+                'timestamp' => 1309478400 // 6am at UTC+0
             ),
             array (
                 'day' => 1,
@@ -136,7 +136,7 @@ class dateselector_form_element_testcase extends basic_testcase {
                 'year' => 2011,
                 'usertimezone' => 0.0,
                 'timezone' => 99,
-                'timestamp' => 1309449600
+                'timestamp' => 1309478400 // 6am at UTC+0
             )
         );
     }
index b0ce16e..abd5cea 100644 (file)
@@ -138,7 +138,7 @@ class datetimeselector_form_element_testcase extends basic_testcase {
                 'year' => 2011,
                 'usertimezone' => 0.0,
                 'timezone' => 0.0,
-                'timestamp' => 1309449600
+                'timestamp' => 1309478400 // 6am at UTC+0
             ),
             array (
                 'minute' => 0,
@@ -148,7 +148,7 @@ class datetimeselector_form_element_testcase extends basic_testcase {
                 'year' => 2011,
                 'usertimezone' => 0.0,
                 'timezone' => 99,
-                'timestamp' => 1309449600
+                'timestamp' => 1309478400 // 6am at UTC+0
             )
         );
     }
index fe2b933..c2d529f 100644 (file)
@@ -185,12 +185,21 @@ class google_docs {
      *
      * @param string $url url of file
      * @param string $path path to save file to
+     * @param int $timeout request timeout, default 0 which means no timeout
      * @return array stucture for repository download_file
      */
-    public function download_file($url, $path) {
-        $content = $this->googleoauth->get($url);
-        file_put_contents($path, $content);
-        return array('path'=>$path, 'url'=>$url);
+    public function download_file($url, $path, $timeout = 0) {
+        $result = $this->googleoauth->download_one($url, null, array('filepath' => $path, 'timeout' => $timeout));
+        if ($result === true) {
+            $info = $this->googleoauth->get_info();
+            if (isset($info['http_code']) && $info['http_code'] == 200) {
+                return array('path'=>$path, 'url'=>$url);
+            } else {
+                throw new moodle_exception('cannotdownload', 'repository');
+            }
+        } else {
+            throw new moodle_exception('errorwhiledownload', 'repository', '', $result);
+        }
     }
 }
 
index 8b1d312..12afe84 100644 (file)
@@ -742,7 +742,8 @@ class grade_item extends grade_object {
 
             // Standardise score to the new grade range
             // NOTE: this is not compatible with current assignment grading
-            if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
+            $isassignmentmodule = ($this->itemmodule == 'assignment') || ($this->itemmodule == 'assign');
+            if (!$isassignmentmodule && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
                 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
             }
 
index f4674ce..1fee1b0 100644 (file)
@@ -60,7 +60,7 @@ function message_send($eventdata) {
     //TODO: we need to solve problems with database transactions here somehow, for now we just prevent transactions - sorry
     $DB->transactions_forbidden();
 
-    if (is_int($eventdata->userto)) {
+    if (is_number($eventdata->userto)) {
         $eventdata->userto = $DB->get_record('user', array('id' => $eventdata->userto));
     }
     if (is_int($eventdata->userfrom)) {
index db9b7f6..0739f76 100644 (file)
@@ -1196,6 +1196,10 @@ function get_fast_modinfo(&$course, $userid=0) {
         debugging('Coding problem - missing course modinfo property in get_fast_modinfo() call');
     }
 
+    if (!property_exists($course, 'sectioncache')) {
+        debugging('Coding problem - missing course sectioncache property in get_fast_modinfo() call');
+    }
+
     unset($cache[$course->id]); // prevent potential reference problems when switching users
 
     $cache[$course->id] = new course_modinfo($course, $userid);
index f229b6b..1c8f966 100644 (file)
@@ -480,6 +480,12 @@ define('MOODLE_OFFICIAL_MOBILE_SERVICE', 'moodle_mobile_app');
  */
 define('USER_CAN_IGNORE_FILE_SIZE_LIMITS', -1);
 
+/**
+ * Course display settings
+ */
+define('COURSE_DISPLAY_SINGLEPAGE', 0); // display all sections on one page
+define('COURSE_DISPLAY_MULTIPAGE', 1); // split pages into a page per section
+
 /// PARAMETER HANDLING ////////////////////////////////////////////////////
 
 /**
@@ -2324,10 +2330,10 @@ function get_user_timezone($tz = 99) {
 
     $tz = 99;
 
-    while(($tz == '' || $tz == 99 || $tz == NULL) && $next = each($timezones)) {
+    // Loop while $tz is, empty but not zero, or 99, and there is another timezone is the array
+    while(((empty($tz) && !is_numeric($tz)) || $tz == 99) && $next = each($timezones)) {
         $tz = $next['value'];
     }
-
     return is_numeric($tz) ? (float) $tz : $tz;
 }
 
@@ -3283,11 +3289,24 @@ function get_user_key($script, $userid, $instance=null, $iprestriction=null, $va
 function update_user_login_times() {
     global $USER, $DB;
 
+    $now = time();
+
     $user = new stdClass();
+    $user->id = $USER->id;
+
+    // Make sure all users that logged in have some firstaccess.
+    if ($USER->firstaccess == 0) {
+        $USER->firstaccess = $user->firstaccess = $now;
+    }
+
+    // Store the previous current as lastlogin.
     $USER->lastlogin = $user->lastlogin = $USER->currentlogin;
-    $USER->currentlogin = $user->lastaccess = $user->currentlogin = time();
 
-    $user->id = $USER->id;
+    $USER->currentlogin = $user->currentlogin = $now;
+
+    // Function user_accesstime_log() may not update immediately, better do it here.
+    $USER->lastaccess = $user->lastaccess = $now;
+    $USER->lastip = $user->lastip = getremoteaddr();
 
     $DB->update_record('user', $user);
     return true;
@@ -3896,15 +3915,45 @@ function truncate_userinfo($info) {
  * Any plugin that needs to purge user data should register the 'user_deleted' event.
  *
  * @param stdClass $user full user object before delete
- * @return boolean always true
+ * @return boolean success
+ * @throws coding_exception if invalid $user parameter detected
  */
-function delete_user($user) {
+function delete_user(stdClass $user) {
     global $CFG, $DB;
     require_once($CFG->libdir.'/grouplib.php');
     require_once($CFG->libdir.'/gradelib.php');
     require_once($CFG->dirroot.'/message/lib.php');
     require_once($CFG->dirroot.'/tag/lib.php');
 
+    // Make sure nobody sends bogus record type as parameter.
+    if (!property_exists($user, 'id') or !property_exists($user, 'username')) {
+        throw new coding_exception('Invalid $user parameter in delete_user() detected');
+    }
+
+    // Better not trust the parameter and fetch the latest info,
+    // this will be very expensive anyway.
+    if (!$user = $DB->get_record('user', array('id'=>$user->id))) {
+        debugging('Attempt to delete unknown user account.');
+        return false;
+    }
+
+    // There must be always exactly one guest record,
+    // originally the guest account was identified by username only,
+    // now we use $CFG->siteguest for performance reasons.
+    if ($user->username === 'guest' or isguestuser($user)) {
+        debugging('Guest user account can not be deleted.');
+        return false;
+    }
+
+    // Admin can be theoretically from different auth plugin,
+    // but we want to prevent deletion of internal accoutns only,
+    // if anything goes wrong ppl may force somebody to be admin via
+    // config.php setting $CFG->siteadmins.
+    if ($user->auth === 'manual' and is_siteadmin($user)) {
+        debugging('Local administrator accounts can not be deleted.');
+        return false;
+    }
+
     // delete all grades - backup is kept in grade_grades_history table
     grade_user_delete($user->id);
 
@@ -4074,10 +4123,6 @@ function authenticate_user_login($username, $password) {
                 $DB->set_field('user', 'auth', $auth, array('username'=>$username));
                 $user->auth = $auth;
             }
-            if (empty($user->firstaccess)) { //prevent firstaccess from remaining 0 for manual account that never required confirmation
-                $DB->set_field('user','firstaccess', $user->timemodified, array('id' => $user->id));
-                $user->firstaccess = $user->timemodified;
-            }
 
             update_internal_user_password($user, $password); // just in case salt or encoding were changed (magic quotes too one day)
 
@@ -4752,7 +4797,7 @@ function shift_course_mod_dates($modname, $fields, $timeshift, $courseid) {
     foreach ($fields as $field) {
         $updatesql = "UPDATE {".$modname."}
                           SET $field = $field + ?
-                        WHERE course=? AND $field<>0 AND $field<>0";
+                        WHERE course=? AND $field<>0";
         $return = $DB->execute($updatesql, array($timeshift, $courseid)) && $return;
     }
 
@@ -4829,12 +4874,13 @@ function reset_course_userdata($data) {
         $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteblogassociations', 'blog'), 'error'=>false);
     }
 
-    if (!empty($data->reset_course_completion)) {
-        // Delete course completion information
+    if (!empty($data->reset_completion)) {
+        // Delete course and activity completion information.
         $course = $DB->get_record('course', array('id'=>$data->courseid));
         $cc = new completion_info($course);
-        $cc->delete_course_completion_data();
-        $status[] = array('component'=>$componentstr, 'item'=>get_string('deletecoursecompletiondata', 'completion'), 'error'=>false);
+        $cc->delete_all_completion_data();
+        $status[] = array('component' => $componentstr,
+                'item' => get_string('deletecompletiondata', 'completion'), 'error' => false);
     }
 
     $componentstr = get_string('roles');
@@ -4945,12 +4991,12 @@ function reset_course_userdata($data) {
     if ($allmods = $DB->get_records('modules') ) {
         foreach ($allmods as $mod) {
             $modname = $mod->name;
-            if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
-                continue; // skip mods with no instances
-            }
             $modfile = $CFG->dirroot.'/mod/'. $modname.'/lib.php';
             $moddeleteuserdata = $modname.'_reset_userdata';   // Function to delete user data
             if (file_exists($modfile)) {
+                if (!$DB->count_records($modname, array('course'=>$data->courseid))) {
+                    continue; // Skip mods with no instances
+                }
                 include_once($modfile);
                 if (function_exists($moddeleteuserdata)) {
                     $modstatus = $moddeleteuserdata($data);
index 92f9029..84ca77f 100644 (file)
@@ -1131,7 +1131,13 @@ class global_navigation extends navigation_node {
                     $addedcategories[$category->id] = $categoryparent->add($category->name, $url, self::TYPE_CATEGORY, $category->name, $category->id);
 
                     if (!$category->visible) {
-                        if (!has_capability('moodle/category:viewhiddencategories', get_context_instance(CONTEXT_COURSECAT, $category->parent))) {
+                        // Let's decide the context where viewhidden cap checks will happen.
+                        if ($category->parent == '0') {
+                            $contexttocheck = context_system::instance();
+                        } else {
+                            $contexttocheck = context_coursecat::instance($category->parent);
+                        }
+                        if (!has_capability('moodle/category:viewhiddencategories', $contexttocheck)) {
                             $addedcategories[$category->id]->display = false;
                         } else {
                             $addedcategories[$category->id]->hidden = true;
index 587ba19..7f5ea64 100644 (file)
@@ -59,6 +59,8 @@ class oauth_helper {
     protected $access_token_api;
     /** @var curl */
     protected $http;
+    /** @var array options to pass to the next curl request */
+    protected $http_options;
 
     /**
      * Contructor for oauth_helper.
@@ -106,6 +108,7 @@ class oauth_helper {
             $this->access_token_secret = $args['access_token_secret'];
         }
         $this->http = new curl(array('debug'=>false));
+        $this->http_options = array();
     }
 
     /**
@@ -202,6 +205,15 @@ class oauth_helper {
         $this->http->setHeader($str);
     }
 
+    /**
+     * Sets the options for the next curl request
+     *
+     * @param array $options
+     */
+    public function setup_oauth_http_options($options) {
+        $this->http_options = $options;
+    }
+
     /**
      * Request token for authentication
      * This is the first step to use OAuth, it will return oauth_token and oauth_token_secret
@@ -210,7 +222,7 @@ class oauth_helper {
     public function request_token() {
         $this->sign_secret = $this->consumer_secret.'&';
         $params = $this->prepare_oauth_parameters($this->request_token_api, array(), 'GET');
-        $content = $this->http->get($this->request_token_api, $params);
+        $content = $this->http->get($this->request_token_api, $params, $this->http_options);
         // Including:
         //     oauth_token
         //     oauth_token_secret
@@ -252,7 +264,7 @@ class oauth_helper {
         $this->sign_secret = $this->consumer_secret.'&'.$secret;
         $params = $this->prepare_oauth_parameters($this->access_token_api, array('oauth_token'=>$token, 'oauth_verifier'=>$verifier), 'POST');
         $this->setup_oauth_http_header($params);
-        $content = $this->http->post($this->access_token_api, $params);
+        $content = $this->http->post($this->access_token_api, $params, $this->http_options);
         $keys = $this->parse_result($content);
         $this->set_access_token($keys['oauth_token'], $keys['oauth_token_secret']);
         return $keys;
@@ -274,9 +286,16 @@ class oauth_helper {
         }
         // to access protected resource, sign_secret will alwasy be consumer_secret+token_secret
         $this->sign_secret = $this->consumer_secret.'&'.$secret;
-        $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method);
+        if (strtolower($method) === 'post' && !empty($params)) {
+            $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token) + $params, $method);
+        } else {
+            $oauth_params = $this->prepare_oauth_parameters($url, array('oauth_token'=>$token), $method);
+        }
         $this->setup_oauth_http_header($oauth_params);
-        $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params));
+        $content = call_user_func_array(array($this->http, strtolower($method)), array($url, $params, $this->http_options));
+        // reset http header and options to prepare for the next request
+        $this->http->resetHeader();
+        // return request return value
         return $content;
     }
 
index 48a459f..8225f23 100644 (file)
@@ -240,11 +240,11 @@ EOD;
         }
 
         if (!isset($record['descriptionformat'])) {
-            $record['description'] = FORMAT_MOODLE;
+            $record['descriptionformat'] = FORMAT_MOODLE;
         }
 
         if (!isset($record['parent'])) {
-            $record['descriptionformat'] = 0;
+            $record['parent'] = 0;
         }
 
         if (empty($record['parent'])) {
@@ -310,12 +310,12 @@ EOD;
             $record['numsections'] = 5;
         }
 
-        if (!isset($record['description'])) {
-            $record['description'] = "Test course $i\n$this->loremipsum";
+        if (!isset($record['summary'])) {
+            $record['summary'] = "Test course $i\n$this->loremipsum";
         }
 
-        if (!isset($record['descriptionformat'])) {
-            $record['description'] = FORMAT_MOODLE;
+        if (!isset($record['summaryformat'])) {
+            $record['summaryformat'] = FORMAT_MOODLE;
         }
 
         if (!isset($record['category'])) {
index 9e46811..33ec793 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class Hint_ResultPrinter extends PHPUnit_TextUI_ResultPrinter {
+    public function __construct() {
+        // ARRGH - PHPUnit does not give us commandline arguments or xml config, so let's hack hard!
+        if (defined('DEBUG_BACKTRACE_PROVIDE_OBJECT')) {
+            $backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
+            if (isset($backtrace[2]['object']) and ($backtrace[2]['object'] instanceof PHPUnit_TextUI_Command)) {
+                list($verbose, $colors, $debug) = Hacky_TextUI_Command_reader::get_settings_hackery($backtrace[2]['object']);
+                parent::__construct(null, $verbose, $colors, $debug);
+                return;
+            }
+        }
+        // Fallback if something goes wrong.
+        parent::__construct(null, false, false, false);
+    }
+
     protected function printDefectTrace(PHPUnit_Framework_TestFailure $defect) {
         global $CFG;
 
@@ -83,3 +97,30 @@ class Hint_ResultPrinter extends PHPUnit_TextUI_ResultPrinter {
         $this->write("\nTo re-run:\n $executable $testName $file\n");
     }
 }
+
+
+/**
+ * Class used in bloody hack that works around result printer constructor troubles.
+ *
+ * @package    core
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class Hacky_TextUI_Command_reader extends PHPUnit_TextUI_Command {
+    public static function get_settings_hackery(PHPUnit_TextUI_Command $toread) {
+        $arguments = $toread->arguments;
+        $config = PHPUnit_Util_Configuration::getInstance($arguments['configuration'])->getPHPUnitConfiguration();
+
+        $verbose = isset($config['verbose']) ? $config['verbose'] : false;
+        $verbose = isset($arguments['verbose']) ? $arguments['verbose'] : $verbose;
+
+        $colors = isset($config['colors']) ? $config['colors'] : false;
+        $colors = isset($arguments['colors']) ? $arguments['colors'] : $colors;
+
+        $debug = isset($config['debug']) ? $config['debug'] : false;
+        $debug = isset($arguments['debug']) ? $arguments['debug'] : $debug;
+
+        return array($verbose, $colors, $debug);
+    }
+}
index 3380549..b580259 100644 (file)
@@ -762,16 +762,20 @@ class phpunit_util {
      * Note: To be used from CLI scripts only.
      *
      * @static
+     * @param bool $displayprogress if true, this method will echo progress information.
      * @return void may terminate execution with exit code
      */
-    public static function drop_site() {
+    public static function drop_site($displayprogress = false) {
         global $DB, $CFG;
 
         if (!self::is_test_site()) {
             phpunit_bootstrap_error(PHPUNIT_EXITCODE_CONFIGERROR, 'Can not drop non-test site!!');
         }
 
-        // purge dataroot
+        // Purge dataroot
+        if ($displayprogress) {
+            echo "Purging dataroot:\n";
+        }
         self::reset_dataroot();
         phpunit_bootstrap_initdataroot($CFG->dataroot);
         $keep = array('.', '..', 'lock', 'webrunner.xml');
@@ -795,9 +799,28 @@ class phpunit_util {
             unset($tables['config']);
             $tables['config'] = 'config';
         }
+
+        if ($displayprogress) {
+            echo "Dropping tables:\n";
+        }
+        $dotsonline = 0;
         foreach ($tables as $tablename) {
             $table = new xmldb_table($tablename);
             $DB->get_manager()->drop_table($table);
+
+            if ($dotsonline == 60) {
+                if ($displayprogress) {
+                    echo "\n";
+                }
+                $dotsonline = 0;
+            }
+            if ($displayprogress) {
+                echo '.';
+            }
+            $dotsonline += 1;
+        }
+        if ($displayprogress) {
+            echo "\n";
         }
     }
 
index eb5df6b..316ee70 100644 (file)
@@ -48,10 +48,22 @@ class core_phpunit_generator_testcase extends advanced_testcase {
         $count = $DB->count_records('course_categories');
         $category = $generator->create_category();
         $this->assertEquals($count+1, $DB->count_records('course_categories'));
+        $this->assertRegExp('/^Course category \d/', $category->name);
+        $this->assertSame('', $category->idnumber);
+        $this->assertRegExp('/^Test course category \d/', $category->description);
+        $this->assertSame(FORMAT_MOODLE, $category->descriptionformat);
 
         $count = $DB->count_records('course');
         $course = $generator->create_course();
         $this->assertEquals($count+1, $DB->count_records('course'));
+        $this->assertRegExp('/^Test course \d/', $course->fullname);
+        $this->assertRegExp('/^tc_\d/', $course->shortname);
+        $this->assertSame('', $course->idnumber);
+        $this->assertSame('topics', $course->format);
+        $this->assertEquals(0, $course->newsitems);
+        $this->assertEquals(5, $course->numsections);
+        $this->assertRegExp('/^Test course \d/', $course->summary);
+        $this->assertSame(FORMAT_MOODLE, $course->summaryformat);
 
         $section = $generator->create_course_section(array('course'=>$course->id, 'section'=>3));
         $this->assertEquals($course->id, $section->course);
index 43de510..65e1c35 100644 (file)
@@ -1064,7 +1064,7 @@ class available_update_checker {
             return true;
         }
 
-        if ($now - $recent > HOURSECS) {
+        if ($now - $recent > 24 * HOURSECS) {
             return false;
         }
 
index db7be64..c8aeacd 100644 (file)
@@ -209,39 +209,44 @@ function get_grade_options() {
 }
 
 /**
- * match grade options
- * if no match return error or match nearest
+ * Check whether a given grade is one of a list of allowed options. If not,
+ * depending on $matchgrades, either return the nearest match, or return false
+ * to signal an error.
  * @param array $gradeoptionsfull list of valid options
  * @param int $grade grade to be tested
  * @param string $matchgrades 'error' or 'nearest'
- * @return mixed either 'fixed' value or false if erro
+ * @return mixed either 'fixed' value or false if error.
  */
-function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') {
+function match_grade_options($gradeoptionsfull, $grade, $matchgrades = 'error') {
+
     if ($matchgrades == 'error') {
-        // if we just need an error...
+        // (Almost) exact match, or an error.
         foreach ($gradeoptionsfull as $value => $option) {
-            // slightly fuzzy test, never check floats for equality :-)
+            // Slightly fuzzy test, never check floats for equality.
             if (abs($grade - $value) < 0.00001) {
-                return $grade;
+                return $value; // Be sure the return the proper value.
             }
         }
-        // didn't find a match so that's an error
+        // Didn't find a match so that's an error.
         return false;
+
     } else if ($matchgrades == 'nearest') {
-        // work out nearest value
-        $hownear = array();
+        // Work out nearest value
+        $best = false;
+        $bestmismatch = 2;
         foreach ($gradeoptionsfull as $value => $option) {
-            if ($grade==$value) {
-                return $grade;
+            $newmismatch = abs($grade - $value);
+            if ($newmismatch < $bestmismatch) {
+                $best = $value;
+                $bestmismatch = $newmismatch;
             }
-            $hownear[ $value ] = abs( $grade - $value );
         }
-        // reverse sort list of deltas and grab the last (smallest)
-        asort( $hownear, SORT_NUMERIC );
-        reset( $hownear );
-        return key( $hownear );
+        return $best;
+
     } else {
-        return false;
+        // Unknow option passed.
+        throw new coding_exception('Unknown $matchgrades ' . $matchgrades .
+                ' passed to match_grade_options');
     }
 }
 
@@ -780,14 +785,22 @@ function question_load_questions($questionids, $extrafields = '', $join = '') {
  */
 function _tidy_question($question, $loadtags = false) {
     global $CFG;
+
+    // Load question-type specific fields.
     if (!question_bank::is_qtype_installed($question->qtype)) {
         $question->questiontext = html_writer::tag('p', get_string('warningmissingtype',
                 'qtype_missingtype')) . $question->questiontext;
     }
     question_bank::get_qtype($question->qtype)->get_question_options($question);
+
+    // Convert numeric fields to float. (Prevents these being displayed as 1.0000000.)
+    $question->defaultmark += 0;
+    $question->penalty += 0;
+
     if (isset($question->_partiallyloaded)) {
         unset($question->_partiallyloaded);
     }
+
     if ($loadtags && !empty($CFG->usetags)) {
         require_once($CFG->dirroot . '/tag/lib.php');
         $question->tags = tag_get_tags_array('question', $question->id);
@@ -1104,16 +1117,18 @@ function question_category_options($contexts, $top = false, $currentcat = 0,
 
     // sort cats out into different contexts
     $categoriesarray = array();
-    foreach ($pcontexts as $pcontext) {
-        $contextstring = print_context_name(
-                get_context_instance_by_id($pcontext), true, true);
+    foreach ($pcontexts as $contextid) {
+        $context = context::instance_by_id($contextid);
+        $contextstring = $context->get_context_name(true, true);
         foreach ($categories as $category) {
-            if ($category->contextid == $pcontext) {
+            if ($category->contextid == $contextid) {
                 $cid = $category->id;
                 if ($currentcat != $cid || $currentcat == 0) {
                     $countstring = !empty($category->questioncount) ?
                             " ($category->questioncount)" : '';
-                    $categoriesarray[$contextstring][$cid] = $category->indentedname.$countstring;
+                    $categoriesarray[$contextstring][$cid] =
+                            format_string($category->indentedname, true,
+                                array('context' => $context)) . $countstring;
                 }
             }
         }
diff --git a/lib/tests/backup_test.php b/lib/tests/backup_test.php
deleted file mode 100644 (file)
index 1830f56..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-<?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 backups.
- *
- * @package   core
- * @category  phpunit
- * @copyright 2012 Frédéric Massart <fred@moodle.com>
- * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-global $CFG;
-require_once($CFG->dirroot . '/backup/util/helper/backup_cron_helper.class.php');
-
-/**
- * Unit tests for backup system
- */
-class backup_testcase extends advanced_testcase {
-
-    public function test_next_automated_backup() {
-
-        $this->resetAfterTest();
-        $admin = get_admin();
-        $timezone = $admin->timezone;
-
-        // Notes
-        // - The next automated backup will never be on the same date than $now
-        // - backup_auto_weekdays starts on Sunday
-        // - Tests cannot be done in the past.
-
-        // Every Wed and Sat at 11pm.
-        set_config('backup_auto_active', '1', 'backup');
-        set_config('backup_auto_weekdays', '0010010', 'backup');
-        set_config('backup_auto_hour', '23', 'backup');
-        set_config('backup_auto_minute', '0', 'backup');
-
-        $now = strtotime('next Monday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Tuesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('5-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Wednesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('5-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Thursday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('5-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Friday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Saturday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-23:00', date('w-H:i', $next));
-
-        $now = strtotime('next Sunday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-23:00', date('w-H:i', $next));
-
-        // Every Sun and Sat at 12pm.
-        set_config('backup_auto_active', '1', 'backup');
-        set_config('backup_auto_weekdays', '1000001', 'backup');
-        set_config('backup_auto_hour', '0', 'backup');
-        set_config('backup_auto_minute', '0', 'backup');
-
-        $now = strtotime('next Monday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Tuesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Wednesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Thursday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Friday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Saturday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-00:00', date('w-H:i', $next));
-
-        $now = strtotime('next Sunday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-00:00', date('w-H:i', $next));
-
-        // Every Sun at 4am.
-        set_config('backup_auto_active', '1', 'backup');
-        set_config('backup_auto_weekdays', '1000000', 'backup');
-        set_config('backup_auto_hour', '4', 'backup');
-        set_config('backup_auto_minute', '0', 'backup');
-
-        $now = strtotime('next Monday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Tuesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Wednesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Thursday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Friday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Saturday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        $now = strtotime('next Sunday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-04:00', date('w-H:i', $next));
-
-        // Every day but Wed at 8:30pm.
-        set_config('backup_auto_active', '1', 'backup');
-        set_config('backup_auto_weekdays', '1110111', 'backup');
-        set_config('backup_auto_hour', '20', 'backup');
-        set_config('backup_auto_minute', '30', 'backup');
-
-        $now = strtotime('next Monday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('2-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Tuesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('4-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Wednesday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('4-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Thursday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('5-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Friday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('6-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Saturday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('0-20:30', date('w-H:i', $next));
-
-        $now = strtotime('next Sunday');
-        $next = backup_cron_automated_helper::calculate_next_automated_backup($timezone, $now);
-        $this->assertEquals('1-20:30', date('w-H:i', $next));
-
-    }
-}
index 6e81cd7..5092831 100644 (file)
@@ -257,7 +257,7 @@ class completionlib_testcase extends basic_testcase {
             ->will($this->returnValue(true));
         $c->expects($this->at(1))
             ->method('get_data')
-            ->with($cm, 1337)
+            ->with($cm, false, 1337)
             ->will($this->returnValue((object)array('viewed'=>COMPLETION_NOT_VIEWED)));
         $c->expects($this->at(2))
             ->method('internal_set_data')
index 2d03ffc..1abdf80 100644 (file)
@@ -1549,9 +1549,9 @@ class moodlelib_testcase extends advanced_testcase {
                 'hour' => '10',
                 'minutes' => '00',
                 'seconds' => '00',
-                'timezone' => '0.0', //no dst offset
-                'applydst' => false,
-                'expectedoutput' => '1309528800'
+                'timezone' => '0.0',
+                'applydst' => false, //no dst offset
+                'expectedoutput' => '1309514400' // 6pm at UTC+0
             ),
             array(
                 'usertimezone' => 'America/Moncton',
@@ -1910,4 +1910,64 @@ class moodlelib_testcase extends advanced_testcase {
         $this->assertEquals('5.43000', format_float(5.43, 5, false));
         $this->assertEquals('5.43', format_float(5.43, 5, false, true));
     }
+
+    /**
+     * Test deleting of users.
+     */
+    public function test_delete_user() {
+        global $DB, $CFG;
+
+        $this->resetAfterTest();
+
+        $guest = $DB->get_record('user', array('id'=>$CFG->siteguest), '*', MUST_EXIST);
+        $admin = $DB->get_record('user', array('id'=>$CFG->siteadmins), '*', MUST_EXIST);
+        $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1)));
+
+        $user = $this->getDataGenerator()->create_user(array('idnumber'=>'abc'));
+        $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'xyz'));
+
+        $result = delete_user($user);
+        $this->assertTrue($result);
+        $deluser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
+        $this->assertEquals(1, $deluser->deleted);
+        $this->assertEquals(0, $deluser->picture);
+        $this->assertSame('', $deluser->idnumber);
+        $this->assertSame(md5($user->username), $deluser->email);
+        $this->assertRegExp('/^'.preg_quote($user->email, '/').'\.\d*$/', $deluser->username);
+
+        $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1)));
+
+        // Try invalid params.
+
+        $record = new stdClass();
+        $record->grrr = 1;
+        try {
+            delete_user($record);
+            $this->fail('Expecting exception for invalid delete_user() $user parameter');
+        } catch (coding_exception $e) {
+            $this->assertTrue(true);
+        }
+        $record->id = 1;
+        try {
+            delete_user($record);
+            $this->fail('Expecting exception for invalid delete_user() $user parameter');
+        } catch (coding_exception $e) {
+            $this->assertTrue(true);
+        }
+
+        $CFG->debug = DEBUG_MINIMAL; // Prevent standard debug warnings.
+
+        $record = new stdClass();
+        $record->id = 666;
+        $record->username = 'xx';
+        $this->assertFalse($DB->record_exists('user', array('id'=>666))); // Any non-existent id is ok.
+        $result = delete_user($record);
+        $this->assertFalse($result);
+
+        $result = delete_user($guest);
+        $this->assertFalse($result);
+
+        $result = delete_user($admin);
+        $this->assertFalse($result);
+    }
 }
index eb29268..2ec4d0d 100644 (file)
@@ -103,7 +103,7 @@ class available_update_checker_test extends advanced_testcase {
      */
     public function test_cron_has_fresh_fetch() {
         $provider = testable_available_update_checker::instance();
-        $provider->fakerecentfetch = time() - 59 * MINSECS; // fetched an hour ago
+        $provider->fakerecentfetch = time() - 23 * HOURSECS; // fetched 23 hours ago
         $provider->fakecurrenttimestamp = -1;
         $provider->cron();
         $this->assertTrue(true); // we should get here with no exception thrown
@@ -127,23 +127,52 @@ class available_update_checker_test extends advanced_testcase {
      */
     public function test_cron_offset_execution_not_yet() {
         $provider = testable_available_update_checker::instance();
-        $provider->fakerecentfetch = time() - 24 * HOURSECS;
-        $provider->fakecurrenttimestamp = mktime(1, 40, 02); // 01:40:02 AM
+        $provider->fakecurrenttimestamp = mktime(1, 40, 02); // 01:40:02 AM today
+        $provider->fakerecentfetch = $provider->fakecurrenttimestamp - 24 * HOURSECS;
         $provider->cron();
         $this->assertTrue(true); // we should get here with no exception thrown<