Merge branch 'wip-MDL-36967-master' of git://github.com/marinaglancy/moodle
authorSam Hemelryk <sam@moodle.com>
Tue, 15 Jan 2013 22:01:16 +0000 (11:01 +1300)
committerSam Hemelryk <sam@moodle.com>
Tue, 15 Jan 2013 22:01:16 +0000 (11:01 +1300)
Conflicts:
lib/deprecatedlib.php

184 files changed:
admin/blocks.php
admin/filters.php
admin/localplugins.php
admin/plugins.php
admin/qtypes.php
admin/renderer.php
admin/repository.php
admin/roles/assign.php
admin/roles/manage.php
admin/settings/appearance.php
admin/settings/plugins.php
admin/tool/uploaduser/index.php
admin/tool/xmldb/actions/main_view/main_view.class.php
admin/user.php
backup/moodle2/backup_gradingform_plugin.class.php
backup/moodle2/restore_gradingform_plugin.class.php
backup/moodle2/restore_stepslib.php
blocks/completionstatus/details.php
blocks/site_main_menu/block_site_main_menu.php
blocks/social_activities/block_social_activities.php
calendar/export_execute.php
calendar/externallib.php [new file with mode: 0644]
calendar/renderer.php
calendar/tests/externallib_tests.php [new file with mode: 0644]
cohort/index.php
comment/comment.js
comment/locallib.php
course/dndupload.js
course/dndupload.php
course/dnduploadlib.php
course/edit_form.php
course/editsection_form.php
course/externallib.php
course/format/renderer.php
course/index.php
course/jumpto.php
course/lib.php
course/modduplicate.php
course/renderer.php
course/tests/externallib_test.php
course/togglecompletion.php
course/view.php
course/yui/modchooser/modchooser.js
course/yui/toolboxes/toolboxes.js
enrol/imsenterprise/lang/en/enrol_imsenterprise.php
enrol/imsenterprise/lib.php
enrol/imsenterprise/locallib.php
enrol/imsenterprise/settings.php
enrol/imsenterprise/tests/imsenterprise_test.php [new file with mode: 0644]
enrol/ldap/cli/sync.php
enrol/ldap/lib.php
enrol/ldap/tests/ldap_test.php [new file with mode: 0644]
enrol/paypal/cli/sync.php [new file with mode: 0644]
enrol/paypal/lang/en/enrol_paypal.php
enrol/paypal/lib.php
enrol/paypal/settings.php
enrol/paypal/tests/paypal_test.php [new file with mode: 0644]
enrol/paypal/version.php
enrol/self/db/upgrade.php
enrol/self/edit.php
enrol/self/edit_form.php
enrol/self/lang/en/enrol_self.php
enrol/self/lib.php
enrol/self/settings.php
enrol/self/tests/self_test.php
enrol/self/version.php
enrol/tests/externallib_test.php
filter/manage.php
filter/tex/README.mimetex
filter/tex/mimetex.darwin
filter/tex/mimetex.exe
filter/tex/mimetex.freebsd
filter/tex/mimetex.linux
filter/tex/readme_moodle.txt
grade/grading/form/lib.php
grade/grading/form/rubric/backup/moodle2/backup_gradingform_rubric_plugin.class.php
grade/grading/form/rubric/backup/moodle2/restore_gradingform_rubric_plugin.class.php
grade/grading/form/rubric/db/upgrade.php
grade/grading/form/rubric/edit.php
grade/grading/form/rubric/edit_form.php
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/lib.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/rubriceditor.php
grade/grading/form/rubric/version.php
grade/grading/lib.php
grade/grading/manage.php
grade/grading/pick.php
grade/grading/pick_form.php
grade/grading/renderer.php
grade/grading/tests/lib_test.php
grade/report/user/lib.php
grade/tests/reportuser_test.php [new file with mode: 0644]
group/lib.php
group/tests/externallib_test.php
index.php
lang/en/admin.php
lang/en/cache.php
lang/en/calendar.php
lang/en/condition.php
lang/en/error.php
lang/en/question.php
lib/accesslib.php
lib/adminlib.php
lib/blocklib.php
lib/completionlib.php
lib/conditionlib.php
lib/db/caches.php
lib/db/services.php
lib/deprecatedlib.php
lib/external/externallib.php
lib/external/tests/externallib_test.php
lib/externallib.php
lib/filelib.php
lib/form/dndupload.js
lib/form/filemanager.js
lib/form/filemanager.php
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/plagiarismlib.php
lib/tests/accesslib_test.php
lib/tests/externallib_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/weblib.php
lib/yui/chooserdialogue/chooserdialogue.js
lib/yui/formautosubmit/formautosubmit.js
message/externallib.php
message/lib.php
message/tests/externallib_test.php
mod/assign/assignmentplugin.php
mod/assign/db/services.php
mod/assign/externallib.php
mod/assign/feedback/file/importziplib.php
mod/assign/locallib.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/tests/externallib_test.php
mod/assign/version.php
mod/book/tool/importhtml/locallib.php
mod/forum/lib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/lang/en/quiz.php
mod/scorm/module.js
mod/wiki/lang/en/wiki.php
mod/wiki/lib.php
notes/tests/externallib_test.php
phpunit.xml.dist
question/format/xml/format.php
question/format/xml/tests/fixtures/multichoice.xml [new file with mode: 0644]
question/format/xml/tests/fixtures/truefalse.xml [new file with mode: 0644]
question/format/xml/tests/xmlformat_test.php
question/previewlib.php
question/type/calculated/lang/en/qtype_calculated.php
question/type/calculated/questiontype.php
question/type/calculatedmulti/edit_calculatedmulti_form.php
question/type/calculatedsimple/lang/en/qtype_calculatedsimple.php
question/type/edit_question_form.php
question/type/multichoice/lang/en/qtype_multichoice.php
question/type/numerical/edit_numerical_form.php
question/type/numerical/lang/en/qtype_numerical.php
question/type/questiontypebase.php
question/type/shortanswer/edit_shortanswer_form.php
report/completion/index.php
report/configlog/index.php
report/security/index.php
repository/equella/lib.php
repository/flickr_public/lib.php
tag/manage.php
theme/base/style/admin.css
theme/base/style/core.css
theme/base/style/course.css
theme/formal_white/style/course.css
theme/formal_white/style/formal_white.css
theme/standard/style/admin.css
theme/standard/style/course.css
user/editadvanced_form.php
user/lib.php
user/tests/externallib_test.php
user/view.php
version.php
webservice/tests/externallib_test.php

index e703ee8..5b5c3a1 100644 (file)
     $table->define_columns(array('name', 'instances', 'version', 'hideshow', 'undeletable', 'delete', 'settings'));
     $table->define_headers(array($strname, $strcourses, $strversion, $strhide.'/'.$strshow, $strprotecthdr, $strdelete, $strsettings));
     $table->define_baseurl($CFG->wwwroot.'/'.$CFG->admin.'/blocks.php');
-    $table->set_attribute('class', 'compatibleblockstable blockstable generaltable');
+    $table->set_attribute('class', 'admintable blockstable generaltable');
+    $table->set_attribute('id', 'compatibleblockstable');
     $table->setup();
     $tablerows = array();
 
index 64589cf..0dbb415 100644 (file)
     $table = new html_table();
     $table->head  = array(get_string('filter'), get_string('isactive', 'filters'),
             get_string('order'), get_string('applyto', 'filters'), get_string('settings'), get_string('delete'));
-    $table->align = array('left', 'left', 'center', 'left', 'left');
-    $table->width = '100%';
+    $table->colclasses = array ('leftalign', 'leftalign', 'centeralign', 'leftalign', 'leftalign', 'leftalign');
+    $table->attributes['class'] = 'admintable generaltable';
+    $table->id = 'filterssetting';
     $table->data  = array();
 
     $lastactive = null;
index 9146c82..cd2f26e 100644 (file)
@@ -77,7 +77,7 @@ $table->define_columns(array('name', 'version', 'delete'));
 $table->define_headers(array(get_string('plugin'), get_string('version'), get_string('delete')));
 $table->define_baseurl($PAGE->url);
 $table->set_attribute('id', 'localplugins');
-$table->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide');
+$table->set_attribute('class', 'admintable generaltable');
 $table->setup();
 
 $plugins = array();
index 4a99a29..c6738df 100644 (file)
@@ -28,8 +28,8 @@ require_once(dirname(dirname(__FILE__)) . '/config.php');
 require_once($CFG->libdir . '/adminlib.php');
 require_once($CFG->libdir . '/pluginlib.php');
 
-require_capability('moodle/site:config', context_system::instance());
 admin_externalpage_setup('pluginsoverview');
+require_capability('moodle/site:config', context_system::instance());
 
 $fetchremote = optional_param('fetchremote', false, PARAM_BOOL);
 $updatesonly = optional_param('updatesonly', false, PARAM_BOOL);
index 1b35c43..e12d3cc 100644 (file)
@@ -195,7 +195,7 @@ $table->define_headers(array(get_string('questiontype', 'question'), get_string(
         get_string('version'), get_string('requires', 'admin'), get_string('availableq', 'question'),
         get_string('delete'), get_string('settings')));
 $table->set_attribute('id', 'qtypes');
-$table->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide');
+$table->set_attribute('class', 'admintable generaltable');
 $table->setup();
 
 // Add a row for each question type.
index 8d49636..b39976d 100644 (file)
@@ -1245,10 +1245,9 @@ class core_admin_renderer extends plugin_renderer_base {
             get_string('report'),
             get_string('status'),
         );
-        $servertable->align = array('center', 'center', 'left', 'center');
-        $servertable->wrap  = array('nowrap', '', '', 'nowrap');
-        $servertable->size  = array('10', 10, '100%', '10');
-        $servertable->attributes['class'] = 'environmenttable generaltable';
+        $servertable->colclasses = array('centeralign name', 'centeralign status', 'leftalign report', 'centeralign info');
+        $servertable->attributes['class'] = 'admintable environmenttable generaltable';
+        $servertable->id = 'serverstatus';
 
         $serverdata = array('ok'=>array(), 'warn'=>array(), 'error'=>array());
 
@@ -1258,10 +1257,9 @@ class core_admin_renderer extends plugin_renderer_base {
             get_string('report'),
             get_string('status'),
         );
-        $othertable->align = array('center', 'left', 'center');
-        $othertable->wrap  = array('', '', 'nowrap');
-        $othertable->size  = array(10, '100%', '10');
-        $othertable->attributes['class'] = 'environmenttable generaltable';
+        $othertable->colclasses = array('aligncenter info', 'alignleft report', 'aligncenter status');
+        $othertable->attributes['class'] = 'admintable environmenttable generaltable';
+        $othertable->id = 'otherserverstatus';
 
         $otherdata = array('ok'=>array(), 'warn'=>array(), 'error'=>array());
 
index 3eb48b0..a4cdd22 100644 (file)
@@ -281,8 +281,11 @@ if (($action == 'edit') || ($action == 'new')) {
     // Table to list plug-ins
     $table = new html_table();
     $table->head = array(get_string('name'), get_string('isactive', 'repository'), get_string('order'), $settingsstr);
-    $table->align = array('left', 'center', 'center', 'center', 'center');
+
+    $table->colclasses = array('leftalign', 'centeralign', 'centeralign', 'centeralign', 'centeralign');
+    $table->id = 'repositoriessetting';
     $table->data = array();
+    $table->attributes['class'] = 'admintable generaltable';
 
     // Get list of used plug-ins
     $instances = repository::get_types();
index 45ea5f8..4b7cef8 100644 (file)
@@ -189,7 +189,7 @@ $assignurl = new moodle_url($PAGE->url, array('roleid'=>$roleid));
 <form id="assignform" method="post" action="<?php echo $assignurl ?>"><div>
   <input type="hidden" name="sesskey" value="<?php echo sesskey() ?>" />
 
-  <table summary="" class="roleassigntable generaltable generalbox boxaligncenter" cellspacing="0">
+  <table id="assigningrole" summary="" class="admintable roleassigntable generaltable" cellspacing="0">
     <tr>
       <td id="existingcell">
           <p><label for="removeselect"><?php print_string('extusers', 'role'); ?></label></p>
@@ -278,15 +278,13 @@ $assignurl = new moodle_url($PAGE->url, array('roleid'=>$roleid));
 
     // Print overview table
     $table = new html_table();
-    $table->tablealign = 'center';
-    $table->width = '60%';
+    $table->id = 'assignrole';
     $table->head = array(get_string('role'), get_string('description'), get_string('userswiththisrole', 'role'));
-    $table->wrap = array('nowrap', '', 'nowrap');
-    $table->align = array('left', 'left', 'center');
+    $table->colclasses = array('leftalign role', 'leftalign', 'centeralign userrole');
+    $table->attributes['class'] = 'admintable generaltable';
     if ($showroleholders) {
         $table->headspan = array(1, 1, 2);
-        $table->wrap[] = 'nowrap';
-        $table->align[] = 'left';
+        $table->colclasses[] = 'leftalign roleholder';
     }
 
     foreach ($assignableroles as $roleid => $rolename) {
index 745681e..c7a4f69 100644 (file)
 
 /// Initialise table.
     $table = new html_table();
-    $table->tablealign = 'center';
-    $table->align = array('left', 'left', 'left', 'left');
-    $table->wrap = array('nowrap', '', 'nowrap','nowrap');
-    $table->width = '90%';
+    $table->colclasses = array('leftalign', 'leftalign', 'leftalign', 'leftalign');
+    $table->id = 'roles';
+    $table->attributes['class'] = 'admintable generaltable';
     $table->head = array(
         get_string('role') . ' ' . $OUTPUT->help_icon('roles', 'role'),
         get_string('description'),
index 99a5eea..6b527ec 100644 (file)
@@ -67,6 +67,19 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     }
     $temp->add(new admin_setting_configselect('calendar_maxevents',new lang_string('configmaxevents','admin'),new lang_string('helpupcomingmaxevents', 'admin'),10,$options));
     $temp->add(new admin_setting_configcheckbox('enablecalendarexport', new lang_string('enablecalendarexport', 'admin'), new lang_string('configenablecalendarexport','admin'), 1));
+
+    // Calendar custom export settings.
+    $days = array(365 => new lang_string('numdays', '', 365),
+            180 => new lang_string('numdays', '', 180),
+            150 => new lang_string('numdays', '', 150),
+            120 => new lang_string('numdays', '', 120),
+            90  => new lang_string('numdays', '', 90),
+            60  => new lang_string('numdays', '', 60),
+            30  => new lang_string('numdays', '', 30),
+            5  => new lang_string('numdays', '', 5));
+    $temp->add(new admin_setting_configcheckbox('calendar_customexport', new lang_string('configcalendarcustomexport', 'admin'), new lang_string('helpcalendarcustomexport','admin'), 1));
+    $temp->add(new admin_setting_configselect('calendar_exportlookahead', new lang_string('configexportlookahead','admin'), new lang_string('helpexportlookahead', 'admin'), 365, $days));
+    $temp->add(new admin_setting_configselect('calendar_exportlookback', new lang_string('configexportlookback','admin'), new lang_string('helpexportlookback', 'admin'), 5, $days));
     $temp->add(new admin_setting_configtext('calendar_exportsalt', new lang_string('calendarexportsalt','admin'), new lang_string('configcalendarexportsalt', 'admin'), random_string(60)));
     $ADMIN->add('appearance', $temp);
 
index b7fa57e..f2d6f58 100644 (file)
@@ -316,6 +316,7 @@ if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext))
         require_once("$CFG->libdir/pluginlib.php");
         $allplugins = plugin_manager::instance()->get_plugins();
     }
+
     // Question behaviour settings.
     $ADMIN->add('modules', new admin_category('qbehavioursettings', new lang_string('questionbehaviours', 'admin')));
     $ADMIN->add('qbehavioursettings', new admin_page_manageqbehaviours());
@@ -323,6 +324,54 @@ if ($hassiteconfig || has_capability('moodle/question:config', $systemcontext))
     // Question type settings.
     $ADMIN->add('modules', new admin_category('qtypesettings', new lang_string('questiontypes', 'admin')));
     $ADMIN->add('qtypesettings', new admin_page_manageqtypes());
+
+    // Question preview defaults.
+    $settings = new admin_settingpage('qdefaultsetting',
+            get_string('questionpreviewdefaults', 'question'),
+            'moodle/question:config');
+    $ADMIN->add('qtypesettings', $settings);
+
+    $settings->add(new admin_setting_heading('qdefaultsetting_preview_options',
+            '', get_string('questionpreviewdefaults_desc', 'question')));
+
+    // These keys are question_display_options::HIDDEN and VISIBLE.
+    $hiddenofvisible = array(
+        0 => get_string('notshown', 'question'),
+        1 => get_string('shown', 'question'),
+    );
+
+    $settings->add(new admin_setting_question_behaviour('question_preview/behaviour',
+            get_string('howquestionsbehave', 'question'), '',
+                    'deferredfeedback'));
+
+    $settings->add(new admin_setting_configselect('question_preview/correctness',
+            get_string('whethercorrect', 'question'), '', 1, $hiddenofvisible));
+
+    // These keys are question_display_options::HIDDEN, MARK_ONLY and MARK_AND_MAX.
+    $marksoptions = array(
+        0 => get_string('notshown', 'question'),
+        1 => get_string('showmaxmarkonly', 'question'),
+        2 => get_string('showmarkandmax', 'question'),
+    );
+    $settings->add(new admin_setting_configselect('question_preview/marks',
+            get_string('marks', 'question'), '', 1, $marksoptions));
+
+    $settings->add(new admin_setting_configselect('question_preview/markdp',
+            get_string('decimalplacesingrades', 'question'), '', 2, array(0, 1, 2, 3, 4, 5, 6, 7)));
+
+    $settings->add(new admin_setting_configselect('question_preview/feedback',
+            get_string('specificfeedback', 'question'), '', 1, $hiddenofvisible));
+
+    $settings->add(new admin_setting_configselect('question_preview/generalfeedback',
+            get_string('generalfeedback', 'question'), '', 1, $hiddenofvisible));
+
+    $settings->add(new admin_setting_configselect('question_preview/rightanswer',
+            get_string('rightanswer', 'question'), '', 1, $hiddenofvisible));
+
+    $settings->add(new admin_setting_configselect('question_preview/history',
+            get_string('responsehistory', 'question'), '', 0, $hiddenofvisible));
+
+    // Settings for particular question types.
     foreach ($allplugins['qtype'] as $qtype) {
         $qtype->load_settings($ADMIN, 'qtypesettings', $hassiteconfig);
     }
index 9a6a104..384a900 100644 (file)
@@ -527,6 +527,16 @@ if ($formdata = $mform2->is_cancelled()) {
                             }
                         }
 
+                        if ($column === 'lang') {
+                            if (empty($user->lang)) {
+                                // Do not change to not-set value.
+                                continue;
+                            } else if (clean_param($user->lang, PARAM_LANG) === '') {
+                                $upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning');
+                                continue;
+                            }
+                        }
+
                         if (in_array($column, $upt->columns)) {
                             $upt->track($column, s($existinguser->$column).'-->'.s($user->$column), 'info', false);
                         }
@@ -682,6 +692,13 @@ if ($formdata = $mform2->is_cancelled()) {
                 $upt->track('email', get_string('invalidemail'), 'warning');
             }
 
+            if (empty($user->lang)) {
+                $user->lang = '';
+            } else if (clean_param($user->lang, PARAM_LANG) === '') {
+                $upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning');
+                $user->lang = '';
+            }
+
             $forcechangepassword = false;
 
             if ($isinternalauth) {
index 9d4e002..5ac58fc 100644 (file)
@@ -120,7 +120,7 @@ class main_view extends XMLDBAction {
         $result = $this->launch('get_db_directories');
         // Display list of DB directories if everything is ok
         if ($result && !empty($XMLDB->dbdirs)) {
-            $o .= '<table id="listdirectories" border="0" cellpadding="5" cellspacing="1" class="boxaligncenter flexible">';
+            $o .= '<table id="listdirectories" border="0" cellpadding="5" cellspacing="1" class="admintable generaltable">';
             $row = 0;
             foreach ($XMLDB->dbdirs as $key => $dbdir) {
                 // Detect if this is the lastused dir
index aac7112..6f6bc92 100644 (file)
 
         $table = new html_table();
         $table->head = array ();
-        $table->align = array();
+        $table->colclasses = array();
         $table->head[] = $fullnamedisplay;
-        $table->align[] = 'left';
+        $table->attributes['class'] = 'admintable generaltable';
+        $table->colclasses[] = 'leftalign';
         foreach ($extracolumns as $field) {
             $table->head[] = ${$field};
-            $table->align[] = 'left';
+            $table->colclasses[] = 'leftalign';
         }
         $table->head[] = $city;
-        $table->align[] = 'left';
+        $table->colclasses[] = 'leftalign';
         $table->head[] = $country;
-        $table->align[] = 'left';
+        $table->colclasses[] = 'leftalign';
         $table->head[] = $lastaccess;
-        $table->align[] = 'left';
+        $table->colclasses[] = 'leftalign';
         $table->head[] = get_string('edit');
-        $table->align[] = 'center';
+        $table->colclasses[] = 'centeralign';
         $table->head[] = "";
-        $table->align[] = 'center';
+        $table->colclasses[] = 'centeralign';
 
-        $table->width = "95%";
+        $table->id = "users";
         foreach ($users as $user) {
             if (isguestuser($user)) {
                 continue; // do not display guest here
index 7fd4029..aba56d4 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Defines backup_gradingform_plugin class
+ * Contains class backup_gradingform_plugin responsible for advanced grading form plugin backup
  *
  * @package     core_backup
  * @subpackage  moodle2
- * @category    backup
  * @copyright   2011 David Mudrak <david@moodle.com>
  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Base class for all advanced grading form plugins
+ * Base class for backup all advanced grading form plugins
+ *
+ * As an example of implementation see {@link backup_gradingform_rubric_plugin}
+ *
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @category   backup
  */
 abstract class backup_gradingform_plugin extends backup_plugin {
 }
index 6caa7dc..320967f 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Defines restore_gradingform_plugin class
- * @package     core_backup
+ * Contains class restore_gradingform_plugin responsible for advanced grading form plugin backup
+ *
+ * @package    core_backup
  * @subpackage  moodle2
- * @category    backup
- * @copyright   2011 David Mudrak <david@moodle.com>
- * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * Base class for all advanced grading form plugins
+ * Base class for restoring all advanced grading form plugins
+ *
+ * As an example of implementation see {@link restore_gradingform_rubric_plugin}
+ *
+ * @copyright  2011 David Mudrak <david@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @category   backup
  */
 abstract class restore_gradingform_plugin extends restore_plugin {
 
index b7da2d3..c3c688f 100644 (file)
@@ -824,20 +824,12 @@ class restore_groups_structure_step extends restore_structure_step {
     }
 
     public function process_grouping_group($data) {
-        global $DB;
-
-        $data = (object)$data;
-
-        $data->groupingid = $this->get_new_parentid('grouping'); // Use new parentid
-        $data->groupid    = $this->get_mappingid('group', $data->groupid); // Get from mappings
+        global $CFG;
 
-        $params = array();
-        $params['groupingid'] = $data->groupingid;
-        $params['groupid']    = $data->groupid;
+        require_once($CFG->dirroot.'/group/lib.php');
 
-        if (!$DB->record_exists('groupings_groups', $params)) {
-            $DB->insert_record('groupings_groups', $data);  // No need to set this mapping (no child info nor files)
-        }
+        $data = (object)$data;
+        groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
     }
 
     protected function after_execute() {
index bb1b051..3878964 100644 (file)
@@ -46,25 +46,9 @@ if ($userid) {
 
 
 // Check permissions
-require_login($course);
-
-$coursecontext   = context_course::instance($course->id);
-$personalcontext = context_user::instance($user->id);
-
-$can_view = false;
-
-// Can view own report
-if ($USER->id == $user->id) {
-    $can_view = true;
-} else if (has_capability('moodle/user:viewuseractivitiesreport', $personalcontext)) {
-    $can_view = true;
-} else if (has_capability('report/completion:view', $coursecontext)) {
-    $can_view = true;
-} else if (has_capability('report/completion:view', $personalcontext)) {
-    $can_view = true;
-}
+require_login();
 
-if (!$can_view) {
+if (!completion_can_view_data($user->id, $course)) {
     print_error('cannotviewreport');
 }
 
index d4374bc..bc4420c 100644 (file)
@@ -41,8 +41,8 @@ class block_site_main_menu extends block_list {
                         continue;
                     }
 
-                    list($content, $instancename) =
-                            get_print_section_cm_text($cm, $course);
+                    $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+                    $instancename = $cm->get_formatted_name();
 
                     if (!($url = $cm->get_url())) {
                         $this->content->items[] = $content;
@@ -60,14 +60,12 @@ class block_site_main_menu extends block_list {
         }
 
 /// slow & hacky editing mode
+        $courserenderer = $this->page->get_renderer('core', 'course');
         $ismoving = ismoving($course->id);
         course_create_sections_if_missing($course, 0);
         $modinfo = get_fast_modinfo($course);
         $section = $modinfo->get_section_info(0);
 
-        $groupbuttons = $course->groupmode;
-        $groupbuttonslink = (!$course->groupmodeforce);
-
         if ($ismoving) {
             $strmovehere = get_string('movehere');
             $strmovefull = strip_tags(get_string('movefull', '', "'$USER->activitycopyname'"));
@@ -90,15 +88,10 @@ class block_site_main_menu extends block_list {
                     continue;
                 }
                 if (!$ismoving) {
-                    if ($groupbuttons) {
-                        if (! $mod->groupmodelink = $groupbuttonslink) {
-                            $mod->groupmode = $course->groupmode;
-                        }
-
-                    } else {
-                        $mod->groupmode = false;
-                    }
-                    $editbuttons = '<div class="buttons">'.make_editing_buttons($mod, true, true).'</div>';
+                    $actions = course_get_cm_edit_actions($mod, -1);
+                    $editbuttons = html_writer::tag('div',
+                            $courserenderer->course_section_cm_edit_actions($actions),
+                            array('class' => 'buttons'));
                 } else {
                     $editbuttons = '';
                 }
@@ -111,8 +104,8 @@ class block_site_main_menu extends block_list {
                             '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->pix_url('movehere') . '" alt="'.$strmovehere.'" /></a>';
                         $this->content->icons[] = '';
                     }
-                    list($content, $instancename) =
-                            get_print_section_cm_text($modinfo->cms[$modnumber], $course);
+                    $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+                    $instancename = $mod->get_formatted_name();
                     $linkcss = $mod->visible ? '' : ' class="dimmed" ';
 
                     if (!($url = $mod->get_url())) {
@@ -134,7 +127,8 @@ class block_site_main_menu extends block_list {
             $this->content->icons[] = '';
         }
 
-        $this->content->footer = print_section_add_menus($course, 0, null, true, true);
+        $this->content->footer = $courserenderer->course_section_add_cm_control($course,
+                0, null, array('inblock' => true));
 
         return $this->content;
     }
index b98f44f..5e394e0 100644 (file)
@@ -43,8 +43,8 @@ class block_social_activities extends block_list {
                         continue;
                     }
 
-                    list($content, $instancename) =
-                            get_print_section_cm_text($cm, $course);
+                    $content = $cm->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+                    $instancename = $cm->get_formatted_name();
 
                     if (!($url = $cm->get_url())) {
                         $this->content->items[] = $content;
@@ -63,13 +63,11 @@ class block_social_activities extends block_list {
 
 
 /// slow & hacky editing mode
+        $courserenderer = $this->page->get_renderer('core', 'course');
         $ismoving = ismoving($course->id);
         $modinfo = get_fast_modinfo($course);
         $section = $modinfo->get_section_info(0);
 
-        $groupbuttons = $course->groupmode;
-        $groupbuttonslink = (!$course->groupmodeforce);
-
         if ($ismoving) {
             $strmovehere = get_string('movehere');
             $strmovefull = strip_tags(get_string('movefull', '', "'$USER->activitycopyname'"));
@@ -92,15 +90,9 @@ class block_social_activities extends block_list {
                     continue;
                 }
                 if (!$ismoving) {
-                    if ($groupbuttons) {
-                        if (! $mod->groupmodelink = $groupbuttonslink) {
-                            $mod->groupmode = $course->groupmode;
-                        }
-
-                    } else {
-                        $mod->groupmode = false;
-                    }
-                    $editbuttons = '<br />'.make_editing_buttons($mod, true, true);
+                    $actions = course_get_cm_edit_actions($mod, -1);
+                    $editbuttons = '<br />'.
+                            $courserenderer->course_section_cm_edit_actions($actions);
                 } else {
                     $editbuttons = '';
                 }
@@ -113,8 +105,8 @@ class block_social_activities extends block_list {
                             '<img style="height:16px; width:80px; border:0px" src="'.$OUTPUT->pix_url('movehere') . '" alt="'.$strmovehere.'" /></a>';
                         $this->content->icons[] = '';
                     }
-                    list($content, $instancename) =
-                                get_print_section_cm_text($modinfo->cms[$modnumber], $course);
+                    $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+                    $instancename = $mod->get_formatted_name();
 
                     $linkcss = $mod->visible ? '' : ' class="dimmed" ';
 
@@ -137,7 +129,8 @@ class block_social_activities extends block_list {
             $this->content->icons[] = '';
         }
 
-        $this->content->footer = print_section_add_menus($course, 0, null, true, true);
+        $this->content->footer = $courserenderer->course_section_add_cm_control($course,
+                0, null, array('inblock' => true));
 
         return $this->content;
     }
index f4cb51c..7cd585b 100644 (file)
@@ -37,7 +37,7 @@ $time = optional_param('preset_time', 'weeknow', PARAM_ALPHA);
 $now = usergetdate(time());
 // Let's see if we have sufficient and correct data
 $allowed_what = array('all', 'courses');
-$allowed_time = array('weeknow', 'weeknext', 'monthnow', 'monthnext', 'recentupcoming');
+$allowed_time = array('weeknow', 'weeknext', 'monthnow', 'monthnext', 'recentupcoming', 'custom');
 
 if (!empty($generateurl)) {
     $authtoken = sha1($user->id . $user->password . $CFG->calendar_exportsalt);
@@ -127,6 +127,11 @@ if(!empty($what) && !empty($time)) {
                 $timestart = time() - 432000;
                 $timeend = time() + 5184000;
             break;
+            case 'custom':
+                // Events based on custom date range.
+                $timestart = time() - $CFG->calendar_exportlookback * DAYSECS;
+                $timeend = time() + $CFG->calendar_exportlookahead * DAYSECS;
+            break;
         }
     }
     else {
diff --git a/calendar/externallib.php b/calendar/externallib.php
new file mode 100644 (file)
index 0000000..320d81c
--- /dev/null
@@ -0,0 +1,106 @@
+<?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/>.
+
+
+/**
+ * External calendar API
+ *
+ * @package    core_calendar
+ * @category   external
+ * @copyright  2012 Ankit Agarwal
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.5
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+require_once("$CFG->libdir/externallib.php");
+
+/**
+ * Calendar external functions
+ *
+ * @package    core_calendar
+ * @category   external
+ * @copyright  2012 Ankit Agarwal
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.5
+ */
+class core_calendar_external extends external_api {
+
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.5
+     */
+    public static function delete_calendar_events_parameters() {
+        return new external_function_parameters(
+                array('events' => new external_multiple_structure(
+                        new external_single_structure(
+                                array(
+                                        'eventid' => new external_value(PARAM_INT, 'Event ID', VALUE_REQUIRED, '', NULL_NOT_ALLOWED),
+                                        'repeat'  => new external_value(PARAM_BOOL, 'Delete comeplete series if repeated event')
+                                ), 'List of events to delete'
+                        )
+                    )
+                )
+        );
+    }
+
+    /**
+     * Delete Calendar events
+     *
+     * @param array $eventids A list of event ids with repeat flag to delete
+     * @return null
+     * @since Moodle 2.5
+     */
+    public static function delete_calendar_events($events) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot."/calendar/lib.php");
+
+        // Parameter validation.
+        $params = self::validate_parameters(self:: delete_calendar_events_parameters(), array('events' => $events));
+
+        $transaction = $DB->start_delegated_transaction();
+
+        foreach ($params['events'] as $event) {
+            $eventobj = calendar_event::load($event['eventid']);
+
+            // Let's check if the user is allowed to delete an event.
+            if (!calendar_edit_event_allowed($eventobj)) {
+                throw new moodle_exception("nopermissions");
+            }
+            // Time to do the magic.
+            $eventobj->delete($event['repeat']);
+        }
+
+        // Everything done smoothly, let's commit.
+        $transaction->allow_commit();
+
+        return null;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.5
+     */
+    public static function  delete_calendar_events_returns() {
+        return null;
+    }
+}
index b7cbd79..f5e6885 100644 (file)
@@ -43,6 +43,7 @@ class core_calendar_renderer extends plugin_renderer_base {
      * @return string
      */
     public function basic_export_form($allowthisweek, $allownextweek, $allownextmonth, $userid, $authtoken) {
+        global $CFG;
 
         $output  = html_writer::tag('div', get_string('export', 'calendar'), array('class'=>'header'));
         $output .= html_writer::start_tag('fieldset');
@@ -84,8 +85,20 @@ class core_calendar_renderer extends plugin_renderer_base {
         $output .= html_writer::empty_tag('input', array('type'=>'radio', 'name'=>'preset_time', 'id'=>'pt_recupc', 'value'=>'recentupcoming'));
         $output .= html_writer::tag('label', get_string('recentupcoming', 'calendar'), array('for'=>'pt_recupc'));
         $output .= html_writer::empty_tag('br');
-        $output .= html_writer::end_tag('div');
 
+        if ($CFG->calendar_customexport) {
+            $a = new stdClass();
+            $now = time();
+            $time = $now - $CFG->calendar_exportlookback * DAYSECS;
+            $a->timestart = userdate($time, get_string('strftimedatefullshort', 'langconfig'));
+            $time = $now + $CFG->calendar_exportlookahead * DAYSECS;
+            $a->timeend = userdate($time, get_string('strftimedatefullshort', 'langconfig'));
+            $output .= html_writer::empty_tag('input', array('type' => 'radio', 'name' => 'preset_time', 'id' => 'pt_custom', 'value' => 'custom'));
+            $output .= html_writer::tag('label', get_string('customexport', 'calendar', $a), array('for' => 'pt_custom'));
+            $output .= html_writer::empty_tag('br');
+        }
+
+        $output .= html_writer::end_tag('div');
         $output .= html_writer::start_tag('div', array('class'=>'rightalign'));
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'cal_d', 'value'=>''));
         $output .= html_writer::empty_tag('input', array('type'=>'hidden', 'name'=>'cal_m', 'value'=>''));
diff --git a/calendar/tests/externallib_tests.php b/calendar/tests/externallib_tests.php
new file mode 100644 (file)
index 0000000..96071bc
--- /dev/null
@@ -0,0 +1,257 @@
+<?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/>.
+
+/**
+ * External calendar functions unit tests
+ *
+ * @package    core_calendar
+ * @category   external
+ * @copyright  2012 Ankit Agarwal
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * External course functions unit tests
+ *
+ * @package    core_calendar
+ * @category   external
+ * @copyright  2012 Ankit Agarwal
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 2.5
+ */
+class core_calendar_external_testcase extends externallib_advanced_testcase {
+
+    /**
+     * Tests set up
+     */
+    protected function setUp() {
+        global $CFG;
+        require_once($CFG->dirroot . '/calendar/externallib.php');
+    }
+
+    /** Create calendar events or update them
+     * Set $prop->id, if you want to do an update instead of creating an new event
+     *
+     * @param string $name        Event title
+     * @param int    $userid      User id
+     * @param string $type        Event type
+     * @param int    $repeats     Number of repeated events to create
+     * @param int    $timestart   Time stamp of the event start
+     * @param mixed  $prop        List of event properties as array or object
+     * @return mixed              Event object or false;
+     * @since Moodle 2.5
+     */
+
+    public static function create_calendar_event($name, $userid = 0, $type = 'user', $repeats = 0, $timestart  = null, $prop = null) {
+        global $CFG, $DB, $USER, $SITE;
+
+        require_once("$CFG->dirroot/calendar/lib.php");
+        if (!empty($prop)) {
+           if (is_array($prop)) {
+               $prop = (object)$prop;
+           }
+        } else {
+            $prop = new stdClass();
+        }
+        $prop->name = $name;
+        if (empty($prop->eventtype)) {
+            $prop->eventtype = $type;
+        }
+        if (empty($prop->repeats)) {
+            $prop->repeats = $repeats;
+        }
+        if (empty($prop->timestart)) {
+            $prop->timestart = time();
+        }
+        if (empty($prop->timeduration)) {
+            $prop->timeduration = 0;
+        }
+        if (empty($prop->repeats)) {
+            $prop->repeat = 0;
+        } else {
+            $prop->repeat = 1;
+        }
+        if (empty($prop->userid)) {
+           if (!empty($userid)) {
+                $prop->userid = $userid;
+           } else {
+               return false;
+           }
+        }
+        if (empty($prop->courseid)) {
+            $prop->courseid = $SITE->id;
+        }
+        $event = new calendar_event($prop);
+        return $event->create($prop);
+    }
+
+    public function test_create_calendar_events () {
+        global $DB, $USER;
+
+        $this->setAdminUser();
+        $this->resetAfterTest();
+        $prevcount = count($DB->get_records("event"));
+
+        // Create a few events and do asserts.
+        $this->create_calendar_event('test', $USER->id);
+        $count = count($DB->get_records("event", array('name' => 'test')));
+        $this->assertEquals(1, $count);
+        $aftercount = count($DB->get_records("event"));
+        $this->assertEquals($prevcount + 1, $aftercount);
+
+        $this->create_calendar_event('user', $USER->id, 'user', 3);
+        $count = count($DB->get_records("event", array('name' => 'user')));
+        $this->assertEquals(3, $count);
+        $aftercount = count($DB->get_records("event"));
+        $this->assertEquals($prevcount + 4, $aftercount);
+
+    }
+
+    /**
+     * Test delete_courses
+     */
+    public function test_delete_calendar_events() {
+        global $DB, $USER;
+
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        // Create a few stuff to test with.
+        $user = $this->getDataGenerator()->create_user();
+        $course = $this->getDataGenerator()->create_course();
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $group = $this->getDataGenerator()->create_group($record);
+
+        $notdeletedcount = $DB->count_records('event');
+
+        // Let's create a few events.
+        $siteevent = $this->create_calendar_event('site', $USER->id, 'site');
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event('course', $USER->id, 'course', 2, time(), $record);
+        $userevent = $this->create_calendar_event('user', $USER->id);
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $record->groupid = $group->id;
+        $groupevent = $this->create_calendar_event('group', $USER->id, 'group', 0, time(), $record);
+
+        // Now lets try to delete stuff with proper rights.
+        $events = array(
+                array('eventid' => $siteevent->id, 'repeat' => 0),
+                array('eventid' => $courseevent->id, 'repeat' => 1),
+                array('eventid' => $userevent->id, 'repeat' => 0),
+                array('eventid' => $groupevent->id, 'repeat' => 0)
+                );
+        core_calendar_external::delete_calendar_events($events);
+
+        // Check to see if things were deleted properly.
+        $deletedcount = $DB->count_records('event');
+        $this->assertEquals($notdeletedcount, $deletedcount);
+
+        // Let's create a few events.
+        $siteevent = $this->create_calendar_event('site', $USER->id, 'site');
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event('course', $USER->id, 'course', 3, time(), $record);
+        $userevent = $this->create_calendar_event('user', $USER->id);
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $record->groupid = $group->id;
+        $groupevent = $this->create_calendar_event('group', $USER->id, 'group', 0, time(), $record);
+
+        $this->setuser($user);
+        $sitecontext = context_system::instance();
+        $coursecontext = context_course::instance($course->id);
+        $usercontext = context_user::instance($user->id);
+        $role = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, $role->id);
+
+        // Remove all caps.
+        $this->unassignUserCapability('moodle/calendar:manageentries', $sitecontext->id, $role->id);
+        $this->unassignUserCapability('moodle/calendar:manageentries', $coursecontext->id, $role->id);
+        $this->unassignUserCapability('moodle/calendar:managegroupentries', $coursecontext->id, $role->id);
+        $this->unassignUserCapability('moodle/calendar:manageownentries', $usercontext->id, $role->id);
+
+        // Assign proper caps and attempt delete.
+         $this->assignUserCapability('moodle/calendar:manageentries', $sitecontext->id, $role->id);
+         $events = array(
+                array('eventid' => $siteevent->id, 'repeat' => 0),
+                );
+        core_calendar_external::delete_calendar_events($events);
+        $deletedcount = $DB->count_records('event');
+        $count = $notdeletedcount+5;
+        $this->assertEquals($count, $deletedcount);
+
+         $this->assignUserCapability('moodle/calendar:manageentries', $sitecontext->id, $role->id);
+         $events = array(
+                array('eventid' => $courseevent->id, 'repeat' => 0),
+                );
+        core_calendar_external::delete_calendar_events($events);
+        $deletedcount = $DB->count_records('event');
+        $count = $notdeletedcount+4;
+        $this->assertEquals($count, $deletedcount);
+
+         $this->assignUserCapability('moodle/calendar:manageownentries', $usercontext->id, $role->id);
+         $events = array(
+                array('eventid' => $userevent->id, 'repeat' => 0),
+                );
+        core_calendar_external::delete_calendar_events($events);
+        $deletedcount = $DB->count_records('event');
+        $count = $notdeletedcount+3;
+        $this->assertEquals($count, $deletedcount);
+
+         $this->assignUserCapability('moodle/calendar:managegroupentries', $coursecontext->id, $role->id);
+         $events = array(
+                array('eventid' => $groupevent->id, 'repeat' => 0),
+                );
+        core_calendar_external::delete_calendar_events($events);
+        $deletedcount = $DB->count_records('event');
+        $count = $notdeletedcount+2;
+        $this->assertEquals($count, $deletedcount);
+
+        $notdeletedcount = $deletedcount;
+
+        // Let us try deleting without caps.
+
+        $siteevent = $this->create_calendar_event('site', $USER->id, 'site');
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $courseevent = $this->create_calendar_event('course', $USER->id, 'course', 3, time(), $record);
+        $userevent = $this->create_calendar_event('user', $USER->id);
+        $record = new stdClass();
+        $record->courseid = $course->id;
+        $record->groupid = $group->id;
+        $groupevent = $this->create_calendar_event('group', $USER->id, 'group', 0, time(), $record);
+
+        $this->setGuestUser();
+        $this->setExpectedException('moodle_exception');
+        $events = array(
+            array('eventid' => $siteevent->id, 'repeat' => 0),
+            array('eventid' => $courseevent->id, 'repeat' => 0),
+            array('eventid' => $userevent->id, 'repeat' => 0),
+            array('eventid' => $groupevent->id, 'repeat' => 0)
+        );
+        core_calendar_external::delete_calendar_events($events);
+    }
+
+}
\ No newline at end of file
index 35d794f..38ecfe6 100644 (file)
@@ -134,9 +134,9 @@ foreach($cohorts['cohorts'] as $cohort) {
 $table = new html_table();
 $table->head  = array(get_string('name', 'cohort'), get_string('idnumber', 'cohort'), get_string('description', 'cohort'),
                       get_string('memberscount', 'cohort'), get_string('component', 'cohort'), get_string('edit'));
-$table->size  = array('20%', '10%', '40%', '10%', '10%', '10%');
-$table->align = array('left', 'left', 'left', 'left','center', 'center');
-$table->width = '80%';
+$table->colclasses = array('leftalign name', 'leftalign id', 'leftalign description', 'leftalign size','centeralign source', 'centeralign action');
+$table->id = 'cohorts';
+$table->attributes['class'] = 'admintable generaltable';
 $table->data  = $data;
 echo html_writer::table($table);
 echo $OUTPUT->paging_bar($cohorts['totalcohorts'], $page, 25, $baseurl);
index f0f4ddf..544d82f 100644 (file)
@@ -157,8 +157,7 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
                         scope: scope
                     },
                     headers: {
-                        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
-                        'User-Agent': 'MoodleComment/3.0'
+                        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
                     },
                     data: build_querystring(params)
                 };
@@ -472,8 +471,7 @@ bodyContent: '<div class="comment-delete-confirm"><a href="#" id="confirmdelete-
                             scope: this
                         },
                         headers: {
-                            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
-                            'User-Agent': 'MoodleComment/3.0'
+                            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
                         },
                         data: build_querystring(data)
                     };
index e7e3690..d56d954 100644 (file)
@@ -160,8 +160,9 @@ class comment_manager {
             get_string('content'),
             get_string('action')
         );
-        $table->align = array ('left', 'left', 'left', 'left');
-        $table->attributes = array('class'=>'generaltable commentstable');
+        $table->colclasses = array ('leftalign', 'leftalign', 'leftalign', 'leftalign');
+        $table->attributes = array('class'=>'admintable generaltable');
+        $table->id = 'commentstable';
         $table->data = array();
 
         $link = new moodle_url('/comment/index.php', array('action' => 'delete', 'sesskey' => sesskey()));
index d41bfe9..1ee8395 100644 (file)
@@ -729,6 +729,9 @@ M.course_dndupload = {
                             resel.icon.src = result.icon;
                             resel.a.href = result.link;
                             resel.namespan.innerHTML = result.name;
+                            if (!parseInt(result.visible, 10)) {
+                                resel.a.className = 'dimmed';
+                            }
 
                             if (result.groupingname) {
                                 resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
@@ -916,6 +919,9 @@ M.course_dndupload = {
                             resel.icon.src = result.icon;
                             resel.a.href = result.link;
                             resel.namespan.innerHTML = result.name;
+                            if (!parseInt(result.visible, 10)) {
+                                resel.a.className = 'dimmed';
+                            }
 
                             if (result.groupingname) {
                                 resel.groupingspan.innerHTML = '(' + result.groupingname + ')';
index c4df5a8..797e688 100644 (file)
@@ -35,5 +35,7 @@ $modulename = required_param('module', PARAM_PLUGIN);
 $displayname = optional_param('displayname', null, PARAM_TEXT);
 $contents = optional_param('contents', null, PARAM_RAW); // It will be up to each plugin to clean this data, before saving it.
 
+$PAGE->set_url('/course/dndupload.php');
+
 $dndproc = new dndupload_ajax_processor($courseid, $section, $type, $modulename);
 $dndproc->process($displayname, $contents);
index 21dc7a7..ecb519b 100644 (file)
@@ -620,6 +620,9 @@ class dndupload_ajax_processor {
             throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name);
         }
 
+        // Note the section visibility
+        $visible = get_fast_modinfo($this->course)->get_section_info($this->section)->visible;
+
         $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $this->cm->id));
         // Rebuild the course cache after update action
         rebuild_course_cache($this->course->id, true);
@@ -627,7 +630,10 @@ class dndupload_ajax_processor {
 
         $sectionid = course_add_cm_to_section($this->course, $this->cm->id, $this->section);
 
-        set_coursemodule_visible($this->cm->id, true);
+        set_coursemodule_visible($this->cm->id, $visible);
+        if (!$visible) {
+            $DB->set_field('course_modules', 'visibleold', 1, array('id' => $this->cm->id));
+        }
 
         // retrieve the final info about this module.
         $info = get_fast_modinfo($this->course);
@@ -636,7 +642,7 @@ class dndupload_ajax_processor {
             delete_course_module($this->cm->id);
             throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name);
         }
-        $mod = $info->cms[$this->cm->id];
+        $mod = $info->get_cm($this->cm->id);
         $mod->groupmodelink = $this->cm->groupmodelink;
         $mod->groupmode = $this->cm->groupmode;
 
@@ -665,7 +671,8 @@ class dndupload_ajax_processor {
      * @param cm_info $mod details of the mod just created
      */
     protected function send_response($mod) {
-        global $OUTPUT;
+        global $OUTPUT, $PAGE;
+        $courserenderer = $PAGE->get_renderer('core', 'course');
 
         $resp = new stdClass();
         $resp->error = self::ERROR_OK;
@@ -673,8 +680,10 @@ class dndupload_ajax_processor {
         $resp->name = $mod->name;
         $resp->link = $mod->get_url()->out();
         $resp->elementid = 'module-'.$mod->id;
-        $resp->commands = make_editing_buttons($mod, true, true, 0, $mod->sectionnum);
+        $actions = course_get_cm_edit_actions($mod, 0, $mod->sectionnum);
+        $resp->commands = ' '. $courserenderer->course_section_cm_edit_actions($actions);
         $resp->onclick = $mod->get_on_click();
+        $resp->visible = $mod->visible;
 
         // if using groupings, then display grouping name
         if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', $this->context)) {
index 34dbe12..8f7e875 100644 (file)
@@ -252,7 +252,7 @@ class course_edit_form extends moodleform {
                 array(0=>get_string('completiondisabled','completion'), 1=>get_string('completionenabled','completion')));
             $mform->setDefault('enablecompletion', $courseconfig->enablecompletion);
 
-            $mform->addElement('checkbox', 'completionstartonenrol', get_string('completionstartonenrol', 'completion'));
+            $mform->addElement('advcheckbox', 'completionstartonenrol', get_string('completionstartonenrol', 'completion'));
             $mform->setDefault('completionstartonenrol', $courseconfig->completionstartonenrol);
             $mform->disabledIf('completionstartonenrol', 'enablecompletion', 'eq', 0);
         } else {
index 4dffac5..4c405b5 100644 (file)
@@ -71,11 +71,19 @@ class editsection_form extends moodleform {
                 $mform->addHelpButton('groupingid', 'groupingsection', 'group');
             }
 
-            // Date and time conditions
+            // Available from/to defaults to midnight because then the display
+            // will be nicer where it tells users when they can access it (it
+            // shows only the date and not time).
+            $date = usergetdate(time());
+            $midnight = make_timestamp($date['year'], $date['mon'], $date['mday']);
+
+            // Date and time conditions.
             $mform->addElement('date_time_selector', 'availablefrom',
-                    get_string('availablefrom', 'condition'), array('optional' => true));
+                    get_string('availablefrom', 'condition'),
+                    array('optional' => true, 'defaulttime' => $midnight));
             $mform->addElement('date_time_selector', 'availableuntil',
-                    get_string('availableuntil', 'condition'), array('optional' => true));
+                    get_string('availableuntil', 'condition'),
+                    array('optional' => true, 'defaulttime' => $midnight));
 
             // Conditions based on grades
             $gradeoptions = array();
index b5c82f4..3476dcc 100644 (file)
@@ -125,7 +125,7 @@ class core_course_external extends external_api {
                 $sectioncontents = array();
 
                 //for each module of the section
-                foreach ($modinfo->sections[$section->section] as $cmid) { //matching /course/lib.php:print_section() logic
+                foreach ($modinfo->sections[$section->section] as $cmid) {
                     $cm = $modinfo->cms[$cmid];
 
                     // stop here if the module is not visible to the user
index befde23..d9a7edc 100644 (file)
@@ -38,6 +38,20 @@ defined('MOODLE_INTERNAL') || die();
  */
 abstract class format_section_renderer_base extends plugin_renderer_base {
 
+    /** @var contains instance of core course renderer */
+    protected $courserenderer;
+
+    /**
+     * Constructor method, calls the parent constructor
+     *
+     * @param moodle_page $page
+     * @param string $target one of rendering target constants
+     */
+    public function __construct(moodle_page $page, $target) {
+        parent::__construct($page, $target);
+        $this->courserenderer = $this->page->get_renderer('core', 'course');
+    }
+
     /**
      * Generate the starting container html for a list of sections
      * @return string HTML to output.
@@ -175,7 +189,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         }
         $o.= html_writer::end_tag('div');
 
-        $o .= $this->section_availability_message($section);
+        $o .= $this->section_availability_message($section,
+                has_capability('moodle/course:viewhiddensections', $context));
 
         return $o;
     }
@@ -305,7 +320,9 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o.= html_writer::end_tag('div');
         $o.= $this->section_activity_summary($section, $course, null);
 
-        $o.= $this->section_availability_message($section);
+        $context = context_course::instance($course->id);
+        $o .= $this->section_availability_message($section,
+                has_capability('moodle/course:viewhiddensections', $context));
 
         $o .= html_writer::end_tag('div');
         $o .= html_writer::end_tag('li');
@@ -389,22 +406,38 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
     }
 
     /**
-     * If section is not visible to current user, display the message about that
-     * ('Not available until...', that sort of thing). Otherwise, returns blank.
+     * If section is not visible, display the message about that ('Not available
+     * until...', that sort of thing). Otherwise, returns blank.
+     *
+     * For users with the ability to view hidden sections, it shows the
+     * information even though you can view the section and also may include
+     * slightly fuller information (so that teachers can tell when sections
+     * are going to be unavailable etc). This logic is the same as for
+     * activities.
      *
      * @param stdClass $section The course_section entry from DB
+     * @param bool $canviewhidden True if user can view hidden sections
      * @return string HTML to output
      */
-    protected function section_availability_message($section) {
+    protected function section_availability_message($section, $canviewhidden) {
+        global $CFG;
         $o = '';
-        if (!$section->uservisible || $section->availableinfo) {
+        if (!$section->uservisible) {
             $o .= html_writer::start_tag('div', array('class' => 'availabilityinfo'));
-            if (!empty($section->availableinfo)) {
-                $o .= $section->availableinfo;
-            } else {
-                $o .= get_string('notavailable');
-            }
+            // Note: We only get to this function if availableinfo is non-empty,
+            // so there is definitely something to print.
+            $o .= $section->availableinfo;
             $o .= html_writer::end_tag('div');
+        } else if ($canviewhidden && !empty($CFG->enableavailability) && $section->visible) {
+            $ci = new condition_info_section($section);
+            $fullinfo = $ci->get_full_information();
+            if ($fullinfo) {
+                $o .= html_writer::start_tag('div', array('class' => 'availabilityinfo'));
+                $o .= get_string(
+                        ($section->showavailability ? 'userrestriction_visible' : 'userrestriction_hidden'),
+                        'condition', $fullinfo);
+                $o .= html_writer::end_tag('div');
+            }
         }
         return $o;
     }
@@ -568,10 +601,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) {
             echo $this->start_section_list();
             echo $this->section_header($thissection, $course, true, $displaysection);
-            print_section($course, $thissection, null, null, true, "100%", false, $displaysection);
-            if ($PAGE->user_is_editing()) {
-                print_section_add_menus($course, 0, null, false, false, $displaysection);
-            }
+            echo $this->courserenderer->course_section_cm_list($course, $thissection, $displaysection);
+            echo $this->courserenderer->course_section_add_cm_control($course, 0, $displaysection);
             echo $this->section_footer();
             echo $this->end_section_list();
         }
@@ -605,10 +636,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $completioninfo = new completion_info($course);
         echo $completioninfo->display_help_icon();
 
-        print_section($course, $thissection, null, null, true, '100%', false, $displaysection);
-        if ($PAGE->user_is_editing()) {
-            print_section_add_menus($course, $displaysection, null, false, false, $displaysection);
-        }
+        echo $this->courserenderer->course_section_cm_list($course, $thissection, $displaysection);
+        echo $this->courserenderer->course_section_add_cm_control($course, $displaysection, $displaysection);
         echo $this->section_footer();
         echo $this->end_section_list();
 
@@ -658,10 +687,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 // 0-section is displayed a little different then the others
                 if ($thissection->summary or !empty($modinfo->sections[0]) or $PAGE->user_is_editing()) {
                     echo $this->section_header($thissection, $course, false, 0);
-                    print_section($course, $thissection, null, null, true, "100%", false, 0);
-                    if ($PAGE->user_is_editing()) {
-                        print_section_add_menus($course, 0, null, false, false, 0);
-                    }
+                    echo $this->courserenderer->course_section_cm_list($course, $thissection);
+                    echo $this->courserenderer->course_section_add_cm_control($course, 0);
                     echo $this->section_footer();
                 }
                 continue;
@@ -671,9 +698,10 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 continue;
             }
             // Show the section if the user is permitted to access it, OR if it's not available
-            // but showavailability is turned on
+            // but showavailability is turned on (and there is some available info text).
             $showsection = $thissection->uservisible ||
-                    ($thissection->visible && !$thissection->available && $thissection->showavailability);
+                    ($thissection->visible && !$thissection->available && $thissection->showavailability
+                    && !empty($thissection->availableinfo));
             if (!$showsection) {
                 // Hidden section message is overridden by 'unavailable' control
                 // (showavailability option).
@@ -690,10 +718,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             } else {
                 echo $this->section_header($thissection, $course, false, 0);
                 if ($thissection->uservisible) {
-                    print_section($course, $thissection, null, null, true, "100%", false, 0);
-                    if ($PAGE->user_is_editing()) {
-                        print_section_add_menus($course, $section, null, false, false, 0);
-                    }
+                    echo $this->courserenderer->course_section_cm_list($course, $thissection);
+                    echo $this->courserenderer->course_section_add_cm_control($course, $section);
                 }
                 echo $this->section_footer();
             }
@@ -707,7 +733,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                     continue;
                 }
                 echo $this->stealth_section_header($section);
-                print_section($course, $thissection, null, null, true, "100%", false, 0);
+                echo $this->courserenderer->course_section_cm_list($course, $thissection);
                 echo $this->stealth_section_footer();
             }
 
index a1a5e56..f67ac5b 100644 (file)
@@ -67,12 +67,11 @@ $straction = get_string('action');
 $strfulllistofcourses = get_string('fulllistofcourses');
 
 
-/// Unless it's an editing admin, just print the regular listing of courses/categories
+// Unless it's an editing admin, just print the regular listing of courses/categories.
 if (!$adminediting) {
-
-/// Print form for creating new categories
+    $showaddcoursebutton = true;
+    // Print form for creating new categories.
     $countcategories = $DB->count_records('course_categories');
-
     if ($countcategories > 1 || ($countcategories == 1 && $DB->count_records('course') > 200)) {
         $strcourses = get_string('courses');
         $strcategories = get_string('categories');
@@ -96,14 +95,13 @@ if (!$adminediting) {
         echo $OUTPUT->header();
         echo $OUTPUT->skip_link_target();
         echo $OUTPUT->box_start('courseboxes');
-        print_courses(0);
+        $showaddcoursebutton = print_courses(0);
         echo $OUTPUT->box_end();
     }
 
     echo $OUTPUT->container_start('buttons');
-    if (has_capability('moodle/course:create', $systemcontext)) {
-    /// Print link to create a new course
-    /// Get the 1st available category
+    if (has_capability('moodle/course:create', $systemcontext) && $showaddcoursebutton) {
+        // Print link to create a new course, for the 1st available category.
         $options = array('category' => $CFG->defaultrequestcategory);
         echo $OUTPUT->single_button(new moodle_url('edit.php', $options), get_string('addnewcourse'), 'get');
     }
@@ -254,7 +252,7 @@ $parentlist = array();
 $displaylist[0] = get_string('top');
 make_categories_list($displaylist, $parentlist);
 
-echo '<table class="generaltable editcourse boxaligncenter"><tr class="header">';
+echo '<table id="coursecategories" class="admintable generaltable editcourse"><tr class="header">';
 echo '<th class="header" scope="col">'.$strcategories.'</th>';
 echo '<th class="header" scope="col">'.$strcourses.'</th>';
 echo '<th class="header" scope="col">'.$stredit.'</th>';
@@ -309,7 +307,7 @@ function print_category_edit($category, $displaylist, $parentslist, $depth=-1, $
             $category->context = context_coursecat::instance($category->id);
         }
 
-        echo '<tr><td align="left" class="name">';
+        echo '<tr><td class="leftalign name">';
         for ($i=0; $i<$depth;$i++) {
             echo '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
         }
@@ -319,9 +317,9 @@ function print_category_edit($category, $displaylist, $parentslist, $depth=-1, $
              format_string($category->name, true, array('context' => $category->context)).'</a>';
         echo '</td>';
 
-        echo '<td class="count">'.$category->coursecount.'</td>';
+        echo '<td class="centeralign count">'.$category->coursecount.'</td>';
 
-        echo '<td class="icons">';    /// Print little icons
+        echo '<td class="centeralign icons">';  /// Print little icons
 
         if (has_capability('moodle/category:manage', $category->context)) {
             echo '<a title="'.$str->edit.'" href="editcategory.php?id='.$category->id.'"><img'.
@@ -358,7 +356,7 @@ function print_category_edit($category, $displaylist, $parentslist, $depth=-1, $
         }
         echo '</td>';
 
-        echo '<td align="left">';
+        echo '<td class="leftalign">';
         if (has_capability('moodle/category:manage', $category->context)) {
             $tempdisplaylist = $displaylist;
             unset($tempdisplaylist[$category->id]);
index c4d11ef..98c4e49 100644 (file)
@@ -34,7 +34,7 @@ if (!confirm_sesskey()) {
     print_error('confirmsesskeybad');
 }
 
-if (strpos($jump, '/') === 0) {
+if (strpos($jump, '/') === 0 || strpos($jump, $CFG->wwwroot) === 0) {
     redirect(new moodle_url($jump));
 } else {
     print_error('error');
index 288e01c..0affe66 100644 (file)
@@ -1253,576 +1253,6 @@ function set_section_visible($courseid, $sectionnumber, $visibility) {
     return $resourcestotoggle;
 }
 
-/**
- * Obtains shared data that is used in print_section when displaying a
- * course-module entry.
- *
- * Calls format_text or format_string as appropriate, and obtains the correct icon.
- *
- * This data is also used in other areas of the code.
- * @param cm_info $cm Course-module data (must come from get_fast_modinfo)
- * @param object $course Moodle course object
- * @return array An array with the following values in this order:
- *   $content (optional extra content for after link),
- *   $instancename (text of link)
- */
-function get_print_section_cm_text(cm_info $cm, $course) {
-    global $OUTPUT;
-
-    // Get content from modinfo if specified. Content displays either
-    // in addition to the standard link (below), or replaces it if
-    // the link is turned off by setting ->url to null.
-    if (($content = $cm->get_content()) !== '') {
-        // Improve filter performance by preloading filter setttings for all
-        // activities on the course (this does nothing if called multiple
-        // times)
-        filter_preload_activities($cm->get_modinfo());
-
-        // Get module context
-        $modulecontext = context_module::instance($cm->id);
-        $labelformatoptions = new stdClass();
-        $labelformatoptions->noclean = true;
-        $labelformatoptions->overflowdiv = true;
-        $labelformatoptions->context = $modulecontext;
-        $content = format_text($content, FORMAT_HTML, $labelformatoptions);
-    } else {
-        $content = '';
-    }
-
-    // Get course context
-    $coursecontext = context_course::instance($course->id);
-    $stringoptions = new stdClass;
-    $stringoptions->context = $coursecontext;
-    $instancename = format_string($cm->name, true,  $stringoptions);
-    return array($content, $instancename);
-}
-
-/**
- * Prints a section full of activity modules
- *
- * @param stdClass $course The course
- * @param stdClass|section_info $section The section object containing properties id and section
- * @param array $mods (argument not used)
- * @param array $modnamesused (argument not used)
- * @param bool $absolute All links are absolute
- * @param string $width Width of the container
- * @param bool $hidecompletion Hide completion status
- * @param int $sectionreturn The section to return to
- * @return void
- */
-function print_section($course, $section, $mods, $modnamesused, $absolute=false, $width="100%", $hidecompletion=false, $sectionreturn=null) {
-    global $CFG, $USER, $DB, $PAGE, $OUTPUT;
-
-    static $initialised;
-
-    static $groupbuttons;
-    static $groupbuttonslink;
-    static $isediting;
-    static $ismoving;
-    static $strmovehere;
-    static $strmovefull;
-    static $strunreadpostsone;
-
-    if (!isset($initialised)) {
-        $groupbuttons     = ($course->groupmode or (!$course->groupmodeforce));
-        $groupbuttonslink = (!$course->groupmodeforce);
-        $isediting        = $PAGE->user_is_editing();
-        $ismoving         = $isediting && ismoving($course->id);
-        if ($ismoving) {
-            $strmovehere  = get_string("movehere");
-            $strmovefull  = strip_tags(get_string("movefull", "", "'$USER->activitycopyname'"));
-        }
-        $initialised = true;
-    }
-
-    $modinfo = get_fast_modinfo($course);
-    $completioninfo = new completion_info($course);
-
-    //Accessibility: replace table with list <ul>, but don't output empty list.
-    if (!empty($modinfo->sections[$section->section])) {
-
-        // Fix bug #5027, don't want style=\"width:$width\".
-        echo "<ul class=\"section img-text\">\n";
-
-        foreach ($modinfo->sections[$section->section] as $modnumber) {
-            $mod = $modinfo->cms[$modnumber];
-
-            if ($ismoving and $mod->id == $USER->activitycopy) {
-                // do not display moving mod
-                continue;
-            }
-
-            // We can continue (because it will not be displayed at all)
-            // if:
-            // 1) The activity is not visible to users
-            // and
-            // 2a) The 'showavailability' option is not set (if that is set,
-            //     we need to display the activity so we can show
-            //     availability info)
-            // or
-            // 2b) The 'availableinfo' is empty, i.e. the activity was
-            //     hidden in a way that leaves no info, such as using the
-            //     eye icon.
-            if (!$mod->uservisible &&
-                (empty($mod->showavailability) ||
-                  empty($mod->availableinfo))) {
-                // visibility shortcut
-                continue;
-            }
-
-            // In some cases the activity is visible to user, but it is
-            // dimmed. This is done if viewhiddenactivities is true and if:
-            // 1. the activity is not visible, or
-            // 2. the activity has dates set which do not include current, or
-            // 3. the activity has any other conditions set (regardless of whether
-            //    current user meets them)
-            $modcontext = context_module::instance($mod->id);
-            $canviewhidden = has_capability('moodle/course:viewhiddenactivities', $modcontext);
-            $accessiblebutdim = false;
-            $conditionalhidden = false;
-            if ($canviewhidden) {
-                $accessiblebutdim = !$mod->visible;
-                if (!empty($CFG->enableavailability)) {
-                    $conditionalhidden = $mod->availablefrom > time() ||
-                        ($mod->availableuntil && $mod->availableuntil < time()) ||
-                        count($mod->conditionsgrade) > 0 ||
-                        count($mod->conditionscompletion) > 0;
-                }
-                $accessiblebutdim = $conditionalhidden || $accessiblebutdim;
-            }
-
-            $liclasses = array();
-            $liclasses[] = 'activity';
-            $liclasses[] = $mod->modname;
-            $liclasses[] = 'modtype_'.$mod->modname;
-            $extraclasses = $mod->get_extra_classes();
-            if ($extraclasses) {
-                $liclasses = array_merge($liclasses, explode(' ', $extraclasses));
-            }
-            echo html_writer::start_tag('li', array('class'=>join(' ', $liclasses), 'id'=>'module-'.$modnumber));
-            if ($ismoving) {
-                echo '<a title="'.$strmovefull.'"'.
-                     ' href="'.$CFG->wwwroot.'/course/mod.php?moveto='.$mod->id.'&amp;sesskey='.sesskey().'">'.
-                     '<img class="movetarget" src="'.$OUTPUT->pix_url('movehere') . '" '.
-                     ' alt="'.$strmovehere.'" /></a><br />
-                     ';
-            }
-
-            $classes = array('mod-indent');
-            if (!empty($mod->indent)) {
-                $classes[] = 'mod-indent-'.$mod->indent;
-                if ($mod->indent > 15) {
-                    $classes[] = 'mod-indent-huge';
-                }
-            }
-            echo html_writer::start_tag('div', array('class'=>join(' ', $classes)));
-
-            // Get data about this course-module
-            list($content, $instancename) =
-                    get_print_section_cm_text($modinfo->cms[$modnumber], $course);
-
-            //Accessibility: for files get description via icon, this is very ugly hack!
-            $altname = '';
-            $altname = $mod->modfullname;
-            // Avoid unnecessary duplication: if e.g. a forum name already
-            // includes the word forum (or Forum, etc) then it is unhelpful
-            // to include that in the accessible description that is added.
-            if (false !== strpos(textlib::strtolower($instancename),
-                    textlib::strtolower($altname))) {
-                $altname = '';
-            }
-            // File type after name, for alphabetic lists (screen reader).
-            if ($altname) {
-                $altname = get_accesshide(' '.$altname);
-            }
-
-            // Start the div for the activity title, excluding the edit icons.
-            echo html_writer::start_tag('div', array('class' => 'activityinstance'));
-
-            // We may be displaying this just in order to show information
-            // about visibility, without the actual link
-            $contentpart = '';
-            if ($mod->uservisible) {
-                // Nope - in this case the link is fully working for user
-                $linkclasses = '';
-                $textclasses = '';
-                if ($accessiblebutdim) {
-                    $linkclasses .= ' dimmed';
-                    $textclasses .= ' dimmed_text';
-                    if ($conditionalhidden) {
-                        $linkclasses .= ' conditionalhidden';
-                        $textclasses .= ' conditionalhidden';
-                    }
-                    $accesstext = get_accesshide(get_string('hiddenfromstudents').': ');
-                } else {
-                    $accesstext = '';
-                }
-                if ($linkclasses) {
-                    $linkcss = trim($linkclasses) . ' ';
-                } else {
-                    $linkcss = '';
-                }
-                if ($textclasses) {
-                    $textcss = trim($textclasses) . ' ';
-                } else {
-                    $textcss = '';
-                }
-
-                // Get on-click attribute value if specified and decode the onclick - it
-                // has already been encoded for display (puke).
-                $onclick = htmlspecialchars_decode($mod->get_on_click(), ENT_QUOTES);
-
-                $groupinglabel = '';
-                if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', context_course::instance($course->id))) {
-                    $groupings = groups_get_all_groupings($course->id);
-                    $groupinglabel = html_writer::tag('span', '('.format_string($groupings[$mod->groupingid]->name).')',
-                            array('class' => 'groupinglabel'));
-                }
-
-                if ($url = $mod->get_url()) {
-                    // Display link itself.
-                    $activitylink = html_writer::empty_tag('img', array('src' => $mod->get_icon_url(),
-                            'class' => 'iconlarge activityicon', 'alt' => $mod->modfullname)) . $accesstext .
-                            html_writer::tag('span', $instancename . $altname, array('class' => 'instancename'));
-                    echo html_writer::link($url, $activitylink, array('class' => $linkcss, 'onclick' => $onclick)) .
-                            $groupinglabel;
-
-                    // If specified, display extra content after link.
-                    if ($content) {
-                        $contentpart = html_writer::tag('div', $content, array('class' =>
-                                trim('contentafterlink ' . $textclasses)));
-                    }
-                } else {
-                    // No link, so display only content.
-                    $contentpart = html_writer::tag('div', $accesstext . $content, array('class' => $textcss));
-                }
-
-            } else {
-                $textclasses = $extraclasses;
-                $textclasses .= ' dimmed_text';
-                if ($textclasses) {
-                    $textcss = 'class="' . trim($textclasses) . '" ';
-                } else {
-                    $textcss = '';
-                }
-                $accesstext = '<span class="accesshide">' .
-                        get_string('notavailableyet', 'condition') .
-                        ': </span>';
-
-                if ($url = $mod->get_url()) {
-                    // Display greyed-out text of link
-                    echo '<div ' . $textcss . $mod->extra .
-                            ' >' . '<img src="' . $mod->get_icon_url() .
-                            '" class="activityicon" alt="" /> <span>'. $instancename . $altname .
-                            '</span></div>';
-
-                    // Do not display content after link when it is greyed out like this.
-                } else {
-                    // No link, so display only content (also greyed)
-                    $contentpart = '<div ' . $textcss . $mod->extra . '>' .
-                            $accesstext . $content . '</div>';
-                }
-            }
-
-            // Module can put text after the link (e.g. forum unread)
-            echo $mod->get_after_link();
-
-            // Closing the tag which contains everything but edit icons. $contentpart should not be part of this.
-            echo html_writer::end_tag('div');
-
-            // If there is content but NO link (eg label), then display the
-            // content here (BEFORE any icons). In this case cons must be
-            // displayed after the content so that it makes more sense visually
-            // and for accessibility reasons, e.g. if you have a one-line label
-            // it should work similarly (at least in terms of ordering) to an
-            // activity.
-            if (empty($url)) {
-                echo $contentpart;
-            }
-
-            if ($isediting) {
-                if ($groupbuttons and plugin_supports('mod', $mod->modname, FEATURE_GROUPS, 0)) {
-                    if (! $mod->groupmodelink = $groupbuttonslink) {
-                        $mod->groupmode = $course->groupmode;
-                    }
-
-                } else {
-                    $mod->groupmode = false;
-                }
-                echo make_editing_buttons($mod, $absolute, true, $mod->indent, $sectionreturn);
-                echo $mod->get_after_edit_icons();
-            }
-
-            // Completion
-            $completion = $hidecompletion
-                ? COMPLETION_TRACKING_NONE
-                : $completioninfo->is_enabled($mod);
-            if ($completion!=COMPLETION_TRACKING_NONE && isloggedin() &&
-                !isguestuser() && $mod->uservisible) {
-                $completiondata = $completioninfo->get_data($mod,true);
-                $completionicon = '';
-                if ($isediting) {
-                    switch ($completion) {
-                        case COMPLETION_TRACKING_MANUAL :
-                            $completionicon = 'manual-enabled'; break;
-                        case COMPLETION_TRACKING_AUTOMATIC :
-                            $completionicon = 'auto-enabled'; break;
-                        default: // wtf
-                    }
-                } else if ($completion==COMPLETION_TRACKING_MANUAL) {
-                    switch($completiondata->completionstate) {
-                        case COMPLETION_INCOMPLETE:
-                            $completionicon = 'manual-n'; break;
-                        case COMPLETION_COMPLETE:
-                            $completionicon = 'manual-y'; break;
-                    }
-                } else { // Automatic
-                    switch($completiondata->completionstate) {
-                        case COMPLETION_INCOMPLETE:
-                            $completionicon = 'auto-n'; break;
-                        case COMPLETION_COMPLETE:
-                            $completionicon = 'auto-y'; break;
-                        case COMPLETION_COMPLETE_PASS:
-                            $completionicon = 'auto-pass'; break;
-                        case COMPLETION_COMPLETE_FAIL:
-                            $completionicon = 'auto-fail'; break;
-                    }
-                }
-                if ($completionicon) {
-                    $imgsrc = $OUTPUT->pix_url('i/completion-'.$completionicon);
-                    $formattedname = format_string($mod->name, true, array('context' => $modcontext));
-                    $imgalt = get_string('completion-alt-' . $completionicon, 'completion', $formattedname);
-                    if ($completion == COMPLETION_TRACKING_MANUAL && !$isediting) {
-                        $imgtitle = get_string('completion-title-' . $completionicon, 'completion', $formattedname);
-                        $newstate =
-                            $completiondata->completionstate==COMPLETION_COMPLETE
-                            ? COMPLETION_INCOMPLETE
-                            : COMPLETION_COMPLETE;
-                        // In manual mode the icon is a toggle form...
-
-                        // If this completion state is used by the
-                        // conditional activities system, we need to turn
-                        // off the JS.
-                        if (!empty($CFG->enableavailability) &&
-                            condition_info::completion_value_used_as_condition($course, $mod)) {
-                            $extraclass = ' preventjs';
-                        } else {
-                            $extraclass = '';
-                        }
-                        echo html_writer::start_tag('form', array(
-                                'class' => 'togglecompletion' . $extraclass,
-                                'method' => 'post',
-                                'action' => $CFG->wwwroot . '/course/togglecompletion.php'));
-                        echo html_writer::start_tag('div');
-                        echo html_writer::empty_tag('input', array(
-                                'type' => 'hidden', 'name' => 'id', 'value' => $mod->id));
-                        echo html_writer::empty_tag('input', array(
-                                'type' => 'hidden', 'name' => 'modulename',
-                                'value' => $mod->name));
-                        echo html_writer::empty_tag('input', array(
-                                'type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
-                        echo html_writer::empty_tag('input', array(
-                                'type' => 'hidden', 'name' => 'completionstate',
-                                'value' => $newstate));
-                        echo html_writer::empty_tag('input', array(
-                                'type' => 'image', 'src' => $imgsrc, 'alt' => $imgalt, 'title' => $imgtitle));
-                        echo html_writer::end_tag('div');
-                        echo html_writer::end_tag('form');
-                    } else {
-                        // In auto mode, or when editing, the icon is just an image
-                        echo "<span class='autocompletion'>";
-                        echo "<img src='$imgsrc' alt='$imgalt' title='$imgalt' /></span>";
-                    }
-                }
-            }
-
-            // If there is content AND a link, then display the content here
-            // (AFTER any icons). Otherwise it was displayed before
-            if (!empty($url)) {
-                echo $contentpart;
-            }
-
-            // Show availability information (for someone who isn't allowed to
-            // see the activity itself, or for staff)
-            if (!$mod->uservisible) {
-                echo '<div class="availabilityinfo">'.$mod->availableinfo.'</div>';
-            } else if ($canviewhidden && !empty($CFG->enableavailability)) {
-                // Don't add availability information if user is not editing and activity is hidden.
-                if ($mod->visible || $PAGE->user_is_editing()) {
-                    $hidinfoclass = '';
-                    if (!$mod->visible) {
-                        $hidinfoclass = 'hide';
-                    }
-                    $ci = new condition_info($mod);
-                    $fullinfo = $ci->get_full_information();
-                    if($fullinfo) {
-                        echo '<div class="availabilityinfo '.$hidinfoclass.'">'.get_string($mod->showavailability
-                            ? 'userrestriction_visible'
-                            : 'userrestriction_hidden','condition',
-                            $fullinfo).'</div>';
-                    }
-                }
-            }
-
-            echo html_writer::end_tag('div');
-            echo html_writer::end_tag('li')."\n";
-        }
-
-    } elseif ($ismoving) {
-        echo "<ul class=\"section\">\n";
-    }
-
-    if ($ismoving) {
-        echo '<li><a title="'.$strmovefull.'"'.
-             ' href="'.$CFG->wwwroot.'/course/mod.php?movetosection='.$section->id.'&amp;sesskey='.sesskey().'">'.
-             '<img class="movetarget" src="'.$OUTPUT->pix_url('movehere') . '" '.
-             ' alt="'.$strmovehere.'" /></a></li>
-             ';
-    }
-    if (!empty($modinfo->sections[$section->section]) || $ismoving) {
-        echo "</ul><!--class='section'-->\n\n";
-    }
-}
-
-/**
- * Prints the menus to add activities and resources.
- *
- * @param stdClass $course The course
- * @param int $section relative section number (field course_sections.section)
- * @param null|array $modnames An array containing the list of modules and their names
- *     if omitted will be taken from get_module_types_names()
- * @param bool $vertical Vertical orientation
- * @param bool $return Return the menus or send them to output
- * @param int $sectionreturn The section to link back to
- * @return void|string depending on $return
- */
-function print_section_add_menus($course, $section, $modnames = null, $vertical=false, $return=false, $sectionreturn=null) {
-    global $CFG, $OUTPUT;
-
-    if ($modnames === null) {
-        $modnames = get_module_types_names();
-    }
-
-    // check to see if user can add menus and there are modules to add
-    if (!has_capability('moodle/course:manageactivities', context_course::instance($course->id))
-            || empty($modnames)) {
-        if ($return) {
-            return '';
-        } else {
-            return false;
-        }
-    }
-
-    // Retrieve all modules with associated metadata
-    $modules = get_module_metadata($course, $modnames, $sectionreturn);
-
-    // We'll sort resources and activities into two lists
-    $resources = array();
-    $activities = array();
-
-    // We need to add the section section to the link for each module
-    $sectionlink = '&section=' . $section . '&sr=' . $sectionreturn;
-
-    foreach ($modules as $module) {
-        if (isset($module->types)) {
-            // This module has a subtype
-            // NOTE: this is legacy stuff, module subtypes are very strongly discouraged!!
-            $subtypes = array();
-            foreach ($module->types as $subtype) {
-                $subtypes[$subtype->link . $sectionlink] = $subtype->title;
-            }
-
-            // Sort module subtypes into the list
-            if (!empty($module->title)) {
-                // This grouping has a name
-                if ($module->archetype == MOD_CLASS_RESOURCE) {
-                    $resources[] = array($module->title=>$subtypes);
-                } else {
-                    $activities[] = array($module->title=>$subtypes);
-                }
-            } else {
-                // This grouping does not have a name
-                if ($module->archetype == MOD_CLASS_RESOURCE) {
-                    $resources = array_merge($resources, $subtypes);
-                } else {
-                    $activities = array_merge($activities, $subtypes);
-                }
-            }
-        } else {
-            // This module has no subtypes
-            if ($module->archetype == MOD_ARCHETYPE_RESOURCE) {
-                $resources[$module->link . $sectionlink] = $module->title;
-            } else if ($module->archetype === MOD_ARCHETYPE_SYSTEM) {
-                // System modules cannot be added by user, do not add to dropdown
-            } else {
-                $activities[$module->link . $sectionlink] = $module->title;
-            }
-        }
-    }
-
-    $straddactivity = get_string('addactivity');
-    $straddresource = get_string('addresource');
-    $sectionname = get_section_name($course, $section);
-    $strresourcelabel = get_string('addresourcetosection', null, $sectionname);
-    $stractivitylabel = get_string('addactivitytosection', null, $sectionname);
-
-    $output = html_writer::start_tag('div', array('class' => 'section_add_menus', 'id' => 'add_menus-section-' . $section));
-
-    if (!$vertical) {
-        $output .= html_writer::start_tag('div', array('class' => 'horizontal'));
-    }
-
-    if (!empty($resources)) {
-        $select = new url_select($resources, '', array(''=>$straddresource), "ressection$section");
-        $select->set_help_icon('resources');
-        $select->set_label($strresourcelabel, array('class' => 'accesshide'));
-        $output .= $OUTPUT->render($select);
-    }
-
-    if (!empty($activities)) {
-        $select = new url_select($activities, '', array(''=>$straddactivity), "section$section");
-        $select->set_help_icon('activities');
-        $select->set_label($stractivitylabel, array('class' => 'accesshide'));
-        $output .= $OUTPUT->render($select);
-    }
-
-    if (!$vertical) {
-        $output .= html_writer::end_tag('div');
-    }
-
-    $output .= html_writer::end_tag('div');
-
-    if (course_ajax_enabled($course)) {
-        $straddeither = get_string('addresourceoractivity');
-        // The module chooser link
-        $modchooser = html_writer::start_tag('div', array('class' => 'mdl-right'));
-        $modchooser.= html_writer::start_tag('div', array('class' => 'section-modchooser'));
-        $icon = $OUTPUT->pix_icon('t/add', '');
-        $span = html_writer::tag('span', $straddeither, array('class' => 'section-modchooser-text'));
-        $modchooser .= html_writer::tag('span', $icon . $span, array('class' => 'section-modchooser-link'));
-        $modchooser.= html_writer::end_tag('div');
-        $modchooser.= html_writer::end_tag('div');
-
-        // Wrap the normal output in a noscript div
-        $usemodchooser = get_user_preferences('usemodchooser', $CFG->modchooserdefault);
-        if ($usemodchooser) {
-            $output = html_writer::tag('div', $output, array('class' => 'hiddenifjs addresourcedropdown'));
-            $modchooser = html_writer::tag('div', $modchooser, array('class' => 'visibleifjs addresourcemodchooser'));
-        } else {
-            // If the module chooser is disabled, we need to ensure that the dropdowns are shown even if javascript is disabled
-            $output = html_writer::tag('div', $output, array('class' => 'show addresourcedropdown'));
-            $modchooser = html_writer::tag('div', $modchooser, array('class' => 'hide addresourcemodchooser'));
-        }
-        $output = $modchooser . $output;
-    }
-
-    if ($return) {
-        return $output;
-    } else {
-        echo $output;
-    }
-}
-
 /**
  * Retrieve all metadata for the requested modules
  *
@@ -1844,7 +1274,10 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
     }
 
     $return = array();
-    $urlbase = "/course/mod.php?id=$course->id&sesskey=".sesskey().'&sr='.$sectionreturn.'&add=';
+    $urlbase = new moodle_url('/course/mod.php', array('id' => $course->id, 'sesskey' => sesskey()));
+    if ($sectionreturn !== null) {
+        $urlbase->param('sr', $sectionreturn);
+    }
     foreach($modnames as $modname => $modnamestr) {
         if (!course_allowed_module($course, $modname)) {
             continue;
@@ -1891,16 +1324,16 @@ function get_module_metadata($course, $modnames, $sectionreturn = null) {
                     if (get_string_manager()->string_exists('help' . $subtype->name, $modname)) {
                         $subtype->help = get_string('help' . $subtype->name, $modname);
                     }
-                    $subtype->link = $urlbase . $subtype->type;
+                    $subtype->link = new moodle_url($urlbase, array('add' => $subtype->type));
                     $group->types[] = $subtype;
                 }
                 $modlist[$course->id][$modname] = $group;
             }
         } else {
             $module = new stdClass();
-            $module->title = get_string('modulename', $modname);
+            $module->title = $modnamestr;
             $module->name = $modname;
-            $module->link = $urlbase . $modname;
+            $module->link = new moodle_url($urlbase, array('add' => $modname));
             $module->icon = $OUTPUT->pix_icon('icon', '', $module->name, array('class' => 'icon'));
             $sm = get_string_manager();
             if ($sm->string_exists('modulename_help', $modname)) {
@@ -2131,7 +1564,7 @@ function get_course_category_tree($id = 0, $depth = 0) {
  * Recursive function to print out all the categories in a nice format
  * with or without courses included
  */
-function print_whole_category_list($category=NULL, $displaylist=NULL, $parentslist=NULL, $depth=-1, $showcourses = true) {
+function print_whole_category_list($category=NULL, $displaylist=NULL, $parentslist=NULL, $depth=-1, $showcourses = true, $categorycourses=NULL) {
     global $CFG;
 
     // maxcategorydepth == 0 meant no limit
@@ -2143,9 +1576,17 @@ function print_whole_category_list($category=NULL, $displaylist=NULL, $parentsli
         make_categories_list($displaylist, $parentslist);
     }
 
+    if (!$categorycourses) {
+        if ($category) {
+            $categorycourses = get_category_courses($category->id);
+        } else {
+            $categorycourses = get_category_courses();
+        }
+    }
+
     if ($category) {
         if ($category->visible or has_capability('moodle/category:viewhiddencategories', context_system::instance())) {
-            print_category_info($category, $depth, $showcourses);
+            print_category_info($category, $depth, $showcourses, $categorycourses[$category->id]);
         } else {
             return;  // Don't bother printing children of invisible categories
         }
@@ -2169,11 +1610,41 @@ function print_whole_category_list($category=NULL, $displaylist=NULL, $parentsli
             $down = $last ? false : true;
             $first = false;
 
-            print_whole_category_list($cat, $displaylist, $parentslist, $depth + 1, $showcourses);
+            print_whole_category_list($cat, $displaylist, $parentslist, $depth + 1, $showcourses, $categorycourses);
         }
     }
 }
 
+/**
+ * Gets an array whose keys are category ids and whose values are arrays of courses in the corresponding category.
+ *
+ * @param int $categoryid
+ * @return array
+ */
+function get_category_courses_array($categoryid = 0) {
+    $tree = get_course_category_tree($categoryid);
+    $flattened = array();
+    foreach ($tree as $category) {
+        get_category_courses_array_recursively($flattened, $category);
+    }
+    return $flattened;
+}
+
+/**
+ * Recursive function to help flatten the course category tree.
+ *
+ * Do not call this function directly, instead calll its parent function {@link get_category_courses_array}
+ *
+ * @param array &$flattened An array passed by reference in which to store courses for each category.
+ * @param stdClass $category The category to get courses for.
+ */
+function get_category_courses_array_recursively(array &$flattened, $category) {
+    $flattened[$category->id] = $category->courses;
+    foreach ($category->categories as $childcategory) {
+        get_category_courses_array_recursively($flattened, $childcategory);
+    }
+}
+
 /**
  * This function will return $options array for html_writer::select(), with whitespace to denote nesting.
  */
@@ -2192,10 +1663,16 @@ function make_categories_options() {
 }
 
 /**
- * Prints the category info in indented fashion
+ * Prints the category information.
+ *
  * This function is only used by print_whole_category_list() above
+ *
+ * @param stdClass $category
+ * @param int $depth The depth of the category.
+ * @param bool $showcourses If set to true course information will also be printed.
+ * @param array|null $courses An array of courses belonging to the category, or null if you don't have it yet.
  */
-function print_category_info($category, $depth=0, $showcourses = false) {
+function print_category_info($category, $depth = 0, $showcourses = false, array $courses = null) {
     global $CFG, $DB, $OUTPUT;
 
     $strsummary = get_string('summary');
@@ -2216,7 +1693,9 @@ function print_category_info($category, $depth=0, $showcourses = false) {
         $catimage = "&nbsp;";
     }
 
-    $courses = get_courses($category->id, 'c.sortorder ASC', 'c.id,c.sortorder,c.visible,c.fullname,c.shortname,c.summary');
+    if (is_null($courses)) {
+        $courses = get_courses($category->id, 'c.sortorder ASC', 'c.id,c.sortorder,c.visible,c.fullname,c.shortname,c.summary');
+    }
     $context = context_coursecat::instance($category->id);
     $fullname = format_string($category->name, true, array('context' => $context));
 
@@ -2371,7 +1850,9 @@ function update_category_button($categoryid = 0) {
 }
 
 /**
- * Category is 0 (for all courses) or an object
+ * Print courses in category. If category is 0 then all courses are printed.
+ * @param int|stdClass $category category object or id.
+ * @return bool true if courses found and printed, else false.
  */
 function print_courses($category) {
     global $CFG, $OUTPUT;
@@ -2419,8 +1900,10 @@ function print_courses($category) {
             echo html_writer::start_tag('div', array('class'=>'addcoursebutton'));
             echo $OUTPUT->single_button(new moodle_url('/course/edit.php', $options), get_string("addnewcourse"));
             echo html_writer::end_tag('div');
+            return false;
         }
     }
+    return true;
 }
 
 /**
@@ -3107,19 +2590,15 @@ function moveto_module($mod, $section, $beforemod=NULL) {
 }
 
 /**
- * Produces the editing buttons for a module
+ * Returns the list of all editing actions that current user can perform on the module
  *
- * @global core_renderer $OUTPUT
- * @staticvar type $str
- * @param stdClass $mod The module to produce editing buttons for
- * @param bool $absolute_ignored ignored - all links are absolute
- * @param bool $moveselect If true a move seleciton process is used (default true)
- * @param int $indent The current indenting
- * @param int $section The section to link back to
- * @return string XHTML for the editing buttons
+ * @param cm_info $mod The module to produce editing buttons for
+ * @param int $indent The current indenting (default -1 means no move left-right actions)
+ * @param int $sr The section to link back to (used for creating the links)
+ * @return array array of action_link or pix_icon objects
  */
-function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $moveselect = true, $indent=-1, $section=null) {
-    global $CFG, $OUTPUT, $COURSE;
+function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
+    global $COURSE, $SITE;
 
     static $str;
 
@@ -3131,43 +2610,36 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
 
     // no permission to edit anything
     if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) {
-        return false;
+        return array();
     }
 
     $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
 
     if (!isset($str)) {
-        $str = new stdClass;
-        $str->assign         = get_string("assignroles", 'role');
-        $str->delete         = get_string("delete");
-        $str->move           = get_string("move");
-        $str->moveup         = get_string("moveup");
-        $str->movedown       = get_string("movedown");
-        $str->moveright      = get_string("moveright");
-        $str->moveleft       = get_string("moveleft");
-        $str->update         = get_string("update");
-        $str->duplicate      = get_string("duplicate");
-        $str->hide           = get_string("hide");
-        $str->show           = get_string("show");
+        $str = get_strings(array('delete', 'move', 'moveright', 'moveleft',
+            'update', 'duplicate', 'hide', 'show', 'edittitle'), 'moodle');
+        $str->assign         = get_string('assignroles', 'role');
         $str->groupsnone     = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsnone"));
         $str->groupsseparate = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsseparate"));
         $str->groupsvisible  = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsvisible"));
         $str->forcedgroupsnone     = get_string('forcedmodeinbrackets', 'moodle', get_string("groupsnone"));
         $str->forcedgroupsseparate = get_string('forcedmodeinbrackets', 'moodle', get_string("groupsseparate"));
         $str->forcedgroupsvisible  = get_string('forcedmodeinbrackets', 'moodle', get_string("groupsvisible"));
-        $str->edittitle = get_string('edittitle', 'moodle');
     }
 
     $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
 
-    if ($section !== null) {
-        $baseurl->param('sr', $section);
+    if ($sr !== null) {
+        $baseurl->param('sr', $sr);
     }
     $actions = array();
 
     // AJAX edit title
-    if ($mod->modname !== 'label' && $hasmanageactivities && course_ajax_enabled($COURSE)) {
-        $actions[] = new action_link(
+    if ($mod->modname !== 'label' && $hasmanageactivities &&
+                (($mod->course == $COURSE->id && course_ajax_enabled($COURSE)) ||
+                 ($mod->course == SITEID && course_ajax_enabled($SITE)))) {
+        // we will not display link if we are on some other-course page (where we should not see this module anyway)
+        $actions['title'] = new action_link(
             new moodle_url($baseurl, array('update' => $mod->id)),
             new pix_icon('t/editstring', $str->edittitle, 'moodle', array('class' => 'iconsmall visibleifjs', 'title' => '')),
             null,
@@ -3186,7 +2658,7 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
         }
 
         if ($indent > 0) {
-            $actions[] = new action_link(
+            $actions['moveleft'] = new action_link(
                 new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
                 new pix_icon($leftarrow, $str->moveleft, 'moodle', array('class' => 'iconsmall', 'title' => '')),
                 null,
@@ -3194,7 +2666,7 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
             );
         }
         if ($indent >= 0) {
-            $actions[] = new action_link(
+            $actions['moveright'] = new action_link(
                 new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
                 new pix_icon($rightarrow, $str->moveright, 'moodle', array('class' => 'iconsmall', 'title' => '')),
                 null,
@@ -3205,32 +2677,17 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
 
     // move
     if ($hasmanageactivities) {
-        if ($moveselect) {
-            $actions[] = new action_link(
-                new moodle_url($baseurl, array('copy' => $mod->id)),
-                new pix_icon('t/move', $str->move, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_move', 'title' => $str->move)
-            );
-        } else {
-            $actions[] = new action_link(
-                new moodle_url($baseurl, array('id' => $mod->id, 'move' => '-1')),
-                new pix_icon('t/up', $str->moveup, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_moveup', 'title' => $str->moveup)
-            );
-            $actions[] = new action_link(
-                new moodle_url($baseurl, array('id' => $mod->id, 'move' => '1')),
-                new pix_icon('t/down', $str->movedown, 'moodle', array('class' => 'iconsmall', 'title' => '')),
-                null,
-                array('class' => 'editing_movedown', 'title' => $str->movedown)
-            );
-        }
+        $actions['move'] = new action_link(
+            new moodle_url($baseurl, array('copy' => $mod->id)),
+            new pix_icon('t/move', $str->move, 'moodle', array('class' => 'iconsmall', 'title' => '')),
+            null,
+            array('class' => 'editing_move', 'title' => $str->move)
+        );
     }
 
     // Update
     if ($hasmanageactivities) {
-        $actions[] = new action_link(
+        $actions['update'] = new action_link(
             new moodle_url($baseurl, array('update' => $mod->id)),
             new pix_icon('t/edit', $str->update, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             null,
@@ -3239,8 +2696,10 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
     }
 
     // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
-    if (has_all_capabilities($dupecaps, $coursecontext) && plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2)) {
-        $actions[] = new action_link(
+    // note that restoring on front page is never allowed
+    if ($mod->course != SITEID && has_all_capabilities($dupecaps, $coursecontext) &&
+            plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2)) {
+        $actions['duplicate'] = new action_link(
             new moodle_url($baseurl, array('duplicate' => $mod->id)),
             new pix_icon('t/copy', $str->duplicate, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             null,
@@ -3250,7 +2709,7 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
 
     // Delete
     if ($hasmanageactivities) {
-        $actions[] = new action_link(
+        $actions['delete'] = new action_link(
             new moodle_url($baseurl, array('delete' => $mod->id)),
             new pix_icon('t/delete', $str->delete, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             null,
@@ -3261,14 +2720,14 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
     // hideshow
     if (has_capability('moodle/course:activityvisibility', $modcontext)) {
         if ($mod->visible) {
-            $actions[] = new action_link(
+            $actions['hide'] = new action_link(
                 new moodle_url($baseurl, array('hide' => $mod->id)),
                 new pix_icon('t/hide', $str->hide, 'moodle', array('class' => 'iconsmall', 'title' => '')),
                 null,
                 array('class' => 'editing_hide', 'title' => $str->hide)
             );
         } else {
-            $actions[] = new action_link(
+            $actions['show'] = new action_link(
                 new moodle_url($baseurl, array('show' => $mod->id)),
                 new pix_icon('t/show', $str->show, 'moodle', array('class' => 'iconsmall', 'title' => '')),
                 null,
@@ -3278,61 +2737,54 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
     }
 
     // groupmode
-    if ($hasmanageactivities and $mod->groupmode !== false) {
-        if ($mod->groupmode == SEPARATEGROUPS) {
-            $groupmode = 0;
+    if ($hasmanageactivities and plugin_supports('mod', $mod->modname, FEATURE_GROUPS, 0)) {
+        if ($mod->coursegroupmodeforce) {
+            $modgroupmode = $mod->coursegroupmode;
+        } else {
+            $modgroupmode = $mod->groupmode;
+        }
+        if ($modgroupmode == SEPARATEGROUPS) {
+            $groupmode = NOGROUPS;
             $grouptitle = $str->groupsseparate;
             $forcedgrouptitle = $str->forcedgroupsseparate;
-            $groupclass = 'editing_groupsseparate';
+            $actionname = 'groupsseparate';
             $groupimage = 't/groups';
-        } else if ($mod->groupmode == VISIBLEGROUPS) {
-            $groupmode = 1;
+        } else if ($modgroupmode == VISIBLEGROUPS) {
+            $groupmode = SEPARATEGROUPS;
             $grouptitle = $str->groupsvisible;
             $forcedgrouptitle = $str->forcedgroupsvisible;
-            $groupclass = 'editing_groupsvisible';
+            $actionname = 'groupsvisible';
             $groupimage = 't/groupv';
         } else {
-            $groupmode = 2;
+            $groupmode = VISIBLEGROUPS;
             $grouptitle = $str->groupsnone;
             $forcedgrouptitle = $str->forcedgroupsnone;
-            $groupclass = 'editing_groupsnone';
+            $actionname = 'groupsnone';
             $groupimage = 't/groupn';
         }
-        if ($mod->groupmodelink) {
-            $actions[] = new action_link(
+        if (!$mod->coursegroupmodeforce) {
+            $actions[$actionname] = new action_link(
                 new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $groupmode)),
                 new pix_icon($groupimage, $grouptitle, 'moodle', array('class' => 'iconsmall', 'title' => '')),
                 null,
-                array('class' => $groupclass, 'title' => $grouptitle)
+                array('class' => 'editing_'. $actionname, 'title' => $grouptitle)
             );
         } else {
-            $actions[] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('title' => $forcedgrouptitle, 'class' => 'iconsmall'));
+            $actions[$actionname] = new pix_icon($groupimage, $forcedgrouptitle, 'moodle', array('title' => $forcedgrouptitle, 'class' => 'iconsmall'));
         }
     }
 
     // Assign
     if (has_capability('moodle/role:assign', $modcontext)){
-        $actions[] = new action_link(
-            new moodle_url('/'.$CFG->admin.'/roles/assign.php', array('contextid' => $modcontext->id)),
+        $actions['assign'] = new action_link(
+            new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
             new pix_icon('t/assignroles', $str->assign, 'moodle', array('class' => 'iconsmall', 'title' => '')),
             null,
             array('class' => 'editing_assign', 'title' => $str->assign)
         );
     }
 
-    // The space added before the <span> is a ugly hack but required to set the CSS property white-space: nowrap
-    // and having it to work without attaching the preceding text along with it. Hopefully the refactoring of
-    // the course page HTML will allow this to be removed.
-    $output = ' ' . html_writer::start_tag('span', array('class' => 'commands'));
-    foreach ($actions as $action) {
-        if ($action instanceof renderable) {
-            $output .= $OUTPUT->render($action);
-        } else {
-            $output .= $action;
-        }
-    }
-    $output .= html_writer::end_tag('span');
-    return $output;
+    return $actions;
 }
 
 /**
@@ -4500,17 +3952,6 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
     // Load drag and drop upload AJAX.
     dndupload_add_to_course($course, $enabledmodules);
 
-    // Add the module chooser
-    $PAGE->requires->yui_module('moodle-course-modchooser',
-        'M.course.init_chooser',
-        array(array('courseid' => $course->id, 'closeButtonTitle' => get_string('close', 'editor')))
-    );
-    $PAGE->requires->strings_for_js(array(
-            'addresourceoractivity',
-            'modchooserenable',
-            'modchooserdisable',
-    ), 'moodle');
-
     return true;
 }
 
index d175a87..7247671 100644 (file)
@@ -45,7 +45,7 @@ $section    = $DB->get_record('course_sections', array('id' => $cm->section, 'co
 require_login($course);
 require_sesskey();
 require_capability('moodle/course:manageactivities', $context);
-// Require both target import caps to be able to duplicate, see make_editing_buttons()
+// Require both target import caps to be able to duplicate, see course_get_cm_edit_actions()
 require_capability('moodle/backup:backuptargetimport', $context);
 require_capability('moodle/restore:restoretargetimport', $context);
 
index 33c15b4..9562da5 100644 (file)
@@ -49,6 +49,47 @@ class core_course_renderer extends plugin_renderer_base {
     public function __construct(moodle_page $page, $target) {
         $this->strings = new stdClass;
         parent::__construct($page, $target);
+        $this->add_modchoosertoggle();
+    }
+
+    /**
+     * Adds the item in course settings navigation to toggle modchooser
+     *
+     * Theme can overwrite as an empty function to exclude it (for example if theme does not
+     * use modchooser at all)
+     */
+    protected function add_modchoosertoggle() {
+        global $CFG;
+        static $modchoosertoggleadded = false;
+        // Add the module chooser toggle to the course page
+        if ($modchoosertoggleadded || $this->page->state > moodle_page::STATE_PRINTING_HEADER ||
+                $this->page->course->id == SITEID ||
+                !($coursenode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE))) {
+            // too late or we are on site page or we could not find the course settings node
+            return;
+        }
+        $modchoosertoggleadded = true;
+        if ($this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
+            // We are on the course page, retain the current page params e.g. section.
+            $modchoosertoggleurl = clone($this->page->url);
+        } else {
+            // Edit on the main course page.
+            $modchoosertoggleurl = new moodle_url('/course/view.php', array('id' => $this->page->course->id,
+                'return' => $this->page->url->out_as_local_url(false)));
+        }
+        $modchoosertoggleurl->param('sesskey', sesskey());
+        if ($usemodchooser = get_user_preferences('usemodchooser', $CFG->modchooserdefault)) {
+            $modchoosertogglestring = get_string('modchooserdisable', 'moodle');
+            $modchoosertoggleurl->param('modchooser', 'off');
+        } else {
+            $modchoosertogglestring = get_string('modchooserenable', 'moodle');
+            $modchoosertoggleurl->param('modchooser', 'on');
+        }
+        $modchoosertoggle = navigation_node::create($modchoosertogglestring, $modchoosertoggleurl, navigation_node::TYPE_SETTING);
+        $coursenode->add_node($modchoosertoggle, 'editsettings');
+        $modchoosertoggle->add_class('modchoosertoggle');
+        $modchoosertoggle->add_class('visibleifjs');
+        user_preference_allow_ajax_update('usemodchooser', PARAM_BOOL);
     }
 
     /**
@@ -70,7 +111,7 @@ class core_course_renderer extends plugin_renderer_base {
         if (!empty($CFG->coursecontact)) {
             $coursecontactroles = explode(',', $CFG->coursecontact);
             foreach ($coursecontactroles as $roleid) {
-                if ($users = get_role_users($roleid, $context, true)) {
+                if ($users = get_role_users($roleid, $context, true, '', null, false)) {
                     foreach ($users as $teacher) {
                         $role = new stdClass();
                         $role->id = $teacher->roleid;
@@ -211,7 +252,22 @@ class core_course_renderer extends plugin_renderer_base {
      * @return string The composed HTML for the module
      */
     public function course_modchooser($modules, $course) {
-        global $OUTPUT;
+        static $isdisplayed = false;
+        if ($isdisplayed) {
+            return '';
+        }
+        $isdisplayed = true;
+
+        // Add the module chooser
+        $this->page->requires->yui_module('moodle-course-modchooser',
+        'M.course.init_chooser',
+        array(array('courseid' => $course->id, 'closeButtonTitle' => get_string('close', 'editor')))
+        );
+        $this->page->requires->strings_for_js(array(
+                'addresourceoractivity',
+                'modchooserenable',
+                'modchooserdisable',
+        ), 'moodle');
 
         // Add the header
         $header = html_writer::tag('div', get_string('addresourceoractivity', 'moodle'),
@@ -354,4 +410,616 @@ class core_course_renderer extends plugin_renderer_base {
         $module->help = '';
         return $this->course_modchooser_module($module, array('moduletypetitle'));
     }
+
+    /**
+     * Renders HTML for displaying the sequence of course module editing buttons
+     *
+     * @see course_get_cm_edit_actions()
+     *
+     * @param array $actions array of action_link or pix_icon objects
+     * @return string
+     */
+    public function course_section_cm_edit_actions($actions) {
+        $output = html_writer::start_tag('span', array('class' => 'commands'));
+        foreach ($actions as $action) {
+            if ($action instanceof renderable) {
+                $output .= $this->output->render($action);
+            } else {
+                $output .= $action;
+            }
+        }
+        $output .= html_writer::end_tag('span');
+        return $output;
+    }
+
+    /**
+     * Renders HTML for the menus to add activities and resources to the current course
+     *
+     * Note, if theme overwrites this function and it does not use modchooser,
+     * see also {@link core_course_renderer::add_modchoosertoggle()}
+     *
+     * @param stdClass $course
+     * @param int $section relative section number (field course_sections.section)
+     * @param int $sectionreturn The section to link back to
+     * @param array $displayoptions additional display options, for example blocks add
+     *     option 'inblock' => true, suggesting to display controls vertically
+     * @return string
+     */
+    function course_section_add_cm_control($course, $section, $sectionreturn = null, $displayoptions = array()) {
+        global $CFG;
+
+        $vertical = !empty($displayoptions['inblock']);
+
+        // check to see if user can add menus and there are modules to add
+        if (!has_capability('moodle/course:manageactivities', context_course::instance($course->id))
+                || !$this->page->user_is_editing()
+                || !($modnames = get_module_types_names()) || empty($modnames)) {
+            return '';
+        }
+
+        // Retrieve all modules with associated metadata
+        $modules = get_module_metadata($course, $modnames, $sectionreturn);
+        $urlparams = array('section' => $section);
+
+        // We'll sort resources and activities into two lists
+        $activities = array(MOD_CLASS_ACTIVITY => array(), MOD_CLASS_RESOURCE => array());
+
+        foreach ($modules as $module) {
+            if (!array_key_exists($module->archetype, $activities)) {
+                // System modules cannot be added by user, do not add to dropdown
+            } else if (isset($module->types)) {
+                // This module has a subtype
+                // NOTE: this is legacy stuff, module subtypes are very strongly discouraged!!
+                $subtypes = array();
+                foreach ($module->types as $subtype) {
+                    $link = $subtype->link->out(true, $urlparams);
+                    $subtypes[$link] = $subtype->title;
+                }
+
+                // Sort module subtypes into the list
+                if (!empty($module->title)) {
+                    // This grouping has a name
+                    $activities[$module->archetype][] = array($module->title => $subtypes);
+                } else {
+                    // This grouping does not have a name
+                    $activities[$module->archetype] = array_merge($activities[$module->archetype], $subtypes);
+                }
+            } else {
+                // This module has no subtypes
+                $link = $module->link->out(true, $urlparams);
+                $activities[$module->archetype][$link] = $module->title;
+            }
+        }
+
+        $straddactivity = get_string('addactivity');
+        $straddresource = get_string('addresource');
+        $sectionname = get_section_name($course, $section);
+        $strresourcelabel = get_string('addresourcetosection', null, $sectionname);
+        $stractivitylabel = get_string('addactivitytosection', null, $sectionname);
+
+        $output = html_writer::start_tag('div', array('class' => 'section_add_menus', 'id' => 'add_menus-section-' . $section));
+
+        if (!$vertical) {
+            $output .= html_writer::start_tag('div', array('class' => 'horizontal'));
+        }
+
+        if (!empty($activities[MOD_CLASS_RESOURCE])) {
+            $select = new url_select($activities[MOD_CLASS_RESOURCE], '', array(''=>$straddresource), "ressection$section");
+            $select->set_help_icon('resources');
+            $select->set_label($strresourcelabel, array('class' => 'accesshide'));
+            $output .= $this->output->render($select);
+        }
+
+        if (!empty($activities[MOD_CLASS_ACTIVITY])) {
+            $select = new url_select($activities[MOD_CLASS_ACTIVITY], '', array(''=>$straddactivity), "section$section");
+            $select->set_help_icon('activities');
+            $select->set_label($stractivitylabel, array('class' => 'accesshide'));
+            $output .= $this->output->render($select);
+        }
+
+        if (!$vertical) {
+            $output .= html_writer::end_tag('div');
+        }
+
+        $output .= html_writer::end_tag('div');
+
+        if (course_ajax_enabled($course) && $course->id == $this->page->course->id) {
+            // modchooser can be added only for the current course set on the page!
+            $straddeither = get_string('addresourceoractivity');
+            // The module chooser link
+            $modchooser = html_writer::start_tag('div', array('class' => 'mdl-right'));
+            $modchooser.= html_writer::start_tag('div', array('class' => 'section-modchooser'));
+            $icon = $this->output->pix_icon('t/add', '');
+            $span = html_writer::tag('span', $straddeither, array('class' => 'section-modchooser-text'));
+            $modchooser .= html_writer::tag('span', $icon . $span, array('class' => 'section-modchooser-link'));
+            $modchooser.= html_writer::end_tag('div');
+            $modchooser.= html_writer::end_tag('div');
+
+            // Wrap the normal output in a noscript div
+            $usemodchooser = get_user_preferences('usemodchooser', $CFG->modchooserdefault);
+            if ($usemodchooser) {
+                $output = html_writer::tag('div', $output, array('class' => 'hiddenifjs addresourcedropdown'));
+                $modchooser = html_writer::tag('div', $modchooser, array('class' => 'visibleifjs addresourcemodchooser'));
+            } else {
+                // If the module chooser is disabled, we need to ensure that the dropdowns are shown even if javascript is disabled
+                $output = html_writer::tag('div', $output, array('class' => 'show addresourcedropdown'));
+                $modchooser = html_writer::tag('div', $modchooser, array('class' => 'hide addresourcemodchooser'));
+            }
+            $output = $this->course_modchooser($modules, $course) . $modchooser . $output;
+        }
+
+        return $output;
+    }
+
+    /**
+     * Renders html for completion box on course page
+     *
+     * If completion is disabled, returns empty string
+     * If completion is automatic, returns an icon of the current completion state
+     * If completion is manual, returns a form (with an icon inside) that allows user to
+     * toggle completion
+     *
+     * @param stdClass $course course object
+     * @param completion_info $completioninfo completion info for the course, it is recommended
+     *     to fetch once for all modules in course/section for performance
+     * @param cm_info $mod module to show completion for
+     * @param array $displayoptions display options, not used in core
+     * @return string
+     */
+    public function course_section_cm_completion($course, &$completioninfo, cm_info $mod, $displayoptions = array()) {
+        global $CFG;
+        $output = '';
+        if (!empty($displayoptions['hidecompletion']) || !isloggedin() || isguestuser() || !$mod->uservisible) {
+            return $output;
+        }
+        if ($completioninfo === null) {
+            $completioninfo = new completion_info($course);
+        }
+        $completion = $completioninfo->is_enabled($mod);
+        if ($completion == COMPLETION_TRACKING_NONE) {
+            return $output;
+        }
+
+        $completiondata = $completioninfo->get_data($mod, true);
+        $completionicon = '';
+
+        if ($this->page->user_is_editing()) {
+            switch ($completion) {
+                case COMPLETION_TRACKING_MANUAL :
+                    $completionicon = 'manual-enabled'; break;
+                case COMPLETION_TRACKING_AUTOMATIC :
+                    $completionicon = 'auto-enabled'; break;
+            }
+        } else if ($completion == COMPLETION_TRACKING_MANUAL) {
+            switch($completiondata->completionstate) {
+                case COMPLETION_INCOMPLETE:
+                    $completionicon = 'manual-n'; break;
+                case COMPLETION_COMPLETE:
+                    $completionicon = 'manual-y'; break;
+            }
+        } else { // Automatic
+            switch($completiondata->completionstate) {
+                case COMPLETION_INCOMPLETE:
+                    $completionicon = 'auto-n'; break;
+                case COMPLETION_COMPLETE:
+                    $completionicon = 'auto-y'; break;
+                case COMPLETION_COMPLETE_PASS:
+                    $completionicon = 'auto-pass'; break;
+                case COMPLETION_COMPLETE_FAIL:
+                    $completionicon = 'auto-fail'; break;
+            }
+        }
+        if ($completionicon) {
+            $formattedname = $mod->get_formatted_name();
+            $imgalt = get_string('completion-alt-' . $completionicon, 'completion', $formattedname);
+            if ($completion == COMPLETION_TRACKING_MANUAL && !$this->page->user_is_editing()) {
+                $imgtitle = get_string('completion-title-' . $completionicon, 'completion', $formattedname);
+                $newstate =
+                    $completiondata->completionstate == COMPLETION_COMPLETE
+                    ? COMPLETION_INCOMPLETE
+                    : COMPLETION_COMPLETE;
+                // In manual mode the icon is a toggle form...
+
+                // If this completion state is used by the
+                // conditional activities system, we need to turn
+                // off the JS.
+                $extraclass = '';
+                if (!empty($CFG->enableavailability) &&
+                        condition_info::completion_value_used_as_condition($course, $mod)) {
+                    $extraclass = ' preventjs';
+                }
+                $output .= html_writer::start_tag('form', array('method' => 'post',
+                    'action' => new moodle_url('/course/togglecompletion.php'),
+                    'class' => 'togglecompletion'. $extraclass));
+                $output .= html_writer::start_tag('div');
+                $output .= html_writer::empty_tag('input', array(
+                    'type' => 'hidden', 'name' => 'id', 'value' => $mod->id));
+                $output .= html_writer::empty_tag('input', array(
+                    'type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
+                $output .= html_writer::empty_tag('input', array(
+                    'type' => 'hidden', 'name' => 'modulename', 'value' => $mod->name));
+                $output .= html_writer::empty_tag('input', array(
+                    'type' => 'hidden', 'name' => 'completionstate', 'value' => $newstate));
+                $output .= html_writer::empty_tag('input', array(
+                    'type' => 'image',
+                    'src' => $this->output->pix_url('i/completion-'.$completionicon),
+                    'alt' => $imgalt, 'title' => $imgtitle));
+                $output .= html_writer::end_tag('div');
+                $output .= html_writer::end_tag('form');
+            } else {
+                // In auto mode, or when editing, the icon is just an image
+                $completionpixicon = new pix_icon('i/completion-'.$completionicon, $imgalt, '',
+                        array('title' => $imgalt));
+                $output .= html_writer::tag('span', $this->output->render($completionpixicon),
+                        array('class' => 'autocompletion'));
+            }
+        }
+        return $output;
+    }
+
+    /**
+     * Checks if course module has any conditions that may make it unavailable for
+     * all or some of the students
+     *
+     * This function is internal and is only used to create CSS classes for the module name/text
+     *
+     * @param cm_info $mod
+     * @return bool
+     */
+    protected function is_cm_conditionally_hidden(cm_info $mod) {
+        global $CFG;
+        $conditionalhidden = false;
+        if (!empty($CFG->enableavailability)) {
+            $conditionalhidden = $mod->availablefrom > time() ||
+                ($mod->availableuntil && $mod->availableuntil < time()) ||
+                count($mod->conditionsgrade) > 0 ||
+                count($mod->conditionscompletion) > 0;
+        }
+        return $conditionalhidden;
+    }
+
+    /**
+     * Renders html to display a name with the link to the course module on a course page
+     *
+     * If module is unavailable for user but still needs to be displayed
+     * in the list, just the name is returned without a link
+     *
+     * Note, that for course modules that never have separate pages (i.e. labels)
+     * this function return an empty string
+     *
+     * @param cm_info $mod
+     * @param array $displayoptions
+     * @return string
+     */
+    public function course_section_cm_name(cm_info $mod, $displayoptions = array()) {
+        global $CFG;
+        $output = '';
+        if (!$mod->uservisible &&
+                (empty($mod->showavailability) || empty($mod->availableinfo))) {
+            // nothing to be displayed to the user
+            return $output;
+        }
+        $url = $mod->get_url();
+        if (!$url) {
+            return $output;
+        }
+
+        //Accessibility: for files get description via icon, this is very ugly hack!
+        $instancename = $mod->get_formatted_name();
+        $altname = '';
+        $altname = $mod->modfullname;
+        // Avoid unnecessary duplication: if e.g. a forum name already
+        // includes the word forum (or Forum, etc) then it is unhelpful
+        // to include that in the accessible description that is added.
+        if (false !== strpos(textlib::strtolower($instancename),
+                textlib::strtolower($altname))) {
+            $altname = '';
+        }
+        // File type after name, for alphabetic lists (screen reader).
+        if ($altname) {
+            $altname = get_accesshide(' '.$altname);
+        }
+
+        $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
+        $accessiblebutdim = !$mod->visible || $conditionalhidden;
+
+        $linkclasses = '';
+        $accesstext = '';
+        $textclasses = '';
+        if ($accessiblebutdim) {
+            $linkclasses .= ' dimmed';
+            $textclasses .= ' dimmed_text';
+            if ($conditionalhidden) {
+                $linkclasses .= ' conditionalhidden';
+                $textclasses .= ' conditionalhidden';
+            }
+            if ($mod->uservisible) {
+                // show accessibility note only if user can access the module himself
+                $accesstext = get_accesshide(get_string('hiddenfromstudents').': ');
+            }
+        }
+
+        // Get on-click attribute value if specified and decode the onclick - it
+        // has already been encoded for display (puke).
+        $onclick = htmlspecialchars_decode($mod->get_on_click(), ENT_QUOTES);
+
+        $groupinglabel = '';
+        if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', context_course::instance($mod->course))) {
+            $groupings = groups_get_all_groupings($mod->course);
+            $groupinglabel = html_writer::tag('span', '('.format_string($groupings[$mod->groupingid]->name).')',
+                    array('class' => 'groupinglabel '.$textclasses));
+        }
+
+        // Display link itself.
+        $activitylink = html_writer::empty_tag('img', array('src' => $mod->get_icon_url(),
+                'class' => 'iconlarge activityicon', 'alt' => $mod->modfullname)) . $accesstext .
+                html_writer::tag('span', $instancename . $altname, array('class' => 'instancename'));
+        if ($mod->uservisible) {
+            $output .= html_writer::link($url, $activitylink, array('class' => $linkclasses, 'onclick' => $onclick)) .
+                    $groupinglabel;
+        } else {
+            // We may be displaying this just in order to show information
+            // about visibility, without the actual link ($mod->uservisible)
+            $output .= html_writer::tag('div', $activitylink, array('class' => $textclasses)) .
+                    $groupinglabel;
+        }
+        return $output;
+    }
+
+    /**
+     * Renders html to display the module content on the course page (i.e. text of the labels)
+     *
+     * @param cm_info $mod
+     * @param array $displayoptions
+     * @return string
+     */
+    public function course_section_cm_text(cm_info $mod, $displayoptions = array()) {
+        $output = '';
+        if (!$mod->uservisible &&
+                (empty($mod->showavailability) || empty($mod->availableinfo))) {
+            // nothing to be displayed to the user
+            return $output;
+        }
+        $content = $mod->get_formatted_content(array('overflowdiv' => true, 'noclean' => true));
+        $conditionalhidden = $this->is_cm_conditionally_hidden($mod);
+        $accessiblebutdim = !$mod->visible || $conditionalhidden;
+        $textclasses = '';
+        $accesstext = '';
+        if ($accessiblebutdim) {
+            $textclasses .= ' dimmed_text';
+            if ($conditionalhidden) {
+                $textclasses .= ' conditionalhidden';
+            }
+            if ($mod->uservisible) {
+                // show accessibility note only if user can access the module himself
+                $accesstext = get_accesshide(get_string('hiddenfromstudents').': ');
+            }
+        }
+        if ($mod->get_url()) {
+            if ($content) {
+                // If specified, display extra content after link.
+                $output = html_writer::tag('div', $content, array('class' =>
+                        trim('contentafterlink ' . $textclasses)));
+            }
+        } else {
+            // No link, so display only content.
+            $output = html_writer::tag('div', $accesstext . $content, array('class' => $textclasses));
+        }
+        return $output;
+    }
+
+    /**
+     * Renders HTML to show course module availability information (for someone who isn't allowed
+     * to see the activity itself, or for staff)
+     *
+     * @param cm_info $mod
+     * @param array $displayoptions
+     * @return string
+     */
+    public function course_section_cm_availability(cm_info $mod, $displayoptions = array()) {
+        global $CFG;
+        if (!$mod->uservisible) {
+            // this is a student who is not allowed to see the module but might be allowed
+            // to see availability info (i.e. "Available from ...")
+            if (!empty($mod->showavailability) && !empty($mod->availableinfo)) {
+                $output = html_writer::tag('div', $mod->availableinfo, array('class' => 'availabilityinfo'));
+            }
+            return $output;
+        }
+        // this is a teacher who is allowed to see module but still should see the
+        // information that module is not available to all/some students
+        $modcontext = context_module::instance($mod->id);
+        $canviewhidden = has_capability('moodle/course:viewhiddenactivities', $modcontext);
+        if ($canviewhidden && !empty($CFG->enableavailability)) {
+            // Don't add availability information if user is not editing and activity is hidden.
+            if ($mod->visible || $this->page->user_is_editing()) {
+                $hidinfoclass = '';
+                if (!$mod->visible) {
+                    $hidinfoclass = 'hide';
+                }
+                $ci = new condition_info($mod);
+                $fullinfo = $ci->get_full_information();
+                if($fullinfo) {
+                    echo '<div class="availabilityinfo '.$hidinfoclass.'">'.get_string($mod->showavailability
+                        ? 'userrestriction_visible'
+                        : 'userrestriction_hidden','condition',
+                        $fullinfo).'</div>';
+                }
+            }
+        }
+        return '';
+    }
+
+    /**
+     * Renders HTML to display one course module in a course section
+     *
+     * This includes link, content, availability, completion info and additional information
+     * that module type wants to display (i.e. number of unread forum posts)
+     *
+     * This function calls:
+     * {@link core_course_renderer::course_section_cm_name()}
+     * {@link cm_info::get_after_link()}
+     * {@link core_course_renderer::course_section_cm_text()}
+     * {@link core_course_renderer::course_section_cm_availability()}
+     * {@link core_course_renderer::course_section_cm_completion()}
+     * {@link course_get_cm_edit_actions()}
+     * {@link core_course_renderer::course_section_cm_edit_actions()}
+     *
+     * @param stdClass $course
+     * @param completion_info $completioninfo
+     * @param cm_info $mod
+     * @param int|null $sectionreturn
+     * @param array $displayoptions
+     * @return string
+     */
+    public function course_section_cm($course, &$completioninfo, cm_info $mod, $sectionreturn, $displayoptions = array()) {
+        $output = '';
+        // We return empty string (because course module will not be displayed at all)
+        // if:
+        // 1) The activity is not visible to users
+        // and
+        // 2a) The 'showavailability' option is not set (if that is set,
+        //     we need to display the activity so we can show
+        //     availability info)
+        // or
+        // 2b) The 'availableinfo' is empty, i.e. the activity was
+        //     hidden in a way that leaves no info, such as using the
+        //     eye icon.
+        if (!$mod->uservisible &&
+            (empty($mod->showavailability) || empty($mod->availableinfo))) {
+            return $output;
+        }
+
+        $indentclasses = 'mod-indent';
+        if (!empty($mod->indent)) {
+            $indentclasses .= ' mod-indent-'.$mod->indent;
+            if ($mod->indent > 15) {
+                $indentclasses .= ' mod-indent-huge';
+            }
+        }
+        $output .= html_writer::start_tag('div', array('class' => $indentclasses));
+
+        // Start the div for the activity title, excluding the edit icons.
+        $output .= html_writer::start_tag('div', array('class' => 'activityinstance'));
+
+        // Display the link to the module (or do nothing if module has no url)
+        $output .= $this->course_section_cm_name($mod, $displayoptions);
+
+        // Module can put text after the link (e.g. forum unread)
+        $output .= $mod->get_after_link();
+
+        // Closing the tag which contains everything but edit icons. Content part of the module should not be part of this.
+        $output .= html_writer::end_tag('div'); // .activityinstance
+
+        // If there is content but NO link (eg label), then display the
+        // content here (BEFORE any icons). In this case cons must be
+        // displayed after the content so that it makes more sense visually
+        // and for accessibility reasons, e.g. if you have a one-line label
+        // it should work similarly (at least in terms of ordering) to an
+        // activity.
+        $contentpart = $this->course_section_cm_text($mod, $displayoptions);
+        $url = $mod->get_url();
+        if (empty($url)) {
+            $output .= $contentpart;
+        }
+
+        if ($this->page->user_is_editing()) {
+            $editactions = course_get_cm_edit_actions($mod, $mod->indent, $sectionreturn);
+            $output .= ' '. $this->course_section_cm_edit_actions($editactions);
+            $output .= $mod->get_after_edit_icons();
+        }
+
+        $output .= $this->course_section_cm_completion($course, $completioninfo, $mod, $displayoptions);
+
+        // If there is content AND a link, then display the content here
+        // (AFTER any icons). Otherwise it was displayed before
+        if (!empty($url)) {
+            $output .= $contentpart;
+        }
+
+        // show availability info (if module is not available)
+        $output .= $this->course_section_cm_availability($mod, $displayoptions);
+
+        $output .= html_writer::end_tag('div'); // $indentclasses
+        return $output;
+    }
+
+    /**
+     * Renders HTML to display a list of course modules in a course section
+     * Also displays "move here" controls in Javascript-disabled mode
+     *
+     * This function calls {@link core_course_renderer::course_section_cm()}
+     *
+     * @param stdClass $course course object
+     * @param int|stdClass|section_info $section relative section number or section object
+     * @param int $sectionreturn section number to return to
+     * @param int $displayoptions
+     * @return void
+     */
+    public function course_section_cm_list($course, $section, $sectionreturn = null, $displayoptions = array()) {
+        global $USER;
+
+        $output = '';
+        $modinfo = get_fast_modinfo($course);
+        if (is_object($section)) {
+            $section = $modinfo->get_section_info($section->section);
+        } else {
+            $section = $modinfo->get_section_info($section);
+        }
+        $completioninfo = new completion_info($course);
+
+        // check if we are currently in the process of moving a module with JavaScript disabled
+        $ismoving = $this->page->user_is_editing() && ismoving($course->id);
+        if ($ismoving) {
+            $movingpix = new pix_icon('movehere', get_string('movehere'), 'moodle', array('class' => 'movetarget'));
+            $strmovefull = strip_tags(get_string("movefull", "", "'$USER->activitycopyname'"));
+        }
+
+        // Get the list of modules visible to user (excluding the module being moved if there is one)
+        $moduleshtml = array();
+        if (!empty($modinfo->sections[$section->section])) {
+            foreach ($modinfo->sections[$section->section] as $modnumber) {
+                $mod = $modinfo->cms[$modnumber];
+
+                if ($ismoving and $mod->id == $USER->activitycopy) {
+                    // do not display moving mod
+                    continue;
+                }
+
+                if ($modulehtml = $this->course_section_cm($course,
+                        $completioninfo, $mod, $sectionreturn, $displayoptions)) {
+                    $moduleshtml[$modnumber] = $modulehtml;
+                }
+            }
+        }
+
+        if (!empty($moduleshtml) || $ismoving) {
+
+            $output .= html_writer::start_tag('ul', array('class' => 'section img-text'));
+
+            foreach ($moduleshtml as $modnumber => $modulehtml) {
+                if ($ismoving) {
+                    $movingurl = new moodle_url('/course/mod.php', array('moveto' => $modnumber, 'sesskey' => sesskey()));
+                    $output .= html_writer::tag('li', html_writer::link($movingurl, $this->output->render($movingpix)),
+                            array('class' => 'movehere', 'title' => $strmovefull));
+                }
+
+                $mod = $modinfo->cms[$modnumber];
+                $modclasses = 'activity '. $mod->modname. 'modtype_'.$mod->modname. ' '. $mod->get_extra_classes();
+                $output .= html_writer::start_tag('li', array('class' => $modclasses, 'id' => 'module-'. $mod->id));
+                $output .= $modulehtml;
+                $output .= html_writer::end_tag('li');
+            }
+
+            if ($ismoving) {
+                $movingurl = new moodle_url('/course/mod.php', array('movetosection' => $section->id, 'sesskey' => sesskey()));
+                $output .= html_writer::tag('li', html_writer::link($movingurl, $this->output->render($movingpix)),
+                        array('class' => 'movehere', 'title' => $strmovefull));
+            }
+
+            $output .= html_writer::end_tag('ul'); // .section
+        }
+
+        return $output;
+    }
 }
index 82cba69..b763b92 100644 (file)
@@ -76,6 +76,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
 
         $createdcats = core_course_external::create_categories($categories);
 
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats);
+
         // Initially confirm that base data was inserted correctly.
         $this->assertEquals($category1->name, $createdcats[0]['name']);
         $this->assertEquals($category2->name, $createdcats[1]['name']);
@@ -93,6 +96,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
 
         $createdsubcats = core_course_external::create_categories($subcategories);
 
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats);
+
         // Confirm that sub categories were inserted correctly.
         $this->assertEquals($category3->name, $createdsubcats[0]['name']);
 
@@ -197,6 +203,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
             array('key' => 'id', 'value' => $category1->id),
             array('key' => 'visible', 'value' => 1)), 1);
 
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
+
         // Check we retrieve the good total number of categories.
         $this->assertEquals(2, count($categories));
 
@@ -214,6 +223,10 @@ class core_course_external_testcase extends externallib_advanced_testcase {
             array('key' => 'id', 'value' => $category1->id),
             array('key' => 'idnumber', 'value' => $category1->idnumber),
             array('key' => 'visible', 'value' => 1)), 0);
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
+
         $this->assertEquals(1, count($categories));
 
         // Retrieve categories from parent.
@@ -223,6 +236,10 @@ class core_course_external_testcase extends externallib_advanced_testcase {
 
         // Retrieve all categories.
         $categories = core_course_external::get_categories();
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
+
         $this->assertEquals($DB->count_records('course_categories'), count($categories));
 
         // Call without required capability (it will fail cause of the search on idnumber).
@@ -354,6 +371,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
 
         $createdcourses = core_course_external::create_courses($courses);
 
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
+
         // Check that right number of courses were created.
         $this->assertEquals(2, count($createdcourses));
 
@@ -492,6 +512,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
         $courses = core_course_external::get_courses(array('ids' =>
             array($course1->id, $course2->id)));
 
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
+
         // Check we retrieve the good total number of categories.
         $this->assertEquals(2, count($courses));
 
@@ -532,6 +555,10 @@ class core_course_external_testcase extends externallib_advanced_testcase {
 
         // Get all courses in the DB
         $courses = core_course_external::get_courses(array());
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
+
         $this->assertEquals($DB->count_records('course'), count($courses));
     }
 
@@ -559,6 +586,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
 
         $courses = core_course_external::get_course_contents($course->id, array());
 
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $courses = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $courses);
+
         // Check that the course has the 3 created modules
         $this->assertEquals(3, count($courses[0]['modules']));
     }
@@ -602,6 +632,9 @@ class core_course_external_testcase extends externallib_advanced_testcase {
         $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
                 $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
 
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
+
         // Check that the course has been duplicated.
         $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
     }
index 3773890..2977f71 100644 (file)
@@ -123,7 +123,7 @@ switch($targetstate) {
 }
 
 // Get course-modules entry
-$cm = get_coursemodule_from_id(null, $cmid, null, false, MUST_EXIST);
+$cm = get_coursemodule_from_id(null, $cmid, null, true, MUST_EXIST);
 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
 
 // Check user is logged in
@@ -152,8 +152,12 @@ if ($fromajax) {
 } else {
     // In case of use in other areas of code we allow a 'backto' parameter,
     // otherwise go back to course page
-    $backto = optional_param('backto', 'view.php?id='.$course->id, PARAM_URL);
-    redirect($backto);
+
+    if ($backto = optional_param('backto', null, PARAM_URL)) {
+        redirect($backto);
+    } else {
+        redirect(course_get_url($course, $cm->sectionnum));
+    }
 }
 
 // utility functions
index 00c6754..0b1faca 100644 (file)
     }
     add_to_log($course->id, 'course', $loglabel, "view.php?". $logparam, $infoid);
 
-    $course->format = clean_param($course->format, PARAM_ALPHA);
-    if (!file_exists($CFG->dirroot.'/course/format/'.$course->format.'/format.php')) {
-        $course->format = 'weeks';  // Default format is weeks
-    }
+    // Fix course format if it is no longer installed
+    $course->format = course_get_format($course)->get_format();
 
     $PAGE->set_pagelayout('course');
     $PAGE->set_pagetype('course-view-' . $course->format);
     $PAGE->set_other_editing_capability('moodle/course:manageactivities');
 
+    // Preload course format renderer before output starts.
+    // This is a little hacky but necessary since
+    // format.php is not included until after output starts
+    if (file_exists($CFG->dirroot.'/course/format/'.$course->format.'/renderer.php')) {
+        require_once($CFG->dirroot.'/course/format/'.$course->format.'/renderer.php');
+        if (class_exists('format_'.$course->format.'_renderer')) {
+            // call get_renderer only if renderer is defined in format plugin
+            // otherwise an exception would be thrown
+            $PAGE->get_renderer('format_'. $course->format);
+        }
+    }
+
     if ($reset_user_allowed_editing) {
         // ugly hack
         unset($PAGE->_user_allowed_editing);
     echo html_writer::end_tag('div');
 
     // Include course AJAX
-    if (include_course_ajax($course, $modnamesused)) {
-        // Add the module chooser
-        $renderer = $PAGE->get_renderer('core', 'course');
-        echo $renderer->course_modchooser(get_module_metadata($course, $modnames, $displaysection), $course);
-    }
+    include_course_ajax($course, $modnamesused);
 
     echo $OUTPUT->footer();
index cdc93d0..377e5ae 100644 (file)
@@ -150,6 +150,9 @@ YUI.add('moodle-course-modchooser', function(Y) {
     {
         NAME : MODCHOOSERNAME,
         ATTRS : {
+            maxheight : {
+                value : 800
+            }
         }
     });
     M.course = M.course || {};
@@ -158,6 +161,6 @@ YUI.add('moodle-course-modchooser', function(Y) {
     }
 },
 '@VERSION@', {
-    requires:['base', 'overlay', 'moodle-core-chooserdialogue', 'transition']
+    requires:['base', 'overlay', 'moodle-core-chooserdialogue', 'transition', 'moodle-course-coursebase']
 }
 );
index 1b37f40..23a61a3 100644 (file)
@@ -64,7 +64,7 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             var toggle_class;
             if (this.is_label(element)) {
                 toggle_class = CSS.DIMMEDTEXT;
-                dimarea = element.one(CSS.MODINDENTDIV + ' div');
+                dimarea = element.all(CSS.MODINDENTDIV + ' > div').item(1);
             } else {
                 toggle_class = CSS.DIMCLASS;
                 dimarea = element.one('a');
@@ -259,15 +259,6 @@ YUI.add('moodle-course-toolboxes', function(Y) {
         },
         _setup_for_resource : function(toolboxtarget) {
             toolboxtarget = Y.one(toolboxtarget);
-            // "Disable" show/hide icons (change cursor to not look clickable) if section is hidden
-            var showhide = toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.HIDE);
-            showhide.concat(toolboxtarget.all(CSS.COMMANDSPAN + ' ' + CSS.SHOW));
-            showhide.each(function(node) {
-                var section = node.ancestor(CSS.SECTIONLI);
-                if (section && section.hasClass(CSS.SECTIONHIDDENCLASS)) {
-                    node.setStyle('cursor', 'auto');
-                }
-            });
 
             // Set groupmode attribute for use by this.toggle_groupmode()
             var groups;
@@ -708,12 +699,6 @@ YUI.add('moodle-course-toolboxes', function(Y) {
                 if (Y.Array.indexOf(response.resourcestotoggle, activityid) != -1) {
                     this.toggle_hide_resource_ui(button);
                 }
-
-                if (value == 0) {
-                    button.setStyle('cursor', 'auto');
-                } else {
-                    button.setStyle('cursor', 'pointer');
-                }
             }, this);
         },
         toggle_highlight : function(e) {
index 8b91682..511ac82 100644 (file)
@@ -42,9 +42,8 @@ Users are searched for first by their "idnumber", and second by their Moodle use
 $string['cronfrequency'] = 'Frequency of processing';
 $string['deleteusers'] = 'Delete user accounts when specified in IMS data';
 $string['deleteusers_desc'] = 'If enabled, IMS Enterprise enrolment data can specify the deletion of user accounts (if the "recstatus" flag is set to 3, which represents deletion of an account). As is standard in Moodle, the user record isn\'t actually deleted from Moodle\'s database, but a flag is set to mark the account as deleted.';
-$string['pluginname_desc'] = 'This method will repeatedly check for and process a specially-formatted text file in the location that you specify.  The file must follow the IMS Enterprise specifications containing person, group, and membership XML elements.';
 $string['doitnow'] = 'perform an IMS Enterprise import right now';
-$string['pluginname'] = 'IMS Enterprise file';
+$string['emptyattribute'] = 'Leave it empty';
 $string['filelockedmail'] = 'The text file you are using for IMS-file-based enrolments ({$a}) can not be deleted by the cron process.  This usually means the permissions are wrong on it.  Please fix the permissions so that Moodle can delete the file, otherwise it might be processed repeatedly.';
 $string['filelockedmailsubject'] = 'Important error: Enrolment file';
 $string['fixcasepersonalnames'] = 'Change personal names to Title Case';
@@ -56,12 +55,20 @@ $string['mailadmins'] = 'Notify admin by email';
 $string['mailusers'] = 'Notify users by email';
 $string['messageprovider:imsenterprise_enrolment'] = 'IMS Enterprise enrolment messages';
 $string['miscsettings'] = 'Miscellaneous';
+$string['pluginname'] = 'IMS Enterprise file';
+$string['pluginname_desc'] = 'This method will repeatedly check for and process a specially-formatted text file in the location that you specify.  The file must follow the IMS Enterprise specifications containing person, group, and membership XML elements.';
 $string['processphoto'] = 'Add user photo data to profile';
 $string['processphotowarning'] = 'Warning: Image processing is likely to add a significant burden to the server. You are recommended not to activate this option if large numbers of students are expected to be processed.';
 $string['restricttarget'] = 'Only process data if the following target is specified';
 $string['restricttarget_desc'] = 'An IMS Enterprise data file could be intended for multiple "targets" - different LMSes, or different systems within a school/university. It\'s possible to specify in the Enterprise file that the data is intended for one or more named target systems, by naming them in <target> tags contained within the <properties> tag.
 
 In general you don\'t need to worry about this. Leave the setting blank and Moodle will always process the data file, no matter whether a target is specified or not. Otherwise, fill in the exact name that will be output inside the <target> tag.';
+$string['settingfullname'] = 'IMS description tag for the course full name';
+$string['settingfullnamedescription'] = 'The full name is a required course field so you have to define the selected description tag in your IMS Enterprise file';
+$string['settingshortname'] = 'IMS description tag for the course short name';
+$string['settingshortnamedescription'] = 'The short name is a required course field so you have to define the selected description tag in your IMS Enterprise file';
+$string['settingsummary'] = 'IMS description tag for the course summary';
+$string['settingsummarydescription'] = 'Is an optional field, select \'Leave it empty\' if you dont\'t want to specify a course summary';
 $string['sourcedidfallback'] = 'Use the &quot;sourcedid&quot; for a person\'s userid if the &quot;userid&quot; field is not found';
 $string['sourcedidfallback_desc'] = 'In IMS data, the <sourcedid> field represents the persistent ID code for a person as used in the source system. The <userid> field is a separate field which should contain the ID code used by the user when logging in. In many cases these two codes may be the same - but not always.
 
index 9d11e00..5a2070e 100644 (file)
@@ -97,6 +97,8 @@ function cron() {
 
         // Make sure we understand how to map the IMS-E roles to Moodle roles
         $this->load_role_mappings();
+        // Make sure we understand how to map the IMS-E course names to Moodle course names.
+        $this->load_course_mappings();
 
         $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron
         $filemtime = filemtime($filename);
@@ -315,15 +317,17 @@ function process_group_tag($tagcontents) {
     if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
         $group->coursecode = trim($matches[1]);
     }
-    if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)){
-        $group->description = trim($matches[1]);
+
+    if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
+        $group->long = trim($matches[1]);
     }
     if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
-        $group->shortName = trim($matches[1]);
+        $group->short = trim($matches[1]);
     }
     if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
-        $group->fulldescription = trim($matches[1]);
+        $group->full = trim($matches[1]);
     }
+
     if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
         $group->category = trim($matches[1]);
     }
@@ -361,28 +365,36 @@ function process_group_tag($tagcontents) {
                 if (!$createnewcourses) {
                     $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
                 } else {
-                    // Set shortname to description or description to shortname if one is set but not the other.
-                    $nodescription = !isset($group->description);
-                    $noshortname = !isset($group->shortName);
-                    if ( $nodescription && $noshortname) {
-                        // If neither short nor long description are set let if fail
-                        $this->log_line("Neither long nor short name are set for $coursecode");
-                    } else if ($nodescription) {
-                        // If short and ID exist, then give the long short's value, then give short the ID's value
-                        $group->description = $group->shortName;
-                        $group->shortName = $coursecode;
-                    } else if ($noshortname) {
-                        // If long and ID exist, then map long to long, then give short the ID's value.
-                        $group->shortName = $coursecode;
-                    }
+
                     // Create the (hidden) course(s) if not found
                     $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults
+
+                    // New course.
                     $course = new stdClass();
-                    $course->fullname = $group->description;
-                    $course->shortname = $group->shortName;
-                    if (!empty($group->fulldescription)) {
-                        $course->summary = format_text($group->fulldescription, FORMAT_HTML);
+                    foreach ($this->coursemappings as $courseattr => $imsname) {
+
+                        if ($imsname == 'ignore') {
+                            continue;
+                        }
+
+                        // Check if the IMS file contains the mapped tag, otherwise fallback on coursecode.
+                        if ($imsname == 'coursecode') {
+                            $course->{$courseattr} = $coursecode;
+                        } else if (!empty($group->{$imsname})) {
+                            $course->{$courseattr} = $group->{$imsname};
+                        } else {
+                            $this->log_line('No ' . $imsname . ' description tag found for ' . $coursecode . ' coursecode, using ' . $coursecode . ' instead');
+                            $course->{$courseattr} = $coursecode;
+                        }
+
+                        if ($courseattr == 'summary') {
+                            $format = FORMAT_HTML;
+                        } else {
+                            $format = FORMAT_PLAIN;
+                        }
+                        $course->{$courseattr} = format_text($course->$courseattr, $format);
                     }
+
                     $course->idnumber = $coursecode;
                     $course->format = $courseconfig->format;
                     $course->visible = $courseconfig->visible;
@@ -421,7 +433,6 @@ function process_group_tag($tagcontents) {
                     $course->startdate = time();
                     // Choose a sort order that puts us at the start of the list!
                     $course->sortorder = 0;
-
                     $courseid = $DB->insert_record('course', $course);
 
                     // Setup default enrolment plugins
@@ -748,7 +759,10 @@ function process_properties_tag($tagcontents){
 * @param string $string Text to write (newline will be added automatically)
 */
 function log_line($string){
-    mtrace($string);
+
+    if (!PHPUNIT_TEST) {
+        mtrace($string);
+    }
     if($this->logfp) {
         fwrite($this->logfp, $string . "\n");
     }
@@ -789,6 +803,22 @@ function load_role_mappings() {
     }
 }
 
+    /**
+     * Load the name mappings (from the config), so we can easily refer to
+     * how an IMS-E course properties corresponds to a Moodle course properties
+     */
+    function load_course_mappings() {
+        require_once('locallib.php');
+
+        $imsnames = new imsenterprise_courses();
+        $courseattrs = $imsnames->get_courseattrs();
+
+        $this->coursemappings = array();
+        foreach($courseattrs as $courseattr) {
+            $this->coursemappings[$courseattr] = $this->get_config('imscoursemap' . $courseattr);
+        }
+    }
+
     /**
      * Called whenever anybody tries (from the normal interface) to remove a group
      * member which is registered as being created by this component. (Not called
@@ -802,6 +832,7 @@ function load_role_mappings() {
         return false;
     }
 
+
 } // end of class
 
 
index c85f71b..13c6114 100644 (file)
@@ -82,3 +82,74 @@ class imsenterprise_roles {
 
 
 }  // class
+
+
+/**
+ * Mapping between Moodle course attributes and IMS enterprise group description tags
+ *
+ * @package   enrol_imsenterprise
+ * @copyright 2011 Aaron C Spike
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class imsenterprise_courses {
+
+    private $imsnames;
+    private $courseattrs;
+
+    /**
+     * Loads default
+     */
+    function __construct() {
+        $this->imsnames = array(
+            'short' => 'short',
+            'long' => 'long',
+            'full' => 'full',
+            'coursecode' => 'coursecode');
+        $this->courseattrs = array('shortname', 'fullname', 'summary');
+    }
+
+    /**
+     * Returns the assignable values for the course attribute
+     * @param string $courseattr The course attribute (shortname, fullname...)
+     * @return array Array of assignable values
+     */
+    function get_imsnames($courseattr) {
+\r
+        $values = $this->imsnames;\r
+        if ($courseattr == 'summary') {\r
+            $values = array_merge(array('ignore' => get_string('emptyattribute', 'enrol_imsenterprise')), $values);
+        }
+        return $values;
+    }
+
+    /**
+     * courseattrs getter
+     * @return array
+     */
+    function get_courseattrs() {
+        return $this->courseattrs;
+    }
+
+    /**
+     * This function is only used when first setting up the plugin, to
+     * decide which name assignments to recommend by default.
+     *
+     * @param string $coursename
+     * @return string
+     */
+    function determine_default_coursemapping($courseattr) {
+        switch($courseattr) {
+            case 'fullname':
+                $imsname = 'short';
+                break;
+            case 'shortname':
+                $imsname = 'coursecode';
+                break;
+            default:
+                $imsname = 'ignore';
+        }
+
+        return $imsname;
+    }
+
+}  // class
index 1c7f205..b644f49 100644 (file)
@@ -76,6 +76,19 @@ if ($ADMIN->fulltree) {
 
     $settings->add(new admin_setting_configcheckbox('enrol_imsenterprise/imsunenrol', get_string('allowunenrol', 'enrol_imsenterprise'), get_string('allowunenrol_desc', 'enrol_imsenterprise'), 0));
 
+    if (!during_initial_install()) {
+        $imscourses = new imsenterprise_courses();
+        foreach ($imscourses->get_courseattrs() as $courseattr) {
+
+            // The assignable values of this course attribute
+            $assignablevalues = $imscourses->get_imsnames($courseattr);
+            $name = get_string('setting' . $courseattr, 'enrol_imsenterprise');
+            $description = get_string('setting' . $courseattr . 'description', 'enrol_imsenterprise');
+            $defaultvalue = (string) $imscourses->determine_default_coursemapping($courseattr);
+            $settings->add(new admin_setting_configselect('enrol_imsenterprise/imscoursemap' . $courseattr, $name, $description, $defaultvalue, $assignablevalues));
+        }
+    }
+
     //--- miscellaneous -------------------------------------------------------------------------------------
     $settings->add(new admin_setting_heading('enrol_imsenterprise_miscsettings', get_string('miscsettings', 'enrol_imsenterprise'), ''));
 
diff --git a/enrol/imsenterprise/tests/imsenterprise_test.php b/enrol/imsenterprise/tests/imsenterprise_test.php
new file mode 100644 (file)
index 0000000..a49ba89
--- /dev/null
@@ -0,0 +1,327 @@
+<?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/>.
+
+/**
+ * IMS Enterprise enrolment tests.
+ *
+ * @package    enrol_imsenterprise
+ * @category   phpunit
+ * @copyright  2012 David MonllaĆ³
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot . '/enrol/imsenterprise/locallib.php');
+require_once($CFG->dirroot . '/enrol/imsenterprise/lib.php');
+
+/**
+ * IMS Enterprise test case
+ *
+ * @package    enrol_imsenterprise
+ * @category   phpunit
+ * @copyright  2012 David MonllaĆ³
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class enrol_imsenterprise_testcase extends advanced_testcase {
+
+    protected $imsplugin;
+
+    protected function setUp() {
+        $this->resetAfterTest(true);
+        $this->imsplugin = enrol_get_plugin('imsenterprise');
+        $this->set_test_config();
+    }
+
+    /**
+     * With an empty IMS enterprise file
+     */
+    public function test_emptyfile() {
+        global $DB;
+
+        $prevncourses = $DB->count_records('course');
+        $prevnusers = $DB->count_records('user');
+
+        $this->set_xml_file(false, false);
+        $this->imsplugin->cron();
+
+        $this->assertEquals($prevncourses, $DB->count_records('course'));
+        $this->assertEquals($prevnusers, $DB->count_records('user'));
+    }
+
+
+    /**
+     * Existing users are not created again
+     */
+    public function test_users_existing() {
+        global $DB;
+
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $prevnusers = $DB->count_records('user');
+
+        $users = array($user1, $user2);
+        $this->set_xml_file($users);
+        $this->imsplugin->cron();
+
+        $this->assertEquals($prevnusers, $DB->count_records('user'));
+    }
+
+
+    /**
+     * Add new users
+     */
+    public function test_users_add() {
+        global $DB;
+
+        $prevnusers = $DB->count_records('user');
+
+        $user1 = new StdClass();
+        $user1->username = 'u1';
+        $user1->email = 'u1@u1.org';
+        $user1->firstname = 'U';
+        $user1->lastname = '1';
+
+        $users = array($user1);
+        $this->set_xml_file($users);
+        $this->imsplugin->cron();
+
+        $this->assertEquals(($prevnusers + 1), $DB->count_records('user'));
+    }
+
+
+    /**
+     * Existing courses are not created again
+     */
+    public function test_courses_existing() {
+        global $DB;
+
+        $course1 = $this->getDataGenerator()->create_course(array('idnumber' => 'id1'));
+        $course2 = $this->getDataGenerator()->create_course(array('idnumber' => 'id2'));
+
+        // Default mapping according to default course attributes - IMS description tags mapping.
+        $course1->imsshort = $course1->fullname;
+        $course2->imsshort = $course2->fullname;
+
+        $prevncourses = $DB->count_records('course');
+
+        $courses = array($course1, $course2);
+        $this->set_xml_file(false, $courses);
+        $this->imsplugin->cron();
+
+        $this->assertEquals($prevncourses, $DB->count_records('course'));
+    }
+
+
+    /**
+     * Add new courses
+     */
+    public function test_courses_add() {
+        global $DB;
+
+        $prevncourses = $DB->count_records('course');
+
+        $course1 = new StdClass();
+        $course1->idnumber = 'id1';
+        $course1->imsshort = 'id1';
+        $course1->category = 'DEFAULT CATNAME';
+
+        $course2 = new StdClass();
+        $course2->idnumber = 'id2';
+        $course2->imsshort = 'id2';
+        $course2->category = 'DEFAULT CATNAME';
+
+        $courses = array($course1, $course2);
+        $this->set_xml_file(false, $courses);
+        $this->imsplugin->cron();
+
+        $this->assertEquals(($prevncourses + 2), $DB->count_records('course'));
+    }
+
+
+    /**
+     * Course attributes mapping to IMS enterprise group description tags
+     */
+    public function test_courses_attrmapping() {
+        global $DB;
+
+        // Setting a all = coursecode (idnumber) mapping.
+        $this->imsplugin->set_config('imscoursemapshortname', 'coursecode');
+        $this->imsplugin->set_config('imscoursemapfullname', 'coursecode');
+        $this->imsplugin->set_config('imscoursemapsummary', 'coursecode');
+
+        $course1 = new StdClass();
+        $course1->idnumber = 'id1';
+        $course1->imsshort = 'description_short1';
+        $course1->imslong = 'description_long';
+        $course1->imsfull = 'description_full';
+        $course1->category = 'DEFAULT CATNAME';
+
+        $this->set_xml_file(false, array($course1));
+        $this->imsplugin->cron();
+
+        $dbcourse = $DB->get_record('course', array('idnumber' => $course1->idnumber));
+        $this->assertFalse(!$dbcourse);
+        $this->assertEquals($dbcourse->shortname, $course1->idnumber);
+        $this->assertEquals($dbcourse->fullname, $course1->idnumber);
+        $this->assertEquals($dbcourse->summary, $course1->idnumber);
+
+
+        // Setting a mapping using all the description tags.
+        $this->imsplugin->set_config('imscoursemapshortname', 'short');
+        $this->imsplugin->set_config('imscoursemapfullname', 'long');
+        $this->imsplugin->set_config('imscoursemapsummary', 'full');
+
+        $course2 = new StdClass();
+        $course2->idnumber = 'id2';
+        $course2->imsshort = 'description_short2';
+        $course2->imslong = 'description_long';
+        $course2->imsfull = 'description_full';
+        $course2->category = 'DEFAULT CATNAME';
+
+        $this->set_xml_file(false, array($course2));
+        $this->imsplugin->cron();
+
+        $dbcourse = $DB->get_record('course', array('idnumber' => $course2->idnumber));
+        $this->assertFalse(!$dbcourse);
+        $this->assertEquals($dbcourse->shortname, $course2->imsshort);
+        $this->assertEquals($dbcourse->fullname, $course2->imslong);
+        $this->assertEquals($dbcourse->summary, $course2->imsfull);
+
+
+        // Setting a mapping where the specified description tags doesn't exist in the XML file (must delegate into idnumber).
+        $this->imsplugin->set_config('imscoursemapshortname', 'short');
+        $this->imsplugin->set_config('imscoursemapfullname', 'long');
+        $this->imsplugin->set_config('imscoursemapsummary', 'full');
+
+        $course3 = new StdClass();
+        $course3->idnumber = 'id3';
+        $course3->imsshort = 'description_short3';
+        $course3->category = 'DEFAULT CATNAME';
+
+        $this->set_xml_file(false, array($course3));
+        $this->imsplugin->cron();
+
+        $dbcourse = $DB->get_record('course', array('idnumber' => $course3->idnumber));
+        $this->assertFalse(!$dbcourse);
+        $this->assertEquals($dbcourse->shortname, $course3->imsshort);
+        $this->assertEquals($dbcourse->fullname, $course3->idnumber);
+        $this->assertEquals($dbcourse->summary, $course3->idnumber);
+
+    }
+
+
+    /**
+     * Sets the plugin configuration for testing
+     */
+    protected function set_test_config() {
+        $this->imsplugin->set_config('mailadmins', false);
+        $this->imsplugin->set_config('prev_path', '');
+        $this->imsplugin->set_config('createnewusers', true);
+        $this->imsplugin->set_config('createnewcourses', true);
+        $this->imsplugin->set_config('createnewcategories', true);
+    }
+
+
+    /**
+     * Creates an IMS enterprise XML file and adds it's path to config settings
+     *
+     * @param array Array of users StdClass
+     * @param array Array of courses StdClass
+     */
+    protected function set_xml_file($users = false, $courses = false) {
+        global $DB;
+
+        $xmlcontent = '<enterprise>';
+
+        // Users.
+        if (!empty($users)) {
+            foreach ($users as $user) {
+                $xmlcontent .= '
+  <person>
+    <sourcedid>
+      <source>TestSource</source>
+      <id>'.$user->username.'</id>
+    </sourcedid>
+    <userid>'.$user->username.'</userid>
+    <name>
+      <fn>'.$user->firstname.' '.$user->lastname.'</fn>
+      <n>
+        <family>'.$user->lastname.'</family>
+        <given>'.$user->firstname.'</given>
+      </n>
+    </name>
+    <email>'.$user->email.'</email>
+  </person>';
+            }
+        }
+
+        // Courses.
+        // Mapping based on default course attributes - IMS group tags mapping.
+        if (!empty($courses)) {
+            foreach ($courses as $course) {
+
+                $xmlcontent .= '
+  <group>
+    <sourcedid>
+      <source>TestSource</source>
+      <id>'.$course->idnumber.'</id>
+    </sourcedid>
+    <description>';
+
+                // Optional to test course attributes mappings.
+                if (!empty($course->imsshort)) {
+                    $xmlcontent .= '
+      <short>'.$course->imsshort.'</short>';
+                }
+
+                // Optional to test course attributes mappings.
+                if (!empty($course->imslong)) {
+                    $xmlcontent .= '
+      <long>'.$course->imslong.'</long>';
+                }
+
+                // Optional to test course attributes mappings.
+                if (!empty($course->imsfull)) {
+                    $xmlcontent .= '
+      <full>'.$course->imsfull.'</full>';
+                }
+
+                // orgunit tag value is used by moodle as category name.
+                $xmlcontent .= '
+    </description>
+    <org>
+      <orgunit>'.$course->category.'</orgunit>
+    </org>
+  </group>';
+            }
+        }
+
+        $xmlcontent .= '
+</enterprise>';
+
+        // Creating the XML file.
+        $filename = 'ims_' . rand(1000, 9999) . '.xml';
+        $tmpdir = make_temp_directory('enrol_imsenterprise');
+        $xmlfilepath = $tmpdir . '/' . $filename;
+        file_put_contents($xmlfilepath, $xmlcontent);
+
+        // Setting the file path in CFG.
+        $this->imsplugin->set_config('imsfilelocation', $xmlfilepath);
+    }
+}
index d9fb913..1979a35 100644 (file)
 
 define('CLI_SCRIPT', true);
 
-require(dirname(dirname(dirname(dirname(__FILE__)))).'/config.php');
+require(__DIR__.'/../../../config.php');
+require_once("$CFG->libdir/clilib.php");
 
-// Ensure errors are well explained
-$CFG->debug = DEBUG_NORMAL;
+// Ensure errors are well explained.
+$CFG->debug = DEBUG_DEVELOPER;
 
 if (!enrol_is_enabled('ldap')) {
-    error_log('[ENROL LDAP] '.get_string('pluginnotenabled', 'enrol_ldap'));
-    die;
+    cli_error(get_string('pluginnotenabled', 'enrol_ldap'), 2);
 }
 
-// Update enrolments -- these handlers should autocreate courses if required
+/** @var enrol_ldap_plugin $enrol */
 $enrol = enrol_get_plugin('ldap');
-$enrol->sync_enrolments();
 
+$trace = new text_progress_trace();
+
+// Update enrolments -- these handlers should autocreate courses if required.
+$enrol->sync_enrolments($trace);
+
+exit(0);
index 2e2b6b6..3a1cb0f 100644 (file)
@@ -131,8 +131,11 @@ class enrol_ldap_plugin extends enrol_plugin {
     public function sync_user_enrolments($user) {
         global $DB;
 
-        $ldapconnection = $this->ldap_connect();
-        if (!$ldapconnection) {
+        // Do not try to print anything to the output because this method is called during interactive login.
+        $trace = new error_log_progress_trace($this->errorlogtag);
+
+        if (!$this->ldap_connect($trace)) {
+            $trace->finished();
             return;
         }
 
@@ -154,7 +157,7 @@ class enrol_ldap_plugin extends enrol_plugin {
         $enrolments = array();
         foreach($roles as $role) {
             // Get external enrolments according to LDAP server
-            $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($ldapconnection, $user->idnumber, $role);
+            $enrolments[$role->id]['ext'] = $this->find_ext_enrolments($user->idnumber, $role);
 
             // Get the list of current user enrolments that come from LDAP
             $sql= "SELECT e.courseid, ue.status, e.id as enrolid, c.shortname
@@ -174,7 +177,7 @@ class enrol_ldap_plugin extends enrol_plugin {
             foreach ($enrolments[$role->id]['ext'] as $enrol) {
                 $course_ext_id = $enrol[$courseidnumber][0];
                 if (empty($course_ext_id)) {
-                    error_log($this->errorlogtag.get_string('extcourseidinvalid', 'enrol_ldap'));
+                    $trace->output(get_string('extcourseidinvalid', 'enrol_ldap'));
                     continue; // Next; skip this one!
                 }
 
@@ -182,14 +185,13 @@ class enrol_ldap_plugin extends enrol_plugin {
                 $course = $DB->get_record('course', array($this->enrol_localcoursefield=>$course_ext_id));
                 if (empty($course)) { // Course doesn't exist
                     if ($this->get_config('autocreate')) { // Autocreate
-                        error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
-                                                                array('courseextid'=>$course_ext_id)));
-                        if ($newcourseid = $this->create_course($enrol)) {
-                            $course = $DB->get_record('course', array('id'=>$newcourseid));
+                        $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
+                        if (!$newcourseid = $this->create_course($enrol, $trace)) {
+                            continue;
                         }
+                        $course = $DB->get_record('course', array('id'=>$newcourseid));
                     } else {
-                        error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap',
-                                                                array('courseextid'=>$course_ext_id)));
+                        $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$course_ext_id)));
                         continue; // Next; skip this one!
                     }
                 }
@@ -226,18 +228,18 @@ class enrol_ldap_plugin extends enrol_plugin {
                     // set it back to active on external re-enrolment. So set it
                     // unconditionnally to cover both cases.
                     $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
-                    error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap',
-                                                            array('user_username'=> $user->username,
-                                                                  'course_shortname'=>$course->shortname,
-                                                                  'course_id'=>$course->id)));
+                    $trace->output(get_string('enroluser', 'enrol_ldap',
+                        array('user_username'=> $user->username,
+                              'course_shortname'=>$course->shortname,
+                              'course_id'=>$course->id)));
                 } else {
                     if ($enrolments[$role->id]['current'][$course->id]->status == ENROL_USER_SUSPENDED) {
                         // Reenable enrolment that was previously disabled. Enrolment refreshed
                         $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$user->id));
-                        error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap',
-                                                                array('user_username'=> $user->username,
-                                                                      'course_shortname'=>$course->shortname,
-                                                                      'course_id'=>$course->id)));
+                        $trace->output(get_string('enroluserenable', 'enrol_ldap',
+                            array('user_username'=> $user->username,
+                                  'course_shortname'=>$course->shortname,
+                                  'course_id'=>$course->id)));
                     }
                 }
 
@@ -255,10 +257,10 @@ class enrol_ldap_plugin extends enrol_plugin {
                 switch ($this->get_config('unenrolaction')) {
                     case ENROL_EXT_REMOVED_UNENROL:
                         $this->unenrol_user($instance, $user->id);
-                        error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap',
-                                                                array('user_username'=> $user->username,
-                                                                      'course_shortname'=>$course->shortname,
-                                                                      'course_id'=>$course->courseid)));
+                        $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
+                            array('user_username'=> $user->username,
+                                  'course_shortname'=>$course->shortname,
+                                  'course_id'=>$course->courseid)));
                         break;
                     case ENROL_EXT_REMOVED_KEEP:
                         // Keep - only adding enrolments
@@ -266,10 +268,10 @@ class enrol_ldap_plugin extends enrol_plugin {
                     case ENROL_EXT_REMOVED_SUSPEND:
                         if ($course->status != ENROL_USER_SUSPENDED) {
                             $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
-                            error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap',
-                                                                    array('user_username'=> $user->username,
-                                                                          'course_shortname'=>$course->shortname,
-                                                                          'course_id'=>$course->courseid)));
+                            $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
+                                array('user_username'=> $user->username,
+                                      'course_shortname'=>$course->shortname,
+                                      'course_id'=>$course->courseid)));
                         }
                         break;
                     case ENROL_EXT_REMOVED_SUSPENDNOROLES:
@@ -277,30 +279,34 @@ class enrol_ldap_plugin extends enrol_plugin {
                             $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$user->id));
                         }
                         role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
-                        error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap',
-                                                                array('user_username'=> $user->username,
-                                                                      'course_shortname'=>$course->shortname,
-                                                                      'course_id'=>$course->courseid)));
+                        $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
+                            array('user_username'=> $user->username,
+                                  'course_shortname'=>$course->shortname,
+                                  'course_id'=>$course->courseid)));
                         break;
                 }
             }
             $transaction->allow_commit();
         }
 
-        $this->ldap_close($ldapconnection);
+        $this->ldap_close();
+
+        $trace->finished();
     }
 
     /**
      * Forces synchronisation of all enrolments with LDAP server.
      * It creates courses if the plugin is configured to do so.
      *
+     * @param progress_trace $trace
+     * @param int|null $onecourse limit sync to one course->id, null if all courses
      * @return void
      */
-    public function sync_enrolments() {
+    public function sync_enrolments(progress_trace $trace, $onecourse = null) {
         global $CFG, $DB;
 
-        $ldapconnection = $this->ldap_connect();
-        if (!$ldapconnection) {
+        if (!$this->ldap_connect($trace)) {
+            $trace->finished();
             return;
         }
 
@@ -310,6 +316,22 @@ class enrol_ldap_plugin extends enrol_plugin {
         @set_time_limit(0);
         raise_memory_limit(MEMORY_HUGE);
 
+        $oneidnumber = null;
+        if ($onecourse) {
+            if (!$course = $DB->get_record('course', array('id'=>$onecourse), 'id,'.$this->enrol_localcoursefield)) {
+                // Course does not exist, nothing to do.
+                $trace->output("Requested course $onecourse does not exist, no sync performed.");
+                $trace->finished();
+                return;
+            }
+            if (empty($course->{$this->enrol_localcoursefield})) {
+                $trace->output("Requested course $onecourse does not have {$this->enrol_localcoursefield}, no sync performed.");
+                $trace->finished();
+                return;
+            }
+            $oneidnumber = ldap_filter_addslashes(textlib::convert($course->idnumber, 'utf-8', $this->get_config('ldapencoding')));
+        }
+
         // Get enrolments for each type of role.
         $roles = get_all_roles();
         $enrolments = array();
@@ -334,6 +356,10 @@ class enrol_ldap_plugin extends enrol_plugin {
             // Define the search pattern
             $ldap_search_pattern = $this->config->objectclass;
 
+            if ($oneidnumber !== null) {
+                $ldap_search_pattern = "(&$ldap_search_pattern({$this->config->course_idnumber}=$oneidnumber))";
+            }
+
             $ldap_cookie = '';
             foreach ($ldap_contexts as $ldap_context) {
                 $ldap_context = trim($ldap_context);
@@ -344,18 +370,18 @@ class enrol_ldap_plugin extends enrol_plugin {
                 $flat_records = array();
                 do {
                     if ($ldap_pagedresults) {
-                        ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldap_cookie);
+                        ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
                     }
 
                     if ($this->config->course_search_sub) {
                         // Use ldap_search to find first user from subtree
-                        $ldap_result = @ldap_search($ldapconnection,
+                        $ldap_result = @ldap_search($this->ldapconnection,
                                                     $ldap_context,
                                                     $ldap_search_pattern,
                                                     $ldap_fields_wanted);
                     } else {
                         // Search only in this context
-                        $ldap_result = @ldap_list($ldapconnection,
+                        $ldap_result = @ldap_list($this->ldapconnection,
                                                   $ldap_context,
                                                   $ldap_search_pattern,
                                                   $ldap_fields_wanted);
@@ -365,11 +391,11 @@ class enrol_ldap_plugin extends enrol_plugin {
                     }
 
                     if ($ldap_pagedresults) {
-                        ldap_control_paged_result_response($ldapconnection, $ldap_result, $ldap_cookie);
+                        ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
                     }
 
                     // Check and push results
-                    $records = ldap_get_entries($ldapconnection, $ldap_result);
+                    $records = ldap_get_entries($this->ldapconnection, $ldap_result);
 
                     // LDAP libraries return an odd array, really. fix it:
                     for ($c = 0; $c < $records['count']; $c++) {
@@ -382,8 +408,8 @@ class enrol_ldap_plugin extends enrol_plugin {
                 // If LDAP paged results were used, the current connection must be completely
                 // closed and a new one created, to work without paged results from here on.
                 if ($ldap_pagedresults) {
-                    $this->ldap_close(true);
-                    $ldapconnection = $this->ldap_connect();
+                    $this->ldap_close();
+                    $this->ldap_connect($trace);
                 }
 
                 if (count($flat_records)) {
@@ -391,21 +417,19 @@ class enrol_ldap_plugin extends enrol_plugin {
                     foreach($flat_records as $course) {
                         $course = array_change_key_case($course, CASE_LOWER);
                         $idnumber = $course{$this->config->course_idnumber}[0];
-                        print_string('synccourserole', 'enrol_ldap',
-                                     array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname));
+                        $trace->output(get_string('synccourserole', 'enrol_ldap', array('idnumber'=>$idnumber, 'role_shortname'=>$role->shortname)));
 
                         // Does the course exist in moodle already?
                         $course_obj = $DB->get_record('course', array($this->enrol_localcoursefield=>$idnumber));
                         if (empty($course_obj)) { // Course doesn't exist
                             if ($this->get_config('autocreate')) { // Autocreate
-                                error_log($this->errorlogtag.get_string('createcourseextid', 'enrol_ldap',
-                                                                        array('courseextid'=>$idnumber)));
-                                if ($newcourseid = $this->create_course($course)) {
-                                    $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
+                                $trace->output(get_string('createcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
+                                if (!$newcourseid = $this->create_course($course, $trace)) {
+                                    continue;
                                 }
+                                $course_obj = $DB->get_record('course', array('id'=>$newcourseid));
                             } else {
-                                error_log($this->errorlogtag.get_string('createnotcourseextid', 'enrol_ldap',
-                                                                        array('courseextid'=>$idnumber)));
+                                $trace->output(get_string('createnotcourseextid', 'enrol_ldap', array('courseextid'=>$idnumber)));
                                 continue; // Next; skip this one!
                             }
                         }
@@ -429,8 +453,7 @@ class enrol_ldap_plugin extends enrol_plugin {
                             if ($this->config->nested_groups) {
                                 $users = array();
                                 foreach ($ldapmembers as $ldapmember) {
-                                    $grpusers = $this->ldap_explode_group($ldapconnection,
-                                                                          $ldapmember,
+                                    $grpusers = $this->ldap_explode_group($ldapmember,
                                                                           $this->config->{'memberattribute_role'.$role->id});
 
                                     $users = array_merge($users, $grpusers);
@@ -447,10 +470,10 @@ class enrol_ldap_plugin extends enrol_plugin {
                                 // as the idnumber does not match their dn and we get dn's from membership.
                                 $memberidnumbers = array();
                                 foreach ($ldapmembers as $ldapmember) {
-                                    $result = ldap_read($ldapconnection, $ldapmember, '(objectClass=*)',
+                                    $result = ldap_read($this->ldapconnection, $ldapmember, '(objectClass=*)',
                                                         array($this->config->idnumber_attribute));
-                                    $entry = ldap_first_entry($ldapconnection, $result);
-                                    $values = ldap_get_values($ldapconnection, $entry, $this->config->idnumber_attribute);
+                                    $entry = ldap_first_entry($this->ldapconnection, $result);
+                                    $values = ldap_get_values($this->ldapconnection, $entry, $this->config->idnumber_attribute);
                                     array_push($memberidnumbers, $values[0]);
                                 }
 
@@ -476,9 +499,9 @@ class enrol_ldap_plugin extends enrol_plugin {
                             unset($params2);
                         } else {
                             $shortname = format_string($course_obj->shortname, true, array('context' => $context));
-                            print_string('emptyenrolment', 'enrol_ldap',
+                            $trace->output(get_string('emptyenrolment', 'enrol_ldap',
                                          array('role_shortname'=> $role->shortname,
-                                               'course_shortname' => $shortname));
+                                               'course_shortname' => $shortname)));
                         }
                         $todelete = $DB->get_records_sql($sql, $params);
 
@@ -489,10 +512,10 @@ class enrol_ldap_plugin extends enrol_plugin {
                                 switch ($this->get_config('unenrolaction')) {
                                 case ENROL_EXT_REMOVED_UNENROL:
                                     $this->unenrol_user($instance, $row->userid);
-                                    error_log($this->errorlogtag.get_string('extremovedunenrol', 'enrol_ldap',
-                                                                            array('user_username'=> $row->username,
-                                                                                  'course_shortname'=>$course_obj->shortname,
-                                                                                  'course_id'=>$course_obj->id)));
+                                    $trace->output(get_string('extremovedunenrol', 'enrol_ldap',
+                                        array('user_username'=> $row->username,
+                                              'course_shortname'=>$course_obj->shortname,
+                                              'course_id'=>$course_obj->id)));
                                     break;
                                 case ENROL_EXT_REMOVED_KEEP:
                                     // Keep - only adding enrolments
@@ -500,10 +523,10 @@ class enrol_ldap_plugin extends enrol_plugin {
                                 case ENROL_EXT_REMOVED_SUSPEND:
                                     if ($row->status != ENROL_USER_SUSPENDED) {
                                         $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
-                                        error_log($this->errorlogtag.get_string('extremovedsuspend', 'enrol_ldap',
-                                                                                array('user_username'=> $row->username,
-                                                                                      'course_shortname'=>$course_obj->shortname,
-                                                                                      'course_id'=>$course_obj->id)));
+                                        $trace->output(get_string('extremovedsuspend', 'enrol_ldap',
+                                            array('user_username'=> $row->username,
+                                                  'course_shortname'=>$course_obj->shortname,
+                                                  'course_id'=>$course_obj->id)));
                                     }
                                     break;
                                 case ENROL_EXT_REMOVED_SUSPENDNOROLES:
@@ -511,10 +534,10 @@ class enrol_ldap_plugin extends enrol_plugin {
                                         $DB->set_field('user_enrolments', 'status', ENROL_USER_SUSPENDED, array('enrolid'=>$instance->id, 'userid'=>$row->userid));
                                     }
                                     role_unassign_all(array('contextid'=>$row->contextid, 'userid'=>$row->userid, 'component'=>'enrol_ldap', 'itemid'=>$instance->id));
-                                    error_log($this->errorlogtag.get_string('extremovedsuspendnoroles', 'enrol_ldap',
-                                                                            array('user_username'=> $row->username,
-                                                                                  'course_shortname'=>$course_obj->shortname,
-                                                                                  'course_id'=>$course_obj->id)));
+                                    $trace->output(get_string('extremovedsuspendnoroles', 'enrol_ldap',
+                                        array('user_username'=> $row->username,
+                                              'course_shortname'=>$course_obj->shortname,
+                                              'course_id'=>$course_obj->id)));
                                     break;
                                 }
                             }
@@ -550,40 +573,45 @@ class enrol_ldap_plugin extends enrol_plugin {
                             $sql = 'SELECT id,username,1 FROM {user} WHERE idnumber = ? AND deleted = 0';
                             $member = $DB->get_record_sql($sql, array($ldapmember));
                             if(empty($member) || empty($member->id)){
-                                print_string ('couldnotfinduser', 'enrol_ldap', $ldapmember);
+                                $trace->output(get_string('couldnotfinduser', 'enrol_ldap', $ldapmember));
                                 continue;
                             }
 
                             $sql= "SELECT ue.status
                                      FROM {user_enrolments} ue
-                                     JOIN {enrol} e ON (e.id = ue.enrolid)
-                                     JOIN {role_assignments} ra ON (ra.itemid = e.id AND ra.component = 'enrol_ldap')
+                                     JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'ldap')
                                     WHERE e.courseid = :courseid AND ue.userid = :userid";
                             $params = array('courseid'=>$course_obj->id, 'userid'=>$member->id);
-                            $userenrolment = $DB->get_record_sql($sql, $params);
+                            $userenrolment = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE);
 
-                            if(empty($userenrolment)) {
+                            if (empty($userenrolment)) {
                                 $this->enrol_user($instance, $member->id, $role->id);
                                 // Make sure we set the enrolment status to active. If the user wasn't
                                 // previously enrolled to the course, enrol_user() sets it. But if we
                                 // configured the plugin to suspend the user enrolments _AND_ remove
                                 // the role assignments on external unenrol, then enrol_user() doesn't
                                 // set it back to active on external re-enrolment. So set it
-                                // unconditionnally to cover both cases.
+                                // unconditionally to cover both cases.
                                 $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
-                                error_log($this->errorlogtag.get_string('enroluser', 'enrol_ldap',
-                                                                        array('user_username'=> $member->username,
-                                                                              'course_shortname'=>$course_obj->shortname,
-                                                                              'course_id'=>$course_obj->id)));
+                                $trace->output(get_string('enroluser', 'enrol_ldap',
+                                    array('user_username'=> $member->username,
+                                          'course_shortname'=>$course_obj->shortname,
+                                          'course_id'=>$course_obj->id)));
 
                             } else {
+                                if (!$DB->record_exists('role_assignments', array('roleid'=>$role->id, 'userid'=>$member->id, 'contextid'=>$context->id, 'component'=>'enrol_ldap', 'itemid'=>$instance->id))) {
+                                    // This happens when reviving users or when user has multiple roles in one course.
+                                    $context = context_course::instance($course_obj->id);
+                                    role_assign($role->id, $member->id, $context->id, 'enrol_ldap', $instance->id);
+                                    $trace->output("Assign role to user '$member->username' in course '$course_obj->shortname ($course_obj->id)'");
+                                }
                                 if ($userenrolment->status == ENROL_USER_SUSPENDED) {
                                     // Reenable enrolment that was previously disabled. Enrolment refreshed
                                     $DB->set_field('user_enrolments', 'status', ENROL_USER_ACTIVE, array('enrolid'=>$instance->id, 'userid'=>$member->id));
-                                    error_log($this->errorlogtag.get_string('enroluserenable', 'enrol_ldap',
-                                                                            array('user_username'=> $member->username,
-                                                                                  'course_shortname'=>$course_obj->shortname,
-                                                                                  'course_id'=>$course_obj->id)));
+                                    $trace->output(get_string('enroluserenable', 'enrol_ldap',
+                                        array('user_username'=> $member->username,
+                                              'course_shortname'=>$course_obj->shortname,
+                                              'course_id'=>$course_obj->id)));
                                 }
                             }
                         }
@@ -593,41 +621,37 @@ class enrol_ldap_plugin extends enrol_plugin {
             }
         }
         @$this->ldap_close();
+        $trace->finished();
     }
 
     /**
      * Connect to the LDAP server, using the plugin configured
      * settings. It's actually a wrapper around ldap_connect_moodle()
      *
-     * @return mixed A valid LDAP connection or false.
+     * @param progress_trace $trace
+     * @return bool success
      */
-    protected function ldap_connect() {
+    protected function ldap_connect(progress_trace $trace = null) {
         global $CFG;
         require_once($CFG->libdir.'/ldaplib.php');
 
-        // Cache ldap connections. They are expensive to set up
-        // and can drain the TCP/IP ressources on the server if we
-        // are syncing a lot of users (as we try to open a new connection
-        // to get the user details). This is the least invasive way
-        // to reuse existing connections without greater code surgery.
-        if(!empty($this->ldapconnection)) {
-            $this->ldapconns++;
-            return $this->ldapconnection;
+        if (isset($this->ldapconnection)) {
+            return true;
         }
 
         if ($ldapconnection = ldap_connect_moodle($this->get_config('host_url'), $this->get_config('ldap_version'),
                                                   $this->get_config('user_type'), $this->get_config('bind_dn'),
                                                   $this->get_config('bind_pw'), $this->get_config('opt_deref'),
                                                   $debuginfo, $this->get_config('start_tls'))) {
-            $this->ldapconns = 1;
             $this->ldapconnection = $ldapconnection;
-            return $ldapconnection;
+            return true;
         }
 
-        // Log the problem, but don't show it to the user. She doesn't
-        // even have a chance to see it, as we redirect instantly to
-        // the user/front page.
-        error_log($this->errorlogtag.$debuginfo);
+        if ($trace) {
+            $trace->output($debuginfo);
+        } else {
+            error_log($this->errorlogtag.$debuginfo);
+        }
 
         return false;
     }
@@ -637,23 +661,22 @@ class enrol_ldap_plugin extends enrol_plugin {
      *
      */
     protected function ldap_close() {
-        $this->ldapconns--;
-        if($this->ldapconns == 0) {
+        if (isset($this->ldapconnection)) {
             @ldap_close($this->ldapconnection);
-            unset($this->ldapconnection);
+            $this->ldapconnection = null;
         }
+        return;
     }
 
     /**
      * Return multidimensional array with details of user courses (at
      * least dn and idnumber).
      *
-     * @param resource $ldapconnection a valid LDAP connection.
      * @param string $memberuid user idnumber (without magic quotes).
      * @param object role is a record from the mdl_role table.
      * @return array
      */
-    protected function find_ext_enrolments (&$ldapconnection, $memberuid, $role) {
+    protected function find_ext_enrolments($memberuid, $role) {
         global $CFG;
         require_once($CFG->libdir.'/ldaplib.php');
 
@@ -671,14 +694,14 @@ class enrol_ldap_plugin extends enrol_plugin {
         $extmemberuid = textlib::convert($memberuid, 'utf-8', $this->get_config('ldapencoding'));
 
         if($this->get_config('memberattribute_isdn')) {
-            if (!($extmemberuid = $this->ldap_find_userdn ($ldapconnection, $extmemberuid))) {
+            if (!($extmemberuid = $this->ldap_find_userdn($extmemberuid))) {
                 return array();
             }
         }
 
         $ldap_search_pattern = '';
         if($this->get_config('nested_groups')) {
-            $usergroups = $this->ldap_find_user_groups($ldapconnection, $extmemberuid);
+            $usergroups = $this->ldap_find_user_groups($extmemberuid);
             if(count($usergroups) > 0) {
                 foreach ($usergroups as $group) {
                     $ldap_search_pattern .= '('.$this->get_config('memberattribute_role'.$role->id).'='.$group.')';
@@ -728,18 +751,18 @@ class enrol_ldap_plugin extends enrol_plugin {
             $flat_records = array();
             do {
                 if ($ldap_pagedresults) {
-                    ldap_control_paged_result($ldapconnection, $this->config->pagesize, true, $ldap_cookie);
+                    ldap_control_paged_result($this->ldapconnection, $this->config->pagesize, true, $ldap_cookie);
                 }
 
                 if ($this->get_config('course_search_sub')) {
                     // Use ldap_search to find first user from subtree
-                    $ldap_result = @ldap_search($ldapconnection,
+                    $ldap_result = @ldap_search($this->ldapconnection,
                                                 $context,
                                                 $ldap_search_pattern,
                                                 $ldap_fields_wanted);
                 } else {
                     // Search only in this context
-                    $ldap_result = @ldap_list($ldapconnection,
+                    $ldap_result = @ldap_list($this->ldapconnection,
                                               $context,
                                               $ldap_search_pattern,
                                               $ldap_fields_wanted);
@@ -750,13 +773,13 @@ class enrol_ldap_plugin extends enrol_plugin {
                 }
 
                 if ($ldap_pagedresults) {
-                    ldap_control_paged_result_response($ldapconnection, $ldap_result, $ldap_cookie);
+                    ldap_control_paged_result_response($this->ldapconnection, $ldap_result, $ldap_cookie);
                 }
 
                 // Check and push results. ldap_get_entries() already
                 // lowercases the attribute index, so there's no need to
                 // use array_change_key_case() later.
-                $records = ldap_get_entries($ldapconnection, $ldap_result);
+                $records = ldap_get_entries($this->ldapconnection, $ldap_result);
 
                 // LDAP libraries return an odd array, really. Fix it.
                 for ($c = 0; $c < $records['count']; $c++) {
@@ -769,8 +792,8 @@ class enrol_ldap_plugin extends enrol_plugin {
             // If LDAP paged results were used, the current connection must be completely
             // closed and a new one created, to work without paged results from here on.
             if ($ldap_pagedresults) {
-                $this->ldap_close(true);
-                $ldapconnection = $this->ldap_connect();
+                $this->ldap_close();
+                $this->ldap_connect();
             }
 
             if (count($flat_records)) {
@@ -786,18 +809,17 @@ class enrol_ldap_plugin extends enrol_plugin {
      * user dn like: cn=username,ou=suborg,o=org. It's actually a wrapper
      * around ldap_find_userdn().
      *
-     * @param resource $ldapconnection a valid LDAP connection
      * @param string $userid the userid to search for (in external LDAP encoding, no magic quotes).
      * @return mixed the user dn or false
      */
-    protected function ldap_find_userdn($ldapconnection, $userid) {
+    protected function ldap_find_userdn($userid) {
         global $CFG;
         require_once($CFG->libdir.'/ldaplib.php');
 
         $ldap_contexts = explode(';', $this->get_config('user_contexts'));
         $ldap_defaults = ldap_getdefaults();
 
-        return ldap_find_userdn($ldapconnection, $userid, $ldap_contexts,
+        return ldap_find_userdn($this->ldapconnection, $userid, $ldap_contexts,
                                 '(objectClass='.$ldap_defaults['objectclass'][$this->get_config('user_type')].')',
                                 $this->get_config('idnumber_attribute'), $this->get_config('user_search_sub'));
     }
@@ -806,14 +828,13 @@ class enrol_ldap_plugin extends enrol_plugin {
      * Find the groups a given distinguished name belongs to, both directly
      * and indirectly via nested groups membership.
      *
-     * @param resource $ldapconnection a valid LDAP connection
      * @param string $memberdn distinguished name to search
      * @return array with member groups' distinguished names (can be emtpy)
      */
-    protected function ldap_find_user_groups($ldapconnection, $memberdn) {
+    protected function ldap_find_user_groups($memberdn) {
         $groups = array();
 
-        $this->ldap_find_user_groups_recursively($ldapconnection, $memberdn, $groups);
+        $this->ldap_find_user_groups_recursively($memberdn, $groups);
         return $groups;
     }
 
@@ -821,23 +842,22 @@ class enrol_ldap_plugin extends enrol_plugin {
      * Recursively process the groups the given member distinguished name
      * belongs to, adding them to the already processed groups array.
      *
-     * @param resource $ldapconnection
      * @param string $memberdn distinguished name to search
      * @param array reference &$membergroups array with already found
      *                        groups, where we'll put the newly found
      *                        groups.
      */
-    protected function ldap_find_user_groups_recursively($ldapconnection, $memberdn, &$membergroups) {
-        $result = @ldap_read($ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
+    protected function ldap_find_user_groups_recursively($memberdn, &$membergroups) {
+        $result = @ldap_read($this->ldapconnection, $memberdn, '(objectClass=*)', array($this->get_config('group_memberofattribute')));
         if (!$result) {
             return;
         }
 
-        if ($entry = ldap_first_entry($ldapconnection, $result)) {
+        if ($entry = ldap_first_entry($this->ldapconnection, $result)) {
             do {
-                $attributes = ldap_get_attributes($ldapconnection, $entry);
+                $attributes = ldap_get_attributes($this->ldapconnection, $entry);
                 for ($j = 0; $j < $attributes['count']; $j++) {
-                    $groups = ldap_get_values_len($ldapconnection, $entry, $attributes[$j]);
+                    $groups = ldap_get_values_len($this->ldapconnection, $entry, $attributes[$j]);
                     foreach ($groups as $key => $group) {
                         if ($key === 'count') {  // Skip the entries count
                             continue;
@@ -846,12 +866,12 @@ class enrol_ldap_plugin extends enrol_plugin {
                             // Only push and recurse if we haven't 'seen' this group before
                             // to prevent loops (MS Active Directory allows them!!).
                             array_push($membergroups, $group);
-                            $this->ldap_find_user_groups_recursively($ldapconnection, $group, $membergroups);
+                            $this->ldap_find_user_groups_recursively($group, $membergroups);
                         }
                     }
                 }
             }
-            while ($entry = ldap_next_entry($ldapconnection, $entry));
+            while ($entry = ldap_next_entry($this->ldapconnection, $entry));
         }
     }
 
@@ -861,30 +881,29 @@ class enrol_ldap_plugin extends enrol_plugin {
      * the intermediate groups and return the full list of users that
      * directly or indirectly belong to the group.
      *
-     * @param resource $ldapconnection a valid LDAP connection
      * @param string $group the group name to search
      * @param string $memberattibute the attribute that holds the members of the group
      * @return array the list of users belonging to the group. If $group
      *         is not actually a group, returns array($group).
      */
-    protected function ldap_explode_group($ldapconnection, $group, $memberattribute) {
+    protected function ldap_explode_group($group, $memberattribute) {
         switch ($this->get_config('user_type')) {
             case 'ad':
                 // $group is already the distinguished name to search.
                 $dn = $group;
 
-                $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
-                $entry = ldap_first_entry($ldapconnection, $result);
-                $objectclass = ldap_get_values($ldapconnection, $entry, 'objectClass');
+                $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array('objectClass'));
+                $entry = ldap_first_entry($this->ldapconnection, $result);
+                $objectclass = ldap_get_values($this->ldapconnection, $entry, 'objectClass');
 
                 if (!in_array('group', $objectclass)) {
                     // Not a group, so return immediately.
                     return array($group);
                 }
 
-                $result = ldap_read($ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
-                $entry = ldap_first_entry($ldapconnection, $result);
-                $members = @ldap_get_values($ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
+                $result = ldap_read($this->ldapconnection, $dn, '(objectClass=*)', array($memberattribute));
+                $entry = ldap_first_entry($this->ldapconnection, $result);
+                $members = @ldap_get_values($this->ldapconnection, $entry, $memberattribute); // Can be empty and throws a warning
                 if ($members['count'] == 0) {
                     // There are no members in this group, return nothing.
                     return array();
@@ -893,7 +912,7 @@ class enrol_ldap_plugin extends enrol_plugin {
 
                 $users = array();
                 foreach ($members as $member) {
-                    $group_members = $this->ldap_explode_group($ldapconnection, $member, $memberattribute);
+                    $group_members = $this->ldap_explode_group($member, $memberattribute);
                     $users = array_merge($users, $group_members);
                 }
 
@@ -910,15 +929,12 @@ class enrol_ldap_plugin extends enrol_plugin {
     /**
      * Will create the moodle course from the template
      * course_ext is an array as obtained from ldap -- flattened somewhat
-     * NOTE: if you pass true for $skip_fix_course_sortorder
-     * you will want to call fix_course_sortorder() after your are done
-     * with course creation.
      *
      * @param array $course_ext
-     * @param boolean $skip_fix_course_sortorder
+     * @param progress_trace $trace
      * @return mixed false on error, id for the newly created course otherwise.
      */
-    function create_course($course_ext, $skip_fix_course_sortorder=false) {
+    function create_course($course_ext, progress_trace $trace) {
         global $CFG, $DB;
 
         require_once("$CFG->dirroot/course/lib.php");
@@ -965,8 +981,7 @@ class enrol_ldap_plugin extends enrol_plugin {
         $course->shortname = $course_ext[$this->get_config('course_shortname')][0];
         if (empty($course->idnumber) || empty($course->fullname) || empty($course->shortname)) {
             // We are in trouble!
-            error_log($this->errorlogtag.get_string('cannotcreatecourse', 'enrol_ldap'));
-            error_log($this->errorlogtag.var_export($course, true));
+            $trace->output(get_string('cannotcreatecourse', 'enrol_ldap').' '.var_export($course, true));
             return false;
         }
 
@@ -980,5 +995,86 @@ class enrol_ldap_plugin extends enrol_plugin {
         $newcourse = create_course($course);
         return $newcourse->id;
     }
+
+    /**
+     * Automatic enrol sync executed during restore.
+     * Useful for automatic sync by course->idnumber or course category.
+     * @param stdClass $course course record
+     */
+    public function restore_sync_course($course) {
+        // TODO: this can not work because restore always nukes the course->idnumber, do not ask me why (MDL-37312)
+        // NOTE: for now restore does not do any real logging yet, let's do the same here...
+        $trace = new error_log_progress_trace();
+        $this->sync_enrolments($trace, $course->id);
+    }
+
+    /**
+     * Restore instance and map settings.
+     *
+     * @param restore_enrolments_structure_step $step
+     * @param stdClass $data
+     * @param stdClass $course
+     * @param int $oldid
+     */
+    public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) {
+        global $DB;
+        // There is only 1 ldap enrol instance per course.
+        if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'ldap'), 'id')) {
+            $instance = reset($instances);
+            $instanceid = $instance->id;
+        } else {
+            $instanceid = $this->add_instance($course, (array)$data);
+        }
+        $step->set_mapping('enrol', $oldid, $instanceid);
+    }
+
+    /**
+     * Restore user enrolment.
+     *
+     * @param restore_enrolments_structure_step $step
+     * @param stdClass $data
+     * @param stdClass $instance
+     * @param int $oldinstancestatus
+     * @param int $userid
+     */
+    public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) {
+        global $DB;
+
+        if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) {
+            // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers.
+
+        } else if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_KEEP) {
+            if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
+                $this->enrol_user($instance, $userid, null, 0, 0, $data->status);
+            }
+
+        } else {
+            if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
+                $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED);
+            }
+        }
+    }
+
+    /**
+     * Restore role assignment.
+     *
+     * @param stdClass $instance
+     * @param int $roleid
+     * @param int $userid
+     * @param int $contextid
+     */
+    public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
+        global $DB;
+
+        if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) {
+            // Skip any roles restore, they should be already synced automatically.
+            return;
+        }
+
+        // Just restore every role.
+        if ($DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) {
+            role_assign($roleid, $userid, $contextid, 'enrol_'.$instance->enrol, $instance->id);
+        }
+    }
 }
 
diff --git a/enrol/ldap/tests/ldap_test.php b/enrol/ldap/tests/ldap_test.php
new file mode 100644 (file)
index 0000000..ded9531
--- /dev/null
@@ -0,0 +1,472 @@
+<?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/>.
+
+/**
+ * LDAP enrolment plugin tests.
+ *
+ * NOTE: in order to execute this test you need to set up
+ *       OpenLDAP server with core, cosine, nis and internet schemas
+ *       and add configuration constants to config.php or phpunit.xml configuration file:
+ *
+ * define('TEST_ENROL_LDAP_HOST_URL', 'ldap://127.0.0.1');
+ * define('TEST_ENROL_LDAP_BIND_DN', 'cn=someuser,dc=example,dc=local');
+ * define('TEST_ENROL_LDAP_BIND_PW', 'somepassword');
+ * define('TEST_ENROL_LDAP_DOMAIN', 'dc=example,dc=local');
+ *
+ * @package    enrol_ldap
+ * @category   phpunit
+ * @copyright  2013 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;
+
+
+class enrol_ldap_testcase extends advanced_testcase {
+
+    public function test_enrol_ldap() {
+        global $CFG, $DB;
+
+        if (!extension_loaded('ldap')) {
+            $this->markTestSkipped('LDAP extension is not loaded.');
+        }
+
+        $this->resetAfterTest();
+
+        require_once($CFG->dirroot.'/enrol/ldap/lib.php');
+        require_once($CFG->libdir.'/ldaplib.php');
+
+        if (!defined('TEST_ENROL_LDAP_HOST_URL') or !defined('TEST_ENROL_LDAP_BIND_DN') or !defined('TEST_ENROL_LDAP_BIND_PW') or !defined('TEST_ENROL_LDAP_DOMAIN')) {
+            $this->markTestSkipped('External LDAP test server not configured.');
+        }
+
+        // Make sure we can connect the server.
+        $debuginfo = '';
+        if (!$connection = ldap_connect_moodle(TEST_ENROL_LDAP_HOST_URL, 3, 'rfc2307', TEST_ENROL_LDAP_BIND_DN, TEST_ENROL_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) {
+            $this->markTestSkipped('Can not connect to LDAP test server.');
+        }
+
+        $this->enable_plugin();
+
+        // Create new empty test container.
+        $topdn = 'dc=moodletest,'.TEST_ENROL_LDAP_DOMAIN;
+
+        $this->recursive_delete($connection, TEST_ENROL_LDAP_DOMAIN, 'dc=moodletest');
+
+        $o = array();
+        $o['objectClass'] = array('dcObject', 'organizationalUnit');
+        $o['dc']         = 'moodletest';
+        $o['ou']         = 'MOODLETEST';
+        if (!ldap_add($connection, 'dc=moodletest,'.TEST_ENROL_LDAP_DOMAIN, $o)) {
+            $this->markTestSkipped('Can not create test LDAP container.');
+        }
+
+        // Configure enrol plugin.
+        /** @var enrol_ldap_plugin $enrol */
+        $enrol = enrol_get_plugin('ldap');
+        $enrol->set_config('host_url', TEST_ENROL_LDAP_HOST_URL);
+        $enrol->set_config('start_tls', 0);
+        $enrol->set_config('ldap_version', 3);
+        $enrol->set_config('ldapencoding', 'utf-8');
+        $enrol->set_config('page_size', '2');
+        $enrol->set_config('bind_dn', TEST_ENROL_LDAP_BIND_DN);
+        $enrol->set_config('bind_pw', TEST_ENROL_LDAP_BIND_PW);
+        $enrol->set_config('course_search_sub', 0);
+        $enrol->set_config('memberattribute_isdn', 0);
+        $enrol->set_config('user_contexts', '');
+        $enrol->set_config('user_search_sub', 0);
+        $enrol->set_config('user_type', 'rfc2307');
+        $enrol->set_config('opt_deref', LDAP_DEREF_NEVER);
+        $enrol->set_config('objectclass', '(objectClass=posixGroup)');
+        $enrol->set_config('course_idnumber', 'cn');
+        $enrol->set_config('course_shortname', 'cn');
+        $enrol->set_config('course_fullname', 'cn');
+        $enrol->set_config('course_summary', '');
+        $enrol->set_config('ignorehiddencourses', 0);
+        $enrol->set_config('nested_groups', 0);
+        $enrol->set_config('autocreate', 0);
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP);
+
+        $roles = get_all_roles();
+        foreach ($roles as $role) {
+            $enrol->set_config('contexts_role'.$role->id, '');
+            $enrol->set_config('memberattribute_role'.$role->id, '');
+        }
+
+        // Create group for teacher enrolments.
+        $teacherrole = $DB->get_record('role', array('shortname'=>'teacher'));
+        $this->assertNotEmpty($teacherrole);
+        $o = array();
+        $o['objectClass'] = array('organizationalUnit');
+        $o['ou']          = 'teachers';
+        ldap_add($connection, 'ou=teachers,'.$topdn, $o);
+        $enrol->set_config('contexts_role'.$teacherrole->id, 'ou=teachers,'.$topdn);
+        $enrol->set_config('memberattribute_role'.$teacherrole->id, 'memberuid');
+
+        // Create group for student enrolments.
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->assertNotEmpty($studentrole);
+        $o = array();
+        $o['objectClass'] = array('organizationalUnit');
+        $o['ou']          = 'students';
+        ldap_add($connection, 'ou=students,'.$topdn, $o);
+        $enrol->set_config('contexts_role'.$studentrole->id, 'ou=students,'.$topdn);
+        $enrol->set_config('memberattribute_role'.$studentrole->id, 'memberuid');
+
+        // Create some users and courses.
+        $user1 = $this->getDataGenerator()->create_user(array('idnumber'=>'user1', 'username'=>'user1'));
+        $user2 = $this->getDataGenerator()->create_user(array('idnumber'=>'user2', 'username'=>'user2'));
+        $user3 = $this->getDataGenerator()->create_user(array('idnumber'=>'user3', 'username'=>'user3'));
+        $user4 = $this->getDataGenerator()->create_user(array('idnumber'=>'user4', 'username'=>'user4'));
+        $user5 = $this->getDataGenerator()->create_user(array('idnumber'=>'user5', 'username'=>'user5'));
+        $user6 = $this->getDataGenerator()->create_user(array('idnumber'=>'user6', 'username'=>'user6'));
+
+        $course1 = $this->getDataGenerator()->create_course(array('idnumber'=>'course1', 'shortname'=>'course1'));
+        $course2 = $this->getDataGenerator()->create_course(array('idnumber'=>'course2', 'shortname'=>'course2'));
+        $course3 = $this->getDataGenerator()->create_course(array('idnumber'=>'course3', 'shortname'=>'course3'));
+
+        // Set up some ldap data.
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user1', 'user2', 'user3', 'userx');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '2';
+        $o['memberUid']   = array('user5');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=teachers,'.$topdn, $o);
+
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course2';
+        $o['gidNumber']   = '3';
+        $o['memberUid']   = array('user1', 'user2', 'user3', 'user4');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course4';
+        $o['gidNumber']   = '4';
+        $o['memberUid']   = array('user1', 'user2');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course4';
+        $o['gidNumber']   = '5';
+        $o['memberUid']   = array('user5', 'user6');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=teachers,'.$topdn, $o);
+
+
+        // Test simple test without creation.
+
+        $this->assertEquals(0, $DB->count_records('user_enrolments'));
+        $this->assertEquals(0, $DB->count_records('role_assignments'));
+        $this->assertEquals(4, $DB->count_records('course'));
+
+        $enrol->sync_enrolments(new null_progress_trace());
+
+        $this->assertEquals(8, $DB->count_records('user_enrolments'));
+        $this->assertEquals(8, $DB->count_records('role_assignments'));
+        $this->assertEquals(4, $DB->count_records('course'));
+
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id);
+        $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id);
+        $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id);
+        $this->assertIsEnrolled($course1->id, $user5->id, $teacherrole->id);
+
+        $this->assertIsEnrolled($course2->id, $user1->id, $studentrole->id);
+        $this->assertIsEnrolled($course2->id, $user2->id, $studentrole->id);
+        $this->assertIsEnrolled($course2->id, $user3->id, $studentrole->id);
+        $this->assertIsEnrolled($course2->id, $user4->id, $studentrole->id);
+
+
+        // Test course creation.
+        $enrol->set_config('autocreate', 1);
+
+        $enrol->sync_enrolments(new null_progress_trace());
+
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(12, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+
+        $course4 = $DB->get_record('course', array('idnumber'=>'course4'), '*', MUST_EXIST);
+
+        $this->assertIsEnrolled($course4->id, $user1->id, $studentrole->id);
+        $this->assertIsEnrolled($course4->id, $user2->id, $studentrole->id);
+        $this->assertIsEnrolled($course4->id, $user5->id, $teacherrole->id);
+        $this->assertIsEnrolled($course4->id, $user6->id, $teacherrole->id);
+
+
+        // Test unenrolment.
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP);
+        $enrol->sync_enrolments(new null_progress_trace());
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(12, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND);
+        $enrol->sync_enrolments(new null_progress_trace());
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(12, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_SUSPENDED);
+        $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_SUSPENDED);
+        $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_SUSPENDED);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user1', 'user2', 'user3');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->sync_enrolments(new null_progress_trace());
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(12, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);
+        $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_ACTIVE);
+        $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_ACTIVE);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
+        $enrol->sync_enrolments(new null_progress_trace());
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, 0, ENROL_USER_SUSPENDED);
+        $this->assertIsEnrolled($course1->id, $user2->id, 0, ENROL_USER_SUSPENDED);
+        $this->assertIsEnrolled($course1->id, $user3->id, 0, ENROL_USER_SUSPENDED);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user1', 'user2', 'user3');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->sync_enrolments(new null_progress_trace());
+        $this->assertEquals(12, $DB->count_records('user_enrolments'));
+        $this->assertEquals(12, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);
+        $this->assertIsEnrolled($course1->id, $user2->id, $studentrole->id, ENROL_USER_ACTIVE);
+        $this->assertIsEnrolled($course1->id, $user3->id, $studentrole->id, ENROL_USER_ACTIVE);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
+        $enrol->sync_enrolments(new null_progress_trace());
+        $this->assertEquals(9, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsNotEnrolled($course1->id, $user1->id);
+        $this->assertIsNotEnrolled($course1->id, $user2->id);
+        $this->assertIsNotEnrolled($course1->id, $user3->id);
+
+
+        // Individual user enrolments-
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user1', 'user2', 'user3');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->sync_user_enrolments($user1);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user2', 'user3');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_KEEP);
+        $enrol->sync_user_enrolments($user1);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);
+
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPEND);
+        $enrol->sync_user_enrolments($user1);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_SUSPENDED);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user1', 'user2', 'user3');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->sync_user_enrolments($user1);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user2', 'user3');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_SUSPENDNOROLES);
+        $enrol->sync_user_enrolments($user1);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, 0, ENROL_USER_SUSPENDED);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user1', 'user2', 'user3');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->sync_user_enrolments($user1);
+        $this->assertEquals(10, $DB->count_records('user_enrolments'));
+        $this->assertEquals(10, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsEnrolled($course1->id, $user1->id, $studentrole->id, ENROL_USER_ACTIVE);
+
+        ldap_delete($connection, 'cn=course1,ou=students,'.$topdn);
+        $o = array();
+        $o['objectClass'] = array('posixGroup');
+        $o['cn']          = 'course1';
+        $o['gidNumber']   = '1';
+        $o['memberUid']   = array('user2', 'user3');
+        ldap_add($connection, 'cn='.$o['cn'].',ou=students,'.$topdn, $o);
+
+        $enrol->set_config('unenrolaction', ENROL_EXT_REMOVED_UNENROL);
+        $enrol->sync_user_enrolments($user1);
+        $this->assertEquals(9, $DB->count_records('user_enrolments'));
+        $this->assertEquals(9, $DB->count_records('role_assignments'));
+        $this->assertEquals(5, $DB->count_records('course'));
+        $this->assertIsNotEnrolled($course1->id, $user1->id);
+
+        $this->recursive_delete($connection, TEST_ENROL_LDAP_DOMAIN, 'dc=moodletest');
+        ldap_close($connection);
+
+        // NOTE: multiple roles in one course is not supported, sorry
+    }
+
+    public function assertIsEnrolled($courseid, $userid, $roleid, $status=null) {
+        global $DB;
+
+        $context = context_course::instance($courseid);
+        $instance = $DB->get_record('enrol', array('courseid'=>$courseid, 'enrol'=>'lda