Merge branch 'MDL-32309-master' of git://github.com/FMCorz/moodle
authorDan Poltawski <dan@moodle.com>
Wed, 19 Sep 2012 08:10:50 +0000 (16:10 +0800)
committerDan Poltawski <dan@moodle.com>
Wed, 19 Sep 2012 08:10:50 +0000 (16:10 +0800)
111 files changed:
admin/settings/appearance.php
auth/db/auth.php
auth/db/lang/en/auth_db.php
auth/shibboleth/index.php
blocks/dock.js
blocks/navigation/renderer.php
blocks/navigation/yui/navigation/navigation.js
blocks/rss_client/block_rss_client.php
blocks/settings/renderer.php
calendar/view.php
cohort/assign.php
cohort/edit.php
cohort/edit_form.php
cohort/index.php
cohort/lib.php
cohort/locallib.php [new file with mode: 0644]
cohort/tests/cohortlib_test.php [new file with mode: 0644]
course/tests/courserequest_test.php
enrol/cohort/yui/quickenrolment/assets/skins/sam/quickenrolment.css
enrol/database/lib.php
enrol/manual/cli/sync.php [new file with mode: 0644]
enrol/manual/lang/en/enrol_manual.php
enrol/manual/lib.php
enrol/manual/settings.php
enrol/manual/tests/lib_test.php
enrol/manual/version.php
enrol/self/cli/sync.php [new file with mode: 0644]
enrol/self/lang/en/enrol_self.php
enrol/self/lib.php
enrol/self/settings.php
enrol/self/tests/self_test.php [new file with mode: 0644]
enrol/self/version.php
lang/en/admin.php
lang/en/calendar.php
lib/blocklib.php
lib/ddl/tests/ddl_test.php
lib/dml/moodle_database.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/dtl/database_exporter.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/filestorage/tests/zip_packer_test.php
lib/formslib.php
lib/javascript-static.js
lib/moodlelib.php
lib/outputrenderers.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/data_generator.php
lib/phpunit/classes/database_driver_testcase.php
lib/phpunit/classes/util.php
lib/phpunit/tests/advanced_test.php
lib/phpunit/tests/generator_test.php
lib/pluginlib.php
lib/tests/accesslib_test.php
lib/tests/moodlelib_test.php
lib/tests/outputlib_test.php
lib/tests/textlib_test.php
lib/upgrade.txt
lib/weblib.php
mod/assign/backup/moodle2/backup_assign_stepslib.php
mod/assign/lib.php
mod/assign/locallib.php
mod/book/tool/print/index.php
mod/book/tool/print/locallib.php
mod/book/view.php
mod/forum/db/access.php
mod/forum/db/events.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/version.php
mod/lesson/continue.php
mod/lesson/edit.php
mod/lesson/editpage.php
mod/lesson/essay.php
mod/lesson/format.php
mod/lesson/highscores.php
mod/lesson/lang/en/lesson.php
mod/lesson/lesson.php
mod/lesson/renderer.php
mod/lesson/report.php
mod/lesson/view.php
mod/quiz/report/attemptsreport_table.php
mod/scorm/lib.php
mod/upgrade.txt
my/index.php
phpunit.xml.dist
question/behaviour/behaviourbase.php
question/engine/lib.php
question/engine/questionattempt.php
question/engine/tests/questionutils_test.php
question/format.php
question/format/aiken/format.php
question/format/blackboard/format.php
question/format/blackboard_six/formatpool.php
question/format/blackboard_six/formatqti.php
question/format/examview/format.php
question/format/gift/format.php
question/format/learnwise/format.php
question/format/missingword/format.php
question/format/multianswer/format.php
question/format/multianswer/tests/multianswerformat_test.php
question/format/webct/format.php
question/format/xml/format.php
question/tests/importexport_test.php
report/log/index.php
report/log/lang/en/report_log.php
theme/base/style/admin.css
theme/base/style/core.css
theme/base/style/course.css

index 8130da4..acd62df 100644 (file)
@@ -97,6 +97,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
         HOMEPAGE_USER => new lang_string('userpreference', 'admin')
     );
     $temp->add(new admin_setting_configselect('defaulthomepage', new lang_string('defaulthomepage', 'admin'), new lang_string('configdefaulthomepage', 'admin'), HOMEPAGE_SITE, $choices));
+    $temp->add(new admin_setting_configcheckbox('allowguestmymoodle', new lang_string('allowguestmymoodle', 'admin'), new lang_string('configallowguestmymoodle', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('navshowcategories', new lang_string('navshowcategories', 'admin'), new lang_string('confignavshowcategories', 'admin'), 1));
     $temp->add(new admin_setting_configcheckbox('navshowmycoursecategories', new lang_string('navshowmycoursecategories', 'admin'), new lang_string('navshowmycoursecategories_help', 'admin'), 0));
     $temp->add(new admin_setting_configcheckbox('navshowallcourses', new lang_string('navshowallcourses', 'admin'), new lang_string('confignavshowallcourses', 'admin'), 0));
index f72b318..60babc8 100644 (file)
@@ -65,7 +65,7 @@ class auth_plugin_db extends auth_plugin_base {
                 $authdb->Close();
                 // user exists externally
                 // check username/password internally
-                if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
+                if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype))) {
                     return validate_internal_user_password($user, $password);
                 }
             } else {
@@ -191,8 +191,16 @@ class auth_plugin_db extends auth_plugin_base {
      * @return bool                  True on success
      */
     function user_update_password($user, $newpassword) {
+        global $DB;
+
         if ($this->is_internal()) {
-            return update_internal_user_password($user, $newpassword);
+            $puser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST);
+            if (update_internal_user_password($puser, $newpassword)) {
+                $user->password = $puser->password;
+                return true;
+            } else {
+                return false;
+            }
         } else {
             // we should have never been called!
             return false;
@@ -356,7 +364,7 @@ class auth_plugin_db extends auth_plugin_base {
             if ($verbose) {
                 mtrace(get_string('auth_dbuserstoadd','auth_db',count($add_users)));
             }
-            $transaction = $DB->start_delegated_transaction();
+            // Do not use transactions around this foreach, we want to skip problematic users, not revert everything.
             foreach($add_users as $user) {
                 $username = $user;
                 if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) {
@@ -382,6 +390,12 @@ class auth_plugin_db extends auth_plugin_base {
                 }
                 $user->timecreated = time();
                 $user->timemodified = $user->timecreated;
+                if ($collision = $DB->get_record_select('user', "username = :username AND mnethostid = :mnethostid AND auth <> :auth", array('username'=>$user->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype), 'id,username,auth')) {
+                    if ($verbose) {
+                        mtrace("\t".get_string('auth_dbinsertuserduplicate', 'auth_db', array('username'=>$user->username, 'auth'=>$collision->auth)));
+                    }
+                    continue;
+                }
                 try {
                     $id = $DB->insert_record ('user', $user); // it is truly a new user
                     if ($verbose) {
@@ -391,14 +405,16 @@ class auth_plugin_db extends auth_plugin_base {
                     if ($verbose) {
                         mtrace("\t".get_string('auth_dbinsertusererror', 'auth_db', $user->username));
                     }
+                    continue;
                 }
                 // if relevant, tag for password generation
                 if ($this->is_internal()) {
                     set_user_preference('auth_forcepasswordchange', 1, $id);
                     set_user_preference('create_password',          1, $id);
                 }
+                // Make sure user context is present.
+                context_user::instance($id);
             }
-            $transaction->allow_commit();
             unset($add_users); // free mem
         }
         return 0;
index 6f42064..76967ea 100644 (file)
@@ -40,6 +40,7 @@ $string['auth_dbhost'] = 'The computer hosting the database server.';
 $string['auth_dbhost_key'] = 'Host';
 $string['auth_dbchangepasswordurl_key'] = 'Password-change URL';
 $string['auth_dbinsertuser'] = 'Inserted user {$a->name} id {$a->id}';
+$string['auth_dbinsertuserduplicate'] = 'Error inserting user {$a->username} - user with this username was already created through \'{$a->auth}\' plugin.';
 $string['auth_dbinsertusererror'] = 'Error inserting user {$a}';
 $string['auth_dbname'] = 'Name of the database itself';
 $string['auth_dbname_key'] = 'DB name';
index bd4eddb..051ac4f 100644 (file)
@@ -6,6 +6,9 @@
 
     $PAGE->set_url('/auth/shibboleth/index.php');
 
+    // Support for WAYFless URLs.
+    $SESSION->wantsurl = optional_param('target', $SESSION->wantsurl, PARAM_LOCALURL);
+
     if (isloggedin() && !isguestuser()) {      // Nothing to do
         if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) {
             $urltogo = $SESSION->wantsurl;    /// Because it's an address in this site
index 5e4a56f..4eff68c 100644 (file)
@@ -164,7 +164,7 @@ M.core_dock.init = function(Y) {
     var dock = Y.one('#dock');
     if (!dock) {
         // Start the construction of the dock
-        dock = Y.Node.create('<div id="dock" class="'+css.dock+' '+css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation+'"></div>')
+        dock = Y.Node.create('<div id="dock" role="menubar" class="'+css.dock+' '+css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation+'"></div>')
                     .append(Y.Node.create('<div class="'+css.buttonscontainer+'"></div>')
                         .append(Y.Node.create('<div class="'+css.dockeditemcontainer+'"></div>')));
         this.nodes.body.append(dock);
@@ -1036,7 +1036,7 @@ M.core_dock.item.prototype = {
         var Y = this.Y;
         var css = M.core_dock.css;
 
-        this.nodes.docktitle = Y.Node.create('<div id="dock_item_'+this.id+'_title" class="'+css.dockedtitle+'"></div>');
+        this.nodes.docktitle = Y.Node.create('<div id="dock_item_'+this.id+'_title" role="menu" aria-haspopup="true" class="'+css.dockedtitle+'"></div>');
         this.nodes.docktitle.append(this.title);
         this.nodes.dockitem = Y.Node.create('<div id="dock_item_'+this.id+'" class="'+css.dockeditem+'" tabindex="0"></div>');
         this.nodes.dockitem.on('dock:actionkey', this.toggle, this);
@@ -1065,6 +1065,8 @@ M.core_dock.item.prototype = {
         this.active = true;
         // Add active item class first up
         this.nodes.docktitle.addClass(css.activeitem);
+        // Set aria-exapanded property to true.
+        this.nodes.docktitle.set('aria-expanded', "true");
         this.fire('dockeditem:showcomplete');
         M.core_dock.resize();
         return true;
@@ -1081,6 +1083,8 @@ M.core_dock.item.prototype = {
         this.nodes.docktitle.removeClass(css.activeitem);
         // Hide the panel
         M.core_dock.getPanel().hide();
+        // Set aria-exapanded property to false
+        this.nodes.docktitle.set('aria-expanded', "false");
         this.fire('dockeditem:hidecomplete');
     },
     /**
index 4c3d28c..92e2831 100644 (file)
@@ -121,18 +121,20 @@ class block_navigation_renderer extends plugin_renderer_base {
 
             // this applies to the li item which contains all child lists too
             $liclasses = array($item->get_css_type(), 'depth_'.$depth);
+            $liexpandable = array();
             if ($item->has_children() && (!$item->forceopen || $item->collapse)) {
                 $liclasses[] = 'collapsed';
             }
             if ($isbranch) {
                 $liclasses[] = 'contains_branch';
+                $liexpandable = array('aria-expanded' => in_array('collapsed', $liclasses) ? "false" : "true");
             } else if ($hasicon) {
                 $liclasses[] = 'item_with_icon';
             }
             if ($item->isactive === true) {
                 $liclasses[] = 'current_branch';
             }
-            $liattr = array('class'=>join(' ',$liclasses));
+            $liattr = array('class' => join(' ',$liclasses)) + $liexpandable;
             // class attribute on the div item which only contains the item content
             $divclasses = array('tree_item');
             if ($isbranch) {
index 180977d..2189d51 100644 (file)
@@ -162,16 +162,20 @@ TREE.prototype = {
                 switch (e.action) {
                     case 'expand' :
                         target.removeClass('collapsed');
+                        target.set('aria-expanded', true);
                         break;
                     case 'collapse' :
                         target.addClass('collapsed');
+                        target.set('aria-expanded', false);
                         break;
                     default :
                         target.toggleClass('collapsed');
+                        target.set('aria-expanded', !target.hasClass('collapsed'));
                 }
                 e.halt();
             } else {
                 target.toggleClass('collapsed');
+                target.set('aria-expanded', !target.hasClass('collapsed'));
             }
         }
 
@@ -180,6 +184,7 @@ TREE.prototype = {
             target.siblings('li').each(function(){
                 if (this.get('id') !== target.get('id') && !this.hasClass('collapsed')) {
                     this.addClass('collapsed');
+                    this.set('aria-expanded', false);
                 }
             });
         }
@@ -287,6 +292,7 @@ BRANCH.prototype = {
         }
         if (isbranch) {
             branchli.addClass('collapsed').addClass('contains_branch');
+            branchli.set('aria-expanded', false);
             branchp.addClass('branch');
         }
 
index 086983d..38f8856 100644 (file)
 
         $r = html_writer::start_tag('li');
             $r.= html_writer::start_tag('div',array('class'=>'link'));
-                $r.= html_writer::link(clean_param($link,PARAM_URL), s($title), array('onclick'=>'this.target="_blank"'));
+                $r.= html_writer::link($link, s($title), array('onclick'=>'this.target="_blank"'));
             $r.= html_writer::end_tag('div');
 
             if($this->config->display_description && !empty($description)){
index 41cea87..a19d41e 100644 (file)
@@ -40,18 +40,20 @@ class block_settings_renderer extends plugin_renderer_base {
 
             // this applies to the li item which contains all child lists too
             $liclasses = array($item->get_css_type());
+            $liexpandable = array();
             if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count()==0  && $item->nodetype==navigation_node::NODETYPE_BRANCH)) {
                 $liclasses[] = 'collapsed';
             }
             if ($isbranch) {
                 $liclasses[] = 'contains_branch';
+                $liexpandable = array('aria-expanded' => in_array('collapsed', $liclasses) ? "false" : "true");
             } else if ($hasicon) {
                 $liclasses[] = 'item_with_icon';
             }
             if ($item->isactive === true) {
                 $liclasses[] = 'current_branch';
             }
-            $liattr = array('class'=>join(' ',$liclasses));
+            $liattr = array('class' => join(' ',$liclasses)) + $liexpandable;
             // class attribute on the div item which only contains the item content
             $divclasses = array('tree_item');
             if ($isbranch) {
index fbad9e0..8f56956 100644 (file)
@@ -98,11 +98,11 @@ $time = make_timestamp($yr, $mon, $day);
 switch($view) {
     case 'day':
         $PAGE->navbar->add(userdate($time, get_string('strftimedate')));
-        $pagetitle = get_string('dayview', 'calendar');
+        $pagetitle = get_string('dayviewtitle', 'calendar', userdate($time, get_string('strftimedaydate')));
     break;
     case 'month':
         $PAGE->navbar->add(userdate($time, get_string('strftimemonthyear')));
-        $pagetitle = get_string('detailedmonthview', 'calendar');
+        $pagetitle = get_string('detailedmonthviewtitle', 'calendar', userdate($time, get_string('strftimemonthyear')));
     break;
     case 'upcoming':
         $pagetitle = get_string('upcomingevents', 'calendar');
index 5eaa2a7..0d6c83a 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
 /**
  * Cohort related management functions, this file needs to be included manually.
  *
- * @package    core
- * @subpackage cohort
+ * @package    core_cohort
  * @copyright  2010 Petr Skoda  {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require_once('../config.php');
-require_once($CFG->dirroot.'/cohort/lib.php');
+require('../config.php');
+require_once($CFG->dirroot.'/cohort/locallib.php');
 
 $id = required_param('id', PARAM_INT);
 
@@ -42,7 +40,7 @@ $PAGE->set_url('/cohort/assign.php', array('id'=>$id));
 $returnurl = new moodle_url('/cohort/index.php', array('contextid'=>$cohort->contextid));
 
 if (!empty($cohort->component)) {
-    // we can not manually edit cohorts that were created by external systems, sorry
+    // We can not manually edit cohorts that were created by external systems, sorry.
     redirect($returnurl);
 }
 
@@ -80,10 +78,7 @@ if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
     if (!empty($userstoassign)) {
 
         foreach ($userstoassign as $adduser) {
-            // no duplicates please
-            if (!$DB->record_exists('cohort_members', array('cohortid'=>$cohort->id, 'userid'=>$adduser->id))) {
-                cohort_add_member($cohort->id, $adduser->id);
-            }
+            cohort_add_member($cohort->id, $adduser->id);
         }
 
         $potentialuserselector->invalidate_selected_users();
index 721d790..3a18ddb 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
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
-
 /**
  * Cohort related management functions, this file needs to be included manually.
  *
- * @package    core
- * @subpackage cohort
+ * @package    core_cohort
  * @copyright  2010 Petr Skoda  {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -58,7 +55,7 @@ require_capability('moodle/cohort:manage', $context);
 $returnurl = new moodle_url('/cohort/index.php', array('contextid'=>$context->id));
 
 if (!empty($cohort->component)) {
-    // we can not manually edit cohorts that were created by external systems, sorry
+    // We can not manually edit cohorts that were created by external systems, sorry.
     redirect($returnurl);
 }
 
@@ -97,12 +94,12 @@ if ($delete and $cohort->id) {
 
 $editoroptions = array('maxfiles'=>0, 'context'=>$context);
 if ($cohort->id) {
-    // edit existing
+    // Edit existing.
     $cohort = file_prepare_standard_editor($cohort, 'description', $editoroptions, $context);
     $strheading = get_string('editcohort', 'cohort');
 
 } else {
-    // add new
+    // Add new.
     $cohort = file_prepare_standard_editor($cohort, 'description', $editoroptions, $context);
     $strheading = get_string('addcohort', 'cohort');
 }
@@ -125,7 +122,7 @@ if ($editform->is_cancelled()) {
         cohort_add_cohort($data);
     }
 
-    // use new context id, it could have been changed
+    // Use new context id, it could have been changed.
     redirect(new moodle_url('/cohort/index.php', array('contextid'=>$data->contextid)));
 }
 
index 81e7a7c..c23c249 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
 /**
  * Cohort related management functions, this file needs to be included manually.
  *
- * @package    core
- * @subpackage cohort
+ * @package    core_cohort
  * @copyright  2010 Petr Skoda  {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-if (!defined('MOODLE_INTERNAL')) {
-    die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
-}
+defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->dirroot . '/lib/formslib.php');
 
@@ -49,7 +45,7 @@ class cohort_edit_form extends moodleform {
         $mform->addElement('select', 'contextid', get_string('context', 'role'), $options);
 
         $mform->addElement('text', 'idnumber', get_string('idnumber', 'cohort'), 'maxlength="254" size="50"');
-        $mform->setType('idnumber', PARAM_RAW); // idnumbers are plain text, must not be changed
+        $mform->setType('idnumber', PARAM_RAW); // Idnumbers are plain text, must not be changed.
 
         $mform->addElement('editor', 'description_editor', get_string('description', 'cohort'), null, $editoroptions);
         $mform->setType('description_editor', PARAM_RAW);
@@ -69,7 +65,7 @@ class cohort_edit_form extends moodleform {
 
         $idnumber = trim($data['idnumber']);
         if ($idnumber === '') {
-            // fine, empty is ok
+            // Fine, empty is ok.
 
         } else if ($data['id']) {
             $current = $DB->get_record('cohort', array('id'=>$data['id']), '*', MUST_EXIST);
@@ -95,16 +91,16 @@ class cohort_edit_form extends moodleform {
         $options = array();
         $syscontext = context_system::instance();
         if (has_capability('moodle/cohort:manage', $syscontext)) {
-            $options[$syscontext->id] = print_context_name($syscontext);
+            $options[$syscontext->id] = $syscontext->get_context_name();
         }
         foreach ($displaylist as $cid=>$name) {
             $context = context_coursecat::instance($cid);
             $options[$context->id] = $name;
         }
-        // always add current - this is not likely, but if the logic gets changed it might be a problem
+        // Always add current - this is not likely, but if the logic gets changed it might be a problem.
         if (!isset($options[$currentcontextid])) {
             $context = context::instance_by_id($currentcontextid, MUST_EXIST);
-            $options[$context->id] = print_context_name($syscontext);
+            $options[$context->id] = $syscontext->get_context_name();
         }
         return $options;
     }
index 00aab66..35d794f 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 @@
 /**
  * Cohort related management functions, this file needs to be included manually.
  *
- * @package    core
- * @subpackage cohort
+ * @package    core_cohort
  * @copyright  2010 Petr Skoda  {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -69,21 +67,31 @@ if ($category) {
 
 echo $OUTPUT->header();
 
-echo $OUTPUT->heading(get_string('cohortsin', 'cohort', print_context_name($context)));
+$cohorts = cohort_get_cohorts($context->id, $page, 25, $searchquery);
+
+$count = '';
+if ($cohorts['allcohorts'] > 0) {
+    if ($searchquery === '') {
+        $count = ' ('.$cohorts['allcohorts'].')';
+    } else {
+        $count = ' ('.$cohorts['totalcohorts'].'/'.$cohorts['allcohorts'].')';
+    }
+}
+
+echo $OUTPUT->heading(get_string('cohortsin', 'cohort', $context->get_context_name()).$count);
 
-// add search form
+// Add search form.
 $search  = html_writer::start_tag('form', array('id'=>'searchcohortquery', 'method'=>'get'));
 $search .= html_writer::start_tag('div');
-$search .= html_writer::label(get_string('searchcohort', 'cohort').':', 'cohort_search_q');
+$search .= html_writer::label(get_string('searchcohort', 'cohort'), 'cohort_search_q'); // No : in form labels!
 $search .= html_writer::empty_tag('input', array('id'=>'cohort_search_q', 'type'=>'text', 'name'=>'search', 'value'=>$searchquery));
 $search .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('search', 'cohort')));
 $search .= html_writer::end_tag('div');
 $search .= html_writer::end_tag('form');
 echo $search;
 
-$cohorts = cohort_get_cohorts($context->id, $page, 25, $searchquery);
 
-// output pagination bar
+// Output pagination bar.
 $params = array('page' => $page);
 if ($contextid) {
     $params['contextid'] = $contextid;
@@ -98,7 +106,7 @@ $data = array();
 foreach($cohorts['cohorts'] as $cohort) {
     $line = array();
     $line[] = format_string($cohort->name);
-    $line[] = s($cohort->idnumber); // plain text
+    $line[] = s($cohort->idnumber); // All idnumbers are plain text.
     $line[] = format_text($cohort->description, $cohort->descriptionformat);
 
     $line[] = $DB->count_records('cohort_members', array('cohortid'=>$cohort->id));
@@ -137,4 +145,4 @@ if ($manager) {
     echo $OUTPUT->single_button(new moodle_url('/cohort/edit.php', array('contextid'=>$context->id)), get_string('add'));
 }
 
-echo $OUTPUT->footer();
\ No newline at end of file
+echo $OUTPUT->footer();
index 67c2bf4..89a79e6 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
 /**
  * Cohort related management functions, this file needs to be included manually.
  *
- * @package    core
- * @subpackage cohort
+ * @package    core_cohort
  * @copyright  2010 Petr Skoda  {@link http://skodak.org}
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-require_once($CFG->dirroot . '/user/selector/lib.php');
+defined('MOODLE_INTERNAL') || die();
 
 /**
  * Add new cohort.
  *
- * @param  object $cohort
- * @return int
+ * @param  stdClass $cohort
+ * @return int new cohort id
  */
 function cohort_add_cohort($cohort) {
     global $DB;
@@ -42,7 +40,8 @@ function cohort_add_cohort($cohort) {
         $cohort->idnumber = NULL;
     }
     if (!isset($cohort->description)) {
-        $cohort->description = $DB->sql_empty();
+        // sql_empty() does not belong here, this crazy Oracle hack is implemented in insert_record()!
+        $cohort->description = '';
     }
     if (!isset($cohort->descriptionformat)) {
         $cohort->descriptionformat = FORMAT_HTML;
@@ -66,7 +65,7 @@ function cohort_add_cohort($cohort) {
 
 /**
  * Update existing cohort.
- * @param  object $cohort
+ * @param  stdClass $cohort
  * @return void
  */
 function cohort_update_cohort($cohort) {
@@ -83,7 +82,7 @@ function cohort_update_cohort($cohort) {
 
 /**
  * Delete cohort.
- * @param  object $cohort
+ * @param  stdClass $cohort
  * @return void
  */
 function cohort_delete_cohort($cohort) {
@@ -103,7 +102,7 @@ function cohort_delete_cohort($cohort) {
  * Somehow deal with cohorts when deleting course category,
  * we can not just delete them because they might be used in enrol
  * plugins or referenced in external systems.
- * @param  object $category
+ * @param  stdClass $category
  * @return void
  */
 function cohort_delete_category($category) {
@@ -133,6 +132,10 @@ function cohort_delete_category($category) {
  */
 function cohort_add_member($cohortid, $userid) {
     global $DB;
+    if ($DB->record_exists('cohort_members', array('cohortid'=>$cohortid, 'userid'=>$userid))) {
+        // No duplicates!
+        return;
+    }
     $record = new stdClass();
     $record->cohortid  = $cohortid;
     $record->userid    = $userid;
@@ -168,34 +171,46 @@ function cohort_is_member($cohortid, $userid) {
 }
 
 /**
- * Returns list of visible cohorts in course.
+ * Returns list of cohorts from course parent contexts.
+ *
+ * Note: this function does not implement any capability checks,
+ *       it means it may disclose existence of cohorts,
+ *       make sure it is displayed to users with appropriate rights only.
  *
- * @param  object $course
- * @param  bool $enrolled true means include only cohorts with enrolled users
- * @return array
+ * @param  stdClass $course
+ * @param  bool $onlyenrolled true means include only cohorts with enrolled users
+ * @return array of cohort names with number of enrolled users
  */
-function cohort_get_visible_list($course) {
-    global $DB, $USER;
+function cohort_get_visible_list($course, $onlyenrolled=true) {
+    global $DB;
 
     $context = context_course::instance($course->id);
     list($esql, $params) = get_enrolled_sql($context);
-    $parentsql = get_related_contexts_string($context);
+    list($parentsql, $params2) = $DB->get_in_or_equal($context->get_parent_context_ids(), SQL_PARAMS_NAMED);
+    $params = array_merge($params, $params2);
 
-    $sql = "SELECT c.id, c.name, c.idnumber, COUNT(u.id) AS cnt
+    if ($onlyenrolled) {
+        $left = "";
+        $having = "HAVING COUNT(u.id) > 0";
+    } else {
+        $left = "LEFT";
+        $having = "";
+    }
+
+    $sql = "SELECT c.id, c.name, c.contextid, c.idnumber, COUNT(u.id) AS cnt
               FROM {cohort} c
-              JOIN {cohort_members} cm ON cm.cohortid = c.id
-              JOIN ($esql) u ON u.id = cm.userid
+        $left JOIN ({cohort_members} cm
+                   JOIN ($esql) u ON u.id = cm.userid) ON cm.cohortid = c.id
              WHERE c.contextid $parentsql
-          GROUP BY c.id, c.name, c.idnumber
-            HAVING COUNT(u.id) > 0
+          GROUP BY c.id, c.name, c.contextid, c.idnumber
+           $having
           ORDER BY c.name, c.idnumber";
-    $params['ctx'] = $context->id;
 
     $cohorts = $DB->get_records_sql($sql, $params);
 
     foreach ($cohorts as $cid=>$cohort) {
-        $cohorts[$cid] = format_string($cohort->name);
-        if ($cohort->idnumber) {
+        $cohorts[$cid] = format_string($cohort->name, true, array('context'=>$cohort->contextid));
+        if ($cohort->cnt) {
             $cohorts[$cid] .= ' (' . $cohort->cnt . ')';
         }
     }
@@ -204,170 +219,40 @@ function cohort_get_visible_list($course) {
 }
 
 /**
- * Get all the cohorts.
+ * Get all the cohorts defined in given context.
  *
- * @global moodle_database $DB
  * @param int $contextid
  * @param int $page number of the current page
  * @param int $perpage items per page
  * @param string $search search string
- * @return array    Array(totalcohorts => int, cohorts => array)
+ * @return array    Array(totalcohorts => int, cohorts => array, allcohorts => int)
  */
 function cohort_get_cohorts($contextid, $page = 0, $perpage = 25, $search = '') {
     global $DB;
 
-    $cohorts = array();
-
     // Add some additional sensible conditions
     $tests = array('contextid = ?');
     $params = array($contextid);
 
     if (!empty($search)) {
-        $conditions = array(
-            'name',
-            'idnumber',
-            'description',
-        );
-        $searchparam = '%' . $search . '%';
+        $conditions = array('name', 'idnumber', 'description');
+        $searchparam = '%' . $DB->sql_like_escape($search) . '%';
         foreach ($conditions as $key=>$condition) {
-            $conditions[$key] = $DB->sql_like($condition,"?", false);
+            $conditions[$key] = $DB->sql_like($condition, "?", false);
             $params[] = $searchparam;
         }
         $tests[] = '(' . implode(' OR ', $conditions) . ')';
     }
     $wherecondition = implode(' AND ', $tests);
 
-    $fields = 'SELECT *';
-    $countfields = 'SELECT COUNT(1)';
+    $fields = "SELECT *";
+    $countfields = "SELECT COUNT(1)";
     $sql = " FROM {cohort}
              WHERE $wherecondition";
-    $order = ' ORDER BY name ASC';
+    $order = " ORDER BY name ASC, idnumber ASC";
+    $allcohorts = $DB->count_records('cohort', array('contextid'=>$contextid));
     $totalcohorts = $DB->count_records_sql($countfields . $sql, $params);
     $cohorts = $DB->get_records_sql($fields . $sql . $order, $params, $page*$perpage, $perpage);
 
-    return array('totalcohorts' => $totalcohorts, 'cohorts' => $cohorts);
-}
-
-/**
- * Cohort assignment candidates
- */
-class cohort_candidate_selector extends user_selector_base {
-    protected $cohortid;
-
-    public function __construct($name, $options) {
-        $this->cohortid = $options['cohortid'];
-        parent::__construct($name, $options);
-    }
-
-    /**
-     * Candidate users
-     * @param <type> $search
-     * @return array
-     */
-    public function find_users($search) {
-        global $DB;
-        //by default wherecondition retrieves all users except the deleted, not confirmed and guest
-        list($wherecondition, $params) = $this->search_sql($search, 'u');
-        $params['cohortid'] = $this->cohortid;
-
-        $fields      = 'SELECT ' . $this->required_fields_sql('u');
-        $countfields = 'SELECT COUNT(1)';
-
-        $sql = " FROM {user} u
-            LEFT JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid)
-                WHERE cm.id IS NULL AND $wherecondition";
-
-        $order = ' ORDER BY u.lastname ASC, u.firstname ASC';
-
-        if (!$this->is_validating()) {
-            $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
-            if ($potentialmemberscount > 100) {
-                return $this->too_many_results($search, $potentialmemberscount);
-            }
-        }
-
-        $availableusers = $DB->get_records_sql($fields . $sql . $order, $params);
-
-        if (empty($availableusers)) {
-            return array();
-        }
-
-
-        if ($search) {
-            $groupname = get_string('potusersmatching', 'cohort', $search);
-        } else {
-            $groupname = get_string('potusers', 'cohort');
-        }
-
-        return array($groupname => $availableusers);
-    }
-
-    protected function get_options() {
-        $options = parent::get_options();
-        $options['cohortid'] = $this->cohortid;
-        $options['file'] = 'cohort/lib.php';
-        return $options;
-    }
-}
-
-/**
- * Cohort assignment candidates
- */
-class cohort_existing_selector extends user_selector_base {
-    protected $cohortid;
-
-    public function __construct($name, $options) {
-        $this->cohortid = $options['cohortid'];
-        parent::__construct($name, $options);
-    }
-
-    /**
-     * Candidate users
-     * @param <type> $search
-     * @return array
-     */
-    public function find_users($search) {
-        global $DB;
-        //by default wherecondition retrieves all users except the deleted, not confirmed and guest
-        list($wherecondition, $params) = $this->search_sql($search, 'u');
-        $params['cohortid'] = $this->cohortid;
-
-        $fields      = 'SELECT ' . $this->required_fields_sql('u');
-        $countfields = 'SELECT COUNT(1)';
-
-        $sql = " FROM {user} u
-                 JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid)
-                WHERE $wherecondition";
-
-        $order = ' ORDER BY u.lastname ASC, u.firstname ASC';
-
-        if (!$this->is_validating()) {
-            $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
-            if ($potentialmemberscount > 100) {
-                return $this->too_many_results($search, $potentialmemberscount);
-            }
-        }
-
-        $availableusers = $DB->get_records_sql($fields . $sql . $order, $params);
-
-        if (empty($availableusers)) {
-            return array();
-        }
-
-
-        if ($search) {
-            $groupname = get_string('currentusersmatching', 'cohort', $search);
-        } else {
-            $groupname = get_string('currentusers', 'cohort');
-        }
-
-        return array($groupname => $availableusers);
-    }
-
-    protected function get_options() {
-        $options = parent::get_options();
-        $options['cohortid'] = $this->cohortid;
-        $options['file'] = 'cohort/lib.php';
-        return $options;
-    }
+    return array('totalcohorts' => $totalcohorts, 'cohorts' => $cohorts, 'allcohorts'=>$allcohorts);
 }
diff --git a/cohort/locallib.php b/cohort/locallib.php
new file mode 100644 (file)
index 0000000..b257223
--- /dev/null
@@ -0,0 +1,155 @@
+<?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/>.
+
+/**
+ * Cohort UI related functions and classes.
+ *
+ * @package    core_cohort
+ * @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();
+
+require_once($CFG->dirroot . '/cohort/lib.php');
+require_once($CFG->dirroot . '/user/selector/lib.php');
+
+
+/**
+ * Cohort assignment candidates
+ */
+class cohort_candidate_selector extends user_selector_base {
+    protected $cohortid;
+
+    public function __construct($name, $options) {
+        $this->cohortid = $options['cohortid'];
+        parent::__construct($name, $options);
+    }
+
+    /**
+     * Candidate users
+     * @param string $search
+     * @return array
+     */
+    public function find_users($search) {
+        global $DB;
+        // By default wherecondition retrieves all users except the deleted, not confirmed and guest.
+        list($wherecondition, $params) = $this->search_sql($search, 'u');
+        $params['cohortid'] = $this->cohortid;
+
+        $fields      = 'SELECT ' . $this->required_fields_sql('u');
+        $countfields = 'SELECT COUNT(1)';
+
+        $sql = " FROM {user} u
+            LEFT JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid)
+                WHERE cm.id IS NULL AND $wherecondition";
+
+        $order = ' ORDER BY u.lastname ASC, u.firstname ASC';
+
+        if (!$this->is_validating()) {
+            $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
+            if ($potentialmemberscount > 100) {
+                return $this->too_many_results($search, $potentialmemberscount);
+            }
+        }
+
+        $availableusers = $DB->get_records_sql($fields . $sql . $order, $params);
+
+        if (empty($availableusers)) {
+            return array();
+        }
+
+
+        if ($search) {
+            $groupname = get_string('potusersmatching', 'cohort', $search);
+        } else {
+            $groupname = get_string('potusers', 'cohort');
+        }
+
+        return array($groupname => $availableusers);
+    }
+
+    protected function get_options() {
+        $options = parent::get_options();
+        $options['cohortid'] = $this->cohortid;
+        $options['file'] = 'cohort/locallib.php';
+        return $options;
+    }
+}
+
+
+/**
+ * Cohort assignment candidates
+ */
+class cohort_existing_selector extends user_selector_base {
+    protected $cohortid;
+
+    public function __construct($name, $options) {
+        $this->cohortid = $options['cohortid'];
+        parent::__construct($name, $options);
+    }
+
+    /**
+     * Candidate users
+     * @param string $search
+     * @return array
+     */
+    public function find_users($search) {
+        global $DB;
+        // By default wherecondition retrieves all users except the deleted, not confirmed and guest.
+        list($wherecondition, $params) = $this->search_sql($search, 'u');
+        $params['cohortid'] = $this->cohortid;
+
+        $fields      = 'SELECT ' . $this->required_fields_sql('u');
+        $countfields = 'SELECT COUNT(1)';
+
+        $sql = " FROM {user} u
+                 JOIN {cohort_members} cm ON (cm.userid = u.id AND cm.cohortid = :cohortid)
+                WHERE $wherecondition";
+
+        $order = ' ORDER BY u.lastname ASC, u.firstname ASC';
+
+        if (!$this->is_validating()) {
+            $potentialmemberscount = $DB->count_records_sql($countfields . $sql, $params);
+            if ($potentialmemberscount > 100) {
+                return $this->too_many_results($search, $potentialmemberscount);
+            }
+        }
+
+        $availableusers = $DB->get_records_sql($fields . $sql . $order, $params);
+
+        if (empty($availableusers)) {
+            return array();
+        }
+
+
+        if ($search) {
+            $groupname = get_string('currentusersmatching', 'cohort', $search);
+        } else {
+            $groupname = get_string('currentusers', 'cohort');
+        }
+
+        return array($groupname => $availableusers);
+    }
+
+    protected function get_options() {
+        $options = parent::get_options();
+        $options['cohortid'] = $this->cohortid;
+        $options['file'] = 'cohort/locallib.php';
+        return $options;
+    }
+}
+
diff --git a/cohort/tests/cohortlib_test.php b/cohort/tests/cohortlib_test.php
new file mode 100644 (file)
index 0000000..db0b2f0
--- /dev/null
@@ -0,0 +1,313 @@
+<?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/>.
+
+/**
+ * Cohort library tests.
+ *
+ * @package    core_cohort
+ * @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/cohort/lib.php");
+
+
+/**
+ * Cohort library tests.
+ *
+ * @package    core_cohort
+ * @category   phpunit
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class cohort_testcase extends advanced_testcase {
+
+    public function test_cohort_add_cohort() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $cohort = new stdClass();
+        $cohort->contextid = context_system::instance()->id;
+        $cohort->name = 'test cohort';
+        $cohort->idnumber = 'testid';
+        $cohort->description = 'test cohort desc';
+        $cohort->descriptionformat = FORMAT_HTML;
+
+        $id = cohort_add_cohort($cohort);
+        $this->assertNotEmpty($id);
+
+        $newcohort = $DB->get_record('cohort', array('id'=>$id));
+        $this->assertEquals($cohort->contextid, $newcohort->contextid);
+        $this->assertSame($cohort->name, $newcohort->name);
+        $this->assertSame($cohort->description, $newcohort->description);
+        $this->assertEquals($cohort->descriptionformat, $newcohort->descriptionformat);
+        $this->assertNotEmpty($newcohort->timecreated);
+        $this->assertSame($newcohort->component, '');
+        $this->assertSame($newcohort->timecreated, $newcohort->timemodified);
+
+        try {
+            $cohort = new stdClass();
+            $cohort->contextid = context_system::instance()->id;
+            $cohort->name = null;
+            $cohort->idnumber = 'testid';
+            $cohort->description = 'test cohort desc';
+            $cohort->descriptionformat = FORMAT_HTML;
+            cohort_add_cohort($cohort);
+
+            $this->fail('Exception expected when trying to add cohort without name');
+        } catch (Exception $e) {
+            $this->assertInstanceOf('coding_exception', $e);
+        }
+    }
+
+    public function test_cohort_update_cohort() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $cohort = new stdClass();
+        $cohort->contextid = context_system::instance()->id;
+        $cohort->name = 'test cohort';
+        $cohort->idnumber = 'testid';
+        $cohort->description = 'test cohort desc';
+        $cohort->descriptionformat = FORMAT_HTML;
+        $id = cohort_add_cohort($cohort);
+        $this->assertNotEmpty($id);
+        $DB->set_field('cohort', 'timecreated', $cohort->timecreated - 10, array('id'=>$id));
+        $DB->set_field('cohort', 'timemodified', $cohort->timemodified - 10, array('id'=>$id));
+        $cohort = $DB->get_record('cohort', array('id'=>$id));
+
+        $cohort->name = 'test cohort 2';
+        cohort_update_cohort($cohort);
+
+        $newcohort = $DB->get_record('cohort', array('id'=>$id));
+
+        $this->assertSame($cohort->contextid, $newcohort->contextid);
+        $this->assertSame($cohort->name, $newcohort->name);
+        $this->assertSame($cohort->description, $newcohort->description);
+        $this->assertSame($cohort->descriptionformat, $newcohort->descriptionformat);
+        $this->assertSame($cohort->timecreated, $newcohort->timecreated);
+        $this->assertSame($cohort->component, $newcohort->component);
+        $this->assertGreaterThan($newcohort->timecreated, $newcohort->timemodified);
+        $this->assertLessThanOrEqual(time(), $newcohort->timemodified);
+    }
+
+    public function test_cohort_delete_cohort() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+
+        cohort_delete_cohort($cohort);
+
+        $this->assertFalse($DB->record_exists('cohort', array('id'=>$cohort->id)));
+    }
+
+    public function test_cohort_delete_category() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $category = $this->getDataGenerator()->create_category();
+
+        $cohort = $this->getDataGenerator()->create_cohort(array('contextid'=>context_coursecat::instance($category->id)->id));
+
+        cohort_delete_category($category);
+
+        $this->assertTrue($DB->record_exists('cohort', array('id'=>$cohort->id)));
+        $newcohort = $DB->get_record('cohort', array('id'=>$cohort->id));
+        $this->assertEquals(context_system::instance()->id, $newcohort->contextid);
+    }
+
+    public function test_cohort_add_member() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->assertFalse($DB->record_exists('cohort_members', array('cohortid'=>$cohort->id, 'userid'=>$user->id)));
+        cohort_add_member($cohort->id, $user->id);
+        $this->assertTrue($DB->record_exists('cohort_members', array('cohortid'=>$cohort->id, 'userid'=>$user->id)));
+    }
+
+    public function test_cohort_remove_member() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+        $user = $this->getDataGenerator()->create_user();
+
+        cohort_add_member($cohort->id, $user->id);
+        $this->assertTrue($DB->record_exists('cohort_members', array('cohortid'=>$cohort->id, 'userid'=>$user->id)));
+
+        cohort_remove_member($cohort->id, $user->id);
+        $this->assertFalse($DB->record_exists('cohort_members', array('cohortid'=>$cohort->id, 'userid'=>$user->id)));
+    }
+
+    public function test_cohort_is_member() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $cohort = $this->getDataGenerator()->create_cohort();
+        $user = $this->getDataGenerator()->create_user();
+
+        $this->assertFalse(cohort_is_member($cohort->id, $user->id));
+        cohort_add_member($cohort->id, $user->id);
+        $this->assertTrue(cohort_is_member($cohort->id, $user->id));
+    }
+
+    public function test_cohort_get_visible_list() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $category1 = $this->getDataGenerator()->create_category();
+        $category2 = $this->getDataGenerator()->create_category();
+
+        $course1 = $this->getDataGenerator()->create_course(array('category'=>$category1->id));
+        $course2 = $this->getDataGenerator()->create_course(array('category'=>$category2->id));
+        $course3 = $this->getDataGenerator()->create_course();
+
+        $cohort1 = $this->getDataGenerator()->create_cohort(array('contextid'=>context_coursecat::instance($category1->id)->id));
+        $cohort2 = $this->getDataGenerator()->create_cohort(array('contextid'=>context_coursecat::instance($category2->id)->id));
+        $cohort3 = $this->getDataGenerator()->create_cohort(array('contextid'=>context_system::instance()->id));
+        $cohort4 = $this->getDataGenerator()->create_cohort(array('contextid'=>context_system::instance()->id));
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+        $user5 = $this->getDataGenerator()->create_user();
+
+        $manualenrol = enrol_get_plugin('manual');
+        $enrol1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'));
+        $enrol2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'));
+
+        $manualenrol->enrol_user($enrol1, $user1->id);
+        $manualenrol->enrol_user($enrol1, $user3->id);
+        $manualenrol->enrol_user($enrol1, $user4->id);
+        $manualenrol->enrol_user($enrol2, $user2->id);
+
+        cohort_add_member($cohort1->id, $user1->id);
+        cohort_add_member($cohort3->id, $user1->id);
+        cohort_add_member($cohort1->id, $user3->id);
+        cohort_add_member($cohort2->id, $user2->id);
+
+        $list = cohort_get_visible_list($course1);
+        $this->assertEquals(2, count($list));
+        $this->assertNotEmpty($list[$cohort1->id]);
+        $this->assertRegExp('/\(2\)$/', $list[$cohort1->id]);
+        $this->assertNotEmpty($list[$cohort3->id]);
+        $this->assertRegExp('/\(1\)$/', $list[$cohort3->id]);
+
+        $list = cohort_get_visible_list($course1, false);
+        $this->assertEquals(3, count($list));
+        $this->assertNotEmpty($list[$cohort1->id]);
+        $this->assertRegExp('/\(2\)$/', $list[$cohort1->id]);
+        $this->assertNotEmpty($list[$cohort3->id]);
+        $this->assertRegExp('/\(1\)$/', $list[$cohort3->id]);
+        $this->assertNotEmpty($list[$cohort4->id]);
+        $this->assertRegExp('/[^\)]$/', $list[$cohort4->id]);
+
+        $list = cohort_get_visible_list($course2);
+        $this->assertEquals(1, count($list));
+        $this->assertNotEmpty($list[$cohort2->id]);
+        $this->assertRegExp('/\(1\)$/', $list[$cohort2->id]);
+
+        $list = cohort_get_visible_list($course2, false);
+        $this->assertEquals(3, count($list));
+        $this->assertNotEmpty($list[$cohort2->id]);
+        $this->assertRegExp('/\(1\)$/', $list[$cohort2->id]);
+        $this->assertNotEmpty($list[$cohort3->id]);
+        $this->assertRegExp('/[^\)]$/', $list[$cohort3->id]);
+        $this->assertNotEmpty($list[$cohort4->id]);
+        $this->assertRegExp('/[^\)]$/', $list[$cohort4->id]);
+
+        $list = cohort_get_visible_list($course3);
+        $this->assertEquals(0, count($list));
+
+        $list = cohort_get_visible_list($course3, false);
+        $this->assertEquals(2, count($list));
+        $this->assertNotEmpty($list[$cohort3->id]);
+        $this->assertRegExp('/[^\)]$/', $list[$cohort3->id]);
+        $this->assertNotEmpty($list[$cohort4->id]);
+        $this->assertRegExp('/[^\)]$/', $list[$cohort4->id]);
+    }
+
+    public function test_cohort_get_cohorts() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $category1 = $this->getDataGenerator()->create_category();
+        $category2 = $this->getDataGenerator()->create_category();
+
+        $cohort1 = $this->getDataGenerator()->create_cohort(array('contextid'=>context_coursecat::instance($category1->id)->id, 'name'=>'aaagrrryyy', 'idnumber'=>'','description'=>''));
+        $cohort2 = $this->getDataGenerator()->create_cohort(array('contextid'=>context_coursecat::instance($category1->id)->id, 'name'=>'bbb', 'idnumber'=>'', 'description'=>'yyybrrr'));
+        $cohort3 = $this->getDataGenerator()->create_cohort(array('contextid'=>context_coursecat::instance($category1->id)->id, 'name'=>'ccc', 'idnumber'=>'xxarrrghyyy', 'description'=>'po_us'));
+        $cohort4 = $this->getDataGenerator()->create_cohort(array('contextid'=>context_system::instance()->id));
+
+        $result = cohort_get_cohorts(context_coursecat::instance($category2->id)->id);
+        $this->assertEquals(0, $result['totalcohorts']);
+        $this->assertEquals(0, count($result['cohorts']));
+        $this->assertEquals(0, $result['allcohorts']);
+
+        $result = cohort_get_cohorts(context_coursecat::instance($category1->id)->id);
+        $this->assertEquals(3, $result['totalcohorts']);
+        $this->assertEquals(array($cohort1->id=>$cohort1, $cohort2->id=>$cohort2, $cohort3->id=>$cohort3), $result['cohorts']);
+        $this->assertEquals(3, $result['allcohorts']);
+
+        $result = cohort_get_cohorts(context_coursecat::instance($category1->id)->id, 0, 100, 'arrrgh');
+        $this->assertEquals(1, $result['totalcohorts']);
+        $this->assertEquals(array($cohort3->id=>$cohort3), $result['cohorts']);
+        $this->assertEquals(3, $result['allcohorts']);
+
+        $result = cohort_get_cohorts(context_coursecat::instance($category1->id)->id, 0, 100, 'brrr');
+        $this->assertEquals(1, $result['totalcohorts']);
+        $this->assertEquals(array($cohort2->id=>$cohort2), $result['cohorts']);
+        $this->assertEquals(3, $result['allcohorts']);
+
+        $result = cohort_get_cohorts(context_coursecat::instance($category1->id)->id, 0, 100, 'grrr');
+        $this->assertEquals(1, $result['totalcohorts']);
+        $this->assertEquals(array($cohort1->id=>$cohort1), $result['cohorts']);
+        $this->assertEquals(3, $result['allcohorts']);
+
+        $result = cohort_get_cohorts(context_coursecat::instance($category1->id)->id, 1, 1, 'yyy');
+        $this->assertEquals(3, $result['totalcohorts']);
+        $this->assertEquals(array($cohort2->id=>$cohort2), $result['cohorts']);
+        $this->assertEquals(3, $result['allcohorts']);
+
+        $result = cohort_get_cohorts(context_coursecat::instance($category1->id)->id, 0, 100, 'po_us');
+        $this->assertEquals(1, $result['totalcohorts']);
+        $this->assertEquals(array($cohort3->id=>$cohort3), $result['cohorts']);
+        $this->assertEquals(3, $result['allcohorts']);
+
+        $result = cohort_get_cohorts(context_coursecat::instance($category1->id)->id, 0, 100, 'pokus');
+        $this->assertEquals(0, $result['totalcohorts']);
+        $this->assertEquals(array(), $result['cohorts']);
+        $this->assertEquals(3, $result['allcohorts']);
+    }
+}
index a4c6f6d..d376772 100644 (file)
@@ -101,6 +101,7 @@ class courserequest_testcase extends advanced_testcase {
         // Test without category.
         $cr = course_request::create($data);
         $id = $cr->approve();
+        $this->assertDebuggingCalled(); // Caused by sending of message.
         $course = $DB->get_record('course', array('id' => $id));
         $this->assertEquals($data->fullname, $course->fullname);
         $this->assertEquals($data->shortname, $course->shortname);
@@ -116,6 +117,7 @@ class courserequest_testcase extends advanced_testcase {
         $data->category = $cat1->id;
         $cr = course_request::create($data);
         $id = $cr->approve();
+        $this->assertDebuggingCalled(); // Caused by sending of message.
         $course = $DB->get_record('course', array('id' => $id));
         $this->assertEquals($data->category, $course->category);
     }
@@ -139,6 +141,7 @@ class courserequest_testcase extends advanced_testcase {
         $cr = course_request::create($data);
         $this->assertTrue($DB->record_exists('course_request', array('id' => $cr->id)));
         $cr->reject('Sorry!');
+        $this->assertDebuggingCalled(); // Caused by sending of message.
         $this->assertFalse($DB->record_exists('course_request', array('id' => $cr->id)));
     }
 
index 9509c01..ae247a0 100644 (file)
@@ -2,6 +2,7 @@
 .qce-panel .yui3-widget-hd {background:url("sprite.png");background-repeat:repeat-x;background-color:#DDD;background-position: 0 -15px;border-bottom:1px solid #555;border-top:1px solid #fff;}
 .qce-panel .yui3-widget-hd h2 {margin:3px 5px 2px;padding:0;font-size:110%;}
 .qce-panel .yui3-widget-hd .close {width:25px;height:15px;position:absolute;top:3px;right:1em;cursor:pointer;background:url("sprite.png") no-repeat scroll 0 0 transparent;}
+.dir-rtl .qce-panel .yui3-widget-hd .close {right:auto;left:1em;}
 .qce-panel .yui3-overlay-content {background-color:#F6F6F6;border:1px solid #555;margin-top:-2px;margin-left:-2px;}
 .qce-panel .qce-enrollable-cohorts {margin:5px;}
 .qce-panel .qce-cohorts {border:1px solid #666;min-width:408px;background-color:#FFF;height:375px;overflow:auto;}
index e53600b..79de6be 100644 (file)
@@ -368,6 +368,7 @@ class enrol_database_plugin extends enrol_plugin {
                 continue;
             }
             $existing[$course->mapping] = $course;
+            unset($externalcourses[$course->mapping]);
         }
         $rs->close();
 
@@ -388,18 +389,22 @@ class enrol_database_plugin extends enrol_plugin {
                 continue;
             }
             if (!isset($externalcourses[$course->mapping])) {
-                // course not synced
-                continue;
-            }
-            if (isset($existing[$course->mapping])) {
-                // some duplicate, sorry
+                // Course not synced or duplicate.
                 continue;
             }
             $course->enrolid = $this->add_instance($course);
             $existing[$course->mapping] = $course;
+            unset($externalcourses[$course->mapping]);
         }
         $rs->close();
 
+        // Print list of missing courses.
+        if ($verbose and $externalcourses) {
+            $list = implode(', ', array_keys($externalcourses));
+            mtrace("  error: following courses do not exist - $list");
+            unset($list);
+        }
+
         // free memory
         unset($externalcourses);
 
@@ -452,13 +457,18 @@ class enrol_database_plugin extends enrol_plugin {
                     while ($fields = $rs->FetchRow()) {
                         $fields = array_change_key_case($fields, CASE_LOWER);
                         if (empty($fields[$userfield])) {
-                            //user identification is mandatory!
+                            if ($verbose) {
+                                mtrace("  error: skipping user without mandatory $localuserfield in course '$course->mapping'");
+                            }
+                            continue;
                         }
                         $mapping = $fields[$userfield];
                         if (!isset($user_mapping[$mapping])) {
                             $usersearch[$localuserfield] = $mapping;
                             if (!$user = $DB->get_record('user', $usersearch, 'id', IGNORE_MULTIPLE)) {
-                                // user does not exist or was deleted
+                                if ($verbose) {
+                                    mtrace("  error: skipping unknown user $localuserfield '$mapping' in course '$course->mapping'");
+                                }
                                 continue;
                             }
                             $user_mapping[$mapping] = $user->id;
@@ -468,7 +478,9 @@ class enrol_database_plugin extends enrol_plugin {
                         }
                         if (empty($fields[$rolefield]) or !isset($roles[$fields[$rolefield]])) {
                             if (!$defaultrole) {
-                                // role is mandatory
+                                if ($verbose) {
+                                    mtrace("  error: skipping user '$userid' in course '$course->mapping' - missing course and default role");
+                                }
                                 continue;
                             }
                             $roleid = $defaultrole;
diff --git a/enrol/manual/cli/sync.php b/enrol/manual/cli/sync.php
new file mode 100644 (file)
index 0000000..c18488e
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * CLI update for manual enrolments expiration.
+ *
+ * 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_manual
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(dirname(dirname(dirname(dirname(__FILE__)))).'/config.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 manual enrolments expiration sync.
+
+Options:
+-v, --verbose         Print verbose progress information
+-h, --help            Print out this help
+
+Example:
+\$sudo -u www-data /usr/bin/php enrol/self/manual/sync.php
+";
+
+    echo $help;
+    die;
+}
+
+$verbose = !empty($options['verbose']);
+
+$plugin = enrol_get_plugin('manual');
+
+$result = $plugin->sync(null, $verbose);
+
+exit($result);
index 9e51fd1..7abb510 100644 (file)
@@ -37,6 +37,8 @@ $string['editenrolment'] = 'Edit enrolment';
 $string['editselectedusers'] = 'Edit selected user enrolments';
 $string['enrolledincourserole'] = 'Enrolled in "{$a->course}" as "{$a->role}"';
 $string['enrolusers'] = 'Enrol users';
+$string['expiredaction'] = 'Enrolment expiration action';
+$string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.';
 $string['manual:config'] = 'Configure manual enrol instances';
 $string['manual:enrol'] = 'Enrol users';
 $string['manual:manage'] = 'Manage user enrolments';
index fb648e0..d55c68c 100644 (file)
@@ -257,6 +257,106 @@ class enrol_manual_plugin extends enrol_plugin {
         return $button;
     }
 
+    /**
+     * Enrol cron support.
+     * @return void
+     */
+    public function cron() {
+        $this->sync(null, true);
+    }
+
+    /**
+     * Sync all meta course links.
+     *
+     * @param int $courseid one course, empty mean all
+     * @param bool $verbose verbose CLI output
+     * @return int 0 means ok, 1 means error, 2 means plugin disabled
+     */
+    public function sync($courseid = null, $verbose = false) {
+        global $DB;
+
+        if (!enrol_is_enabled('manual')) {
+            return 2;
+        }
+
+        // Unfortunately this may take a long time, execution can be interrupted safely here.
+        @set_time_limit(0);
+        raise_memory_limit(MEMORY_HUGE);
+
+        if ($verbose) {
+            mtrace('Verifying manual enrolment expiration...');
+        }
+
+        $params = array('now'=>time(), 'useractive'=>ENROL_USER_ACTIVE, 'courselevel'=>CONTEXT_COURSE);
+        $coursesql = "";
+        if ($courseid) {
+            $coursesql = "AND e.courseid = :courseid";
+            $params['courseid'] = $courseid;
+        }
+
+        // Deal with expired accounts.
+        $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
+
+        if ($action == ENROL_EXT_REMOVED_UNENROL) {
+            $instances = array();
+            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
+                      FROM {user_enrolments} ue
+                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'manual')
+                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
+                     WHERE ue.timeend > 0 AND ue.timeend < :now
+                           $coursesql";
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $ue) {
+                if (empty($instances[$ue->enrolid])) {
+                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
+                }
+                $instance = $instances[$ue->enrolid];
+                // Always remove all manually assigned roles here, this may break enrol_self roles but we do not want hardcoded hacks here.
+                role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
+                $this->unenrol_user($instance, $ue->userid);
+                if ($verbose) {
+                    mtrace("  unenrolling expired user $ue->userid from course $instance->courseid");
+                }
+            }
+            $rs->close();
+            unset($instances);
+
+        } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+            $instances = array();
+            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
+                      FROM {user_enrolments} ue
+                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'manual')
+                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
+                     WHERE ue.timeend > 0 AND ue.timeend < :now
+                           AND ue.status = :useractive
+                           $coursesql";
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $ue) {
+                if (empty($instances[$ue->enrolid])) {
+                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
+                }
+                $instance = $instances[$ue->enrolid];
+                // Always remove all manually assigned roles here, this may break enrol_self roles but we do not want hardcoded hacks here.
+                role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
+                $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
+                if ($verbose) {
+                    mtrace("  suspending expired user $ue->userid in course $instance->courseid");
+                }
+            }
+            $rs->close();
+            unset($instances);
+
+        } else {
+            // ENROL_EXT_REMOVED_KEEP means no changes.
+        }
+
+        if ($verbose) {
+            mtrace('...manual enrolment updates finished.');
+        }
+
+        return 0;
+    }
+
     /**
      * Gets an array of the user enrolment actions.
      *
index 79686aa..f793485 100644 (file)
@@ -29,6 +29,15 @@ if ($ADMIN->fulltree) {
     //--- general settings -----------------------------------------------------------------------------------
     $settings->add(new admin_setting_heading('enrol_manual_settings', '', get_string('pluginname_desc', 'enrol_manual')));
 
+    // Note: let's reuse the ext sync constants and strings here, internally it is very similar,
+    //       it describes what should happend when users are not supposed to be enerolled any more.
+    $options = array(
+        ENROL_EXT_REMOVED_KEEP           => get_string('extremovedkeep', 'enrol'),
+        ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'enrol'),
+        ENROL_EXT_REMOVED_UNENROL        => get_string('extremovedunenrol', 'enrol'),
+    );
+    $settings->add(new admin_setting_configselect('enrol_manual/expiredaction', get_string('expiredaction', 'enrol_manual'), get_string('expiredaction_help', 'enrol_manual'), ENROL_EXT_REMOVED_KEEP, $options));
+
 
     //--- enrol instance defaults ----------------------------------------------------------------------------
     $settings->add(new admin_setting_heading('enrol_manual_defaults',
index 745bac0..6d712f7 100644 (file)
@@ -201,4 +201,104 @@ class enrol_manual_lib_testcase extends advanced_testcase {
         enrol_manual_migrate_plugin_enrolments('manual');
         enrol_manual_migrate_plugin_enrolments('yyyy');
     }
+
+    public function test_expired() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $manualplugin = enrol_get_plugin('manual');
+
+        $now = time();
+
+        // Prepare some data.
+
+        $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);
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+        $context1 = context_course::instance($course1->id);
+        $context2 = context_course::instance($course2->id);
+        $context3 = context_course::instance($course3->id);
+
+        $this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'manual')));
+        $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $this->assertEquals($studentrole->id, $instance1->roleid);
+        $instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $this->assertEquals($studentrole->id, $instance2->roleid);
+        $instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $this->assertEquals($studentrole->id, $instance3->roleid);
+
+        $this->assertEquals(0, $DB->count_records('user_enrolments'));
+        $this->assertEquals(0, $DB->count_records('role_assignments'));
+
+        $manualplugin->enrol_user($instance1, $user1->id, $studentrole->id);
+        $manualplugin->enrol_user($instance1, $user2->id, $studentrole->id);
+        $manualplugin->enrol_user($instance1, $user3->id, $studentrole->id, 0, $now-60);
+
+        $manualplugin->enrol_user($instance3, $user1->id, $studentrole->id, 0, 0);
+        $manualplugin->enrol_user($instance3, $user2->id, $studentrole->id, 0, $now+60*60);
+        $manualplugin->enrol_user($instance3, $user3->id, $teacherrole->id, 0, $now-60*60);
+
+        role_assign($managerrole->id, $user4->id, $context1->id);
+
+        $this->assertEquals(6, $DB->count_records('user_enrolments'));
+        $this->assertEquals(7, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$managerrole->id)));
+
+        // Execute tests.
+
+        $this->assertEquals(ENROL_EXT_REMOVED_KEEP, $manualplugin->get_config('expiredaction'));
+        $manualplugin->sync(null, false);
+        $this->assertEquals(6, $DB->count_records('user_enrolments'));
+        $this->assertEquals(7, $DB->count_records('role_assignments'));
+
+
+        $manualplugin->set_config('expiredaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
+        $manualplugin->sync($course2->id, false);
+        $this->assertEquals(6, $DB->count_records('user_enrolments'));
+        $this->assertEquals(7, $DB->count_records('role_assignments'));
+
+        $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context1->id, 'userid'=>$user3->id, 'roleid'=>$studentrole->id)));
+        $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user3->id, 'roleid'=>$teacherrole->id)));
+        $manualplugin->sync(null, false);
+        $this->assertEquals(6, $DB->count_records('user_enrolments'));
+        $this->assertEquals(5, $DB->count_records('role_assignments'));
+        $this->assertEquals(4, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(0, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+        $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context1->id, 'userid'=>$user3->id, 'roleid'=>$studentrole->id)));
+        $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user3->id, 'roleid'=>$teacherrole->id)));
+
+
+        $manualplugin->set_config('expiredaction', ENROL_EXT_REMOVED_UNENROL);
+
+        role_assign($studentrole->id, $user3->id, $context1->id);
+        role_assign($teacherrole->id, $user3->id, $context3->id);
+        $this->assertEquals(6, $DB->count_records('user_enrolments'));
+        $this->assertEquals(7, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$managerrole->id)));
+
+        $manualplugin->sync(null, false);
+        $this->assertEquals(4, $DB->count_records('user_enrolments'));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user3->id)));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user3->id)));
+        $this->assertEquals(5, $DB->count_records('role_assignments'));
+        $this->assertEquals(4, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(0, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$managerrole->id)));
+    }
 }
index 2107a67..a878cb2 100644 (file)
@@ -24,6 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012061700;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2012061700;        // Requires this Moodle version
+$plugin->version   = 2012091500;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2012091400;        // Requires this Moodle version
 $plugin->component = 'enrol_manual';    // Full name of the plugin (used for diagnostics)
+$plugin->cron      = 600;
diff --git a/enrol/self/cli/sync.php b/enrol/self/cli/sync.php
new file mode 100644 (file)
index 0000000..aa1ea5c
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * CLI update for self enrolments, use for debugging or immediate update
+ * of all courses.
+ *
+ * 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_self
+ * @copyright  2012 Petr Skoda {@link http://skodak.org}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(dirname(dirname(dirname(dirname(__FILE__)))).'/config.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 self course enrol updates.
+
+Options:
+-v, --verbose         Print verbose progress information
+-h, --help            Print out this help
+
+Example:
+\$sudo -u www-data /usr/bin/php enrol/self/cli/sync.php
+";
+
+    echo $help;
+    die;
+}
+
+$verbose = !empty($options['verbose']);
+
+$plugin = enrol_get_plugin('self');
+
+$result = $plugin->sync(null, $verbose);
+
+exit($result);
index 06ba0e4..5492c5d 100644 (file)
@@ -44,6 +44,8 @@ $string['enrolperiod_desc'] = 'Default length of time that the enrolment is vali
 $string['enrolperiod_help'] = 'Length of time that the enrolment is valid, starting with the moment the user enrols themselves. If disabled, the enrolment duration will be unlimited.';
 $string['enrolstartdate'] = 'Start date';
 $string['enrolstartdate_help'] = 'If enabled, users can enrol themselves from this date onward only.';
+$string['expiredaction'] = 'Enrolment expiration action';
+$string['expiredaction_help'] = 'Select action to carry out when user enrolment expires. Please note that some user data and settings are purged from course during course unenrolment.';
 $string['groupkey'] = 'Use group enrolment keys';
 $string['groupkey_desc'] = 'Use group enrolment keys by default.';
 $string['groupkey_help'] = 'In addition to restricting access to the course to only those who know the key, use of a group enrolment key means users are automatically added to the group when they enrol in the course.
index 8c459f9..e4ac835 100644 (file)
@@ -334,31 +334,57 @@ class enrol_self_plugin extends enrol_plugin {
      * @return void
      */
     public function cron() {
+        $this->sync(null, true);
+    }
+
+    /**
+     * Sync all meta course links.
+     *
+     * @param int $courseid one course, empty mean all
+     * @param bool $verbose verbose CLI output
+     * @return int 0 means ok, 1 means error, 2 means plugin disabled
+     */
+    public function sync($courseid = null, $verbose = false) {
         global $DB;
 
         if (!enrol_is_enabled('self')) {
-            return;
+            return 2;
         }
 
-        $plugin = enrol_get_plugin('self');
+        // Unfortunately this may take a long time, execution can be interrupted safely here.
+        @set_time_limit(0);
+        raise_memory_limit(MEMORY_HUGE);
+
+        if ($verbose) {
+            mtrace('Verifying self-enrolments...');
+        }
 
-        $now = time();
+        $params = array('now'=>time(), 'useractive'=>ENROL_USER_ACTIVE, 'courselevel'=>CONTEXT_COURSE);
+        $coursesql = "";
+        if ($courseid) {
+            $coursesql = "AND e.courseid = :courseid";
+            $params['courseid'] = $courseid;
+        }
 
         // Note: the logic of self enrolment guarantees that user logged in at least once (=== u.lastaccess set)
         //       and that user accessed course at least once too (=== user_lastaccess record exists).
 
-        // First deal with users that did not log in for a really long time.
+        // First deal with users that did not log in for a really long time - they do not have user_lastaccess records.
         $sql = "SELECT e.*, ue.userid
                   FROM {user_enrolments} ue
                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self' AND e.customint2 > 0)
                   JOIN {user} u ON u.id = ue.userid
-                 WHERE :now - u.lastaccess > e.customint2";
-        $rs = $DB->get_recordset_sql($sql, array('now'=>$now));
+                 WHERE :now - u.lastaccess > e.customint2
+                       $coursesql";
+        $rs = $DB->get_recordset_sql($sql, $params);
         foreach ($rs as $instance) {
             $userid = $instance->userid;
             unset($instance->userid);
-            $plugin->unenrol_user($instance, $userid);
-            mtrace("unenrolling user $userid from course $instance->courseid as they have did not log in for $instance->customint2 days");
+            $this->unenrol_user($instance, $userid);
+            if ($verbose) {
+                $days = $instance->customint2 / 60*60*24;
+                mtrace("  unenrolling user $userid from course $instance->courseid as they have did not log in for at least $days days");
+            }
         }
         $rs->close();
 
@@ -367,17 +393,85 @@ class enrol_self_plugin extends enrol_plugin {
                   FROM {user_enrolments} ue
                   JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self' AND e.customint2 > 0)
                   JOIN {user_lastaccess} ul ON (ul.userid = ue.userid AND ul.courseid = e.courseid)
-                 WHERE :now - ul.timeaccess > e.customint2";
-        $rs = $DB->get_recordset_sql($sql, array('now'=>$now));
+                 WHERE :now - ul.timeaccess > e.customint2
+                       $coursesql";
+        $rs = $DB->get_recordset_sql($sql, $params);
         foreach ($rs as $instance) {
             $userid = $instance->userid;
             unset($instance->userid);
-            $plugin->unenrol_user($instance, $userid);
-            mtrace("unenrolling user $userid from course $instance->courseid as they have did not access course for $instance->customint2 days");
+            $this->unenrol_user($instance, $userid);
+            if ($verbose) {
+                $days = $instance->customint2 / 60*60*24;
+                mtrace("  unenrolling user $userid from course $instance->courseid as they have did not access course for at least $days days");
+            }
         }
         $rs->close();
 
-        flush();
+        // Deal with expired accounts.
+        $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP);
+
+        if ($action == ENROL_EXT_REMOVED_UNENROL) {
+            $instances = array();
+            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
+                      FROM {user_enrolments} ue
+                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self')
+                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
+                     WHERE ue.timeend > 0 AND ue.timeend < :now
+                           $coursesql";
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $ue) {
+                if (empty($instances[$ue->enrolid])) {
+                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
+                }
+                $instance = $instances[$ue->enrolid];
+                if ($instance->roleid) {
+                    role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
+                }
+                $this->unenrol_user($instance, $ue->userid);
+                if ($verbose) {
+                    mtrace("  unenrolling expired user $ue->userid from course $instance->courseid");
+                }
+            }
+            $rs->close();
+            unset($instances);
+
+        } else if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+            $instances = array();
+            $sql = "SELECT ue.*, e.courseid, c.id AS contextid
+                      FROM {user_enrolments} ue
+                      JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self')
+                      JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)
+                     WHERE ue.timeend > 0 AND ue.timeend < :now
+                           AND ue.status = :useractive
+                           $coursesql";
+            $rs = $DB->get_recordset_sql($sql, $params);
+            foreach ($rs as $ue) {
+                if (empty($instances[$ue->enrolid])) {
+                    $instances[$ue->enrolid] = $DB->get_record('enrol', array('id'=>$ue->enrolid));
+                }
+                $instance = $instances[$ue->enrolid];
+                if (1 == $DB->count_records('role_assignments', array('userid'=>$ue->userid, 'contextid'=>$ue->contextid))) {
+                    role_unassign_all(array('userid'=>$ue->userid, 'contextid'=>$ue->contextid, 'component'=>'', 'itemid'=>0), true);
+                } else if ($instance->roleid) {
+                    role_unassign($instance->roleid, $ue->userid, $ue->contextid, '', 0);
+                }
+                $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED);
+                if ($verbose) {
+                    mtrace("  suspending expired user $ue->userid in course $instance->courseid");
+                }
+            }
+            $rs->close();
+            unset($instances);
+
+        } else {
+            // ENROL_EXT_REMOVED_KEEP means no changes.
+        }
+
+        if ($verbose) {
+            mtrace('...user self-enrolment updates finished.');
+        }
+
+        return 0;
     }
 
      /**
index c8c2f02..25d561e 100644 (file)
@@ -38,6 +38,15 @@ if ($ADMIN->fulltree) {
     $settings->add(new admin_setting_configcheckbox('enrol_self/showhint',
         get_string('showhint', 'enrol_self'), get_string('showhint_desc', 'enrol_self'), 0));
 
+    // Note: let's reuse the ext sync constants and strings here, internally it is very similar,
+    //       it describes what should happend when users are not supposed to be enerolled any more.
+    $options = array(
+        ENROL_EXT_REMOVED_KEEP           => get_string('extremovedkeep', 'enrol'),
+        ENROL_EXT_REMOVED_SUSPENDNOROLES => get_string('extremovedsuspendnoroles', 'enrol'),
+        ENROL_EXT_REMOVED_UNENROL        => get_string('extremovedunenrol', 'enrol'),
+    );
+    $settings->add(new admin_setting_configselect('enrol_self/expiredaction', get_string('expiredaction', 'enrol_self'), get_string('expiredaction_help', 'enrol_self'), ENROL_EXT_REMOVED_KEEP, $options));
+
     //--- enrol instance defaults ----------------------------------------------------------------------------
     $settings->add(new admin_setting_heading('enrol_self_defaults',
         get_string('enrolinstancedefaults', 'admin'), get_string('enrolinstancedefaults_desc', 'admin')));
diff --git a/enrol/self/tests/self_test.php b/enrol/self/tests/self_test.php
new file mode 100644 (file)
index 0000000..f84f8c5
--- /dev/null
@@ -0,0 +1,264 @@
+<?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/>.
+
+/**
+ * Self enrolment plugin tests.
+ *
+ * @package    enrol_self
+ * @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/self/lib.php');
+require_once($CFG->dirroot.'/enrol/self/locallib.php');
+
+class enrol_self_testcase extends advanced_testcase {
+
+    public function test_basics() {
+        $this->assertTrue(enrol_is_enabled('self'));
+        $plugin = enrol_get_plugin('self');
+        $this->assertInstanceOf('enrol_self_plugin', $plugin);
+        $this->assertEquals(1, get_config('enrol_self', 'defaultenrol'));
+        $this->assertEquals(ENROL_EXT_REMOVED_KEEP, get_config('enrol_self', 'expiredaction'));
+    }
+
+    public function test_sync_nothing() {
+        global $SITE;
+
+        $selfplugin = enrol_get_plugin('self');
+
+        // Just make sure the sync does not throw any errors when nothing to do.
+        $selfplugin->sync(NULL, false);
+        $selfplugin->sync($SITE->id, false);
+    }
+
+    public function test_longtimnosee() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $selfplugin = enrol_get_plugin('self');
+        $manualplugin = enrol_get_plugin('manual');
+        $this->assertNotEmpty($manualplugin);
+
+        $now = time();
+
+        // Prepare some data.
+
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+        $teacherrole = $DB->get_record('role', array('shortname'=>'teacher'));
+        $this->assertNotEmpty($teacherrole);
+
+        $record = array('firstaccess'=>$now-60*60*24*800);
+        $record['lastaccess'] = $now-60*60*24*100;
+        $user1 = $this->getDataGenerator()->create_user($record);
+        $record['lastaccess'] = $now-60*60*24*10;
+        $user2 = $this->getDataGenerator()->create_user($record);
+        $record['lastaccess'] = $now-60*60*24*1;
+        $user3 = $this->getDataGenerator()->create_user($record);
+        $record['lastaccess'] = $now-10;
+        $user4 = $this->getDataGenerator()->create_user($record);
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+        $context1 = context_course::instance($course1->id);
+        $context2 = context_course::instance($course2->id);
+        $context3 = context_course::instance($course3->id);
+
+        $this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'self')));
+        $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'self'), '*', MUST_EXIST);
+        $instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'self'), '*', MUST_EXIST);
+        $instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'self'), '*', MUST_EXIST);
+        $id = $selfplugin->add_instance($course3, array('status'=>ENROL_INSTANCE_ENABLED, 'roleid'=>$teacherrole->id));
+        $instance3b = $DB->get_record('enrol', array('id'=>$id), '*', MUST_EXIST);
+        unset($id);
+
+        $this->assertEquals($studentrole->id, $instance1->roleid);
+        $instance1->customint2 = 60*60*24*14;
+        $DB->update_record('enrol', $instance1);
+        $selfplugin->enrol_user($instance1, $user1->id, $studentrole->id);
+        $selfplugin->enrol_user($instance1, $user2->id, $studentrole->id);
+        $selfplugin->enrol_user($instance1, $user3->id, $studentrole->id);
+        $this->assertEquals(3, $DB->count_records('user_enrolments'));
+        $DB->insert_record('user_lastaccess', array('userid'=>$user2->id, 'courseid'=>$course1->id, 'timeaccess'=>$now-60*60*24*20));
+        $DB->insert_record('user_lastaccess', array('userid'=>$user3->id, 'courseid'=>$course1->id, 'timeaccess'=>$now-60*60*24*2));
+        $DB->insert_record('user_lastaccess', array('userid'=>$user4->id, 'courseid'=>$course1->id, 'timeaccess'=>$now-60));
+
+        $this->assertEquals($studentrole->id, $instance3->roleid);
+        $instance3->customint2 = 60*60*24*50;
+        $DB->update_record('enrol', $instance3);
+        $selfplugin->enrol_user($instance3, $user1->id, $studentrole->id);
+        $selfplugin->enrol_user($instance3, $user2->id, $studentrole->id);
+        $selfplugin->enrol_user($instance3, $user3->id, $studentrole->id);
+        $selfplugin->enrol_user($instance3b, $user1->id, $teacherrole->id);
+        $selfplugin->enrol_user($instance3b, $user4->id, $teacherrole->id);
+        $this->assertEquals(8, $DB->count_records('user_enrolments'));
+        $DB->insert_record('user_lastaccess', array('userid'=>$user2->id, 'courseid'=>$course3->id, 'timeaccess'=>$now-60*60*24*11));
+        $DB->insert_record('user_lastaccess', array('userid'=>$user3->id, 'courseid'=>$course3->id, 'timeaccess'=>$now-60*60*24*200));
+        $DB->insert_record('user_lastaccess', array('userid'=>$user4->id, 'courseid'=>$course3->id, 'timeaccess'=>$now-60*60*24*200));
+
+        $maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $maninstance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+
+        $manualplugin->enrol_user($maninstance2, $user1->id, $studentrole->id);
+        $manualplugin->enrol_user($maninstance3, $user1->id, $teacherrole->id);
+
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(7, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+
+        // Execute sync - this is the same thing used from cron.
+
+        $selfplugin->sync($course2->id, false);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user1->id)));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user2->id)));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user1->id)));
+        $this->assertTrue($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user3->id)));
+        $selfplugin->sync(null, false);
+        $this->assertEquals(6, $DB->count_records('user_enrolments'));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user1->id)));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user2->id)));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user1->id)));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user3->id)));
+
+        $this->assertEquals(6, $DB->count_records('role_assignments'));
+        $this->assertEquals(4, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+    }
+
+    public function test_expired() {
+        global $DB;
+        $this->resetAfterTest();
+
+        $selfplugin = enrol_get_plugin('self');
+        $manualplugin = enrol_get_plugin('manual');
+        $this->assertNotEmpty($manualplugin);
+
+        $now = time();
+
+        // Prepare some data.
+
+        $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);
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        $user4 = $this->getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course3 = $this->getDataGenerator()->create_course();
+        $context1 = context_course::instance($course1->id);
+        $context2 = context_course::instance($course2->id);
+        $context3 = context_course::instance($course3->id);
+
+        $this->assertEquals(3, $DB->count_records('enrol', array('enrol'=>'self')));
+        $instance1 = $DB->get_record('enrol', array('courseid'=>$course1->id, 'enrol'=>'self'), '*', MUST_EXIST);
+        $this->assertEquals($studentrole->id, $instance1->roleid);
+        $instance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'self'), '*', MUST_EXIST);
+        $this->assertEquals($studentrole->id, $instance2->roleid);
+        $instance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'self'), '*', MUST_EXIST);
+        $this->assertEquals($studentrole->id, $instance3->roleid);
+        $id = $selfplugin->add_instance($course3, array('status'=>ENROL_INSTANCE_ENABLED, 'roleid'=>$teacherrole->id));
+        $instance3b = $DB->get_record('enrol', array('id'=>$id), '*', MUST_EXIST);
+        $this->assertEquals($teacherrole->id, $instance3b->roleid);
+        unset($id);
+
+        $maninstance2 = $DB->get_record('enrol', array('courseid'=>$course2->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+        $maninstance3 = $DB->get_record('enrol', array('courseid'=>$course3->id, 'enrol'=>'manual'), '*', MUST_EXIST);
+
+        $manualplugin->enrol_user($maninstance2, $user1->id, $studentrole->id);
+        $manualplugin->enrol_user($maninstance3, $user1->id, $teacherrole->id);
+
+        $this->assertEquals(2, $DB->count_records('user_enrolments'));
+        $this->assertEquals(2, $DB->count_records('role_assignments'));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+
+        $selfplugin->enrol_user($instance1, $user1->id, $studentrole->id);
+        $selfplugin->enrol_user($instance1, $user2->id, $studentrole->id);
+        $selfplugin->enrol_user($instance1, $user3->id, $studentrole->id, 0, $now-60);
+
+        $selfplugin->enrol_user($instance3, $user1->id, $studentrole->id, 0, 0);
+        $selfplugin->enrol_user($instance3, $user2->id, $studentrole->id, 0, $now-60*60);
+        $selfplugin->enrol_user($instance3, $user3->id, $studentrole->id, 0, $now+60*60);
+        $selfplugin->enrol_user($instance3b, $user1->id, $teacherrole->id, $now-60*60*24*7, $now-60);
+        $selfplugin->enrol_user($instance3b, $user4->id, $teacherrole->id);
+
+        role_assign($managerrole->id, $user3->id, $context1->id);
+
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(7, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+
+        // Execute tests.
+
+        $this->assertEquals(ENROL_EXT_REMOVED_KEEP, $selfplugin->get_config('expiredaction'));
+        $selfplugin->sync(null, false);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+
+
+        $selfplugin->set_config('expiredaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
+        $selfplugin->sync($course2->id, false);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+
+        $selfplugin->sync(null, false);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(7, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+        $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context1->id, 'userid'=>$user3->id, 'roleid'=>$studentrole->id)));
+        $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user2->id, 'roleid'=>$studentrole->id)));
+        $this->assertFalse($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user1->id, 'roleid'=>$teacherrole->id)));
+        $this->assertTrue($DB->record_exists('role_assignments', array('contextid'=>$context3->id, 'userid'=>$user1->id, 'roleid'=>$studentrole->id)));
+
+
+        $selfplugin->set_config('expiredaction', ENROL_EXT_REMOVED_UNENROL);
+
+        role_assign($studentrole->id, $user3->id, $context1->id);
+        role_assign($studentrole->id, $user2->id, $context3->id);
+        role_assign($teacherrole->id, $user1->id, $context3->id);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(7, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+
+        $selfplugin->sync(null, false);
+        $this->assertEquals(7, $DB->count_records('user_enrolments'));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance1->id, 'userid'=>$user3->id)));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3->id, 'userid'=>$user2->id)));
+        $this->assertFalse($DB->record_exists('user_enrolments', array('enrolid'=>$instance3b->id, 'userid'=>$user1->id)));
+        $this->assertEquals(6, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('role_assignments', array('roleid'=>$studentrole->id)));
+        $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$teacherrole->id)));
+    }
+}
index 1153b05..e5bbf47 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2012082300;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2012082300;        // Requires this Moodle version
+$plugin->version   = 2012091500;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2012091400;        // Requires this Moodle version
 $plugin->component = 'enrol_self';      // Full name of the plugin (used for diagnostics)
-$plugin->cron      = 180;
\ No newline at end of file
+$plugin->cron      = 600;
index 9f62c29..aa0ecca 100644 (file)
@@ -49,6 +49,7 @@ $string['allowediplist'] = 'Allowed IP list';
 $string['allowemailaddresses'] = 'Allowed email domains';
 $string['allowframembedding'] = 'Allow frame embedding';
 $string['allowframembedding_help'] = 'Allow embedding of this site in frames on external sites. Enabling of this feature is not recommended for security reasons.';
+$string['allowguestmymoodle'] = 'Allow guest access to My Moodle';
 $string['allowobjectembed'] = 'Allow EMBED and OBJECT tags';
 $string['allowthemechangeonurl'] = 'Allow theme changes in the URL';
 $string['allowuserblockhiding'] = 'Allow users to hide blocks';
@@ -123,6 +124,7 @@ $string['configallowblockstodock'] = 'If enabled and supported by the selected t
 $string['configallowcategorythemes'] = 'If you enable this, then themes can be set at the category level. This will affect all child categories and courses unless they have specifically set their own theme. WARNING: Enabling category themes may affect performance.';
 $string['configallowcoursethemes'] = 'If you enable this, then courses will be allowed to set their own themes.  Course themes override all other theme choices (site, user, or session themes)';
 $string['configallowemailaddresses'] = 'If you want to restrict all new email addresses to particular domains, then list them here separated by spaces.  All other domains will be rejected.  To allow subdomains add the domain with a preceding \'.\'. eg <strong>ourcollege.edu.au .gov.au</strong>';
+$string['configallowguestmymoodle'] = 'If enabled guests can access My Moodle, otherwise guests are redirected to the site front page.';
 $string['configallowobjectembed'] = 'As a default security measure, normal users are not allowed to embed multimedia (like Flash) within texts using explicit EMBED and OBJECT tags in their HTML (although it can still be done safely using the mediaplugins filter).  If you wish to allow these tags then enable this option.';
 $string['configallowoverride'] = 'You can allow people with the roles on the left side to override some of the column roles';
 $string['configallowoverride2'] = 'Select which role(s) can be overridden by each role in the left column.<br />Note that these settings only apply to users who have either the capability moodle/role:override or the capability moodle/role:safeoverride allowed.';
index 5e69b26..96b6c4f 100644 (file)
@@ -38,11 +38,13 @@ $string['courseevent'] = 'Course event';
 $string['courseevents'] = 'Course events';
 $string['courses'] = 'Courses';
 $string['dayview'] = 'Day view';
+$string['dayviewtitle'] = 'Day view: {$a}';
 $string['daywithnoevents'] = 'There are no events this day.';
 $string['default'] = 'Default';
 $string['deleteevent'] = 'Delete event';
 $string['deleteevents'] = 'Delete events';
 $string['detailedmonthview'] = 'Detailed month view';
+$string['detailedmonthviewtitle'] = 'Detailed month view: {$a}';
 $string['durationminutes'] = 'Duration in minutes';
 $string['durationnone'] = 'Without duration';
 $string['durationuntil'] = 'Until';
index 246ddee..e9c133f 100644 (file)
@@ -1158,7 +1158,7 @@ class block_manager {
             $PAGE->set_title($blocktitle . ': ' . $strdeletecheck);
             $PAGE->set_heading($site->fullname);
             echo $OUTPUT->header();
-            $confirmurl = new moodle_url("$deletepage->url?", array('sesskey' => sesskey(), 'bui_deleteid' => $block->instance->id, 'bui_confirm' => 1));
+            $confirmurl = new moodle_url($deletepage->url, array('sesskey' => sesskey(), 'bui_deleteid' => $block->instance->id, 'bui_confirm' => 1));
             $cancelurl = new moodle_url($deletepage->url);
             $yesbutton = new single_button($confirmurl, get_string('yes'));
             $nobutton = new single_button($cancelurl, get_string('no'));
index dfe7f45..3484b04 100644 (file)
@@ -160,13 +160,12 @@ class ddl_testcase extends database_driver_testcase {
         // first make sure it returns false if table does not exist
         $table = $this->tables['test_table0'];
 
-        ob_start(); // hide debug warning
         try {
             $result = $DB->get_records('test_table0');
         } catch (dml_exception $e) {
             $result = false;
         }
-        ob_end_clean();
+        $this->resetDebugging();
 
         $this->assertFalse($result);
 
@@ -183,13 +182,12 @@ class ddl_testcase extends database_driver_testcase {
         // drop table and test again
         $dbman->drop_table($table);
 
-        ob_start(); // hide debug warning
         try {
             $result = $DB->get_records('test_table0');
         } catch (dml_exception $e) {
             $result = false;
         }
-        ob_end_clean();
+        $this->resetDebugging();
 
         $this->assertFalse($result);
 
@@ -1123,13 +1121,12 @@ class ddl_testcase extends database_driver_testcase {
         $record = new stdClass();
         $record->name = NULL;
 
-        ob_start(); // hide debug warning
         try {
             $result = $DB->insert_record('test_table_cust0', $record, false);
         } catch (dml_exception $e) {
             $result = false;
         }
-        ob_end_clean();
+        $this->resetDebugging();
         $this->assertFalse($result);
 
         $field = new xmldb_field('name');
@@ -1145,13 +1142,12 @@ class ddl_testcase extends database_driver_testcase {
         $field->set_attributes(XMLDB_TYPE_CHAR, '30', null, XMLDB_NOTNULL, null, null);
         $dbman->change_field_notnull($table, $field);
 
-        ob_start(); // hide debug warning
         try {
             $result = $DB->insert_record('test_table_cust0', $record, false);
         } catch (dml_exception $e) {
             $result = false;
         }
-        ob_end_clean();
+        $this->resetDebugging();
         $this->assertFalse($result);
 
         $dbman->drop_table($table);
@@ -1214,13 +1210,12 @@ class ddl_testcase extends database_driver_testcase {
         $index->set_attributes(XMLDB_INDEX_UNIQUE, array('onenumber', 'name'));
         $dbman->add_index($table, $index);
 
-        ob_start(); // hide debug warning
         try {
             $result = $DB->insert_record('test_table_cust0', $record, false);
         } catch (dml_exception $e) {
             $result = false;;
         }
-        ob_end_clean();
+        $this->resetDebugging();
         $this->assertFalse($result);
 
         $dbman->drop_table($table);
@@ -1398,12 +1393,10 @@ class ddl_testcase extends database_driver_testcase {
 
         // feed nonexistent file
         try {
-            ob_start(); // hide debug warning
             $dbman->delete_tables_from_xmldb_file('fpsoiudfposui');
-            ob_end_clean();
             $this->assertTrue(false);
         } catch (Exception $e) {
-            ob_end_clean();
+            $this->resetDebugging();
             $this->assertTrue($e instanceof moodle_exception);
         }
 
@@ -1414,12 +1407,10 @@ class ddl_testcase extends database_driver_testcase {
             $devhack = true;
         }
         try {
-            ob_start(); // hide debug warning
             $dbman->delete_tables_from_xmldb_file(__DIR__ . '/fixtures/invalid.xml');
             $this->assertTrue(false);
-            ob_end_clean();
         } catch (Exception $e) {
-            ob_end_clean();
+            $this->resetDebugging();
             $this->assertTrue($e instanceof moodle_exception);
         }
         if ($devhack) {
@@ -1443,12 +1434,10 @@ class ddl_testcase extends database_driver_testcase {
 
         // feed nonexistent file
         try {
-            ob_start(); // hide debug warning
             $dbman->install_from_xmldb_file('fpsoiudfposui');
-            ob_end_clean();
             $this->assertTrue(false);
         } catch (Exception $e) {
-            ob_end_clean();
+            $this->resetDebugging();
             $this->assertTrue($e instanceof moodle_exception);
         }
 
@@ -1459,12 +1448,10 @@ class ddl_testcase extends database_driver_testcase {
             $devhack = true;
         }
         try {
-            ob_start(); // hide debug warning
             $dbman->install_from_xmldb_file(__DIR__ . '/fixtures/invalid.xml');
-            ob_end_clean();
             $this->assertTrue(false);
         } catch (Exception $e) {
-            ob_end_clean();
+            $this->resetDebugging();
             $this->assertTrue($e instanceof moodle_exception);
         }
         if ($devhack) {
@@ -1555,10 +1542,9 @@ class ddl_testcase extends database_driver_testcase {
         $this->assertTrue($dbman->table_exists('test_table1'));
 
         // Make sure it can be dropped using deprecated drop_temp_table()
-        $CFG->debug = 0;
         $dbman->drop_temp_table($table1);
         $this->assertFalse($dbman->table_exists('test_table1'));
-        $CFG->debug = DEBUG_DEVELOPER;
+        $this->assertDebuggingCalled();
     }
 
     public function test_concurrent_temp_tables() {
index 4fa3ca4..ef53f08 100644 (file)
@@ -1101,6 +1101,20 @@ abstract class moodle_database {
      */
     public abstract function get_recordset_sql($sql, array $params=null, $limitfrom=0, $limitnum=0);
 
+    /**
+     * Get all records from a table.
+     *
+     * This method works around potential memory problems and may improve performance,
+     * this method may block access to table until the recordset is closed.
+     *
+     * @param string $table Name of database table.
+     * @return moodle_recordset A moodle_recordset instance {@link function get_recordset}.
+     * @throws dml_exception A DML specific exception is thrown for any errors.
+     */
+    public function export_table_recordset($table) {
+        return $this->get_recordset($table, array());
+    }
+
     /**
      * Get a number of records as an array of objects where all the given conditions met.
      *
index 38eb523..5a91e9c 100644 (file)
@@ -909,6 +909,27 @@ class mysqli_native_moodle_database extends moodle_database {
         return $this->create_recordset($result);
     }
 
+    /**
+     * Get all records from a table.
+     *
+     * This method works around potential memory problems and may improve performance,
+     * this method may block access to table until the recordset is closed.
+     *
+     * @param string $table Name of database table.
+     * @return moodle_recordset A moodle_recordset instance {@link function get_recordset}.
+     * @throws dml_exception A DML specific exception is thrown for any errors.
+     */
+    public function export_table_recordset($table) {
+        $sql = $this->fix_table_names("SELECT * FROM {{$table}}");
+
+        $this->query_start($sql, array(), SQL_QUERY_SELECT);
+        // MYSQLI_STORE_RESULT may eat all memory for large tables, unfortunately MYSQLI_USE_RESULT blocks other queries.
+        $result = $this->mysqli->query($sql, MYSQLI_USE_RESULT);
+        $this->query_end($result);
+
+        return $this->create_recordset($result);
+    }
+
     protected function create_recordset($result) {
         return new mysqli_native_moodle_recordset($result);
     }
index 4289cbb..11e53bc 100644 (file)
@@ -51,17 +51,6 @@ class dml_testcase extends database_driver_testcase {
         return new xmldb_table($tablename);
     }
 
-    protected function enable_debugging() {
-        ob_start(); // hide debug warning
-    }
-
-    protected function get_debugging() {
-        $debuginfo = ob_get_contents();
-        ob_end_clean();
-
-        return $debuginfo;
-    }
-
     function test_diagnose() {
         $DB = $this->tdb;
         $result = $DB->diagnose();
@@ -982,10 +971,7 @@ class dml_testcase extends database_driver_testcase {
         $conditions = array('onetext' => '1');
         try {
             $rs = $DB->get_recordset($tablename, $conditions);
-            if (debugging()) {
-                // only in debug mode - hopefully all devs test code in debug mode...
-                $this->fail('An Exception is missing, expected due to equating of text fields');
-            }
+            $this->fail('An Exception is missing, expected due to equating of text fields');
         } catch (exception $e) {
             $this->assertTrue($e instanceof dml_exception);
             $this->assertEquals($e->errorcode, 'textconditionsnotallowed');
@@ -1300,6 +1286,36 @@ class dml_testcase extends database_driver_testcase {
         // note: fetching nulls, empties, LOBs already tested by test_insert_record() no needed here
     }
 
+    public function test_export_table_recordset() {
+        $DB = $this->tdb;
+        $dbman = $DB->get_manager();
+
+        $table = $this->get_test_table();
+        $tablename = $table->getName();
+
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('course', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+        $dbman->create_table($table);
+
+        $ids = array();
+        $ids[] = $DB->insert_record($tablename, array('course' => 3));
+        $ids[] = $DB->insert_record($tablename, array('course' => 5));
+        $ids[] = $DB->insert_record($tablename, array('course' => 4));
+        $ids[] = $DB->insert_record($tablename, array('course' => 3));
+        $ids[] = $DB->insert_record($tablename, array('course' => 2));
+        $ids[] = $DB->insert_record($tablename, array('course' => 1));
+        $ids[] = $DB->insert_record($tablename, array('course' => 0));
+
+        $rs = $DB->export_table_recordset($tablename);
+        $rids = array();
+        foreach ($rs as $record) {
+            $rids[] = $record->id;
+        }
+        $rs->close();
+        $this->assertEquals($ids, $rids, '', 0, 0, true);
+    }
+
     public function test_get_records() {
         $DB = $this->tdb;
         $dbman = $DB->get_manager();
@@ -1439,6 +1455,8 @@ class dml_testcase extends database_driver_testcase {
     }
 
     public function test_get_records_sql() {
+        global $CFG;
+
         $DB = $this->tdb;
         $dbman = $DB->get_manager();
 
@@ -1477,10 +1495,14 @@ class dml_testcase extends database_driver_testcase {
         $this->assertEquals($inskey4, next($records)->id);
 
         // Awful test, requires debug enabled and sent to browser. Let's do that and restore after test
-        $this->enable_debugging();
         $records = $DB->get_records_sql("SELECT course AS id, course AS course FROM {{$tablename}}", null);
-        $this->assertFalse($this->get_debugging() === '');
+        $this->assertDebuggingCalled();
         $this->assertEquals(6, count($records));
+        $CFG->debug = DEBUG_MINIMAL;
+        $records = $DB->get_records_sql("SELECT course AS id, course AS course FROM {{$tablename}}", null);
+        $this->assertDebuggingNotCalled();
+        $this->assertEquals(6, count($records));
+        $CFG->debug = DEBUG_DEVELOPER;
 
         // negative limits = no limits
         $records = $DB->get_records_sql("SELECT * FROM {{$tablename}} ORDER BY id", null, -1, -1);
@@ -1692,6 +1714,8 @@ class dml_testcase extends database_driver_testcase {
     }
 
     public function test_get_record_sql() {
+        global $CFG;
+
         $DB = $this->tdb;
         $dbman = $DB->get_manager();
 
@@ -1728,9 +1752,12 @@ class dml_testcase extends database_driver_testcase {
             $this->assertTrue(true);
         }
 
-        $this->enable_debugging();
         $this->assertNotEmpty($DB->get_record_sql("SELECT * FROM {{$tablename}}", array(), IGNORE_MISSING));
-        $this->assertFalse($this->get_debugging() === '');
+        $this->assertDebuggingCalled();
+        $CFG->debug = DEBUG_MINIMAL;
+        $this->assertNotEmpty($DB->get_record_sql("SELECT * FROM {{$tablename}}", array(), IGNORE_MISSING));
+        $this->assertDebuggingNotCalled();
+        $CFG->debug = DEBUG_DEVELOPER;
 
         // multiple matches ignored
         $this->assertNotEmpty($DB->get_record_sql("SELECT * FROM {{$tablename}}", array(), IGNORE_MULTIPLE));
@@ -1772,13 +1799,11 @@ class dml_testcase extends database_driver_testcase {
             $this->assertTrue(true);
         }
 
-        $this->enable_debugging();
         $this->assertEquals(5, $DB->get_field($tablename, 'course', array('course' => 5), IGNORE_MULTIPLE));
-        $this->assertSame($this->get_debugging(), '');
+        $this->assertDebuggingNotCalled();
 
-        $this->enable_debugging();
         $this->assertEquals(5, $DB->get_field($tablename, 'course', array('course' => 5), IGNORE_MISSING));
-        $this->assertFalse($this->get_debugging() === '');
+        $this->assertDebuggingCalled();
 
         // test for exception throwing on text conditions being compared. (MDL-24863, unwanted auto conversion of param to int)
         $conditions = array('onetext' => '1');
index 7051c82..4d9b7bb 100644 (file)
@@ -144,7 +144,7 @@ abstract class database_exporter {
         $tables = $this->schema->getTables();
         $this->begin_database_export($CFG->version, $CFG->release, date('c'), $description);
         foreach ($tables as $table) {
-            $rs = $this->mdb->get_recordset_sql('SELECT * FROM {'.$table->getName().'}');
+            $rs = $this->mdb->export_table_recordset($table->getName());
             if (!$rs) {
                 throw new ddl_table_missing_exception($table->getName());
             }
@@ -153,6 +153,7 @@ abstract class database_exporter {
                 $this->export_table_data($table, $row);
             }
             $this->finish_table_export($table);
+            $rs->close();
         }
         $this->finish_database_export();
     }
index cef949a..401d781 100644 (file)
@@ -832,7 +832,7 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
             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);
+                $fs->update_references_to_storedfile($oldfile);
             }
 
             // unchanged file or directory - we keep it as is
index ae8df9c..917705b 100644 (file)
@@ -1821,7 +1821,7 @@ class file_storage {
      * @param stored_file $storedfile
      */
     public function update_references_to_storedfile(stored_file $storedfile) {
-        global $CFG;
+        global $CFG, $DB;
         $params = array();
         $params['contextid'] = $storedfile->get_contextid();
         $params['component'] = $storedfile->get_component();
index 3d1ab7f..ed0ff8e 100644 (file)
@@ -147,21 +147,17 @@ class zip_packer_testcase extends advanced_testcase {
 
         $this->assertFalse(file_exists(__DIR__.'/xx/yy/ee.txt'));
         $files = array('xtest.txt'=>__DIR__.'/xx/yy/ee.txt');
-        ob_start();
         $result = $packer->archive_to_pathname($files, $archive);
-        $d = ob_end_clean();
-        $this->assertTrue($d !== '');
         $this->assertFalse($result);
+        $this->assertDebuggingCalled();
 
         $this->assertTrue(file_exists(__DIR__.'/fixtures/test.txt'));
         $files = array();
         $files['""""'] = null; // Invalid directory name.
         $files['test.txt'] = __DIR__.'/fixtures/test.txt';
-        ob_start();
         $result = $packer->archive_to_pathname($files, $archive);
-        $d = ob_end_clean();
-        $this->assertTrue($d !== '');
         $this->assertTrue($result);
+        $this->resetDebugging();
 
         @unlink($archive);
     }
index 412ce2b..03bb5f0 100644 (file)
@@ -1301,13 +1301,15 @@ class MoodleQuickForm extends HTML_QuickForm_DHTMLRulesTableless {
         } else {
             $this->_pageparams = '';
         }
-        //no 'name' atttribute for form in xhtml strict :
-        $attributes = array('action'=>$action, 'method'=>$method,
-                'accept-charset'=>'utf-8', 'id'=>'mform'.$formcounter) + $target;
+        // No 'name' atttribute for form in xhtml strict :
+        $attributes = array('action' => $action, 'method' => $method, 'accept-charset' => 'utf-8') + $target;
+        if (is_null($this->getAttribute('id'))) {
+            $attributes['id'] = 'mform' . $formcounter;
+        }
         $formcounter++;
         $this->updateAttributes($attributes);
 
-        //this is custom stuff for Moodle :
+        // This is custom stuff for Moodle :
         $oldclass=   $this->getAttribute('class');
         if (!empty($oldclass)){
             $this->updateAttributes(array('class'=>$oldclass.' mform'));
index b8fc705..b23e184 100644 (file)
@@ -88,17 +88,17 @@ M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
 
     // Get the caption for the collapsible region
     var caption = this.div.one('#'+id + '_caption');
-    caption.setAttribute('title', strtooltip);
 
     // Create a link
     var a = Y.Node.create('<a href="#"></a>');
-    // Create a local scoped lamba function to move nodes to a new link
-    var movenode = function(node){
-        node.remove();
-        a.append(node);
-    };
-    // Apply the lamba function on each of the captions child nodes
-    caption.get('children').each(movenode, this);
+    a.setAttribute('title', strtooltip);
+
+    // Get all the nodes from caption, remove them and append them to <a>
+    while (caption.hasChildNodes()) {
+        child = caption.get('firstChild');
+        child.remove();
+        a.append(child);
+    }
     caption.append(a);
 
     // Get the height of the div at this point before we shrink it if required
@@ -341,7 +341,7 @@ M.util.init_maximised_embed = function(Y, id) {
 
         var headerheight = get_htmlelement_size('page-header', 'height');
         var footerheight = get_htmlelement_size('page-footer', 'height');
-        var newheight = parseInt(Y.YUI2.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
+        var newheight = parseInt(Y.one('body').get('winHeight')) - footerheight - headerheight - 100;
         if (newheight < 400) {
             newheight = 400;
         }
@@ -376,30 +376,49 @@ M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
             })();
             // Make sure we have the form
             if (form) {
+                var buttonflag = 0;
                 // Create a function to handle our change event
                 var processchange = function(e, paramobject) {
                     if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
-                        //prevent event bubbling and detach handlers to prevent multiple submissions caused by double clicking
-                        e.halt();
-                        paramobject.eventkeypress.detach();
-                        paramobject.eventblur.detach();
-                        paramobject.eventchangeorblur.detach();
-
-                        this.submit();
+                        // chrome doesn't pick up on a click when selecting an element in a select menu, so we use
+                        // the on change event to fire this function. This just checks to see if a button was
+                        // first pressed before redirecting to the appropriate page.
+                        if (Y.UA.os == 'windows' && Y.UA.chrome){
+                            if (buttonflag == 1) {
+                                buttonflag = 0;
+                                this.submit();
+                            }
+                        } else {
+                            this.submit();
+                        }
+                    }
+                    if (e.button == 1) {
+                        buttonflag = 1;
                     }
                 };
-                // Attach the change event to the keypress, blur, and click actions.
-                // We don't use the change event because IE fires it on every arrow up/down
-                // event.... usability
+
+                var changedown = function(e, paramobject) {
+                    if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
+                        if(e.keyCode == 13) {
+                            form.submit();
+                        }
+                    }
+                }
+
                 var paramobject = new Object();
                 paramobject.lastindex = select.get('selectedIndex');
-                paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
-                paramobject.eventblur = select.on('blur', processchange, form, paramobject);
-                //little hack for chrome that need onChange event instead of onClick - see MDL-23224
-                if (Y.UA.webkit) {
-                    paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
+                paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
+                // Bad hack to circumvent problems with different browsers on different systems.
+                if (Y.UA.os == 'macintosh') {
+                    if(Y.UA.webkit) {
+                        paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
+                    }
+                    paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
                 } else {
-                    paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
+                    if(Y.UA.os == 'windows' && Y.UA.chrome) {
+                        paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
+                    }
+                    paramobject.eventkeypress = Y.on('keydown', changedown, select, '', form, paramobject);
                 }
             }
         }
@@ -408,6 +427,9 @@ M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
 
 /**
  * Attach handler to url_select
+ * Deprecated from 2.4 onwards.
+ * Please use @see init_select_autosubmit() for redirecting to a url (above).
+ * This function has accessability issues and also does not use the formid passed through as a parameter.
  */
 M.util.init_url_select = function(Y, formid, selectid, nothing) {
     YUI().use('node', function(Y) {
@@ -1445,6 +1467,7 @@ M.util.help_icon = {
                     },
 
                     display_callback : function(content) {
+                        content = '<div role="alert">' + content + '</div>';
                         this.overlay.set('bodyContent', content);
                     },
 
index c591f87..2e3460f 100644 (file)
@@ -5980,16 +5980,19 @@ function get_max_upload_sizes($sitebytes = 0, $coursebytes = 0, $modulebytes = 0
         return array();
     }
 
+    $filesize = array();
     $filesize[intval($maxsize)] = display_size($maxsize);
 
     $sizelist = array(10240, 51200, 102400, 512000, 1048576, 2097152,
                       5242880, 10485760, 20971520, 52428800, 104857600);
 
-    // If custombytes is given then add it to the list.
-    if (!is_null($custombytes)) {
-        if (is_number($custombytes)) {
-            $custombytes = array((int)$custombytes);
+    // If custombytes is given and is valid then add it to the list.
+    if (is_number($custombytes) and $custombytes > 0) {
+        $custombytes = (int)$custombytes;
+        if (!in_array($custombytes, $sizelist)) {
+            $sizelist[] = $custombytes;
         }
+    } else if (is_array($custombytes)) {
         $sizelist = array_unique(array_merge($sizelist, $custombytes));
     }
 
index 28b8490..918235b 100644 (file)
@@ -1490,7 +1490,7 @@ class core_renderer extends renderer_base {
             $go = html_writer::empty_tag('input', array('type'=>'submit', 'value'=>get_string('go')));
             $output .= html_writer::tag('noscript', html_writer::tag('div', $go), array('style'=>'inline'));
             $nothing = empty($select->nothing) ? false : key($select->nothing);
-            $output .= $this->page->requires->js_init_call('M.util.init_url_select', array($select->formid, $select->attributes['id'], $nothing));
+            $output .= $this->page->requires->js_init_call('M.util.init_select_autosubmit', array($select->formid, $select->attributes['id'], $nothing));
         } else {
             $output .= html_writer::empty_tag('input', array('type'=>'submit', 'value'=>$select->showbutton));
         }
@@ -1746,7 +1746,7 @@ class core_renderer extends renderer_base {
         // note: this title is displayed only if JS is disabled, otherwise the link will have the new ajax tooltip
         $title = get_string('helpprefix2', '', trim($helpicon->title, ". \t"));
 
-        $attributes = array('href'=>$url, 'title'=>$title);
+        $attributes = array('href'=>$url, 'title'=>$title, 'aria-haspopup' => 'true');
         $id = html_writer::random_id('helpicon');
         $attributes['id'] = $id;
         $output = html_writer::tag('a', $output, $attributes);
@@ -1811,7 +1811,7 @@ class core_renderer extends renderer_base {
         // note: this title is displayed only if JS is disabled, otherwise the link will have the new ajax tooltip
         $title = get_string('helpprefix2', '', trim($title, ". \t"));
 
-        $attributes = array('href'=>$url, 'title'=>$title);
+        $attributes = array('href'=>$url, 'title'=>$title, 'aria-haspopup' => 'true');
         $id = html_writer::random_id('helpicon');
         $attributes['id'] = $id;
         $output = html_writer::tag('a', $output, $attributes);
index 2afb8c3..e4bace9 100644 (file)
@@ -76,6 +76,11 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
             parent::runBare();
             // set DB reference in case somebody mocked it in test
             $DB = phpunit_util::get_global_backup('DB');
+
+            // Deal with any debugging messages.
+            phpunit_util::display_debugging_messages();
+            phpunit_util::reset_debugging();
+
         } catch (Exception $e) {
             // cleanup after failed expectation
             phpunit_util::reset_all_data();
@@ -234,6 +239,73 @@ abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
         $this->resetAfterTest = $reset;
     }
 
+    /**
+     * Return debugging messages from the current test.
+     * @return array with instances having 'message', 'level' and 'stacktrace' property.
+     */
+    public function getDebuggingMessages() {
+        return phpunit_util::get_debugging_messages();
+    }
+
+    /**
+     * Clear all previous debugging messages in current test.
+     */
+    public function resetDebugging() {
+        phpunit_util::reset_debugging();
+    }
+
+    /**
+     * Assert that exactly debugging was just called once.
+     *
+     * Discards the debugging message if successful.
+     *
+     * @param null|string $debugmessage null means any
+     * @param null|string $debuglevel null means any
+     * @param string $message
+     */
+    public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
+        $debugging = phpunit_util::get_debugging_messages();
+        $count = count($debugging);
+
+        if ($count == 0) {
+            if ($message === '') {
+                $message = 'Expectation failed, debugging() not triggered.';
+            }
+            $this->fail($message);
+        }
+        if ($count > 1) {
+            if ($message === '') {
+                $message = 'Expectation failed, debugging() triggered '.$count.' times.';
+            }
+            $this->fail($message);
+        }
+        $this->assertEquals(1, $count);
+
+        $debug = reset($debugging);
+        if ($debugmessage !== null) {
+            $this->assertSame($debugmessage, $debug->message, $message);
+        }
+        if ($debuglevel !== null) {
+            $this->assertSame($debuglevel, $debug->level, $message);
+        }
+
+        phpunit_util::reset_debugging();
+    }
+
+    /**
+     * Call when no debugging() messages expected.
+     * @param string $message
+     */
+    public function assertDebuggingNotCalled($message = '') {
+        $debugging = phpunit_util::get_debugging_messages();
+        $count = count($debugging);
+
+        if ($message === '') {
+            $message = 'Expectation failed, debugging() was triggered.';
+        }
+        $this->assertEquals(0, $count, $message);
+    }
+
     /**
      * Cleanup after all tests are executed.
      *
index 8225f23..515f062 100644 (file)
@@ -36,6 +36,7 @@
 class phpunit_data_generator {
     protected $usercounter = 0;
     protected $categorycount = 0;
+    protected $cohortcount = 0;
     protected $coursecount = 0;
     protected $scalecount = 0;
     protected $groupcount = 0;
@@ -218,7 +219,7 @@ EOD;
      * @param array $options
      * @return stdClass course category record
      */
-    function create_category($record=null, array $options=null) {
+    public function create_category($record=null, array $options=null) {
         global $DB, $CFG;
         require_once("$CFG->dirroot/course/lib.php");
 
@@ -270,6 +271,50 @@ EOD;
         return $DB->get_record('course_categories', array('id'=>$catid), '*', MUST_EXIST);
     }
 
+    /**
+     * Create test cohort.
+     * @param array|stdClass $record
+     * @param array $options
+     * @return stdClass cohort record
+     */
+    public function create_cohort($record=null, array $options=null) {
+        global $DB, $CFG;
+        require_once("$CFG->dirroot/cohort/lib.php");
+
+        $this->cohortcount++;
+        $i = $this->cohortcount;
+
+        $record = (array)$record;
+
+        if (!isset($record['contextid'])) {
+            $record['contextid'] = context_system::instance()->id;
+        }
+
+        if (!isset($record['name'])) {
+            $record['name'] = 'Cohort '.$i;
+        }
+
+        if (!isset($record['idnumber'])) {
+            $record['idnumber'] = '';
+        }
+
+        if (!isset($record['description'])) {
+            $record['description'] = "Test cohort $i\n$this->loremipsum";
+        }
+
+        if (!isset($record['descriptionformat'])) {
+            $record['descriptionformat'] = FORMAT_MOODLE;
+        }
+
+        if (!isset($record['component'])) {
+            $record['component'] = '';
+        }
+
+        $id = cohort_add_cohort((object)$record);
+
+        return $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST);
+    }
+
     /**
      * Create a test course
      * @param array|stdClass $record
@@ -277,7 +322,7 @@ EOD;
      *      'createsections'=>bool precreate all sections
      * @return stdClass course record
      */
-    function create_course($record=null, array $options=null) {
+    public function create_course($record=null, array $options=null) {
         global $DB, $CFG;
         require_once("$CFG->dirroot/course/lib.php");
 
index dc07596..4f56eb8 100644 (file)
@@ -133,4 +133,71 @@ abstract class database_driver_testcase extends PHPUnit_Framework_TestCase {
         phpunit_util::reset_all_data();
         parent::tearDownAfterClass();
     }
+
+    /**
+     * Return debugging messages from the current test.
+     * @return array with instances having 'message', 'level' and 'stacktrace' property.
+     */
+    public function getDebuggingMessages() {
+        return phpunit_util::get_debugging_messages();
+    }
+
+    /**
+     * Clear all previous debugging messages in current test.
+     */
+    public function resetDebugging() {
+        phpunit_util::reset_debugging();
+    }
+
+    /**
+     * Assert that exactly debugging was just called once.
+     *
+     * Discards the debugging message if successful.
+     *
+     * @param null|string $debugmessage null means any
+     * @param null|string $debuglevel null means any
+     * @param string $message
+     */
+    public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
+        $debugging = phpunit_util::get_debugging_messages();
+        $count = count($debugging);
+
+        if ($count == 0) {
+            if ($message === '') {
+                $message = 'Expectation failed, debugging() not triggered.';
+            }
+            $this->fail($message);
+        }
+        if ($count > 1) {
+            if ($message === '') {
+                $message = 'Expectation failed, debugging() triggered '.$count.' times.';
+            }
+            $this->fail($message);
+        }
+        $this->assertEquals(1, $count);
+
+        $debug = reset($debugging);
+        if ($debugmessage !== null) {
+            $this->assertSame($debugmessage, $debug->message, $message);
+        }
+        if ($debuglevel !== null) {
+            $this->assertSame($debuglevel, $debug->level, $message);
+        }
+
+        phpunit_util::reset_debugging();
+    }
+
+    /**
+     * Call when no debugging() messages expected.
+     * @param string $message
+     */
+    public function assertDebuggingNotCalled($message = '') {
+        $debugging = phpunit_util::get_debugging_messages();
+        $count = count($debugging);
+
+        if ($message === '') {
+            $message = 'Expectation failed, debugging() was triggered.';
+        }
+        $this->assertEquals(0, $count, $message);
+    }
 }
index de96f3d..d9b0d57 100644 (file)
@@ -57,6 +57,9 @@ class phpunit_util {
     /** @var resource used for prevention of parallel test execution */
     protected static $lockhandle = null;
 
+    /** @var array list of debugging messages triggered during the last test execution */
+    protected static $debuggings = array();
+
     /**
      * Prevent parallel test execution - this can not work in Moodle because we modify database and dataroot.
      *
@@ -544,6 +547,10 @@ class phpunit_util {
     public static function reset_all_data($logchanges = false) {
         global $DB, $CFG, $USER, $SITE, $COURSE, $PAGE, $OUTPUT, $SESSION, $GROUPLIB_CACHE;
 
+        // Show any unhandled debugging messages, the runbare() could already reset it.
+        self::display_debugging_messages();
+        self::reset_debugging();
+
         // reset global $DB in case somebody mocked it
         $DB = self::get_global_backup('DB');
 
@@ -1160,4 +1167,70 @@ class phpunit_util {
         }
         return false;
     }
+
+    /**
+     * To be called from debugging() only.
+     * @param string $message
+     * @param int $level
+     * @param string $from
+     */
+    public static function debugging_triggered($message, $level, $from) {
+        // Store only if debugging triggered from actual test,
+        // we need normal debugging outside of tests to find problems in our phpunit integration.
+        $backtrace = debug_backtrace();
+
+        foreach ($backtrace as $bt) {
+            $intest = false;
+            if (isset($bt['object']) and is_object($bt['object'])) {
+                if ($bt['object'] instanceof PHPUnit_Framework_TestCase) {
+                    if (strpos($bt['function'], 'test') === 0) {
+                        $intest = true;
+                        break;
+                    }
+                }
+            }
+        }
+        if (!$intest) {
+            return false;
+        }
+
+        $debug = new stdClass();
+        $debug->message = $message;
+        $debug->level   = $level;
+        $debug->from    = $from;
+
+        self::$debuggings[] = $debug;
+
+        return true;
+    }
+
+    /**
+     * Resets the list of debugging messages.
+     */
+    public static function reset_debugging() {
+        self::$debuggings = array();
+    }
+
+    /**
+     * Returns all debugging messages triggered during test.
+     * @return array with instances having message, level and stacktrace property.
+     */
+    public static function get_debugging_messages() {
+        return self::$debuggings;
+    }
+
+    /**
+     * Prints out any debug messages accumulated during test execution.
+     * @return bool false if no debug messages, true if debug triggered
+     */
+    public static function display_debugging_messages() {
+        if (empty(self::$debuggings)) {
+            return false;
+        }
+        foreach(self::$debuggings as $debug) {
+            echo 'Debugging: ' . $debug->message . "\n" . trim($debug->from) . "\n";
+        }
+
+        return true;
+    }
 }
index 302ab1f..4b6e75f 100644 (file)
@@ -36,6 +36,40 @@ defined('MOODLE_INTERNAL') || die();
  */
 class core_phpunit_advanced_testcase extends advanced_testcase {
 
+    public function test_debugging() {
+        global $CFG;
+        $this->resetAfterTest();
+
+        debugging('hokus');
+        $this->assertDebuggingCalled();
+        debugging('pokus');
+        $this->assertDebuggingCalled('pokus');
+        debugging('pokus', DEBUG_MINIMAL);
+        $this->assertDebuggingCalled('pokus', DEBUG_MINIMAL);
+        $this->assertDebuggingNotCalled();
+
+        debugging('a');
+        debugging('b', DEBUG_MINIMAL);
+        debugging('c', DEBUG_DEVELOPER);
+        $debuggings = $this->getDebuggingMessages();
+        $this->assertEquals(3, count($debuggings));
+        $this->assertSame('a', $debuggings[0]->message);
+        $this->assertSame(DEBUG_NORMAL, $debuggings[0]->level);
+        $this->assertSame('b', $debuggings[1]->message);
+        $this->assertSame(DEBUG_MINIMAL, $debuggings[1]->level);
+        $this->assertSame('c', $debuggings[2]->message);
+        $this->assertSame(DEBUG_DEVELOPER, $debuggings[2]->level);
+
+        $this->resetDebugging();
+        $this->assertDebuggingNotCalled();
+        $debuggings = $this->getDebuggingMessages();
+        $this->assertEquals(0, count($debuggings));
+
+        $CFG->debug = DEBUG_NONE;
+        debugging('hokus');
+        $this->assertDebuggingNotCalled();
+    }
+
     public function test_set_user() {
         global $USER, $DB;
 
index 316ee70..9c6f4f6 100644 (file)
@@ -53,6 +53,18 @@ class core_phpunit_generator_testcase extends advanced_testcase {
         $this->assertRegExp('/^Test course category \d/', $category->description);
         $this->assertSame(FORMAT_MOODLE, $category->descriptionformat);
 
+        $count = $DB->count_records('cohort');
+        $cohort = $generator->create_cohort();
+        $this->assertEquals($count+1, $DB->count_records('cohort'));
+        $this->assertEquals(context_system::instance()->id, $cohort->contextid);
+        $this->assertRegExp('/^Cohort \d/', $cohort->name);
+        $this->assertSame('', $cohort->idnumber);
+        $this->assertRegExp('/^Test cohort \d/', $cohort->description);
+        $this->assertSame(FORMAT_MOODLE, $cohort->descriptionformat);
+        $this->assertSame('', $cohort->component);
+        $this->assertLessThanOrEqual(time(), $cohort->timecreated);
+        $this->assertSame($cohort->timecreated, $cohort->timemodified);
+
         $count = $DB->count_records('course');
         $course = $generator->create_course();
         $this->assertEquals($count+1, $DB->count_records('course'));
index b1139eb..0d41223 100644 (file)
@@ -378,7 +378,7 @@ class plugin_manager {
             ),
 
             'assignfeedback' => array(
-                'comments', 'file'
+                'comments', 'file', 'offline'
             ),
 
             'auth' => array(
@@ -861,6 +861,11 @@ class available_update_checker {
     /**
      * Loads the most recent raw response record we have fetched
      *
+     * After this method is called, $this->recentresponse is set to an array. If the
+     * array is empty, then either no data have been fetched yet or the fetched data
+     * do not have expected format (and thence they are ignored and a debugging
+     * message is displayed).
+     *
      * This implementation uses the config_plugins table as the permanent storage.
      *
      * @param bool $forcereload reload even if it was already loaded
@@ -880,7 +885,8 @@ class available_update_checker {
                 $this->recentfetch = $config->recentfetch;
                 $this->recentresponse = $this->decode_response($config->recentresponse);
             } catch (available_update_checker_exception $e) {
-                // do not set recentresponse if the validation fails
+                debugging('Invalid info about available updates detected and will be ignored: '.$e->getMessage(), DEBUG_ALL);
+                $this->recentresponse = array();
             }
 
         } else {
index 4542513..9695adb 100644 (file)
@@ -1696,9 +1696,8 @@ class accesslib_testcase extends advanced_testcase {
 
         // ====== $context->get_child_contexts() ================================
 
-        $CFG->debug = 0;
         $children = $systemcontext->get_child_contexts();
-        $CFG->debug = DEBUG_DEVELOPER;
+        $this->resetDebugging();
         $this->assertEquals(count($children)+1, $DB->count_records('context'));
 
         $context = context_coursecat::instance($testcategories[3]);
@@ -2281,9 +2280,8 @@ class accesslib_testcase extends advanced_testcase {
             }
         }
 
-        $CFG->debug = 0;
         $children = get_child_contexts($systemcontext);
-        $CFG->debug = DEBUG_DEVELOPER;
+        $this->resetDebugging();
         $this->assertEquals(count($children), $DB->count_records('context')-1);
         unset($children);
 
index 1abdf80..2df90dd 100644 (file)
@@ -357,26 +357,9 @@ class moodlelib_testcase extends advanced_testcase {
         }
 
         // make sure warning is displayed if array submitted - TODO: throw exception in Moodle 2.3
-        $debugging = isset($CFG->debug) ? $CFG->debug : null;
-        $debugdisplay = isset($CFG->debugdisplay) ? $CFG->debugdisplay : null;
-        $CFG->debug = DEBUG_DEVELOPER;
-        $CFG->debugdisplay = true;
-
-        ob_start();
+        $_POST['username'] = array('a'=>'a');
         $this->assertSame(optional_param('username', 'default_user', PARAM_RAW), $_POST['username']);
-        $d = ob_end_clean();
-        $this->assertTrue($d !== '');
-
-        if ($debugging !== null) {
-            $CFG->debug = $debugging;
-        } else {
-            unset($CFG->debug);
-        }
-        if ($debugdisplay !== null) {
-            $CFG->debugdisplay = $debugdisplay;
-        } else {
-            unset($CFG->debugdisplay);
-        }
+        $this->assertDebuggingCalled();
     }
 
     function test_optional_param_array() {
@@ -429,34 +412,14 @@ class moodlelib_testcase extends advanced_testcase {
         }
 
         // do not allow non-arrays
-        $debugging = isset($CFG->debug) ? $CFG->debug : null;
-        $debugdisplay = isset($CFG->debugdisplay) ? $CFG->debugdisplay : null;
-        $CFG->debug = DEBUG_DEVELOPER;
-        $CFG->debugdisplay = true;
-
-        ob_start();
         $_POST['username'] = 'post_user';
         $this->assertSame(optional_param_array('username', array('a'=>'default_user'), PARAM_RAW), array('a'=>'default_user'));
-        $d = ob_end_clean();
-        $this->assertTrue($d !== '');
+        $this->assertDebuggingCalled();
 
         // make sure array keys are sanitised
-        ob_start();
         $_POST['username'] = array('abc123_;-/*-+ '=>'arrggh', 'a1_-'=>'post_user');
         $this->assertSame(optional_param_array('username', array(), PARAM_RAW), array('a1_-'=>'post_user'));
-        $d = ob_end_clean();
-        $this->assertTrue($d !== '');
-
-        if ($debugging !== null) {
-            $CFG->debug = $debugging;
-        } else {
-            unset($CFG->debug);
-        }
-        if ($debugdisplay !== null) {
-            $CFG->debugdisplay = $debugdisplay;
-        } else {
-            unset($CFG->debugdisplay);
-        }
+        $this->assertDebuggingCalled();
     }
 
     function test_required_param() {
@@ -499,26 +462,9 @@ class moodlelib_testcase extends advanced_testcase {
         }
 
         // make sure warning is displayed if array submitted - TODO: throw exception in Moodle 2.3
-        $debugging = isset($CFG->debug) ? $CFG->debug : null;
-        $debugdisplay = isset($CFG->debugdisplay) ? $CFG->debugdisplay : null;
-        $CFG->debug = DEBUG_DEVELOPER;
-        $CFG->debugdisplay = true;
-
-        ob_start();
+        $_POST['username'] = array('a'=>'a');
         $this->assertSame(required_param('username', PARAM_RAW), $_POST['username']);
-        $d = ob_end_clean();
-        $this->assertTrue($d !== '');
-
-        if ($debugging !== null) {
-            $CFG->debug = $debugging;
-        } else {
-            unset($CFG->debug);
-        }
-        if ($debugdisplay !== null) {
-            $CFG->debugdisplay = $debugdisplay;
-        } else {
-            unset($CFG->debugdisplay);
-        }
+        $this->assertDebuggingCalled();
     }
 
     function test_required_param_array() {
@@ -570,29 +516,10 @@ class moodlelib_testcase extends advanced_testcase {
             $this->assertTrue(true);
         }
 
-        // do not allow non-arrays
-        $debugging = isset($CFG->debug) ? $CFG->debug : null;
-        $debugdisplay = isset($CFG->debugdisplay) ? $CFG->debugdisplay : null;
-        $CFG->debug = DEBUG_DEVELOPER;
-        $CFG->debugdisplay = true;
-
         // make sure array keys are sanitised
-        ob_start();
         $_POST['username'] = array('abc123_;-/*-+ '=>'arrggh', 'a1_-'=>'post_user');
         $this->assertSame(required_param_array('username', PARAM_RAW), array('a1_-'=>'post_user'));
-        $d = ob_end_clean();
-        $this->assertTrue($d !== '');
-
-        if ($debugging !== null) {
-            $CFG->debug = $debugging;
-        } else {
-            unset($CFG->debug);
-        }
-        if ($debugdisplay !== null) {
-            $CFG->debugdisplay = $debugdisplay;
-        } else {
-            unset($CFG->debugdisplay);
-        }
+        $this->assertDebuggingCalled();
     }
 
     function test_clean_param() {
@@ -1955,8 +1882,6 @@ class moodlelib_testcase extends advanced_testcase {
             $this->assertTrue(true);
         }
 
-        $CFG->debug = DEBUG_MINIMAL; // Prevent standard debug warnings.
-
         $record = new stdClass();
         $record->id = 666;
         $record->username = 'xx';
@@ -1969,5 +1894,7 @@ class moodlelib_testcase extends advanced_testcase {
 
         $result = delete_user($admin);
         $this->assertFalse($result);
+
+        $this->resetDebugging();
     }
 }
index ee6227d..5fe9ea5 100644 (file)
@@ -32,34 +32,21 @@ require_once($CFG->libdir . '/outputlib.php');
 /**
  * Unit tests for the xhtml_container_stack class.
  *
- * These tests assume that developer debug mode is on, which, at the time of
- * writing, is true. admin/tool/unittest/index.php forces it on.
+ * These tests assume that developer debug mode is on which is enforced by our phpunit integration.
  *
  * @copyright 2009 Tim Hunt
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class xhtml_container_stack_testcase extends basic_testcase {
-    protected function start_capture() {
-        ob_start();
-    }
-
-    protected function end_capture() {
-        $result = ob_get_contents();
-        ob_end_clean();
-        return $result;
-    }
-
+class xhtml_container_stack_testcase extends advanced_testcase {
     public function test_push_then_pop() {
         // Set up.
         $stack = new xhtml_container_stack();
         // Exercise SUT.
-        $this->start_capture();
         $stack->push('testtype', '</div>');
         $html = $stack->pop('testtype');
-        $errors = $this->end_capture();
         // Verify outcome
         $this->assertEquals('</div>', $html);
-        $this->assertEquals('', $errors);
+        $this->assertDebuggingNotCalled();
     }
 
     public function test_mismatched_pop_prints_warning() {
@@ -67,40 +54,34 @@ class xhtml_container_stack_testcase extends basic_testcase {
         $stack = new xhtml_container_stack();
         $stack->push('testtype', '</div>');
         // Exercise SUT.
-        $this->start_capture();
         $html = $stack->pop('mismatch');
-        $errors = $this->end_capture();
         // Verify outcome
         $this->assertEquals('</div>', $html);
-        $this->assertNotEquals('', $errors);
+        $this->assertDebuggingCalled();
     }
 
     public function test_pop_when_empty_prints_warning() {
         // Set up.
         $stack = new xhtml_container_stack();
         // Exercise SUT.
-        $this->start_capture();
         $html = $stack->pop('testtype');
-        $errors = $this->end_capture();
         // Verify outcome
         $this->assertEquals('', $html);
-        $this->assertNotEquals('', $errors);
+        $this->assertDebuggingCalled();
     }
 
     public function test_correct_nesting() {
         // Set up.
         $stack = new xhtml_container_stack();
         // Exercise SUT.
-        $this->start_capture();
         $stack->push('testdiv', '</div>');
         $stack->push('testp', '</p>');
         $html2 = $stack->pop('testp');
         $html1 = $stack->pop('testdiv');
-        $errors = $this->end_capture();
         // Verify outcome
         $this->assertEquals('</p>', $html2);
         $this->assertEquals('</div>', $html1);
-        $this->assertEquals('', $errors);
+        $this->assertDebuggingNotCalled();
     }
 
     public function test_pop_all_but_last() {
@@ -110,12 +91,10 @@ class xhtml_container_stack_testcase extends basic_testcase {
         $stack->push('test2', '</h2>');
         $stack->push('test3', '</h3>');
         // Exercise SUT.
-        $this->start_capture();
         $html = $stack->pop_all_but_last();
-        $errors = $this->end_capture();
         // Verify outcome
         $this->assertEquals('</h3></h2>', $html);
-        $this->assertEquals('', $errors);
+        $this->assertDebuggingNotCalled();
         // Tear down.
         $stack->discard();
     }
@@ -125,12 +104,10 @@ class xhtml_container_stack_testcase extends basic_testcase {
         $stack = new xhtml_container_stack();
         $stack->push('test1', '</h1>');
         // Exercise SUT.
-        $this->start_capture();
         $html = $stack->pop_all_but_last();
-        $errors = $this->end_capture();
         // Verify outcome
         $this->assertEquals('', $html);
-        $this->assertEquals('', $errors);
+        $this->assertDebuggingNotCalled();
         // Tear down.
         $stack->discard();
     }
@@ -139,12 +116,10 @@ class xhtml_container_stack_testcase extends basic_testcase {
         // Set up.
         $stack = new xhtml_container_stack();
         // Exercise SUT.
-        $this->start_capture();
         $html = $stack->pop_all_but_last();
-        $errors = $this->end_capture();
         // Verify outcome
         $this->assertEquals('', $html);
-        $this->assertEquals('', $errors);
+        $this->assertDebuggingNotCalled();
     }
 
     public function test_discard() {
@@ -153,10 +128,8 @@ class xhtml_container_stack_testcase extends basic_testcase {
         $stack->push('test1', '</somethingdistinctive>');
         $stack->discard();
         // Exercise SUT.
-        $this->start_capture();
         $stack = null;
-        $errors = $this->end_capture();
         // Verify outcome
-        $this->assertEquals('', $errors);
+        $this->assertDebuggingNotCalled();
     }
 }
index 261cfdd..1760074 100644 (file)
@@ -34,7 +34,7 @@ defined('MOODLE_INTERNAL') || die();
  * @copyright  2010 Petr Skoda (http://skodak.org)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class core_textlib_testcase extends basic_testcase {
+class core_textlib_testcase extends advanced_testcase {
 
     /**
      * Tests the static parse charset method
@@ -345,11 +345,8 @@ class core_textlib_testcase extends basic_testcase {
      * @return void
      */
     public function test_deprecated_textlib_get_instance() {
-        ob_start();
         $textlib = textlib_get_instance();
-        $output = ob_get_contents();
-        $this->assertNotEmpty($output);
-        ob_end_clean();
+        $this->assertDebuggingCalled();
         $this->assertSame($textlib->substr('abc', 1, 1), 'b');
         $this->assertSame($textlib->strlen('abc'), 3);
         $this->assertSame($textlib->strtoupper('Abc'), 'ABC');
index 34db5a0..61d350a 100644 (file)
@@ -11,6 +11,11 @@ YUI changes:
 * moodle-enrol-notification has been renamed to moodle-core-notification
 * YUI2 code must now use 2in3, see http://yuilibrary.com/yui/docs/yui/yui-yui2.html
 
+Unit testing changes:
+* output debugging() is not sent to standard output any more,
+  use $this->assertDebuggingCalled(), $this->assertDebuggingNotCalled(),
+  $this->getDebuggingMessages() or $this->assertResetDebugging() instead.
+
 === 2.3 ===
 
 Database layer changes:
index d185462..3c0dc19 100644 (file)
@@ -2853,9 +2853,13 @@ function debugging($message = '', $level = DEBUG_NORMAL, $backtrace = null) {
         }
         $from = format_backtrace($backtrace, CLI_SCRIPT);
         if (PHPUNIT_TEST) {
-            echo 'Debugging: ' . $message . "\n" . $from;
+            if (phpunit_util::debugging_triggered($message, $level, $from)) {
+                // We are inside test, the debug message was logged.
+                return true;
+            }
+        }
 
-        } else if (NO_DEBUG_DISPLAY) {
+        if (NO_DEBUG_DISPLAY) {
             // script does not want any errors or debugging in output,
             // we send the info to error log instead
             error_log('Debugging: ' . $message . $from);
index 494fd14..cb4dd39 100644 (file)
@@ -103,8 +103,9 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
         $pluginconfigs->add_child($pluginconfig);
 
 
-        // Define sources
+        // Define sources.
         $assign->set_source_table('assign', array('id' => backup::VAR_ACTIVITYID));
+        $pluginconfig->set_source_table('assign_plugin_config', array('assignment' => backup::VAR_PARENTID));
 
         if ($userinfo) {
             $submission->set_source_table('assign_submission',
@@ -112,15 +113,12 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
 
             $grade->set_source_table('assign_grades',
                                      array('assignment' => backup::VAR_PARENTID));
-            $pluginconfig->set_source_table('assign_plugin_config',
-                                     array('assignment' => backup::VAR_PARENTID));
 
-            // support 2 types of subplugins
+            // Support 2 types of subplugins.
             $this->add_subplugin_structure('assignsubmission', $submission, true);
             $this->add_subplugin_structure('assignfeedback', $grade, true);
         }
 
-
         // Define id annotations
         $submission->annotate_ids('user', 'userid');
         $submission->annotate_ids('group', 'groupid');
index 71daf6a..80f3d49 100644 (file)
@@ -31,7 +31,7 @@ defined('MOODLE_INTERNAL') || die();
  * @param mod_assign_mod_form $form
  * @return int The instance id of the new assignment
  */
-function assign_add_instance(stdClass $data, mod_assign_mod_form $form) {
+function assign_add_instance(stdClass $data, mod_assign_mod_form $form = null) {
     global $CFG;
     require_once($CFG->dirroot . '/mod/assign/locallib.php');
 
index 05a73fa..277b05b 100644 (file)
@@ -132,7 +132,6 @@ class assign {
 
         $this->submissionplugins = $this->load_plugins('assignsubmission');
         $this->feedbackplugins = $this->load_plugins('assignfeedback');
-        $this->output = $PAGE->get_renderer('mod_assign');
     }
 
     /**
@@ -308,15 +307,6 @@ class assign {
         return $result;
     }
 
-    /**
-     * Expose the renderer to plugins
-     *
-     * @return assign_renderer
-     */
-    public function get_renderer() {
-        return $this->output;
-    }
-
     /**
      * Display the assignment, used by view.php
      *
@@ -1431,7 +1421,7 @@ class assign {
         } else {
             $data->batchusers = $batchusers;
         }
-        $o .= $this->output->render(new assign_header($this->get_instance(),
+        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                       $this->get_context(),
                                                       $this->show_intro(),
                                                       $this->get_course_module()->id,
@@ -1444,7 +1434,7 @@ class assign {
                                                                $this->get_instance(),
                                                                $data));
         }
-        $o .= $this->output->render(new assign_form('extensionform', $mform));
+        $o .= $this->get_renderer()->render(new assign_form('extensionform', $mform));
         $o .= $this->view_footer();
         return $o;
     }
@@ -1644,12 +1634,12 @@ class assign {
             if ($item->userid != $USER->id) {
                 require_capability('mod/assign:grade', $this->context);
             }
-            $o .= $this->output->render(new assign_header($this->get_instance(),
+            $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                               $this->get_context(),
                                                               $this->show_intro(),
                                                               $this->get_course_module()->id,
                                                               $plugin->get_name()));
-            $o .= $this->output->render(new assign_submission_plugin_submission($plugin,
+            $o .= $this->get_renderer()->render(new assign_submission_plugin_submission($plugin,
                                                               $item,
                                                               assign_submission_plugin_submission::FULL,
                                                               $this->get_course_module()->id,
@@ -1667,12 +1657,12 @@ class assign {
             if ($item->userid != $USER->id) {
                 require_capability('mod/assign:grade', $this->context);
             }
-            $o .= $this->output->render(new assign_header($this->get_instance(),
+            $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                               $this->get_context(),
                                                               $this->show_intro(),
                                                               $this->get_course_module()->id,
                                                               $plugin->get_name()));
-            $o .= $this->output->render(new assign_feedback_plugin_feedback($plugin,
+            $o .= $this->get_renderer()->render(new assign_feedback_plugin_feedback($plugin,
                                                               $item,
                                                               assign_feedback_plugin_feedback::FULL,
                                                               $this->get_course_module()->id,
@@ -1738,12 +1728,12 @@ class assign {
      */
     private function view_quickgrading_result($message) {
         $o = '';
-        $o .= $this->output->render(new assign_header($this->get_instance(),
+        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                       $this->get_context(),
                                                       $this->show_intro(),
                                                       $this->get_course_module()->id,
                                                       get_string('quickgradingresult', 'assign')));
-        $o .= $this->output->render(new assign_quickgrading_result($message, $this->get_course_module()->id));
+        $o .= $this->get_renderer()->render(new assign_quickgrading_result($message, $this->get_course_module()->id));
         $o .= $this->view_footer();
         return $o;
     }
@@ -1754,7 +1744,7 @@ class assign {
      * @return string
      */
     private function view_footer() {
-        return $this->output->render_footer();
+        return $this->get_renderer()->render_footer();
     }
 
     /**
@@ -1869,6 +1859,20 @@ class assign {
         add_to_log($this->get_course()->id, 'assign', $action, $fullurl, $info, $this->get_course_module()->id, $USER->id);
     }
 
+    /**
+     * Lazy load the page renderer and expose the renderer to plugins
+     *
+     * @return assign_renderer
+     */
+    private function get_renderer() {
+        global $PAGE;
+        if ($this->output) {
+            return $this->output;
+        }
+        $this->output = $PAGE->get_renderer('mod_assign');
+        return $this->output;
+    }
+
     /**
      * Load the submission object for a particular user, optionally creating it if required
      *
@@ -1990,7 +1994,7 @@ class assign {
         // Need submit permission to submit an assignment
         require_capability('mod/assign:grade', $this->context);
 
-        $o .= $this->output->render(new assign_header($this->get_instance(),
+        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                       $this->get_context(), false, $this->get_course_module()->id,get_string('grading', 'assign')));
 
         $rownum = required_param('rownum', PARAM_INT) + $offset;
@@ -2014,7 +2018,7 @@ class assign {
         }
         $user = $DB->get_record('user', array('id' => $userid));
         if ($user) {
-            $o .= $this->output->render(new assign_user_summary($user,
+            $o .= $this->get_renderer()->render(new assign_user_summary($user,
                                                                 $this->get_course()->id,
                                                                 has_capability('moodle/site:viewfullnames',
                                                                 $this->get_course_context()),
@@ -2054,7 +2058,7 @@ class assign {
             }
             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
 
-            $o .= $this->output->render(new assign_submission_status($this->get_instance()->allowsubmissionsfromdate,
+            $o .= $this->get_renderer()->render(new assign_submission_status($this->get_instance()->allowsubmissionsfromdate,
                                                               $this->get_instance()->alwaysshowdescription,
                                                               $submission,
                                                               $this->get_instance()->teamsubmission,
@@ -2100,7 +2104,7 @@ class assign {
                                                '',
                                                array('class'=>'gradeform'));
         }
-        $o .= $this->output->render(new assign_form('gradingform',$mform));
+        $o .= $this->get_renderer()->render(new assign_form('gradingform',$mform));
 
         $msg = get_string('viewgradingformforstudent', 'assign', array('id'=>$user->id, 'fullname'=>fullname($user)));
         $this->add_to_log('view grading form', $msg);
@@ -2120,7 +2124,7 @@ class assign {
         require_capability('mod/assign:revealidentities', $this->get_context());
 
         $o = '';
-        $o .= $this->output->render(new assign_header($this->get_instance(),
+        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                       $this->get_context(), false, $this->get_course_module()->id));
 
         $confirmurl = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
@@ -2130,7 +2134,7 @@ class assign {
         $cancelurl = new moodle_url('/mod/assign/view.php', array('id'=>$this->get_course_module()->id,
                                                                     'action'=>'grading'));
 
-        $o .= $this->output->confirm(get_string('revealidentitiesconfirm', 'assign'), $confirmurl, $cancelurl);
+        $o .= $this->get_renderer()->confirm(get_string('revealidentitiesconfirm', 'assign'), $confirmurl, $cancelurl);
         $o .= $this->view_footer();
         $this->add_to_log('view', get_string('viewrevealidentitiesconfirm', 'assign'));
         return $o;
@@ -2153,7 +2157,7 @@ class assign {
         parse_str($returnparams, $params);
         $params = array_merge( array('id' => $this->get_course_module()->id, 'action' => $returnaction), $params);
 
-        return $this->output->single_button(new moodle_url('/mod/assign/view.php', $params), get_string('back'), 'get');
+        return $this->get_renderer()->single_button(new moodle_url('/mod/assign/view.php', $params), get_string('back'), 'get');
 
     }
 
@@ -2235,8 +2239,8 @@ class assign {
         $gradingoptionsdata->filter = $filter;
         $gradingoptionsform->set_data($gradingoptionsdata);
 
-        $actionformtext = $this->output->render($gradingactions);
-        $o .= $this->output->render(new assign_header($this->get_instance(),
+        $actionformtext = $this->get_renderer()->render($gradingactions);
+        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                       $this->get_context(), false, $this->get_course_module()->id, get_string('grading', 'assign'), $actionformtext));
         $o .= groups_print_activity_menu($this->get_course_module(), $CFG->wwwroot . '/mod/assign/view.php?id=' . $this->get_course_module()->id.'&action=grading', true);
 
@@ -2249,22 +2253,22 @@ class assign {
 
         // load and print the table of submissions
         if ($showquickgrading && $quickgrading) {
-            $table = $this->output->render(new assign_grading_table($this, $perpage, $filter, 0, true));
+            $table = $this->get_renderer()->render(new assign_grading_table($this, $perpage, $filter, 0, true));
             $quickgradingform = new mod_assign_quick_grading_form(null,
                                                                   array('cm'=>$this->get_course_module()->id,
                                                                         'gradingtable'=>$table));
-            $o .= $this->output->render(new assign_form('quickgradingform', $quickgradingform));
+            $o .= $this->get_renderer()->render(new assign_form('quickgradingform', $quickgradingform));
         } else {
-            $o .= $this->output->render(new assign_grading_table($this, $perpage, $filter, 0, false));
+            $o .= $this->get_renderer()->render(new assign_grading_table($this, $perpage, $filter, 0, false));
         }
 
         $currentgroup = groups_get_activity_group($this->get_course_module(), true);
         $users = array_keys($this->list_participants($currentgroup, true));
         if (count($users) != 0) {
             // if no enrolled user in a course then don't display the batch operations feature
-            $o .= $this->output->render(new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform));
+            $o .= $this->get_renderer()->render(new assign_form('gradingbatchoperationsform', $gradingbatchoperationsform));
         }
-        $o .= $this->output->render(new assign_form('gradingoptionsform', $gradingoptionsform, 'M.mod_assign.init_grading_options'));
+        $o .= $this->get_renderer()->render(new assign_form('gradingoptionsform', $gradingoptionsform, 'M.mod_assign.init_grading_options'));
         return $o;
     }
 
@@ -2321,13 +2325,13 @@ class assign {
         // Need submit permission to submit an assignment
         require_capability('mod/assign:submit', $this->context);
 
-        $o .= $this->output->render(new assign_header($this->get_instance(),
+        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                       $this->get_context(),
                                                       $this->show_intro(),
                                                       $this->get_course_module()->id,
                                                       get_string('editsubmission', 'assign')));
 
-        $o .= $this->output->notification(get_string('submissionsclosed', 'assign'));
+        $o .= $this->get_renderer()->notification(get_string('submissionsclosed', 'assign'));
 
         $o .= $this->view_footer();
 
@@ -2353,7 +2357,7 @@ class assign {
         if (!$this->submissions_open()) {
             return $this->view_student_error_message();
         }
-        $o .= $this->output->render(new assign_header($this->get_instance(),
+        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                       $this->get_context(),
                                                       $this->show_intro(),
                                                       $this->get_course_module()->id,
@@ -2365,7 +2369,7 @@ class assign {
             $mform = new mod_assign_submission_form(null, array($this, $data));
         }
 
-        $o .= $this->output->render(new assign_form('editsubmissionform',$mform));
+        $o .= $this->get_renderer()->render(new assign_form('editsubmissionform',$mform));
 
         $o .= $this->view_footer();
         $this->add_to_log('view submit assignment form', get_string('viewownsubmissionform', 'assign'));
@@ -2526,8 +2530,8 @@ class assign {
                                                                         $data));
         }
         $o = '';
-        $o .= $this->output->header();
-        $o .= $this->output->render(new assign_submit_for_grading_page($notifications, $this->get_course_module()->id, $mform));
+        $o .= $this->get_renderer()->header();
+        $o .= $this->get_renderer()->render(new assign_submit_for_grading_page($notifications, $this->get_course_module()->id, $mform));
         $o .= $this->view_footer();
 
         $this->add_to_log('view confirm submit assignment form', get_string('viewownsubmissionform', 'assign'));
@@ -2589,7 +2593,7 @@ class assign {
                 $extensionduedate = $grade->extensionduedate;
             }
             $viewfullnames = has_capability('moodle/site:viewfullnames', $this->get_course_context());
-            $o .= $this->output->render(new assign_submission_status($this->get_instance()->allowsubmissionsfromdate,
+            $o .= $this->get_renderer()->render(new assign_submission_status($this->get_instance()->allowsubmissionsfromdate,
                                                               $this->get_instance()->alwaysshowdescription,
                                                               $submission,
                                                               $this->get_instance()->teamsubmission,
@@ -2668,7 +2672,7 @@ class assign {
                                                       $this->get_return_action(),
                                                       $this->get_return_params());
 
-                $o .= $this->output->render($feedbackstatus);
+                $o .= $this->get_renderer()->render($feedbackstatus);
             }
 
         }
@@ -2684,7 +2688,7 @@ class assign {
         global $CFG, $DB, $USER, $PAGE;
 
         $o = '';
-        $o .= $this->output->render(new assign_header($this->get_instance(),
+        $o .= $this->get_renderer()->render(new assign_header($this->get_instance(),
                                                       $this->get_context(),
                                                       $this->show_intro(),
                                                       $this->get_course_module()->id));
@@ -2701,7 +2705,7 @@ class assign {
                                                       $this->get_course_module()->id,
                                                       $this->count_submissions_need_grading(),
                                                       $this->get_instance()->teamsubmission);
-                $o .= $this->output->render($summary);
+                $o .= $this->get_renderer()->render($summary);
             } else {
                 $summary = new assign_grading_summary($this->count_participants(0),
                                                       $this->get_instance()->submissiondrafts,
@@ -2713,7 +2717,7 @@ class assign {
                                                       $this->get_course_module()->id,
                                                       $this->count_submissions_need_grading(),
                                                       $this->get_instance()->teamsubmission);
-                $o .= $this->output->render($summary);
+                $o .= $this->get_renderer()->render($summary);
             }
         }
         $grade = $this->get_user_grade($USER->id, false);
@@ -2988,7 +2992,7 @@ class assign {
         $fs = get_file_storage();
         $browser = get_file_browser();
         $files = $fs->get_area_files($this->get_context()->id, $component, $area , $submissionid , "timemodified", false);
-        return $this->output->assign_files($this->context, $submissionid, $area, $component);
+        return $this->get_renderer()->assign_files($this->context, $submissionid, $area, $component);
 
     }
 
@@ -3829,7 +3833,7 @@ class assign {
         }
 
         if (has_all_capabilities(array('gradereport/grader:view', 'moodle/grade:viewall'), $this->get_course_context())) {
-            $gradestring = $this->output->action_link(new moodle_url('/grade/report/grader/index.php',
+            $gradestring = $this->get_renderer()->action_link(new moodle_url('/grade/report/grader/index.php',
                                                               array('id'=>$this->get_course()->id)),
                                                 $gradinginfo->items[0]->grades[$userid]->str_grade);
         } else {
index 9077b84..f33f9a1 100644 (file)
@@ -91,6 +91,7 @@ if ($chapter) {
     </head>
     <body>
     <a name="top"></a>
+    <h1 class="book_title"><?php echo format_string($book->name, true, array('context'=>$context)) ?></h1>
     <div class="chapter">
     <?php
 
@@ -98,11 +99,11 @@ if ($chapter) {
     if (!$book->customtitles) {
         if (!$chapter->subchapter) {
             $currtitle = book_get_chapter_title($chapter->id, $chapters, $book, $context);
-            echo '<p class="book_chapter_title">'.$currtitle.'</p>';
+            echo '<h2 class="book_chapter_title">'.$currtitle.'</h2>';
         } else {
             $currtitle = book_get_chapter_title($chapters[$chapter->id]->parent, $chapters, $book, $context);
             $currsubtitle = book_get_chapter_title($chapter->id, $chapters, $book, $context);
-            echo '<p class="book_chapter_title">'.$currtitle.'<br />'.$currsubtitle.'</p>';
+            echo '<h2 class="book_chapter_title">'.$currtitle.'</h2><h3 class="book_chapter_title">'.$currsubtitle.'</h3>';
         }
     }
 
@@ -128,7 +129,7 @@ if ($chapter) {
     </head>
     <body>
     <a name="top"></a>
-    <p class="book_title"><?php echo format_string($book->name, true, array('context'=>$context)) ?></p>
+    <h1 class="book_title"><?php echo format_string($book->name, true, array('context'=>$context)) ?></h1>
     <p class="book_summary"><?php echo format_text($book->intro, $book->introformat, array('noclean'=>true, 'context'=>$context)) ?></p>
     <div class="book_info"><table>
     <tr>
@@ -162,7 +163,11 @@ if ($chapter) {
         }
         echo '<div class="book_chapter"><a name="ch'.$ch->id.'"></a>';
         if (!$book->customtitles) {
-            echo '<p class="book_chapter_title">'.$titles[$ch->id].'</p>';
+            if (!$chapter->subchapter) {
+                echo '<h2 class="book_chapter_title">'.$titles[$ch->id].'</h2>';
+            } else {
+                echo '<h3 class="book_chapter_title">'.$titles[$ch->id].'</h3>';
+            }
         }
         $content = str_replace($link1, '#ch', $chapter->content);
         $content = str_replace($link2, '#top', $content);
index 200bee7..27838a9 100644 (file)
@@ -60,11 +60,7 @@ function booktool_print_get_toc($chapters, $book, $cm) {
 
     $toc .= html_writer::tag('a', '', array('name' => 'toc')); // Representation of toc (HTML).
 
-    if ($book->customtitles) {
-        $toc .= html_writer::tag('h1', get_string('toc', 'mod_book'));
-    } else {
-        $toc .= html_writer::tag('p', get_string('toc', 'mod_book'), array('class' => 'book_chapter_title'));
-    }
+    $toc .= html_writer::tag('h2', get_string('toc', 'mod_book'), array('class' => 'book_chapter_title'));
     $toc .= html_writer::start_tag('ul');
     foreach ($chapters as $ch) {
         if (!$ch->hidden) {
index f5643d3..91b83b8 100644 (file)
@@ -181,11 +181,12 @@ if (!$book->customtitles) {
     $hidden = $chapter->hidden ? 'dimmed_text' : '';
     if (!$chapter->subchapter) {
         $currtitle = book_get_chapter_title($chapter->id, $chapters, $book, $context);
-        echo '<p class="book_chapter_title '.$hidden.'">'.$currtitle.'</p>';
+        echo $OUTPUT->heading($currtitle, 2, array('class' => 'book_chapter_title '.$hidden));
     } else {
         $currtitle = book_get_chapter_title($chapters[$chapter->id]->parent, $chapters, $book, $context);
         $currsubtitle = book_get_chapter_title($chapter->id, $chapters, $book, $context);
-        echo '<p class="book_chapter_title '.$hidden.'">'.$currtitle.'<br />'.$currsubtitle.'</p>';
+        echo $OUTPUT->heading($currtitle, 2, array('class' => 'book_chapter_title '.$hidden));
+        echo $OUTPUT->heading($currsubtitle, 3, array('class' => 'book_chapter_title '.$hidden));
     }
 }
 $chaptertext = file_rewrite_pluginfile_urls($chapter->content, 'pluginfile.php', $context->id, 'mod_book', 'chapter', $chapter->id);
index a92df57..43151c6 100644 (file)
@@ -334,5 +334,15 @@ $capabilities = array(
             'manager' => CAP_ALLOW
         )
     ),
+    'mod/forum:allowforcesubscribe' => array(
+
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'student' => CAP_ALLOW,
+            'teacher' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW
+        )
+    ),
 );
 
index f11bd37..ded6583 100644 (file)
@@ -25,9 +25,9 @@
 
 /* List of handlers */
 $handlers = array (
-    'user_enrolled' => array (
+    'role_assigned' => array (
         'handlerfile'      => '/mod/forum/lib.php',
-        'handlerfunction'  => 'forum_user_enrolled',
+        'handlerfunction'  => 'forum_user_role_assigned',
         'schedule'         => 'instant',
         'internal'         => 1,
     ),
index 697fc0c..3f995da 100644 (file)
@@ -156,6 +156,7 @@ $string['forum'] = 'Forum';
 $string['forum:addinstance'] = 'Add a new forum';
 $string['forum:addnews'] = 'Add news';
 $string['forum:addquestion'] = 'Add question';
+$string['forum:allowforcesubscribe'] = 'Allow force subscribe';
 $string['forumauthorhidden'] = 'Author (hidden)';
 $string['forumblockingalmosttoomanyposts'] = 'You are approaching the posting threshold. You have posted {$a->numposts} times in the last {$a->blockperiod} and the limit is {$a->blockafter} posts.';
 $string['forumbodyhidden'] = 'This post cannot be viewed by you, probably because you have not posted in the discussion or the maximum editing time hasn\'t passed yet.';
index f66fc79..74af4b3 100644 (file)
@@ -2861,7 +2861,7 @@ function forum_get_potential_subscribers($forumcontext, $groupid, $fields, $sort
     global $DB;
 
     // only active enrolled users or everybody on the frontpage
-    list($esql, $params) = get_enrolled_sql($forumcontext, '', $groupid, true);
+    list($esql, $params) = get_enrolled_sql($forumcontext, 'mod/forum:allowforcesubscribe', $groupid, true);
 
     $sql = "SELECT $fields
               FROM {user} u
@@ -3353,7 +3353,7 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
     }
 
     if ($reply) {
-        $commands[] = array('url'=>new moodle_url('/mod/forum/post.php', array('reply'=>$post->id)), 'text'=>$str->reply);
+        $commands[] = array('url'=>new moodle_url('/mod/forum/post.php#mformforum', array('reply'=>$post->id)), 'text'=>$str->reply);
     }
 
     if ($CFG->enableportfolios && ($cm->cache->caps['mod/forum:exportpost'] || ($ownpost && $cm->cache->caps['mod/forum:exportownpost']))) {
@@ -4680,7 +4680,9 @@ function forum_is_subscribed($userid, $forum) {
     if (is_numeric($forum)) {
         $forum = $DB->get_record('forum', array('id' => $forum));
     }
-    if (forum_is_forcesubscribed($forum)) {
+    // If forum is force subscribed and has allowforcesubscribe, then user is subscribed.
+    if (forum_is_forcesubscribed($forum) &&
+            has_capability('mod/forum:allowforcesubscribe', context_module::instance($forum->id), $userid)) {
         return true;
     }
     return $DB->record_exists("forum_subscriptions", array("userid" => $userid, "forum" => $forum->id));
@@ -6133,6 +6135,7 @@ function forum_update_subscriptions_button($courseid, $forumid) {
 /**
  * This function gets run whenever user is enrolled into course
  *
+ * @deprecated deprecating this function as we will be using forum_user_role_assigned
  * @param stdClass $cp
  * @return void
  */
@@ -6155,6 +6158,38 @@ function forum_user_enrolled($cp) {
     }
 }
 
+/**
+ * This function gets run whenever user is assigned role in course
+ *
+ * @param stdClass $cp
+ * @return void
+ */
+function forum_user_role_assigned($cp) {
+    global $DB;
+
+    $context = context::instance_by_id($cp->contextid, MUST_EXIST);
+
+    // If contextlevel is course then only subscribe user. Role assignment
+    // at course level means user is enroled in course and can subscribe to forum.
+    if ($context->contextlevel != CONTEXT_COURSE) {
+        return;
+    }
+
+    $sql = "SELECT f.id
+              FROM {forum} f
+         LEFT JOIN {forum_subscriptions} fs ON (fs.forum = f.id AND fs.userid = :userid)
+             WHERE f.course = :courseid AND f.forcesubscribe = :initial AND fs.id IS NULL";
+    $params = array('courseid'=>$context->instanceid, 'userid'=>$cp->userid, 'initial'=>FORUM_INITIALSUBSCRIBE);
+
+    $forums = $DB->get_records_sql($sql, $params);
+    foreach ($forums as $forum) {
+        // If user doesn't have allowforcesubscribe capability then don't subscribe.
+        if (has_capability('mod/forum:allowforcesubscribe', context_module::instance($forum->id), $cp->userid)) {
+            forum_subscribe($cp->userid, $forum->id);
+        }
+    }
+}
+
 /**
  * This function gets run whenever user is unenrolled from course
  *
index 2d11a2e..c2bbbfe 100644 (file)
@@ -507,7 +507,7 @@ if (!isset($forum->maxattachments)) {  // TODO - delete this once we add a field
 
 require_once('post_form.php');
 
-$mform_post = new mod_forum_post_form('post.php', array('course'=>$course, 'cm'=>$cm, 'coursecontext'=>$coursecontext, 'modcontext'=>$modcontext, 'forum'=>$forum, 'post'=>$post));
+$mform_post = new mod_forum_post_form('post.php', array('course'=>$course, 'cm'=>$cm, 'coursecontext'=>$coursecontext, 'modcontext'=>$modcontext, 'forum'=>$forum, 'post'=>$post), 'post', '', array('id' => 'mformforum'));
 
 $draftitemid = file_get_submitted_draft_itemid('attachments');
 file_prepare_draft_area($draftitemid, $modcontext->id, 'mod_forum', 'attachment', empty($post->id)?null:$post->id, mod_forum_post_form::attachment_options($forum));
@@ -528,8 +528,10 @@ if ($USER->id != $post->userid) {   // Not the original author, so add a message
     unset($data);
 }
 
+$formheading = '';
 if (!empty($parent)) {
     $heading = get_string("yourreply", "forum");
+    $formheading = get_string('reply', 'forum');
 } else {
     if ($forum->type == 'qanda') {
         $heading = get_string('yournewquestion', 'forum');
@@ -888,6 +890,9 @@ if (!empty($parent)) {
     }
 }
 
+if (!empty($formheading)) {
+    echo $OUTPUT->heading($formheading, 2, array('class' => 'accesshide'));
+}
 $mform_post->display();
 
 echo $OUTPUT->footer();
index 0413ee7..8a1af57 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$module->version   = 2012061700;       // The current module version (Date: YYYYMMDDXX)
+$module->version   = 2012061701;       // The current module version (Date: YYYYMMDDXX)
 $module->requires  = 2012061700;    // Requires this Moodle version
 $module->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
 $module->cron      = 60;
index 1f773a5..9677e22 100644 (file)
@@ -173,7 +173,7 @@ $PAGE->set_subpage($page->id);
 
 /// Print the header, heading and tabs
 lesson_add_fake_blocks($PAGE, $cm, $lesson, $timer);
-echo $lessonoutput->header($lesson, $cm, 'view', true, $page->id);
+echo $lessonoutput->header($lesson, $cm, 'view', true, $page->id, get_string('continue', 'lesson'));
 
 if ($lesson->displayleft) {
     echo '<a name="maincontent" id="maincontent" title="'.get_string('anchortitle', 'lesson').'"></a>';
index 1fdeab6..e7c8f0a 100644 (file)
@@ -47,7 +47,7 @@ if ($mode != get_user_preferences('lesson_view', 'collapsed') && $mode !== 'sing
 
 $lessonoutput = $PAGE->get_renderer('mod_lesson');
 $PAGE->navbar->add(get_string('edit'));
-echo $lessonoutput->header($lesson, $cm, $mode);
+echo $lessonoutput->header($lesson, $cm, $mode, false, null, get_string('edit', 'lesson'));
 
 if (!$lesson->has_pages()) {
     // There are no pages; give teacher some options
index 50b7f48..827f1a4 100644 (file)
@@ -64,7 +64,7 @@ $editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'max
 // tie up with the current form name, which in turn means the "submitted"
 // check ends up evaluating as false, thus it's not possible to check whether
 // the Question type selection was cancelled. For this reason, a dummy form
-// is created here solely to check whether the selection was cancelled. 
+// is created here solely to check whether the selection was cancelled.
 if ($qtype) {
     $mformdummy = $manager->get_page_form(0, array('editoroptions'=>$editoroptions, 'jumpto'=>$jumpto, 'lesson'=>$lesson, 'edit'=>$edit, 'maxbytes'=>$PAGE->course->maxbytes));
     if ($mformdummy->is_cancelled()) {
@@ -125,6 +125,6 @@ if ($data = $mform->get_data()) {
 }
 
 $lessonoutput = $PAGE->get_renderer('mod_lesson');
-echo $lessonoutput->header($lesson, $cm);
+echo $lessonoutput->header($lesson, $cm, '', false, null, get_string('edit', 'lesson'));
 $mform->display();
 echo $lessonoutput->footer();
index e3bc096..51d35cf 100644 (file)
@@ -303,7 +303,7 @@ switch ($mode) {
 add_to_log($course->id, 'lesson', 'view grade', "essay.php?id=$cm->id", get_string('manualgrading', 'lesson'), $cm->id);
 
 $lessonoutput = $PAGE->get_renderer('mod_lesson');
-echo $lessonoutput->header($lesson, $cm, 'essay');
+echo $lessonoutput->header($lesson, $cm, 'essay', false, null, get_string('manualgrading', 'lesson'));
 
 switch ($mode) {
     case 'display':
index a7c7307..f1027ba 100644 (file)
@@ -520,6 +520,37 @@ class qformat_default {
         return NULL;
     }
 
+    /**
+     * Construct a reasonable default question name, based on the start of the question text.
+     * @param string $questiontext the question text.
+     * @param string $default default question name to use if the constructed one comes out blank.
+     * @return string a reasonable question name.
+     */
+    public function create_default_question_name($questiontext, $default) {
+        $name = $this->clean_question_name(shorten_text($questiontext, 80));
+        if ($name) {
+            return $name;
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * Ensure that a question name does not contain anything nasty, and will fit in the DB field.
+     * @param string $name the raw question name.
+     * @return string a safe question name.
+     */
+    public function clean_question_name($name) {
+        $name = clean_param($name, PARAM_TEXT); // Matches what the question editing form does.
+        $name = trim($name);
+        $trimlength = 251;
+        while (textlib::strlen($name) > 255 && $trimlength > 0) {
+            $name = shorten_text($name, $trimlength);
+            $trimlength -= 10;
+        }
+        return $name;
+    }
+
     function defaultquestion() {
     // returns an "empty" question
     // Somewhere to specify question parameters that are not handled
index 86c7336..aefb7dc 100644 (file)
@@ -160,7 +160,7 @@ switch ($mode) {
 add_to_log($course->id, 'lesson', 'view highscores', "highscores.php?id=$cm->id", $lesson->name, $cm->id);
 
 $lessonoutput = $PAGE->get_renderer('mod_lesson');
-echo $lessonoutput->header($lesson, $cm, 'highscores');
+echo $lessonoutput->header($lesson, $cm, 'highscores', false, null, get_string('viewhighscores', 'lesson'));
 
 switch ($mode) {
     case 'add':
index f52affa..b389979 100644 (file)
@@ -206,6 +206,7 @@ $string['jump'] = 'Jump';
 $string['jumps'] = 'Jumps';
 $string['jumps_help'] = 'Each answer (for questions) or description (for content pages) has a corresponding jump. The jump can be relative, such as this page or next page, or absolute, specifying any one of the pages in the lesson.';
 $string['jumpsto'] = 'Jumps to <em>{$a}</em>';
+$string['leftduringtimedsession'] = 'You have left during a timed lesson.';
 $string['leftduringtimed'] = 'You have left during a timed lesson.<br />Please click on Continue to restart the lesson.';
 $string['leftduringtimednoretake'] = 'You have left during a timed lesson and you are<br />not allowed to retake or continue the lesson.';
 $string['lesson:addinstance'] = 'Add a new lesson';
index 11e7c9d..e76c910 100644 (file)
@@ -58,7 +58,7 @@ switch ($action) {
 
         $thispage = $lesson->load_page($pageid);
 
-        echo $lessonoutput->header($lesson, $cm);
+        echo $lessonoutput->header($lesson, $cm, '', false, null, get_string('deletingpage', 'lesson', format_string($thispage->title)));
         echo $OUTPUT->heading(get_string("deletingpage", "lesson", format_string($thispage->title)));
         // print the jumps to this page
         $params = array("lessonid" => $lesson->id, "pageid" => $pageid);
@@ -80,7 +80,7 @@ switch ($action) {
 
         $title = $DB->get_field("lesson_pages", "title", array("id" => $pageid));
 
-        echo $lessonoutput->header($lesson, $cm);
+        echo $lessonoutput->header($lesson, $cm, '', false, null, get_string('moving', 'lesson', format_String($title)));
         echo $OUTPUT->heading(get_string("moving", "lesson", format_string($title)));
 
         $params = array ("lessonid" => $lesson->id, "prevpageid" => 0);
index 11e645a..d7e95ca 100644 (file)
@@ -30,17 +30,22 @@ class mod_lesson_renderer extends plugin_renderer_base {
     /**
      * Returns the header for the lesson module
      *
-     * @param lesson $lesson
-     * @param string $currenttab
-     * @param bool $extraeditbuttons
-     * @param int $lessonpageid
+     * @param lesson $lesson a lesson object.
+     * @param string $currenttab current tab that is shown.
+     * @param bool   $extraeditbuttons if extra edit buttons should be displayed.
+     * @param int    $lessonpageid id of the lesson page that needs to be displayed.
+     * @param string $extrapagetitle String to appent to the page title.
      * @return string
      */
-    public function header($lesson, $cm, $currenttab = '', $extraeditbuttons = false, $lessonpageid = null) {
+    public function header($lesson, $cm, $currenttab = '', $extraeditbuttons = false, $lessonpageid = null, $extrapagetitle = null) {
         global $CFG;
 
         $activityname = format_string($lesson->name, true, $lesson->course);
-        $title = $this->page->course->shortname.": ".$activityname;
+        if (empty($extrapagetitle)) {
+            $title = $this->page->course->shortname.": ".$activityname;
+        } else {
+            $title = $this->page->course->shortname.": ".$activityname.": ".$extrapagetitle;
+        }
 
         // Build the buttons
         $context = context_module::instance($cm->id);
index 0657ca3..65410d9 100644 (file)
@@ -94,7 +94,7 @@ if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id)
 }
 
 if ($nothingtodisplay) {
-    echo $lessonoutput->header($lesson, $cm, $action);
+    echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('nolessonattempts', 'lesson'));
     echo $OUTPUT->notification(get_string('nolessonattempts', 'lesson'));
     echo $OUTPUT->footer();
     exit();
@@ -160,7 +160,7 @@ if ($action === 'delete') {
     /**************************************************************************
     this action is for default view and overview view
     **************************************************************************/
-    echo $lessonoutput->header($lesson, $cm, $action);
+    echo $lessonoutput->header($lesson, $cm, $action, false, null, get_string('overview', 'lesson'));
 
     $course_context = context_course::instance($course->id);
     if (has_capability('gradereport/grader:view', $course_context) && has_capability('moodle/grade:viewall', $course_context)) {
@@ -376,7 +376,7 @@ if ($action === 'delete') {
     4.  Print out the object which contains all the try info
 
 **************************************************************************/
-    echo $lessonoutput->header($lesson, $cm, $action);
+    echo $lessonoutput->header($lesson, $cm, $actionfalse, null, get_string('detailedstats', 'lesson'));
 
     $course_context = context_course::instance($course->id);
     if (has_capability('gradereport/grader:view', $course_context) && has_capability('moodle/grade:viewall', $course_context)) {
@@ -440,7 +440,7 @@ if ($action === 'delete') {
         $page = $lessonpages[$pageid];
         $answerpage = new stdClass;
         $data ='';
-        
+
         $answerdata = new stdClass;
         // Set some defaults for the answer data.
         $answerdata->score = NULL;
@@ -461,7 +461,7 @@ if ($action === 'delete') {
         if (empty($userid)) {
             // there is no userid, so set these vars and display stats.
             $answerpage->grayout = 0;
-            $useranswer = NULL;    
+            $useranswer = NULL;
         } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$try,"pageid"=>$page->id), "timeseen")) {
             // get the user's answer for this page
             // need to find the right one
index 3c3346d..30b219f 100644 (file)
@@ -73,7 +73,7 @@ if ($userhasgrade && !$lesson->retake) {
 ///     Check for high scores
 if (!$canmanage) {
     if (!$lesson->is_accessible()) {  // Deadline restrictions
-        echo $lessonoutput->header($lesson, $cm);
+        echo $lessonoutput->header($lesson, $cm, '', false, null, get_string('lessonnotready2', 'lesson'));
         if ($lesson->deadline != 0 && time() > $lesson->deadline) {
             echo $lessonoutput->lesson_inaccessible(get_string('lessonclosed', 'lesson', userdate($lesson->deadline)));
         } else {
@@ -91,7 +91,7 @@ if (!$canmanage) {
                 redirect("$CFG->wwwroot/mod/lesson/view.php?id=$cm->id");
             }
         } else {
-            echo $lessonoutput->header($lesson, $cm);
+            echo $lessonoutput->header($lesson, $cm, '', false, null, get_string('passwordprotectedlesson', 'lesson', format_string($lesson->name)));
             echo $lessonoutput->login_prompt($lesson, $userpassword !== '');
             echo $lessonoutput->footer();
             exit();
@@ -144,7 +144,7 @@ if (!$canmanage) {
             }
 
             if (!empty($errors)) {  // print out the errors if any
-                echo $lessonoutput->header($lesson, $cm);
+                echo $lessonoutput->header($lesson, $cm, '', false, null, get_string('completethefollowingconditions', 'lesson', format_string($lesson->name)));
                 echo $lessonoutput->dependancy_errors($dependentlesson, $errors);
                 echo $lessonoutput->footer();
                 exit();
@@ -226,7 +226,7 @@ if (empty($pageid)) {
         }
     }
     if (isset($lastpageseen) && $DB->count_records('lesson_attempts', array('lessonid'=>$lesson->id, 'userid'=>$USER->id, 'retry'=>$retries)) > 0) {
-        echo $lessonoutput->header($lesson, $cm);
+        echo $lessonoutput->header($lesson, $cm, '', false, null, get_string('leftduringtimedsession', 'lesson'));
         if ($lesson->timed) {
             if ($lesson->retake) {
                 $continuelink = new single_button(new moodle_url('/mod/lesson/view.php', array('id'=>$cm->id, 'pageid'=>$lesson->firstpageid, 'startlastseen'=>'no')), get_string('continue', 'lesson'), 'get');
@@ -244,7 +244,7 @@ if (empty($pageid)) {
 
     if ($attemptflag) {
         if (!$lesson->retake) {
-            echo $lessonoutput->header($lesson, $cm, 'view');
+            echo $lessonoutput->header($lesson, $cm, 'view', '', null, get_string("noretake", "lesson"));
             $courselink = new single_button(new moodle_url('/course/view.php', array('id'=>$PAGE->course->id)), get_string('returntocourse', 'lesson'), 'get');
             echo $lessonoutput->message(get_string("noretake", "lesson"), $courselink);
             echo $lessonoutput->footer();
@@ -349,6 +349,7 @@ if ($pageid != LESSON_EOL) {
     $currenttab = 'view';
     $extraeditbuttons = true;
     $lessonpageid = $page->id;
+    $extrapagetitle = $page->title;
 
     if (($edit != -1) && $PAGE->user_allowed_editing()) {
         $USER->editing = $edit;
@@ -387,7 +388,7 @@ if ($pageid != LESSON_EOL) {
     }
 
     lesson_add_fake_blocks($PAGE, $cm, $lesson, $timer);
-    echo $lessonoutput->header($lesson, $cm, $currenttab, $extraeditbuttons, $lessonpageid);
+    echo $lessonoutput->header($lesson, $cm, $currenttab, $extraeditbuttons, $lessonpageid, $extrapagetitle);
     if ($attemptflag) {
         // We are using level 3 header because attempt heading is a sub-heading of lesson title (MDL-30911).
         echo $OUTPUT->heading(get_string('attempt', 'lesson', $retries), 3);
@@ -572,7 +573,7 @@ if ($pageid != LESSON_EOL) {
     $lessoncontent .= html_writer::link($url, get_string('viewgrades', 'lesson'), array('class'=>'centerpadded lessonbutton standardbutton'));
 
     lesson_add_fake_blocks($PAGE, $cm, $lesson, $timer);
-    echo $lessonoutput->header($lesson, $cm, $currenttab, $extraeditbuttons, $lessonpageid);
+    echo $lessonoutput->header($lesson, $cm, $currenttab, $extraeditbuttons, $lessonpageid, get_string("congratulations", "lesson"));
     echo $lessoncontent;
     echo $lessonoutput->footer();
 }
index f119da0..4b57900 100644 (file)
@@ -518,7 +518,7 @@ abstract class quiz_attempts_report_table extends table_sql {
             return;
         }
 
-        $url = new moodle_url($this->reporturl, $this->displayoptions);
+        $url = $this->options->get_url();
         $url->param('sesskey', sesskey());
 
         echo '<div id="tablecontainer">';
index 535fce1..126acb8 100644 (file)
@@ -1183,33 +1183,37 @@ function scorm_get_completion_state($course, $cm, $userid, $type) {
     if (!$scorm = $DB->get_record('scorm', array('id' => $cm->instance))) {
         print_error('cannotfindscorm');
     }
-
-    // Get user's tracks data
-    $tracks = $DB->get_records_sql(
-        "
-        SELECT
-            id,
-            element,
-            value
-        FROM
-            {scorm_scoes_track}
-        WHERE
-            scormid = ?
-        AND userid = ?
-        AND element IN
-        (
-            'cmi.core.lesson_status',
-            'cmi.completion_status',
-            'cmi.success_status',
-            'cmi.core.score.raw',
-            'cmi.score.raw'
-        )
-        ",
-        array($scorm->id, $userid)
-    );
-
-    if (!$tracks) {
-        return completion_info::aggregate_completion_states($type, $result, false);
+    // Only check for existence of tracks and return false if completionstatusrequired or completionscorerequired
+    // this means that if only view is required we don't end up with a false state.
+    if ($scorm->completionstatusrequired !== null ||
+        $scorm->completionscorerequired !== null) {
+        // Get user's tracks data.
+        $tracks = $DB->get_records_sql(
+            "
+            SELECT
+                id,
+                element,
+                value
+            FROM
+                {scorm_scoes_track}
+            WHERE
+                scormid = ?
+            AND userid = ?
+            AND element IN
+            (
+                'cmi.core.lesson_status',
+                'cmi.completion_status',
+                'cmi.success_status',
+                'cmi.core.score.raw',
+                'cmi.score.raw'
+            )
+            ",
+            array($scorm->id, $userid)
+        );
+
+        if (!$tracks) {
+            return completion_info::aggregate_completion_states($type, $result, false);
+        }
     }
 
     // Check for status
index c5b2335..ded9452 100644 (file)
@@ -8,6 +8,10 @@ new features:
 
 * mod/xxx/adminlib.php may now include 'plugininfo_yoursubplugintype' class definition
   used by plugin_manager; it is recommended to store extra admin settings classes in this file
+  
+optional - no changes needed:
+
+* mod_lesson_renderer::header() now accepts an additional parameter $extrapagetitle
 
 === 2.3 ===
 
index dafea3f..aa2852c 100644 (file)
@@ -48,6 +48,11 @@ require_login();
 $strmymoodle = get_string('myhome');
 
 if (isguestuser()) {  // Force them to see system default, no editing allowed
+    // If guests are not allowed my moodle, send them to front page.
+    if (empty($CFG->allowguestmymoodle)) {
+        redirect(new moodle_url('/', array('redirect' => 0)));
+    }
+
     $userid = NULL; 
     $USER->editing = $edit = 0;  // Just in case
     $context = context_system::instance();
@@ -81,11 +86,13 @@ $PAGE->set_subpage($currentpage->id);
 $PAGE->set_title($header);
 $PAGE->set_heading($header);
 
-if (get_home_page() != HOMEPAGE_MY) {
-    if (optional_param('setdefaulthome', false, PARAM_BOOL)) {
-        set_user_preference('user_home_page_preference', HOMEPAGE_MY);
-    } else if (!empty($CFG->defaulthomepage) && $CFG->defaulthomepage == HOMEPAGE_USER) {
-        $PAGE->settingsnav->get('usercurrentsettings')->add(get_string('makethismyhome'), new moodle_url('/my/', array('setdefaulthome'=>true)), navigation_node::TYPE_SETTING);
+if (!isguestuser()) {   // Skip default home page for guests
+    if (get_home_page() != HOMEPAGE_MY) {
+        if (optional_param('setdefaulthome', false, PARAM_BOOL)) {
+            set_user_preference('user_home_page_preference', HOMEPAGE_MY);
+        } else if (!empty($CFG->defaulthomepage) && $CFG->defaulthomepage == HOMEPAGE_USER) {
+            $PAGE->settingsnav->get('usercurrentsettings')->add(get_string('makethismyhome'), new moodle_url('/my/', array('setdefaulthome'=>true)), navigation_node::TYPE_SETTING);
+        }
     }
 }
 
index 1e65824..cf5e58a 100644 (file)
@@ -39,6 +39,9 @@
         <testsuite name="core_files">
             <directory suffix="_test.php">lib/filestorage/tests</directory>
         </testsuite>
+        <testsuite name="core_cohort">
+            <directory suffix="_test.php">cohort/tests</directory>
+        </testsuite>
         <testsuite name="core_grade">
             <directory suffix="_test.php">lib/grade/tests</directory>
             <directory suffix="_test.php">grade/tests</directory>
index 39d6c02..d40d62a 100644 (file)
@@ -477,7 +477,7 @@ abstract class question_behaviour {
      */
     public static function is_manual_grade_in_range($qubaid, $slot) {
         $prefix = 'q' . $qubaid . ':' . $slot . '_';
-        $mark = optional_param($prefix . '-mark', null, PARAM_FLOAT);
+        $mark = question_utils::optional_param_mark($prefix . '-mark');
         $maxmark = optional_param($prefix . '-maxmark', null, PARAM_FLOAT);
         $minfraction = optional_param($prefix . ':minfraction', null, PARAM_FLOAT);
         return is_null($mark) || ($mark >= $minfraction * $maxmark && $mark <= $maxmark);
index d9c7d0f..9fdfc03 100644 (file)
@@ -789,6 +789,33 @@ abstract class question_utils {
         return self::$thousands[$number / 1000 % 10] . self::$hundreds[$number / 100 % 10] .
                 self::$tens[$number / 10 % 10] . self::$units[$number % 10];
     }
+
+    /**
+     * Typically, $mark will have come from optional_param($name, null, PARAM_RAW_TRIMMED).
+     * This method copes with:
+     *  - keeping null or '' input unchanged.
+     *  - nubmers that were typed as either 1.00 or 1,00 form.
+     *
+     * @param string|null $mark raw use input of a mark.
+     * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
+     */
+    public static function clean_param_mark($mark) {
+        if ($mark === '' || is_null($mark)) {
+            return $mark;
+        }
+
+        return clean_param(str_replace(',', '.', $mark), PARAM_FLOAT);
+    }
+
+    /**
+     * Get a sumitted variable (from the GET or POST data) that is a mark.
+     * @param string $parname the submitted variable name.
+     * @return float|string|null cleaned mark as a float if possible. Otherwise '' or null.
+     */
+    public static function optional_param_mark($parname) {
+        return self::clean_param_mark(
+                optional_param($parname, null, PARAM_RAW_TRIMMED));
+    }
 }
 
 
index 8c88d50..57041f0 100644 (file)
@@ -883,12 +883,7 @@ class question_attempt {
         switch ($type) {
             case self::PARAM_MARK:
                 // Special case to work around PARAM_FLOAT converting '' to 0.
-                $mark = $this->get_submitted_var($name, PARAM_RAW_TRIMMED, $postdata);
-                if ($mark === '' || is_null($mark)) {
-                    return $mark;
-                } else {
-                    return clean_param(str_replace(',', '.', $mark), PARAM_FLOAT);
-                }
+                return question_utils::clean_param_mark($this->get_submitted_var($name, PARAM_RAW_TRIMMED, $postdata));
 
             case self::PARAM_FILES:
                 return $this->process_response_files($name, $name, $postdata);
index e51d4db..e5935f4 100644 (file)
@@ -191,4 +191,14 @@ class question_utils_test extends advanced_testcase {
         $this->setExpectedException('moodle_exception');
         question_utils::int_to_roman(1.5);
     }
-}
\ No newline at end of file
+
+    public function test_clean_param_mark() {
+        $this->assertNull(question_utils::clean_param_mark(null));
+        $this->assertSame('', question_utils::clean_param_mark(''));
+        $this->assertSame(0.0, question_utils::clean_param_mark('0'));
+        $this->assertSame(1.5, question_utils::clean_param_mark('1.5'));
+        $this->assertSame(1.5, question_utils::clean_param_mark('1,5'));
+        $this->assertSame(-1.5, question_utils::clean_param_mark('-1.5'));
+        $this->assertSame(-1.5, question_utils::clean_param_mark('-1,5'));
+    }
+}
index 980f209..10f093e 100644 (file)
@@ -637,6 +637,37 @@ class qformat_default {
         return $question;
     }
 
+    /**
+     * Construct a reasonable default question name, based on the start of the question text.
+     * @param string $questiontext the question text.
+     * @param string $default default question name to use if the constructed one comes out blank.
+     * @return string a reasonable question name.
+     */
+    public function create_default_question_name($questiontext, $default) {
+        $name = $this->clean_question_name(shorten_text($questiontext, 80));
+        if ($name) {
+            return $name;
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * Ensure that a question name does not contain anything nasty, and will fit in the DB field.
+     * @param string $name the raw question name.
+     * @return string a safe question name.
+     */
+    public function clean_question_name($name) {
+        $name = clean_param($name, PARAM_TEXT); // Matches what the question editing form does.
+        $name = trim($name);
+        $trimlength = 251;
+        while (textlib::strlen($name) > 255 && $trimlength > 0) {
+            $name = shorten_text($name, $trimlength);
+            $trimlength -= 10;
+        }
+        return $name;
+    }
+
     /**
      * Add a blank combined feedback to a question object.
      * @param object question
index 8c3cbc9..35d5afc 100644 (file)
@@ -95,7 +95,7 @@ class qformat_aiken extends qformat_default {
                 } else {
                     // Must be the first line of a new question, since no recognised prefix.
                     $question->qtype = 'multichoice';
-                    $question->name = shorten_text(s($nowline), 50);
+                    $question->name = $this->create_default_question_name($nowline, get_string('questionname', 'question'));
                     $question->questiontext = htmlspecialchars(trim($nowline), ENT_NOQUOTES);
                     $question->questiontextformat = FORMAT_HTML;
                     $question->generalfeedback = '';
index 1e7fa0d..9d30323 100644 (file)
@@ -123,13 +123,9 @@ class qformat_blackboard extends qformat_based_on_xml {
             $question->questiontext = $text;
         }
         // Put name in question object. We must ensure it is not empty and it is less than 250 chars.
-        $question->name = shorten_text(strip_tags($question->questiontext), 200);
-        $question->name = substr($question->name, 0, 250);
-        if (!$question->name) {
-            $id = $this->getpath($questiondata,
-                    array('@', 'id'), '',  true);
-            $question->name = get_string('defaultname', 'qformat_blackboard' , $id);
-        }
+        $id = $this->getpath($questiondata, array('@', 'id'), '',  true);
+        $question->name = $this->create_default_question_name($question->questiontext,
+                get_string('defaultname', 'qformat_blackboard', $id));
 
         $question->generalfeedback = '';
         $question->generalfeedbackformat = FORMAT_HTML;
index 80867f3..a6c942c 100644 (file)
@@ -92,13 +92,9 @@ class qformat_blackboard_six_pool extends qformat_blackboard_six_base {
         $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it.
 
         // Put name in question object. We must ensure it is not empty and it is less than 250 chars.
-        $question->name = shorten_text(strip_tags($question->questiontext['text']), 200);
-        $question->name = substr($question->name, 0, 250);
-        if (!$question->name) {
-            $id = $this->getpath($questiondata,
-                    array('@', 'id'), '',  true);
-            $question->name = get_string('defaultname', 'qformat_blackboard_six' , $id);
-        }
+        $id = $this->getpath($questiondata, array('@', 'id'), '',  true);
+        $question->name = $this->create_default_question_name($question->questiontext['text'],
+                get_string('defaultname', 'qformat_blackboard_six' , $id));
 
         $question->generalfeedback = '';
         $question->generalfeedbackformat = FORMAT_HTML;
index 223d9f6..37545fd 100644 (file)
@@ -510,11 +510,8 @@ class qformat_blackboard_six_qti extends qformat_blackboard_six_base {
         $question->questiontext = $this->cleaned_text_field($text);
         $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it.
 
-        $question->name = shorten_text(strip_tags($question->questiontext['text']), 200);
-        $question->name = substr($question->name, 0, 250);
-        if (!$question->name) {
-            $question->name = get_string('defaultname', 'qformat_blackboard_six' , $quest->id);
-        }
+        $question->name = $this->create_default_question_name($question->questiontext['text'],
+                get_string('defaultname', 'qformat_blackboard_six' , $quest->id));
         $question->generalfeedback = '';
         $question->generalfeedbackformat = FORMAT_HTML;
         $question->generalfeedbackfiles = array();
index d131266..2155401 100644 (file)
@@ -131,7 +131,7 @@ class qformat_examview extends qformat_based_on_xml {
             $question->questiontext = $htmltext;
             $question->questiontextformat = FORMAT_HTML;
             $question->questiontextfiles = array();
-            $question->name = shorten_text( $question->questiontext, 250 );
+            $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
             $question->qtype = 'match';
             $question = $this->add_blank_combined_feedback($question);
             $question->subquestions = array();
@@ -200,7 +200,7 @@ class qformat_examview extends qformat_based_on_xml {
         $question->questiontext = $this->cleaninput($htmltext);
         $question->questiontextformat = FORMAT_HTML;
         $question->questiontextfiles = array();
-        $question->name = shorten_text( $question->questiontext, 250 );
+        $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
 
         switch ($question->qtype) {
             case 'multichoice':
index eb60839..0bfc038 100644 (file)
@@ -206,7 +206,7 @@ class qformat_gift extends qformat_default {
                 // name will be assigned after processing question text below
             } else {
                 $questionname = substr($text, 0, $namefinish);
-                $question->name = trim($this->escapedchar_post($questionname));
+                $question->name = $this->clean_question_name($this->escapedchar_post($questionname));
                 $text = trim(substr($text, $namefinish+2)); // Remove name from text
             }
         } else {
@@ -265,13 +265,9 @@ class qformat_gift extends qformat_default {
 
         // set question name if not already set
         if ($question->name === false) {
-            $question->name = $question->questiontext;
+            $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
         }
 
-        // ensure name is not longer than 250 characters
-        $question->name = shorten_text($question->name, 200);
-        $question->name = strip_tags(substr($question->name, 0, 250));
-
         // determine QUESTION TYPE
         $question->qtype = NULL;
 
index 3bd31b4..0fbc450 100644 (file)
@@ -126,10 +126,7 @@ class qformat_learnwise extends qformat_default {
 
         $question = $this->defaultquestion();
         $question->qtype = 'multichoice';
-        $question->name = substr($questiontext, 0, 30);
-        if (strlen($questiontext) > 30) {
-            $question->name .= '...';
-        }
+        $question->name = $this->create_default_question_name($questiontext, get_string('questionname', 'question'));
 
         $question->questiontext = $questiontext;
         $question->single = ($type == 'multichoice') ? 1 : 0;
index a1b3cea..c79998d 100644 (file)
@@ -89,7 +89,7 @@ class qformat_missingword extends qformat_default {
 
         /// Save the new question text
         $question->questiontext = substr_replace($text, "_____", $answerstart, $answerlength+1);
-        $question->name = $question->questiontext;
+        $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
 
 
         /// Parse the answers
index d7a7ac7..b7c412e 100644 (file)
@@ -62,18 +62,7 @@ class qformat_multianswer extends qformat_default {
         $question->penalty = 0.3333333;
 
         if (!empty($question)) {
-            $name = html_to_text(implode(' ', $lines));
-            $name = preg_replace('/{[^}]*}/', '', $name);
-            $name = trim($name);
-
-            if ($name) {
-                $question->name = shorten_text($name, 45);
-            } else {
-                // We need some name, so use the current time, since that will be
-                // reasonably unique.
-                $question->name = userdate(time());
-            }
-
+            $question->name = $this->create_default_question_name($question->questiontext, get_string('questionname', 'question'));
             $questions[] = $question;
         }
 
index 29c3224..8e1d301 100644 (file)
@@ -47,7 +47,9 @@ class qformat_multianswer_test extends question_testcase {
         $qs = $importer->readquestions($lines);
 
         $expectedq = (object) array(
-            'name' => 'Match the following cities with the ...',
+            'name' => 'Match the following cities with the correct state:
+* San Francisco: {#1}
+* ...',
             'questiontext' => 'Match the following cities with the correct state:
 * San Francisco: {#1}
 * Tucson: {#2}
index 6facb80..8692542 100644 (file)
@@ -259,11 +259,8 @@ class qformat_webct extends qformat_default {
 
                     // Setup default value of missing fields
                     if (!isset($question->name)) {
-                        $question->name = $question->questiontext;
-                    }
-                    if (strlen($question->name) > 255) {
-                        $question->name = substr($question->name,0,250)."...";
-                        $warnings[] = get_string("questionnametoolong", "qformat_webct", $nQuestionStartLine);
+                        $question->name = $this->create_default_question_name(
+                                $question->questiontext, get_string('questionname', 'question'));
                     }
                     if (!isset($question->defaultmark)) {
                         $question->defaultmark = 1;
@@ -466,11 +463,7 @@ class qformat_webct extends qformat_default {
 
             if (preg_match("~^:TITLE:(.*)~i",$line,$webct_options)) {
                 $name = trim($webct_options[1]);
-                if (strlen($name) > 255) {
-                    $name = substr($name,0,250)."...";
-                    $warnings[] = get_string("questionnametoolong", "qformat_webct", $nLineCounter);
-                }
-                $question->name = $name;
+                $question->name = $this->clean_question_name($name);
                 continue;
             }
 
index 7cdb23c..9b37215 100644 (file)
@@ -166,9 +166,9 @@ class qformat_xml extends qformat_default {
         $qo = $this->defaultquestion();
 
         // Question name
-        $qo->name = $this->getpath($question,
+        $qo->name = $this->clean_question_name($this->getpath($question,
                 array('#', 'name', 0, '#', 'text', 0, '#'), '', true,
-                get_string('xmlimportnoname', 'qformat_xml'));
+                get_string('xmlimportnoname', 'qformat_xml')));
         $qo->questiontext = $this->getpath($question,
                 array('#', 'questiontext', 0, '#', 'text', 0, '#'), '', true);
         $qo->questiontextformat = $this->trans_format($this->getpath(
@@ -437,7 +437,7 @@ class qformat_xml extends qformat_default {
         $qo->course = $this->course;
         $qo->generalfeedback = '';
 
-        $qo->name = $this->import_text($question['#']['name'][0]['#']['text']);
+        $qo->name = $this->clean_question_name($this->import_text($question['#']['name'][0]['#']['text']));
         $qo->questiontextformat = $questiontext['format'];
         $qo->questiontext = $qo->questiontext['text'];
         $qo->questiontextfiles = array();
index 0ffe435..96d0704 100644 (file)
@@ -87,4 +87,71 @@ class qformat_default_test extends advanced_testcase {
         $path = '<evil>Nasty <virus //> thing<//evil>';
         $this->assertEquals(array('Nasty  thing'), $format->split_category_path($path));
     }
+
+    public function test_clean_question_name() {
+        $format = new testable_qformat();
+
+        $name = 'Nice simple name';
+        $this->assertEquals($name, $format->clean_question_name($name));
+
+        $name = 'Question in <span lang="en" class="multilang">English</span><span lang="fr" class="multilang">French</span>';
+        $this->assertEquals($name, $format->clean_question_name($name));
+
+        $name = 'Evil <script type="text/javascrip">alert("You have been hacked!");</script>';
+        $this->assertEquals('Evil alert("You have been hacked!");', $format->clean_question_name($name));
+
+        $name = 'This is a very long question name. It goes on and on and on. ' .
+                'I wonder if it will ever stop. The quetsion name field in the database is only ' .
+                'two hundred and fifty five characters wide, so if the import file contains a ' .
+                'name longer than that, the code had better truncate it!';
+        $this->assertEquals(shorten_text($name, 251), $format->clean_question_name($name));
+
+        // The worst case scenario is a whole lot of single charaters in separate multilang tags.
+        $name = '<span lang="en" class="multilang">A</span>' .
+                '<span lang="fr" class="multilang">B</span>' .
+                '<span lang="fr_ca" class="multilang">C</span>' .
+                '<span lang="en_us" class="multilang">D</span>' .
+                '<span lang="de" class="multilang">E</span>' .
+                '<span lang="cz" class="multilang">F</span>' .
+                '<span lang="it" class="multilang">G</span>' .
+                '<span lang="es" class="multilang">H</span>' .
+                '<span lang="pt" class="multilang">I</span>' .
+                '<span lang="ch" class="multilang">J</span>';
+        $this->assertEquals(shorten_text($name, 1), $format->clean_question_name($name));
+    }
+
+    public function test_create_default_question_name() {
+        $format = new testable_qformat();
+
+        $text = 'Nice simple name';
+        $this->assertEquals($text, $format->create_default_question_name($text, 'Default'));
+
+        $this->assertEquals('Default', $format->create_default_question_name('', 'Default'));
+
+        $text = 'Question in <span lang="en" class="multilang">English</span><span lang="fr" class="multilang">French</span>';
+        $this->assertEquals($text, $format->create_default_question_name($text, 'Default'));
+
+        $text = 'Evil <script type="text/javascrip">alert("You have been hacked!");</script>';
+        $this->assertEquals('Evil alert("You have been hacked!");',
+                $format->create_default_question_name($text, 'Default'));
+
+        $text = 'This is a very long question text. It goes on and on and on. ' .
+                'I wonder if it will ever stop. The question name field in the database is only ' .
+                'two hundred and fifty five characters wide, so if the import file contains a ' .
+                'name longer than that, the code had better truncate it!';
+        $this->assertEquals(shorten_text($text, 80), $format->create_default_question_name($text, 'Default'));
+
+        // The worst case scenario is a whole lot of single charaters in separate multilang tags.
+        $text = '<span lang="en" class="multilang">A</span>' .
+                '<span lang="fr" class="multilang">B</span>' .
+                '<span lang="fr_ca" class="multilang">C</span>' .
+                '<span lang="en_us" class="multilang">D</span>' .
+                '<span lang="de" class="multilang">E</span>' .
+                '<span lang="cz" class="multilang">F</span>' .
+                '<span lang="it" class="multilang">G</span>' .
+                '<span lang="es" class="multilang">H</span>' .
+                '<span lang="pt" class="multilang">I</span>' .
+                '<span lang="ch" class="multilang">J</span>';
+        $this->assertEquals(shorten_text($text, 1), $format->create_default_question_name($text, 'Default'));
+    }
 }
index 342c0ab..dbe5775 100644 (file)
@@ -117,7 +117,11 @@ require_capability('report/log:view', $context);
 
 add_to_log($course->id, "course", "report log", "report/log/index.php?id=$course->id", $course->id);
 
-$strlogs = get_string('logs');
+if (!empty($page)) {
+    $strlogs = get_string('logs'). ": ". get_string('page', 'report_log', $page+1);
+} else {
+    $strlogs = get_string('logs');
+}
 $stradministration = get_string('administration');
 $strreports = get_string('reports');
 
@@ -139,6 +143,7 @@ if (!empty($chooselog)) {
         case 'showashtml':
             if ($hostid != $CFG->mnet_localhost_id || $course->id == SITEID) {
                 admin_externalpage_setup('reportlog');
+                $PAGE->set_title($course->shortname .': '. $strlogs);
                 echo $OUTPUT->header();
 
             } else {
index 677fab5..e613533 100644 (file)
@@ -26,6 +26,7 @@
 
 $string['log:view'] = 'View course logs';
 $string['log:viewtoday'] = 'View today\'s logs';
+$string['page'] = 'Page {$a}';
 $string['page-report-log-x'] = 'Any log report';
 $string['page-report-log-index'] = 'Course log report';
 $string['page-report-log-user'] = 'User course log report';
index df0e64f..ceafc02 100644 (file)
@@ -29,6 +29,8 @@
 #page-admin-course-index .editcourse th,
 #page-admin-course-index .editcourse td {padding-left:10px;padding-right:10px;}
 #page-admin-course-index .editcourse .count {text-align:right;}
+#page-admin-course-index.dir-rtl .editcourse td[align="left"] {text-align: right;}
+#page-admin-course-index.dir-rtl .editcourse td[align="right"] {text-align: left;}
 
 #page-admin-report-security-index .timewarninghidden {display:none;}
 #page-admin-report-security-index .statuswarning {background-color: #f0e000;}
 #adminsettings .form-item .form-label {display: block;float: left;width: 12.5em;text-align: right;}
 #adminsettings .form-item .form-label .form-shortname {display:block;}
 #adminsettings .form-item .form-setting {display: block;margin-left: 13.5em;text-align: left;}
+.dir-rtl #admin-spelllanguagelist textarea,
+#page-admin-setting-editorsettingstinymce.dir-rtl .form-textarea textarea {text-align: left;direction: ltr;}
 #adminsettings .form-item .form-setting .form-htmlarea {width:  640px;display:inline;}
 #adminsettings .form-item .form-setting .form-htmlarea .htmlarea {width:  640px;display:block;}
 #adminsettings .form-item .form-setting .form-multicheckbox li {list-style: none;}
index ce835c1..242735f 100644 (file)
@@ -449,10 +449,15 @@ body.tag .managelink {padding: 5px;}
 .path-backup .mform .grouped_settings.section_level {clear:both;}
 .path-backup .mform .grouped_settings {clear:both;overflow:hidden;}
 .path-backup .mform .grouped_settings .fitem .fitemtitle {width:40%;padding-right:10px;}
+.path-backup.dir-rtl .mform .grouped_settings .fitem .fitemtitle {width: 60%;}
 .path-backup .mform .grouped_settings .fitem .felement {width:50%;}
+.path-backup.dir-rtl .mform .grouped_settings .fitem .felement {width: 99%;}
 .path-backup .mform .grouped_settings.section_level .include_setting {width:50%;margin:0;float:left;clear:left;font-weight:bold;}
+.path-backup.dir-rtl  .mform .grouped_settings.section_level .include_setting {float: right; clear: right;}
 .path-backup .mform .grouped_settings.section_level .normal_setting {width:50%;margin:0;margin-left:50%;}
+.path-backup.dir-rtl  .mform .grouped_settings.section_level .normal_setting {margin:0;}
 .path-backup .mform .grouped_settings.activity_level .include_setting label {font-weight:normal;}
+.path-backup.dir-rtl .mform .grouped_settings.activity_level .include_setting label img {float:right;}
 .path-backup .mform .fitem fieldset.felement {margin-left:0;width:auto;padding-left:0;}
 .path-backup .notification.dependencies_enforced {text-align:center;color:#A00;font-weight:bold;}
 .path-backup .backup_progress {text-align:center;}
@@ -554,6 +559,7 @@ body.tag .managelink {padding: 5px;}
 .userenrolment .col_role {width:262px;}
 .userenrolment .col_role .roles {margin-right:30px;}
 .userenrolment .col_role .role {float:left;padding:3px;margin:3px;}
+.dir-rtl .userenrolment .col_role .role {float:right;}
 .userenrolment .col_role .role a {margin-left:3px;cursor:pointer;}
 .userenrolment .col_role .addrole {float:right;width:18px;margin:3px;height:18px;text-align:center;}
 .userenrolment .col_role .addrole a img {vertical-align:bottom;}
@@ -892,6 +898,9 @@ sup {vertical-align: super;}
     -webkit-box-shadow: inset 0px 0px 30px 0px #CCCCCC;
     -moz-box-shadow: inset 0px 0px 30px 0px #CCCCCC;
 }
+.dir-rtl.jsenabled .choosercontainer #chooseform .alloptions {
+    max-width: 18.3em;
+}
 
 /* Settings for option rows and option subtypes */
 .choosercontainer #chooseform .moduletypetitle,
@@ -936,6 +945,9 @@ sup {vertical-align: super;}
     margin-bottom: 0;
     padding: 0 1.6em 0 3.2em;
 }
+.dir-rtl .choosercontainer #chooseform .subtype {
+    padding: 0 2.6em 0 3.2em;
+}
 
 .choosercontainer #chooseform .subtype .typename {
     margin: 0 0 0 0.2em;
index 0c33568..3a73692 100644 (file)
 .course-content .single-section .section-navigation { display: block; padding: 0.5em; margin-bottom: -0.5em; }
 .course-content .single-section .section-navigation .title { font-weight: bold; font-size: 108%; }
 .course-content .single-section .section-navigation .mdl-left { font-weight: normal; float: left; margin-right: 1em; }
+.dir-rtl .course-content .single-section .section-navigation .mdl-left {float:right;}
 .course-content .single-section .section-navigation .mdl-left .larrow { margin-right: 0.1em; }
 .course-content .single-section .section-navigation .mdl-right { font-weight: normal; float: right; margin-left: 1em; }
+.dir-rtl .course-content .single-section .section-navigation .mdl-right {float: left;}
 .course-content .single-section .section-navigation .mdl-right .rarrow { margin-left: 0.1em; }
 .course-content .single-section .section-navigation .mdl-bottom { margin-top: 0; }