Merge branch 'MDL-33079' of git://github.com/stronk7/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 21 May 2012 05:26:36 +0000 (13:26 +0800)
committerDan Poltawski <dan@moodle.com>
Mon, 21 May 2012 05:26:36 +0000 (13:26 +0800)
273 files changed:
admin/index.php
admin/renderer.php
admin/roles/usersroles.php
admin/settings/server.php
admin/tool/dbtransfer/database_transfer_form.php
admin/tool/innodb/index.php
admin/user/user_bulk_download.php
admin/webservice/testclient_forms.php
backup/converter/moodle1/tests/lib_test.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_final_task.class.php
backup/moodle2/restore_stepslib.php
backup/util/plan/restore_structure_step.class.php
calendar/lib.php
config-dist.php
course/dndupload.js [new file with mode: 0644]
course/dndupload.php [new file with mode: 0644]
course/dnduploadlib.php [new file with mode: 0644]
course/edit.php
course/editcategory.php
course/editsection.php
course/editsection_form.php
course/externallib.php
course/format/renderer.php
course/format/topics/format.js
course/format/weeks/format.js
course/lib.php
course/moodleform_mod.php
course/renderer.php
course/rest.php
course/style.css [new file with mode: 0644]
course/view.php
course/yui/modchooser/modchooser.js [new file with mode: 0644]
course/yui/toolboxes/toolboxes.js
grade/export/grade_export_form.php
grade/export/lib.php
grade/export/ods/export.php
grade/export/ods/grade_export_ods.php
grade/export/ods/index.php
grade/export/txt/export.php
grade/export/txt/grade_export_txt.php
grade/export/txt/index.php
grade/export/xls/export.php
grade/export/xls/grade_export_xls.php
grade/export/xls/index.php
grade/export/xml/export.php
grade/export/xml/grade_export_xml.php
grade/export/xml/index.php
grade/lib.php
grade/report/grader/lib.php
group/delete.php
group/externallib.php
group/group.php
group/group_form.php
group/grouping.php
group/grouping_form.php
group/groupings.php
group/import.php
group/index.php
group/lib.php
group/module.js
index.php
install/lang/ar/admin.php
install/lang/et/admin.php
install/lang/rm_surs/admin.php [new file with mode: 0644]
install/lang/sr_cr/install.php
install/lang/sr_lt/install.php
install/lang/zh_cn/install.php
iplookup/lib.php
iplookup/tests/geoip_test.php [new file with mode: 0644]
iplookup/tests/geoplugin_test.php [new file with mode: 0644]
lang/en/admin.php
lang/en/completion.php
lang/en/condition.php
lang/en/error.php
lang/en/grades.php
lang/en/group.php
lang/en/install.php
lang/en/moodle.php
lang/en/question.php
lang/en/webservice.php
lib/conditionlib.php
lib/cronlib.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/dml/mysqli_native_moodle_database.php
lib/dml/pgsql_native_moodle_database.php
lib/dml/sqlsrv_native_moodle_database.php
lib/dml/tests/dml_test.php
lib/editor/tinymce/lang/en/editor_tinymce.php
lib/editor/tinymce/lib.php
lib/editor/tinymce/settings.php
lib/editor/tinymce/version.php
lib/excel/test.php
lib/externallib.php
lib/filelib.php
lib/form/yui/dateselector/dateselector.js
lib/grouplib.php
lib/modinfolib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputrenderers.php
lib/phpunit/classes/advanced_testcase.php [new file with mode: 0644]
lib/phpunit/classes/arraydataset.php [new file with mode: 0644]
lib/phpunit/classes/basic_testcase.php [new file with mode: 0644]
lib/phpunit/classes/block_generator.php [new file with mode: 0644]
lib/phpunit/classes/data_generator.php [new file with mode: 0644]
lib/phpunit/classes/database_driver_testcase.php [new file with mode: 0644]
lib/phpunit/classes/hint_resultprinter.php [new file with mode: 0644]
lib/phpunit/classes/module_generator.php [new file with mode: 0644]
lib/phpunit/classes/unittestcase.php [new file with mode: 0644]
lib/phpunit/classes/util.php [new file with mode: 0644]
lib/phpunit/generatorlib.php
lib/phpunit/lib.php
lib/phpunit/tests/advanced_test.php [moved from lib/tests/phpunit_test.php with 63% similarity]
lib/phpunit/tests/basic_test.php [new file with mode: 0644]
lib/phpunit/tests/fixtures/sample_dataset.csv [moved from lib/tests/fixtures/sample_dataset.csv with 100% similarity]
lib/phpunit/tests/fixtures/sample_dataset.xml [moved from lib/tests/fixtures/sample_dataset.xml with 100% similarity]
lib/phpunit/tests/generator_test.php [new file with mode: 0644]
lib/pluginlib.php
lib/setup.php
lib/setuplib.php
lib/tests/code_test.php
lib/tests/conditionlib_test.php
lib/tests/grouplib_test.php [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/pagelib_test.php
lib/tests/repositorylib_test.php
lib/yui/chooserdialogue/chooserdialogue.js [new file with mode: 0644]
mod/assign/adminlib.php
mod/assign/assignmentplugin.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/styles.css
mod/assign/upgradelib.php
mod/assignment/lang/en/assignment.php
mod/assignment/tests/generator_test.php
mod/book/README.md [new file with mode: 0644]
mod/book/backup/moodle1/lib.php [new file with mode: 0755]
mod/book/backup/moodle2/backup_book_activity_task.class.php [new file with mode: 0644]
mod/book/backup/moodle2/backup_book_settingslib.php [new file with mode: 0644]
mod/book/backup/moodle2/backup_book_stepslib.php [new file with mode: 0644]
mod/book/backup/moodle2/restore_book_activity_task.class.php [new file with mode: 0644]
mod/book/backup/moodle2/restore_book_stepslib.php [new file with mode: 0644]
mod/book/db/access.php [new file with mode: 0644]
mod/book/db/install.xml [new file with mode: 0644]
mod/book/db/log.php [new file with mode: 0644]
mod/book/db/subplugins.php [new file with mode: 0644]
mod/book/db/upgrade.php [new file with mode: 0644]
mod/book/delete.php [new file with mode: 0644]
mod/book/edit.php [new file with mode: 0644]
mod/book/edit_form.php [new file with mode: 0644]
mod/book/index.php [new file with mode: 0644]
mod/book/lang/en/book.php [new file with mode: 0644]
mod/book/lib.php [new file with mode: 0644]
mod/book/locallib.php [new file with mode: 0644]
mod/book/mod_form.php [new file with mode: 0644]
mod/book/move.php [new file with mode: 0644]
mod/book/pix/add.png [new file with mode: 0644]
mod/book/pix/chapter.png [new file with mode: 0644]
mod/book/pix/icon.png [new file with mode: 0644]
mod/book/pix/nav_exit.png [new file with mode: 0644]
mod/book/pix/nav_next.png [new file with mode: 0644]
mod/book/pix/nav_prev.png [new file with mode: 0644]
mod/book/pix/nav_prev_dis.png [new file with mode: 0644]
mod/book/pix/nav_sep.png [new file with mode: 0644]
mod/book/settings.php [new file with mode: 0644]
mod/book/show.php [new file with mode: 0644]
mod/book/styles.css [new file with mode: 0644]
mod/book/tool/exportimscp/db/access.php [new file with mode: 0644]
mod/book/tool/exportimscp/db/log.php [new file with mode: 0644]
mod/book/tool/exportimscp/imscp.css [new file with mode: 0644]
mod/book/tool/exportimscp/index.php [new file with mode: 0644]
mod/book/tool/exportimscp/lang/en/booktool_exportimscp.php [new file with mode: 0644]
mod/book/tool/exportimscp/lib.php [new file with mode: 0644]
mod/book/tool/exportimscp/locallib.php [new file with mode: 0644]
mod/book/tool/exportimscp/pix/generate.png [new file with mode: 0644]
mod/book/tool/exportimscp/version.php [new file with mode: 0644]
mod/book/tool/importhtml/db/access.php [new file with mode: 0644]
mod/book/tool/importhtml/import_form.php [new file with mode: 0644]
mod/book/tool/importhtml/index.php [new file with mode: 0644]
mod/book/tool/importhtml/lang/en/booktool_importhtml.php [new file with mode: 0644]
mod/book/tool/importhtml/lib.php [new file with mode: 0644]
mod/book/tool/importhtml/locallib.php [new file with mode: 0644]
mod/book/tool/importhtml/version.php [new file with mode: 0644]
mod/book/tool/print/db/access.php [new file with mode: 0644]
mod/book/tool/print/db/log.php [new file with mode: 0644]
mod/book/tool/print/index.php [new file with mode: 0644]
mod/book/tool/print/lang/en/booktool_print.php [new file with mode: 0644]
mod/book/tool/print/lib.php [new file with mode: 0644]
mod/book/tool/print/locallib.php [new file with mode: 0644]
mod/book/tool/print/pix/book.png [new file with mode: 0644]
mod/book/tool/print/pix/chapter.png [new file with mode: 0644]
mod/book/tool/print/print.css [new file with mode: 0644]
mod/book/tool/print/version.php [new file with mode: 0644]
mod/book/version.php [new file with mode: 0644]
mod/book/view.php [new file with mode: 0644]
mod/chat/gui_basic/index.php
mod/chat/lang/en/chat.php
mod/chat/lib.php
mod/choice/lang/en/choice.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/locallib.php
mod/data/view.php
mod/feedback/edit_form.php
mod/feedback/export.php
mod/feedback/lang/en/feedback.php
mod/feedback/lib.php
mod/folder/lang/en/folder.php
mod/folder/lib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/locallib.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/locallib.php
mod/imscp/lang/en/imscp.php
mod/label/lang/en/label.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lti/lang/en/lti.php
mod/lti/lib.php
mod/lti/typessettings.php
mod/page/lang/en/page.php
mod/page/lib.php
mod/quiz/db/upgrade.php
mod/quiz/editlib.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/resource/lang/en/resource.php
mod/resource/lib.php
mod/scorm/lang/en/scorm.php
mod/scorm/module.js
mod/survey/lang/en/survey.php
mod/survey/lib.php
mod/url/lang/en/url.php
mod/url/lib.php
mod/wiki/lang/en/wiki.php
mod/wiki/lib.php
mod/wiki/locallib.php
mod/workshop/form/rubric/lib.php
mod/workshop/form/rubric/tests/lib_test.php
mod/workshop/lang/en/workshop.php
mod/workshop/lib.php
phpunit.xml.dist
pix/t/editstring.png [new file with mode: 0644]
question/editlib.php
question/engine/simpletest/helpers.php [new file with mode: 0644]
question/engine/tests/helpers.php
question/engine/upgrade/tests/helper.php
question/preview.php
question/type/calculated/questiontype.php
question/type/match/tests/walkthrough_test.php
question/type/multianswer/tests/upgradelibnewqe_test.php
report/completion/index.php
report/progress/index.php
repository/upload/lib.php
theme/base/style/core.css
theme/base/style/course.css
theme/base/style/question.css
theme/formal_white/style/core.css
theme/formal_white/style/formal_white.css
user/filesedit.php
version.php
webservice/rest/locallib.php
webservice/soap/locallib.php
webservice/xmlrpc/locallib.php

index 6ef4b20..5b01be2 100644 (file)
@@ -240,7 +240,9 @@ if ($version > $CFG->version) {  // upgrade
 
         if ($fetchupdates) {
             // no sesskey support guaranteed here
-            available_update_checker::instance()->fetch();
+            if (empty($CFG->disableupdatenotifications)) {
+                available_update_checker::instance()->fetch();
+            }
             redirect($reloadurl);
         }
 
index f6a2c11..b7a1bab 100644 (file)
@@ -174,6 +174,7 @@ class core_admin_renderer extends plugin_renderer_base {
      */
     public function upgrade_plugin_check_page(plugin_manager $pluginman, available_update_checker $checker,
             $version, $showallplugins, $reloadurl, $continueurl) {
+        global $CFG;
 
         $output = '';
 
@@ -181,14 +182,16 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->box_start('generalbox');
         $output .= $this->container_start('generalbox', 'notice');
         $output .= html_writer::tag('p', get_string('pluginchecknotice', 'core_plugin'));
-        $output .= $this->container_start('checkforupdates');
-        $output .= $this->single_button(new moodle_url($reloadurl, array('fetchupdates' => 1)), get_string('checkforupdates', 'core_plugin'));
-        if ($timefetched = $checker->get_last_timefetched()) {
-            $output .= $this->container(get_string('checkforupdateslast', 'core_plugin',
-                userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'))));
+        if (empty($CFG->disableupdatenotifications)) {
+            $output .= $this->container_start('checkforupdates');
+            $output .= $this->single_button(new moodle_url($reloadurl, array('fetchupdates' => 1)), get_string('checkforupdates', 'core_plugin'));
+            if ($timefetched = $checker->get_last_timefetched()) {
+                $output .= $this->container(get_string('checkforupdateslast', 'core_plugin',
+                    userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'))));
+            }
+            $output .= $this->container_end();
         }
         $output .= $this->container_end();
-        $output .= $this->container_end();
 
         $output .= $this->plugins_check_table($pluginman, $version, array('full' => $showallplugins));
         $output .= $this->box_end();
@@ -227,11 +230,12 @@ class core_admin_renderer extends plugin_renderer_base {
      */
     public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch) {
+        global $CFG;
         $output = '';
 
         $output .= $this->header();
         $output .= $this->maturity_info($maturity);
-        $output .= $this->available_updates($availableupdates, $availableupdatesfetch);
+        $output .= empty($CFG->disableupdatenotifications) ? $this->available_updates($availableupdates, $availableupdatesfetch) : '';
         $output .= $this->insecure_dataroot_warning($insecuredataroot);
         $output .= $this->display_errors_warning($errorsdisplayed);
         $output .= $this->cron_overdue_warning($cronoverdue);
@@ -256,19 +260,23 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML to output.
      */
     public function plugin_management_page(plugin_manager $pluginman, available_update_checker $checker) {
+        global $CFG;
+
         $output = '';
 
         $output .= $this->header();
         $output .= $this->heading(get_string('pluginsoverview', 'core_admin'));
         $output .= $this->plugins_overview_panel($pluginman);
 
-        $output .= $this->container_start('checkforupdates');
-        $output .= $this->single_button(new moodle_url($this->page->url, array('fetchremote' => 1)), get_string('checkforupdates', 'core_plugin'));
-        if ($timefetched = $checker->get_last_timefetched()) {
-            $output .= $this->container(get_string('checkforupdateslast', 'core_plugin',
-                userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'))));
+        if (empty($CFG->disableupdatenotifications)) {
+            $output .= $this->container_start('checkforupdates');
+            $output .= $this->single_button(new moodle_url($this->page->url, array('fetchremote' => 1)), get_string('checkforupdates', 'core_plugin'));
+            if ($timefetched = $checker->get_last_timefetched()) {
+                $output .= $this->container(get_string('checkforupdateslast', 'core_plugin',
+                    userdate($timefetched, get_string('strftimedatetime', 'core_langconfig'))));
+            }
+            $output .= $this->container_end();
         }
-        $output .= $this->container_end();
 
         $output .= $this->box($this->plugins_control_panel($pluginman), 'generalbox');
         $output .= $this->footer();
@@ -466,7 +474,10 @@ class core_admin_renderer extends plugin_renderer_base {
                 $updateinfo .= $this->moodle_available_update_info($update);
             }
         } else {
-            $updateinfo .= $this->heading(get_string('updateavailablenot', 'core_admin'), 3);
+            $now = time();
+            if ($fetch and ($fetch <= $now) and ($now - $fetch < HOURSECS)) {
+                $updateinfo .= $this->heading(get_string('updateavailablenot', 'core_admin'), 3);
+            }
         }
 
         $updateinfo .= $this->container_start('checkforupdates');
@@ -559,6 +570,8 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     public function plugins_check_table(plugin_manager $pluginman, $version, array $options = null) {
+        global $CFG;
+
         $plugininfo = $pluginman->get_plugins();
 
         if (empty($plugininfo)) {
@@ -641,7 +654,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 $status = get_string('status_' . $statuscode, 'core_plugin');
 
                 $availableupdates = $plugin->available_updates();
-                if (!empty($availableupdates)) {
+                if (!empty($availableupdates) and empty($CFG->disableupdatenotifications)) {
                     foreach ($availableupdates as $availableupdate) {
                         $status .= $this->plugin_available_update_info($availableupdate);
                     }
@@ -771,6 +784,8 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string as usually
      */
     public function plugins_overview_panel(plugin_manager $pluginman) {
+        global $CFG;
+
         $plugininfo = $pluginman->get_plugins();
 
         $numtotal = $numdisabled = $numextension = $numupdatable = 0;
@@ -787,7 +802,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 if (!$plugin->is_standard()) {
                     $numextension++;
                 }
-                if ($plugin->available_updates()) {
+                if (empty($CFG->disableupdatenotifications) and $plugin->available_updates()) {
                     $numupdatable++;
                 }
             }
@@ -813,6 +828,8 @@ class core_admin_renderer extends plugin_renderer_base {
      * @return string HTML code
      */
     public function plugins_control_panel(plugin_manager $pluginman) {
+        global $CFG;
+
         $plugininfo = $pluginman->get_plugins();
 
         if (empty($plugininfo)) {
@@ -916,7 +933,7 @@ class core_admin_renderer extends plugin_renderer_base {
                 }
 
                 $updateinfo = '';
-                if (is_array($plugin->available_updates())) {
+                if (empty($CFG->disableupdatenotifications) and is_array($plugin->available_updates())) {
                     foreach ($plugin->available_updates() as $availableupdate) {
                         $updateinfo .= $this->plugin_available_update_info($availableupdate);
                     }
index 0c4b157..968f025 100644 (file)
@@ -54,6 +54,17 @@ if (!$canview) {
     print_error('nopermissions', 'error', '', get_string('checkpermissions', 'role'));
 }
 
+if ($userid != $USER->id) {
+    // If its not the current user we need to extend the navigation for that user to ensure
+    // their navigation is loaded and this page found upon it.
+    $PAGE->navigation->extend_for_user($user);
+}
+if ($course->id != $SITE->id || $userid != $USER->id) {
+    // If we're within a course OR if we're viewing another user then we need to include the
+    // settings base on the navigation to ensure that the navbar will contain the users name.
+    $PAGE->navbar->includesettingsbase = true;
+}
+
 /// Now get the role assignments for this user.
 $sql = "SELECT
         ra.id, ra.userid, ra.contextid, ra.roleid, ra.component, ra.itemid,
@@ -120,14 +131,9 @@ $title = get_string('xroleassignments', 'role', $fullname);
 $PAGE->set_title($title);
 if ($courseid != SITEID) {
     $PAGE->set_heading($fullname);
-    if (has_capability('moodle/course:viewparticipants', $coursecontext)) {
-        $PAGE->navbar->add(get_string('participants'),new moodle_url('/user/index.php', array('id'=>$courseid)));
-    }
 } else {
     $PAGE->set_heading($course->fullname);
 }
-$PAGE->navbar->add($fullname, new moodle_url("$CFG->wwwroot/user/view.php", array('id'=>$userid,'course'=>$courseid)));
-$PAGE->navbar->add($straction);
 echo $OUTPUT->header();
 echo $OUTPUT->heading($title, 3);
 echo $OUTPUT->box_start('generalbox boxaligncenter boxwidthnormal');
index b8b674c..0188b6d 100644 (file)
@@ -222,19 +222,21 @@ $ADMIN->add('server', $temp);
 $ADMIN->add('server', new admin_externalpage('adminregistration', new lang_string('registration','admin'), "$CFG->wwwroot/$CFG->admin/registration/index.php"));
 
 // "update notifications" settingpage
-$temp = new admin_settingpage('updatenotifications', new lang_string('updatenotifications', 'core_admin'));
-$temp->add(new admin_setting_configcheckbox('updateautocheck', new lang_string('updateautocheck', 'core_admin'),
-                                            new lang_string('updateautocheck_desc', 'core_admin'), 1));
-$temp->add(new admin_setting_configselect('updateminmaturity', new lang_string('updateminmaturity', 'core_admin'),
-                                          new lang_string('updateminmaturity_desc', 'core_admin'), MATURITY_STABLE,
-                                          array(
-                                              MATURITY_ALPHA  => new lang_string('maturity'.MATURITY_ALPHA, 'core_admin'),
-                                              MATURITY_BETA   => new lang_string('maturity'.MATURITY_BETA, 'core_admin'),
-                                              MATURITY_RC     => new lang_string('maturity'.MATURITY_RC, 'core_admin'),
-                                              MATURITY_STABLE => new lang_string('maturity'.MATURITY_STABLE, 'core_admin'),
-                                          )));
-$temp->add(new admin_setting_configcheckbox('updatenotifybuilds', new lang_string('updatenotifybuilds', 'core_admin'),
-                                            new lang_string('updatenotifybuilds_desc', 'core_admin'), 0));
-$ADMIN->add('server', $temp);
+if (empty($CFG->disableupdatenotifications)) {
+    $temp = new admin_settingpage('updatenotifications', new lang_string('updatenotifications', 'core_admin'));
+    $temp->add(new admin_setting_configcheckbox('updateautocheck', new lang_string('updateautocheck', 'core_admin'),
+                                                new lang_string('updateautocheck_desc', 'core_admin'), 1));
+    $temp->add(new admin_setting_configselect('updateminmaturity', new lang_string('updateminmaturity', 'core_admin'),
+                                              new lang_string('updateminmaturity_desc', 'core_admin'), MATURITY_STABLE,
+                                              array(
+                                                  MATURITY_ALPHA  => new lang_string('maturity'.MATURITY_ALPHA, 'core_admin'),
+                                                  MATURITY_BETA   => new lang_string('maturity'.MATURITY_BETA, 'core_admin'),
+                                                  MATURITY_RC     => new lang_string('maturity'.MATURITY_RC, 'core_admin'),
+                                                  MATURITY_STABLE => new lang_string('maturity'.MATURITY_STABLE, 'core_admin'),
+                                              )));
+    $temp->add(new admin_setting_configcheckbox('updatenotifybuilds', new lang_string('updatenotifybuilds', 'core_admin'),
+                                                new lang_string('updatenotifybuilds_desc', 'core_admin'), 0));
+    $ADMIN->add('server', $temp);
+}
 
 } // end of speedup
index 5b32c0f..9b699e9 100644 (file)
@@ -39,7 +39,7 @@ class database_transfer_form extends moodleform {
             'pgsql/native',
             'mssql/native',
             'oci/native',
-            'sqlite3/pdo',
+            'sqlsrv/native',
         );
         $drivers = array();
         foreach($supported as $driver) {
index d629486..3e57e90 100644 (file)
@@ -42,29 +42,40 @@ if ($DB->get_dbfamily() != 'mysql') {
     notice('This function is for MySQL databases only!', new moodle_url('/admin/'));
 }
 
+$prefix = str_replace('_', '\\_', $DB->get_prefix()).'%';
+$sql = "SHOW TABLE STATUS WHERE Name LIKE ? AND Engine <> 'InnoDB'";
+$rs = $DB->get_recordset_sql($sql, array($prefix));
+if (!$rs->valid()) {
+    $rs->close();
+    echo $OUTPUT->box('<p>All tables are already using InnoDB database engine.</p>');
+    echo $OUTPUT->continue_button('/admin/');
+    echo $OUTPUT->footer();
+    die;
+}
+
 if (data_submitted() and $confirm and confirm_sesskey()) {
 
     echo $OUTPUT->notification('Please be patient and wait for this to complete...', 'notifysuccess');
 
     set_time_limit(0);
 
-    if ($tables = $DB->get_tables()) {
+    foreach ($rs as $table) {
         $DB->set_debug(true);
-        foreach ($tables as $table) {
-            $fulltable = $DB->get_prefix().$table;
-            try {
-                $DB->change_database_structure("ALTER TABLE $fulltable ENGINE=INNODB");
-            } catch (moodle_exception $e) {
-                echo $OUTPUT->notification(s($e->getMessage()).'<br />'.s($e->debuginfo));
-            }
+        $fulltable = $table->name;
+        try {
+            $DB->change_database_structure("ALTER TABLE $fulltable ENGINE=INNODB");
+        } catch (moodle_exception $e) {
+            echo $OUTPUT->notification(s($e->getMessage()).'<br />'.s($e->debuginfo));
         }
         $DB->set_debug(false);
     }
+    $rs->close();
     echo $OUTPUT->notification('... done.', 'notifysuccess');
     echo $OUTPUT->continue_button(new moodle_url('/admin/'));
     echo $OUTPUT->footer();
 
 } else {
+    $rs->close();
     $optionsyes = array('confirm'=>'1', 'sesskey'=>sesskey());
     $formcontinue = new single_button(new moodle_url('/admin/tool/innodb/index.php', $optionsyes), get_string('yes'));
     $formcancel = new single_button(new moodle_url('/admin/'), get_string('no'), 'get');
index 6b5881c..27fc9b9 100644 (file)
@@ -152,7 +152,7 @@ function user_download_csv($fields) {
     $filename = clean_filename(get_string('users').'.csv');
 
     header("Content-Type: application/download\n");
-    header("Content-Disposition: attachment; filename=$filename");
+    header("Content-Disposition: attachment; filename=\"$filename\"");
     header("Expires: 0");
     header("Cache-Control: must-revalidate,post-check=0,pre-check=0");
     header("Pragma: public");
index c51c74a..04cc360 100644 (file)
@@ -727,3 +727,255 @@ class moodle_group_delete_groupmembers_form extends moodleform {
         return $params;
     }
 }
+
+/**
+ * Form class for create_categories() web service function test.
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2012 Fabio Souto
+ */
+class core_course_create_categories_form extends moodleform {
+    /**
+     * The form definition.
+     */
+    public function definition() {
+        global $CFG;
+
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
+
+        // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
+        $data = $this->_customdata;
+        if ($data['authmethod'] == 'simple') {
+            $mform->addElement('text', 'wsusername', 'wsusername');
+            $mform->addElement('text', 'wspassword', 'wspassword');
+        } else if ($data['authmethod'] == 'token') {
+            $mform->addElement('text', 'token', 'token');
+        }
+
+        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
+        $mform->setType('authmethod', PARAM_SAFEDIR);
+        $mform->addElement('text', 'name[0]', 'name[0]');
+        $mform->addElement('text', 'parent[0]', 'parent[0]');
+        $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
+        $mform->addElement('text', 'description[0]', 'description[0]');
+        $mform->addElement('text', 'name[1]', 'name[1]');
+        $mform->addElement('text', 'parent[1]', 'parent[1]');
+        $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
+        $mform->addElement('text', 'description[1]', 'description[1]');
+
+        $mform->addElement('hidden', 'function');
+        $mform->setType('function', PARAM_SAFEDIR);
+
+        $mform->addElement('hidden', 'protocol');
+        $mform->setType('protocol', PARAM_SAFEDIR);
+
+        $this->add_action_buttons(true, get_string('execute', 'webservice'));
+    }
+
+    /**
+     * Get the parameters that the user submitted using the form.
+     * @return array|null
+     */
+    public function get_params() {
+        if (!$data = $this->get_data()) {
+            return null;
+        }
+        // Remove unused from form data.
+        unset($data->submitbutton);
+        unset($data->protocol);
+        unset($data->function);
+        unset($data->wsusername);
+        unset($data->wspassword);
+        unset($data->token);
+        unset($data->authmethod);
+
+        $params = array();
+        $params['categories'] = array();
+        for ($i=0; $i<10; $i++) {
+            if (empty($data->name[$i]) or empty($data->parent[$i])) {
+                continue;
+            }
+            $params['categories'][] = array('name'=>$data->name[$i], 'parent'=>$data->parent[$i],
+                                            'idnumber'=>$data->idnumber[$i], 'description'=>$data->description[$i]);
+        }
+        return $params;
+    }
+}
+
+/**
+ * Form class for delete_categories() web service function test.
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2012 Fabio Souto
+ */
+class core_course_delete_categories_form extends moodleform {
+    /**
+     * The form definition.
+     */
+    public function definition() {
+        global $CFG;
+
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
+
+        // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
+        $data = $this->_customdata;
+        if ($data['authmethod'] == 'simple') {
+            $mform->addElement('text', 'wsusername', 'wsusername');
+            $mform->addElement('text', 'wspassword', 'wspassword');
+        } else if ($data['authmethod'] == 'token') {
+            $mform->addElement('text', 'token', 'token');
+        }
+
+        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
+        $mform->setType('authmethod', PARAM_SAFEDIR);
+        $mform->addElement('text', 'id[0]', 'id[0]');
+        $mform->addElement('text', 'newparent[0]', 'newparent[0]');
+        $mform->addElement('text', 'recursive[0]', 'recursive[0]');
+        $mform->addElement('text', 'id[1]', 'id[1]');
+        $mform->addElement('text', 'newparent[1]', 'newparent[1]');
+        $mform->addElement('text', 'recursive[1]', 'recursive[1]');
+
+        $mform->addElement('hidden', 'function');
+        $mform->setType('function', PARAM_SAFEDIR);
+
+        $mform->addElement('hidden', 'protocol');
+        $mform->setType('protocol', PARAM_SAFEDIR);
+
+        $this->add_action_buttons(true, get_string('execute', 'webservice'));
+    }
+
+    /**
+     * Get the parameters that the user submitted using the form.
+     * @return array|null
+     */
+    public function get_params() {
+        if (!$data = $this->get_data()) {
+            return null;
+        }
+        // Remove unused from form data.
+        unset($data->submitbutton);
+        unset($data->protocol);
+        unset($data->function);
+        unset($data->wsusername);
+        unset($data->wspassword);
+        unset($data->token);
+        unset($data->authmethod);
+
+        $params = array();
+        $params['categories'] = array();
+        for ($i=0; $i<10; $i++) {
+            if (empty($data->id[$i])) {
+                continue;
+            }
+            $attrs = array();
+            $attrs['id'] = $data->id[$i];
+            if (!empty($data->newparent[$i])) {
+                $attrs['newparent'] = $data->newparent[$i];
+            }
+            if (!empty($data->recursive[$i])) {
+                $attrs['recursive'] = $data->recursive[$i];
+            }
+            $params['categories'][] = $attrs;
+        }
+        return $params;
+    }
+}
+
+/**
+ * Form class for create_categories() web service function test.
+ *
+ * @package   core_webservice
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @copyright 2012 Fabio Souto
+ */
+class core_course_update_categories_form extends moodleform {
+    /**
+     * The form definition.
+     */
+    public function definition() {
+        global $CFG;
+
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'wstestclienthdr', get_string('testclient', 'webservice'));
+
+        // Note: these values are intentionally PARAM_RAW - we want users to test any rubbish as parameters.
+        $data = $this->_customdata;
+        if ($data['authmethod'] == 'simple') {
+            $mform->addElement('text', 'wsusername', 'wsusername');
+            $mform->addElement('text', 'wspassword', 'wspassword');
+        } else if ($data['authmethod'] == 'token') {
+            $mform->addElement('text', 'token', 'token');
+        }
+
+        $mform->addElement('hidden', 'authmethod', $data['authmethod']);
+        $mform->setType('authmethod', PARAM_SAFEDIR);
+        $mform->addElement('text', 'id[0]', 'id[0]');
+        $mform->addElement('text', 'name[0]', 'name[0]');
+        $mform->addElement('text', 'parent[0]', 'parent[0]');
+        $mform->addElement('text', 'idnumber[0]', 'idnumber[0]');
+        $mform->addElement('text', 'description[0]', 'description[0]');
+        $mform->addElement('text', 'id[1]', 'id[1]');
+        $mform->addElement('text', 'name[1]', 'name[1]');
+        $mform->addElement('text', 'parent[1]', 'parent[1]');
+        $mform->addElement('text', 'idnumber[1]', 'idnumber[1]');
+        $mform->addElement('text', 'description[1]', 'description[1]');
+
+        $mform->addElement('hidden', 'function');
+        $mform->setType('function', PARAM_SAFEDIR);
+
+        $mform->addElement('hidden', 'protocol');
+        $mform->setType('protocol', PARAM_SAFEDIR);
+
+        $this->add_action_buttons(true, get_string('execute', 'webservice'));
+    }
+
+    /**
+     * Get the parameters that the user submitted using the form.
+     * @return array|null
+     */
+    public function get_params() {
+        if (!$data = $this->get_data()) {
+            return null;
+        }
+        // Remove unused from form data.
+        unset($data->submitbutton);
+        unset($data->protocol);
+        unset($data->function);
+        unset($data->wsusername);
+        unset($data->wspassword);
+        unset($data->token);
+        unset($data->authmethod);
+
+        $params = array();
+        $params['categories'] = array();
+        for ($i=0; $i<10; $i++) {
+
+            if (empty($data->id[$i])) {
+                continue;
+            }
+            $attrs = array();
+            $attrs['id'] = $data->id[$i];
+            if (!empty($data->name[$i])) {
+                $attrs['name'] = $data->name[$i];
+            }
+            if (!empty($data->parent[$i])) {
+                $attrs['parent'] = $data->parent[$i];
+            }
+            if (!empty($data->idnumber[$i])) {
+                $attrs['idnumber'] = $data->idnumber[$i];
+            }
+            if (!empty($data->description[$i])) {
+                $attrs['description'] = $data->description[$i];
+            }
+            $params['categories'][] = $attrs;
+        }
+        return $params;
+    }
+}
\ No newline at end of file
index 546aa38..a97720e 100644 (file)
@@ -35,6 +35,9 @@ class moodle1_converter_testcase extends advanced_testcase {
     /** @var string the name of the directory containing the unpacked Moodle 1.9 backup */
     protected $tempdir;
 
+    /** @var string saved hash of an icon file used during testing */
+    protected $iconhash;
+
     protected function setUp() {
         global $CFG;
 
@@ -61,6 +64,7 @@ class moodle1_converter_testcase extends advanced_testcase {
             "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/icon.gif",
             "$CFG->tempdir/backup/$this->tempdir/moddata/unittest/4/icon.gif"
         );
+        $this->iconhash = sha1_file($CFG->tempdir.'/backup/'.$this->tempdir.'/moddata/unittest/4/icon.gif');
         copy(
             "$CFG->dirroot/backup/converter/moodle1/tests/fixtures/icon.gif",
             "$CFG->tempdir/backup/$this->tempdir/moddata/unittest/4/7/icon.gif"
@@ -272,7 +276,8 @@ class moodle1_converter_testcase extends advanced_testcase {
         // migrate a single file
         $fileman->itemid = 4;
         $fileman->migrate_file('moddata/unittest/4/icon.gif');
-        $this->assertTrue(is_file($converter->get_workdir_path().'/files/4e/4ea114b0558f53e3af8dd9afc0e0810a95c2a724'));
+        $subdir = substr($this->iconhash, 0, 2);
+        $this->assertTrue(is_file($converter->get_workdir_path().'/files/'.$subdir.'/'.$this->iconhash));
         // get the file id
         $fileids = $fileman->get_fileids();
         $this->assertEquals(gettype($fileids), 'array');
@@ -285,7 +290,7 @@ class moodle1_converter_testcase extends advanced_testcase {
         $filerecordids = $converter->get_stash_itemids('files');
         foreach ($filerecordids as $filerecordid) {
             $filerecord = $converter->get_stash('files', $filerecordid);
-            $this->assertEquals('4ea114b0558f53e3af8dd9afc0e0810a95c2a724', $filerecord['contenthash']);
+            $this->assertEquals($this->iconhash, $filerecord['contenthash']);
             $this->assertEquals($contextid, $filerecord['contextid']);
             $this->assertEquals('mod_unittest', $filerecord['component']);
             if ($filerecord['filearea'] === 'testarea') {
index fa12036..e7b6018 100644 (file)
@@ -364,19 +364,26 @@ class backup_section_structure_step extends backup_structure_step {
         // Define each element separated
 
         $section = new backup_nested_element('section', array('id'), array(
-            'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible'));
+                'number', 'name', 'summary', 'summaryformat', 'sequence', 'visible',
+                'availablefrom', 'availableuntil', 'showavailability', 'groupingid'));
 
         // attach format plugin structure to $section element, only one allowed
         $this->add_plugin_structure('format', $section, false);
 
-        // Define sources
+        // Add nested elements for _availability table
+        $avail = new backup_nested_element('availability', array('id'), array(
+                'sourcecmid', 'requiredcompletion', 'gradeitemid', 'grademin', 'grademax'));
+        $section->add_child($avail);
 
+        // Define sources
         $section->set_source_table('course_sections', array('id' => backup::VAR_SECTIONID));
+        $avail->set_source_table('course_sections_availability', array('coursesectionid' => backup::VAR_SECTIONID));
 
         // Aliases
         $section->set_source_alias('section', 'number');
 
         // Set annotations
+        $section->annotate_ids('grouping', 'groupingid');
         $section->annotate_files('course', 'section', 'id');
 
         return $section;
@@ -965,7 +972,7 @@ class backup_groups_structure_step extends backup_structure_step {
         $groups = new backup_nested_element('groups');
 
         $group = new backup_nested_element('group', array('id'), array(
-            'name', 'description', 'descriptionformat', 'enrolmentkey',
+            'name', 'idnumber', 'description', 'descriptionformat', 'enrolmentkey',
             'picture', 'hidepicture', 'timecreated', 'timemodified'));
 
         $members = new backup_nested_element('group_members');
@@ -976,7 +983,7 @@ class backup_groups_structure_step extends backup_structure_step {
         $groupings = new backup_nested_element('groupings');
 
         $grouping = new backup_nested_element('grouping', 'id', array(
-            'name', 'description', 'descriptionformat', 'configdata',
+            'name', 'idnumber', 'description', 'descriptionformat', 'configdata',
             'timecreated', 'timemodified'));
 
         $groupinggroups = new backup_nested_element('grouping_groups');
index 3f4a446..e8831d7 100644 (file)
@@ -79,15 +79,16 @@ class restore_final_task extends restore_task {
             $this->add_step(new restore_course_logs_structure_step('course_logs', 'course/logs.xml'));
         }
 
-        // Rebuild course cache to see results, whoah!
-        $this->add_step(new restore_rebuild_course_cache('rebuild_course_cache'));
-
         // Review all the executed tasks having one after_restore method
         // executing it to perform some final adjustments of information
         // not available when the task was executed.
-        // This step is always the last one before the one cleaning the temp stuff!
+        // This step is always the last one performing modifications on restored information
+        // Don't add any new step after it. Only cache rebuild and clean are allowed.
         $this->add_step(new restore_execute_after_restore('executing_after_restore'));
 
+        // Rebuild course cache to see results, whoah!
+        $this->add_step(new restore_rebuild_course_cache('rebuild_course_cache'));
+
         // Clean the temp dir (conditionally) and drop temp table
         $this->add_step(new restore_drop_and_clean_temp_stuff('drop_and_clean_temp_stuff'));
 
index 1472012..fda860c 100644 (file)
@@ -716,6 +716,17 @@ class restore_groups_structure_step extends restore_structure_step {
         $data = (object)$data; // handy
         $data->courseid = $this->get_courseid();
 
+        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
+        // another a group in the same course
+        $context = context_course::instance($data->courseid);
+        if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
+            if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
+                unset($data->idnumber);
+            }
+        } else {
+            unset($data->idnumber);
+        }
+
         $oldid = $data->id;    // need this saved for later
 
         $restorefiles = false; // Only if we end creating the group
@@ -765,6 +776,17 @@ class restore_groups_structure_step extends restore_structure_step {
         $data = (object)$data; // handy
         $data->courseid = $this->get_courseid();
 
+        // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
+        // another a grouping in the same course
+        $context = context_course::instance($data->courseid);
+        if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
+            if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
+                unset($data->idnumber);
+            }
+        } else {
+            unset($data->idnumber);
+        }
+
         $oldid = $data->id;    // need this saved for later
         $restorefiles = false; // Only if we end creating the grouping
 
@@ -992,16 +1014,24 @@ class restore_process_categories_and_questions extends restore_execution_step {
 class restore_section_structure_step extends restore_structure_step {
 
     protected function define_structure() {
+        global $CFG;
+
+        $paths = array();
+
         $section = new restore_path_element('section', '/section');
+        $paths[] = $section;
+        if ($CFG->enableavailability) {
+            $paths[] = new restore_path_element('availability', '/section/availability');
+        }
 
         // Apply for 'format' plugins optional paths at section level
         $this->add_plugin_structure('format', $section);
 
-        return array($section);
+        return $paths;
     }
 
     public function process_section($data) {
-        global $DB;
+        global $CFG, $DB;
         $data = (object)$data;
         $oldid = $data->id; // We'll need this later
 
@@ -1018,6 +1048,18 @@ class restore_section_structure_step extends restore_structure_step {
             $section->summaryformat = $data->summaryformat;
             $section->sequence = '';
             $section->visible = $data->visible;
+            if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
+                $section->availablefrom = 0;
+                $section->availableuntil = 0;
+                $section->showavailability = 0;
+            } else {
+                $section->availablefrom = isset($data->availablefrom) ? $this->apply_date_offset($data->availablefrom) : 0;
+                $section->availableuntil = isset($data->availableuntil) ? $this->apply_date_offset($data->availableuntil) : 0;
+                $section->showavailability = isset($data->showavailability) ? $data->showavailability : 0;
+            }
+            if (!empty($CFG->enablegroupmembersonly)) { // Only if enablegroupmembersonly is enabled
+                $section->groupingid = isset($data->groupingid) ? $this->get_mappingid('grouping', $data->groupingid) : 0;
+            }
             $newitemid = $DB->insert_record('course_sections', $section);
             $restorefiles = true;
 
@@ -1032,6 +1074,16 @@ class restore_section_structure_step extends restore_structure_step {
                 $section->summaryformat = $data->summaryformat;
                 $restorefiles = true;
             }
+            if (empty($secrec->groupingid)) {
+                if (!empty($CFG->enablegroupmembersonly)) { // Only if enablegroupmembersonly is enabled
+                    $section->groupingid = isset($data->groupingid) ? $this->get_mappingid('grouping', $data->groupingid) : 0;
+                }
+            }
+
+            // Don't update available from, available until, or show availability
+            // (I didn't see a useful way to define whether existing or new one should
+            // take precedence).
+
             $DB->update_record('course_sections', $section);
             $newitemid = $secrec->id;
         }
@@ -1055,10 +1107,47 @@ class restore_section_structure_step extends restore_structure_step {
         //}
     }
 
+    public function process_availability($data) {
+        global $DB;
+        $data = (object)$data;
+        $data->coursesectionid = $this->task->get_sectionid();
+        // NOTE: Other values in $data need updating, but these (cm,
+        // grade items) have not yet been restored.
+        $DB->insert_record('course_sections_availability', $data);
+    }
+
     protected function after_execute() {
         // Add section related files, with 'course_section' itemid to match
         $this->add_related_files('course', 'section', 'course_section');
     }
+
+    public function after_restore() {
+        global $DB;
+
+        $sectionid = $this->get_task()->get_sectionid();
+
+        // Get data object for current section availability (if any)
+        // TODO: This can be processing already existing records, we need to be able to know which ones
+        //       are the just restored ones, perhaps creating 'course_sections_availability' mappings for them.
+        // TODO: Also, this must avoid duplicates, so if one course module or one grade item already is being
+        //       used for some availability rule... we need to handle that carefully.
+        $data = $DB->get_record('course_sections_availability',
+                array('coursesectionid' => $sectionid), 'id, sourcecmid, gradeitemid', IGNORE_MISSING);
+
+        // Update mappings
+        if ($data) {
+            $data->sourcecmid = $this->get_mappingid('course_module', $data->sourcecmid);
+            if (!$data->sourcecmid) {
+                $data->sourcecmid = null;
+            }
+            $data->gradeitemid = $this->get_mappingid('grade_item', $data->gradeitemid);
+            if (!$data->gradeitemid) {
+                $data->gradeitemid = null;
+            }
+
+            $DB->update_record('course_sections_availability', $data);
+        }
+    }
 }
 
 
index ddb2fe5..4b0ff58 100644 (file)
@@ -395,6 +395,8 @@ abstract class restore_structure_step extends restore_step {
                 $pobject->launch_after_restore_methods();
             }
         }
+        // Finally execute own (restore_structure_step) after_restore method
+        $this->after_restore();
     }
 
     /**
@@ -408,6 +410,16 @@ abstract class restore_structure_step extends restore_step {
         // do nothing by default
     }
 
+    /**
+     * This method will be executed after the rest of the restore has been processed.
+     *
+     * Use if you need to update IDs based on things which are restored after this
+     * step has completed.
+     */
+    protected function after_restore() {
+        // do nothing by default
+    }
+
     /**
      * Prepare the pathelements for processing, looking for duplicates, applying
      * processing objects and other adjustments
index 7d55605..16c5f28 100644 (file)
@@ -1049,28 +1049,6 @@ function calendar_get_link_href($linkbase, $d, $m, $y) {
     return $linkbase;
 }
 
-/**
- * This function has been deprecated as of Moodle 2.0... DO NOT USE!!!!!
- *
- * @deprecated Moodle 2.0 - MDL-24284 please do not use this function any more.
- * @todo MDL-31134 - will be removed in Moodle 2.3
- * @see calendar_get_link_href()
- *
- * @param string $text
- * @param string|moodle_url $linkbase
- * @param int|null $d The number of the day.
- * @param int|null $m The number of the month.
- * @param int|null $y The number of the year.
- * @return string HTML link
- */
-function calendar_get_link_tag($text, $linkbase, $d, $m, $y) {
-    $url = calendar_get_link_href(new moodle_url($linkbase), $d, $m, $y);
-    if (empty($url)) {
-        return $text;
-    }
-    return html_writer::link($url, $text);
-}
-
 /**
  * Build and return a previous month HTML link, with an arrow.
  *
@@ -1491,15 +1469,16 @@ function calendar_get_default_courses() {
     }
 
     $courses = array();
-    if (!empty($CFG->calendar_adminseesall) && has_capability('moodle/calendar:manageentries', get_context_instance(CONTEXT_SYSTEM))) {
+    if (!empty($CFG->calendar_adminseesall) && has_capability('moodle/calendar:manageentries', context_system::instance())) {
         list ($select, $join) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
-        $sql = "SELECT DISTINCT c.* $select
+        $sql = "SELECT c.* $select
                   FROM {course} c
-                  JOIN {event} e ON e.courseid = c.id
-                  $join";
+                  $join
+                  WHERE EXISTS (SELECT 1 FROM {event} e WHERE e.courseid = c.id)
+                  ";
         $courses = $DB->get_records_sql($sql, null, 0, 20);
         foreach ($courses as $course) {
-            context_instance_preload($course);
+            context_helper::preload_from_record($course);
         }
         return $courses;
     }
index 4b3534f..e82d7bd 100644 (file)
@@ -38,7 +38,7 @@ $CFG = new stdClass();
 // will be stored.  This database must already have been created         //
 // and a username/password created to access it.                         //
 
-$CFG->dbtype    = 'pgsql';      // 'pgsql', 'mysqli', 'mssql' or 'oci'
+$CFG->dbtype    = 'pgsql';      // 'pgsql', 'mysqli', 'mssql', 'sqlsrv' or 'oci'
 $CFG->dblibrary = 'native';     // 'native' only at the moment
 $CFG->dbhost    = 'localhost';  // eg 'localhost' or 'db.isp.com' or IP
 $CFG->dbname    = 'moodle';     // database name, eg moodle
@@ -431,6 +431,11 @@ $CFG->admin = 'admin';
 //
 //      $CFG->cssoptimiserpretty = true;
 //
+// Use the following flag to completely disable the Available update notifications
+// feature and hide it from the server administration UI.
+//
+//      $CFG->disableupdatenotifications = true;
+//
 //=========================================================================
 // 8. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
diff --git a/course/dndupload.js b/course/dndupload.js
new file mode 100644 (file)
index 0000000..b0d35a3
--- /dev/null
@@ -0,0 +1,888 @@
+// 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/>.
+
+/**
+ * Javascript library for enableing a drag and drop upload to courses
+ *
+ * @package    core
+ * @subpackage course
+ * @copyright  2012 Davo Smith
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+M.course_dndupload = {
+    // YUI object.
+    Y: null,
+    // URL for upload requests
+    url: M.cfg.wwwroot + '/course/dndupload.php',
+    // maximum size of files allowed in this form
+    maxbytes: 0,
+    // ID of the course we are on
+    courseid: null,
+    // Data about the different file/data handlers that are available
+    handlers: null,
+    // Nasty hack to distinguish between dragenter(first entry),
+    // dragenter+dragleave(moving between child elements) and dragleave (leaving element)
+    entercount: 0,
+    // Used to keep track of the section we are dragging across - to make
+    // spotting movement between sections more reliable
+    currentsection: null,
+    // Used to store the pending uploads whilst the user is being asked for further input
+    uploadqueue: null,
+    // True if the there is currently a dialog being shown (asking for a name, or giving a
+    // choice of file handlers)
+    uploaddialog: false,
+    // An array containing the last selected file handler for each file type
+    lastselected: null,
+
+    // The following are used to identify specific parts of the course page
+
+    // The type of HTML element that is a course section
+    sectiontypename: 'li',
+    // The classes that an element must have to be identified as a course section
+    sectionclasses: ['section', 'main'],
+    // The ID of the main content area of the page (for adding the 'status' div)
+    pagecontentid: 'page-content',
+    // The selector identifying the list of modules within a section (note changing this may require
+    // changes to the get_mods_element function)
+    modslistselector: 'ul.section',
+
+    /**
+     * Initalise the drag and drop upload interface
+     * Note: one and only one of options.filemanager and options.formcallback must be defined
+     *
+     * @param Y the YUI object
+     * @param object options {
+     *            courseid: ID of the course we are on
+     *            maxbytes: maximum size of files allowed in this form
+     *            handlers: Data about the different file/data handlers that are available
+     *          }
+     */
+    init: function(Y, options) {
+        this.Y = Y;
+
+        if (!this.browser_supported()) {
+            return; // Browser does not support the required functionality
+        }
+
+        this.maxbytes = options.maxbytes;
+        this.courseid = options.courseid;
+        this.handlers = options.handlers;
+        this.uploadqueue = new Array();
+        this.lastselected = new Array();
+
+        var sectionselector = this.sectiontypename + '.' + this.sectionclasses.join('.');
+        var sections = this.Y.all(sectionselector);
+        if (sections.isEmpty()) {
+            return; // No sections - incompatible course format or front page.
+        }
+        sections.each( function(el) {
+            this.add_preview_element(el);
+            this.init_events(el);
+        }, this);
+
+        var div = this.add_status_div();
+        div.setContent(M.util.get_string('dndworking', 'moodle'));
+    },
+
+    /**
+     * Add a div element to tell the user that drag and drop upload
+     * is available (or to explain why it is not available)
+     * @return the DOM element to add messages to
+     */
+    add_status_div: function() {
+        var div = document.createElement('div');
+        div.id = 'dndupload-status';
+        var coursecontents = document.getElementById(this.pagecontentid);
+        if (coursecontents) {
+            coursecontents.insertBefore(div, coursecontents.firstChild);
+        }
+        return this.Y.one(div);
+    },
+
+    /**
+     * Check the browser has the required functionality
+     * @return true if browser supports drag/drop upload
+     */
+    browser_supported: function() {
+        if (typeof FileReader == 'undefined') {
+            return false;
+        }
+        if (typeof FormData == 'undefined') {
+            return false;
+        }
+        return true;
+    },
+
+    /**
+     * Initialise drag events on node container, all events need
+     * to be processed for drag and drop to work
+     * @param el the element to add events to
+     */
+    init_events: function(el) {
+        this.Y.on('dragenter', this.drag_enter, el, this);
+        this.Y.on('dragleave', this.drag_leave, el, this);
+        this.Y.on('dragover',  this.drag_over,  el, this);
+        this.Y.on('drop',      this.drop,       el, this);
+    },
+
+    /**
+     * Work out which course section a given element is in
+     * @param el the child DOM element within the section
+     * @return the DOM element representing the section
+     */
+    get_section: function(el) {
+        var sectionclasses = this.sectionclasses;
+        return el.ancestor( function(test) {
+            var i;
+            for (i=0; i<sectionclasses.length; i++) {
+                if (!test.hasClass(sectionclasses[i])) {
+                    return false;
+                }
+                return true;
+            }
+        }, true);
+    },
+
+    /**
+     * Work out the number of the section we have been dropped on to, from the section element
+     * @param DOMElement section the selected section
+     * @return int the section number
+     */
+    get_section_number: function(section) {
+        var sectionid = section.get('id').split('-');
+        if (sectionid.length < 2 || sectionid[0] != 'section') {
+            return false;
+        }
+        return parseInt(sectionid[1]);
+    },
+
+    /**
+     * Check if the event includes data of the given type
+     * @param e the event details
+     * @param type the data type to check for
+     * @return true if the data type is found in the event data
+     */
+    types_includes: function(e, type) {
+        var i;
+        if (e._event.dataTransfer === null) {
+            // TODO MDL-33054: If we get here then something has gone wrong.
+            return false;
+        }
+        var types = e._event.dataTransfer.types;
+        if (types == null) {
+            return false;
+        }
+        for (i=0; i<types.length; i++) {
+            if (types[i] == type) {
+                return true;
+            }
+        }
+        return false;
+    },
+
+    /**
+     * Look through the event data, checking it against the registered data types
+     * (in order of priority) and return details of the first matching data type
+     * @param e the event details
+     * @return mixed false if not found or an object {
+     *           realtype: the type as given by the browser
+     *           addmessage: the message to show to the user during dragging
+     *           namemessage: the message for requesting a name for the resource from the user
+     *           type: the identifier of the type (may match several 'realtype's)
+     *           }
+     */
+    drag_type: function(e) {
+        if (this.types_includes(e, 'Files')) {
+            if (this.handlers.filehandlers.length == 0) {
+                return false; // No available file handlers - ignore this drag.
+            }
+            return {
+                realtype: 'Files',
+                addmessage: M.util.get_string('addfilehere', 'moodle'),
+                namemessage: null, // Should not be asked for anyway
+                type: 'Files'
+            };
+        }
+
+        // Check each of the registered types
+        var types = this.handlers.types;
+        for (var i=0; i<types.length; i++) {
+            // Check each of the different identifiers for this type
+            var dttypes = types[i].datatransfertypes;
+            for (var j=0; j<dttypes.length; j++) {
+                if (this.types_includes(e, dttypes[j])) {
+                    return {
+                        realtype: dttypes[j],
+                        addmessage: types[i].addmessage,
+                        namemessage: types[i].namemessage,
+                        type: types[i].identifier,
+                        handlers: types[i].handlers
+                    };
+                }
+            }
+        }
+        return false; // No types we can handle
+    },
+
+    /**
+     * Check the content of the drag/drop includes a type we can handle, then, if
+     * it is, notify the browser that we want to handle it
+     * @param event e
+     * @return string type of the event or false
+     */
+    check_drag: function(e) {
+        var type = this.drag_type(e);
+        if (type) {
+            // Notify browser that we will handle this drag/drop
+            e.stopPropagation();
+            e.preventDefault();
+        }
+        return type;
+    },
+
+    /**
+     * Handle a dragenter event: add a suitable 'add here' message
+     * when a drag event occurs, containing a registered data type
+     * @param e event data
+     * @return false to prevent the event from continuing to be processed
+     */
+    drag_enter: function(e) {
+        if (!(type = this.check_drag(e))) {
+            return false;
+        }
+
+        var section = this.get_section(e.currentTarget);
+        if (!section) {
+            return false;
+        }
+
+        if (this.currentsection && this.currentsection != section) {
+            this.currentsection = section;
+            this.entercount = 1;
+        } else {
+            this.entercount++;
+            if (this.entercount > 2) {
+                this.entercount = 2;
+                return false;
+            }
+        }
+
+        this.show_preview_element(section, type);
+
+        return false;
+    },
+
+    /**
+     * Handle a dragleave event: remove the 'add here' message (if present)
+     * @param e event data
+     * @return false to prevent the event from continuing to be processed
+     */
+    drag_leave: function(e) {
+        if (!this.check_drag(e)) {
+            return false;
+        }
+
+        this.entercount--;
+        if (this.entercount == 1) {
+            return false;
+        }
+        this.entercount = 0;
+        this.currentsection = null;
+
+        this.hide_preview_element();
+        return false;
+    },
+
+    /**
+     * Handle a dragover event: just prevent the browser default (necessary
+     * to allow drag and drop handling to work)
+     * @param e event data
+     * @return false to prevent the event from continuing to be processed
+     */
+    drag_over: function(e) {
+        this.check_drag(e);
+        return false;
+    },
+
+    /**
+     * Handle a drop event: hide the 'add here' message, check the attached
+     * data type and start the upload process
+     * @param e event data
+     * @return false to prevent the event from continuing to be processed
+     */
+    drop: function(e) {
+        if (!(type = this.check_drag(e))) {
+            return false;
+        }
+
+        this.hide_preview_element();
+
+        // Work out the number of the section we are on (from its id)
+        var section = this.get_section(e.currentTarget);
+        var sectionnumber = this.get_section_number(section);
+
+        // Process the file or the included data
+        if (type.type == 'Files') {
+            var files = e._event.dataTransfer.files;
+            for (var i=0, f; f=files[i]; i++) {
+                this.handle_file(f, section, sectionnumber);
+            }
+        } else {
+            var contents = e._event.dataTransfer.getData(type.realtype);
+            if (contents) {
+                this.handle_item(type, contents, section, sectionnumber);
+            }
+        }
+
+        return false;
+    },
+
+    /**
+     * Find or create the 'ul' element that contains all of the module
+     * instances in this section
+     * @param section the DOM element representing the section
+     * @return false to prevent the event from continuing to be processed
+     */
+    get_mods_element: function(section) {
+        // Find the 'ul' containing the list of mods
+        var modsel = section.one(this.modslistselector);
+        if (!modsel) {
+            // Create the above 'ul' if it doesn't exist
+            var modsel = document.createElement('ul');
+            modsel.className = 'section img-text';
+            var contentel = section.get('children').pop();
+            var brel = contentel.get('children').pop();
+            contentel.insertBefore(modsel, brel);
+            modsel = this.Y.one(modsel);
+        }
+
+        return modsel;
+    },
+
+    /**
+     * Add a new dummy item to the list of mods, to be replaced by a real
+     * item & link once the AJAX upload call has completed
+     * @param name the label to show in the element
+     * @param section the DOM element reperesenting the course section
+     * @return DOM element containing the new item
+     */
+    add_resource_element: function(name, section) {
+        var modsel = this.get_mods_element(section);
+
+        var resel = {
+            parent: modsel,
+            li: document.createElement('li'),
+            div: document.createElement('div'),
+            a: document.createElement('a'),
+            icon: document.createElement('img'),
+            namespan: document.createElement('span'),
+            progressouter: document.createElement('span'),
+            progress: document.createElement('span')
+        };
+
+        resel.li.className = 'activity resource modtype_resource';
+
+        resel.div.className = 'mod-indent';
+        resel.li.appendChild(resel.div);
+
+        resel.a.href = '#';
+        resel.div.appendChild(resel.a);
+
+        resel.icon.src = M.util.image_url('i/ajaxloader');
+        resel.a.appendChild(resel.icon);
+
+        resel.a.appendChild(document.createTextNode(' '));
+
+        resel.namespan.className = 'instancename';
+        resel.namespan.innerHTML = name;
+        resel.a.appendChild(resel.namespan);
+
+        resel.div.appendChild(document.createTextNode(' '));
+
+        resel.progressouter.className = 'dndupload-progress-outer';
+        resel.progress.className = 'dndupload-progress-inner';
+        resel.progress.innerHTML = '&nbsp;';
+        resel.progressouter.appendChild(resel.progress);
+        resel.div.appendChild(resel.progressouter);
+
+        modsel.insertBefore(resel.li, modsel.get('children').pop()); // Leave the 'preview element' at the bottom
+
+        return resel;
+    },
+
+    /**
+     * Hide any visible dndupload-preview elements on the page
+     */
+    hide_preview_element: function() {
+        this.Y.all('li.dndupload-preview').addClass('dndupload-hidden');
+    },
+
+    /**
+     * Unhide the preview element for the given section and set it to display
+     * the correct message
+     * @param section the YUI node representing the selected course section
+     * @param type the details of the data type detected in the drag (including the message to display)
+     */
+    show_preview_element: function(section, type) {
+        this.hide_preview_element();
+        var preview = section.one('li.dndupload-preview').removeClass('dndupload-hidden');
+        preview.one('span').setContent(type.addmessage);
+    },
+
+    /**
+     * Add the preview element to a course section. Note: this needs to be done before 'addEventListener'
+     * is called, otherwise Firefox will ignore events generated when the mouse is over the preview
+     * element (instead of passing them up to the parent element)
+     * @param section the YUI node representing the selected course section
+     */
+    add_preview_element: function(section) {
+        var modsel = this.get_mods_element(section);
+        var preview = {
+            li: document.createElement('li'),
+            div: document.createElement('div'),
+            icon: document.createElement('img'),
+            namespan: document.createElement('span')
+        };
+
+        preview.li.className = 'dndupload-preview activity resource modtype_resource dndupload-hidden';
+
+        preview.div.className = 'mod-indent';
+        preview.li.appendChild(preview.div);
+
+        preview.icon.src = M.util.image_url('t/addfile');
+        preview.div.appendChild(preview.icon);
+
+        preview.div.appendChild(document.createTextNode(' '));
+
+        preview.namespan.className = 'instancename';
+        preview.namespan.innerHTML = M.util.get_string('addfilehere', 'moodle');
+        preview.div.appendChild(preview.namespan);
+
+        modsel.appendChild(preview.li);
+    },
+
+    /**
+     * Find the registered handler for the given file type. If there is more than one, ask the
+     * user which one to use. Then upload the file to the server
+     * @param file the details of the file, taken from the FileList in the drop event
+     * @param section the DOM element representing the selected course section
+     * @param sectionnumber the number of the selected course section
+     */
+    handle_file: function(file, section, sectionnumber) {
+        var handlers = new Array();
+        var filehandlers = this.handlers.filehandlers;
+        var extension = '';
+        var dotpos = file.name.lastIndexOf('.');
+        if (dotpos != -1) {
+            extension = file.name.substr(dotpos+1, file.name.length);
+        }
+
+        for (var i=0; i<filehandlers.length; i++) {
+            if (filehandlers[i].extension == '*' || filehandlers[i].extension == extension) {
+                handlers.push(filehandlers[i]);
+            }
+        }
+
+        if (handlers.length == 0) {
+            // No handlers at all (not even 'resource'?)
+            return;
+        }
+
+        if (handlers.length == 1) {
+            this.upload_file(file, section, sectionnumber, handlers[0].module);
+            return;
+        }
+
+        this.file_handler_dialog(handlers, extension, file, section, sectionnumber);
+    },
+
+    /**
+     * Show a dialog box, allowing the user to choose what to do with the file they are uploading
+     * @param handlers the available handlers to choose between
+     * @param extension the extension of the file being uploaded
+     * @param file the File object being uploaded
+     * @param section the DOM element of the section being uploaded to
+     * @param sectionnumber the number of the selected course section
+     */
+    file_handler_dialog: function(handlers, extension, file, section, sectionnumber) {
+        if (this.uploaddialog) {
+            var details = new Object();
+            details.isfile = true;
+            details.handlers = handlers;
+            details.extension = extension;
+            details.file = file;
+            details.section = section;
+            details.sectionnumber = sectionnumber;
+            this.uploadqueue.push(details);
+            return;
+        }
+        this.uploaddialog = true;
+
+        var timestamp = new Date().getTime();
+        var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
+        var content = '';
+        var sel;
+        if (extension in this.lastselected) {
+            sel = this.lastselected[extension];
+        } else {
+            sel = handlers[0].module;
+        }
+        content += '<p>'+M.util.get_string('actionchoice', 'moodle', file.name)+'</p>';
+        content += '<div id="dndupload_handlers'+uploadid+'">';
+        for (var i=0; i<handlers.length; i++) {
+            var id = 'dndupload_handler'+uploadid+handlers[i].module;
+            var checked = (handlers[i].module == sel) ? 'checked="checked" ' : '';
+            content += '<input type="radio" name="handler" value="'+handlers[i].module+'" id="'+id+'" '+checked+'/>';
+            content += ' <label for="'+id+'">';
+            content += handlers[i].message;
+            content += '</label><br/>';
+        }
+        content += '</div>';
+
+        var Y = this.Y;
+        var self = this;
+        var panel = new Y.Panel({
+            bodyContent: content,
+            width: 350,
+            zIndex: 5,
+            centered: true,
+            modal: true,
+            visible: true,
+            render: true,
+            buttons: [{
+                value: M.util.get_string('upload', 'moodle'),
+                action: function(e) {
+                    e.preventDefault();
+                    // Find out which module was selected
+                    var module = false;
+                    var div = Y.one('#dndupload_handlers'+uploadid);
+                    div.all('input').each(function(input) {
+                        if (input.get('checked')) {
+                            module = input.get('value');
+                        }
+                    });
+                    if (!module) {
+                        return;
+                    }
+                    panel.hide();
+                    // Remember this selection for next time
+                    self.lastselected[extension] = module;
+                    // Do the upload
+                    self.upload_file(file, section, sectionnumber, module);
+                },
+                section: Y.WidgetStdMod.FOOTER
+            },{
+                value: M.util.get_string('cancel', 'moodle'),
+                action: function(e) {
+                    e.preventDefault();
+                    panel.hide();
+                },
+                section: Y.WidgetStdMod.FOOTER
+            }]
+        });
+        // When the panel is hidden - destroy it and then check for other pending uploads
+        panel.after("visibleChange", function(e) {
+            if (!panel.get('visible')) {
+                panel.destroy(true);
+                self.check_upload_queue();
+            }
+        });
+    },
+
+    /**
+     * Check to see if there are any other dialog boxes to show, now that the current one has
+     * been dealt with
+     */
+    check_upload_queue: function() {
+        this.uploaddialog = false;
+        if (this.uploadqueue.length == 0) {
+            return;
+        }
+
+        var details = this.uploadqueue.shift();
+        if (details.isfile) {
+            this.file_handler_dialog(details.handlers, details.extension, details.file, details.section, details.sectionnumber);
+        } else {
+            this.handle_item(details.type, details.contents, details.section, details.sectionnumber);
+        }
+    },
+
+    /**
+     * Do the file upload: show the dummy element, use an AJAX call to send the data
+     * to the server, update the progress bar for the file, then replace the dummy
+     * element with the real information once the AJAX call completes
+     * @param file the details of the file, taken from the FileList in the drop event
+     * @param section the DOM element representing the selected course section
+     * @param sectionnumber the number of the selected course section
+     */
+    upload_file: function(file, section, sectionnumber, module) {
+
+        // This would be an ideal place to use the Y.io function
+        // however, this does not support data encoded using the
+        // FormData object, which is needed to transfer data from
+        // the DataTransfer object into an XMLHTTPRequest
+        // This can be converted when the YUI issue has been integrated:
+        // http://yuilibrary.com/projects/yui3/ticket/2531274
+        var xhr = new XMLHttpRequest();
+        var self = this;
+
+        if (file.size > this.maxbytes) {
+            alert("'"+file.name+"' "+M.util.get_string('filetoolarge', 'moodle'));
+            return;
+        }
+
+        // Add the file to the display
+        var resel = this.add_resource_element(file.name, section);
+
+        // Update the progress bar as the file is uploaded
+        xhr.upload.addEventListener('progress', function(e) {
+            if (e.lengthComputable) {
+                var percentage = Math.round((e.loaded * 100) / e.total);
+                resel.progress.style.width = percentage + '%';
+            }
+        }, false);
+
+        // Wait for the AJAX call to complete, then update the
+        // dummy element with the returned details
+        xhr.onreadystatechange = function() {
+            if (xhr.readyState == 4) {
+                if (xhr.status == 200) {
+                    var result = JSON.parse(xhr.responseText);
+                    if (result) {
+                        if (result.error == 0) {
+                            // All OK - update the dummy element
+                            resel.icon.src = result.icon;
+                            resel.a.href = result.link;
+                            resel.namespan.innerHTML = result.name;
+                            resel.div.removeChild(resel.progressouter);
+                            resel.li.id = result.elementid;
+                            resel.div.innerHTML += result.commands;
+                            if (result.onclick) {
+                                resel.a.onclick = result.onclick;
+                            }
+                            self.add_editing(result.elementid);
+                        } else {
+                            // Error - remove the dummy element
+                            resel.parent.removeChild(resel.li);
+                            alert(result.error);
+                        }
+                    }
+                } else {
+                    alert(M.util.get_string('servererror', 'moodle'));
+                }
+            }
+        };
+
+        // Prepare the data to send
+        var formData = new FormData();
+        formData.append('repo_upload_file', file);
+        formData.append('sesskey', M.cfg.sesskey);
+        formData.append('course', this.courseid);
+        formData.append('section', sectionnumber);
+        formData.append('module', module);
+        formData.append('type', 'Files');
+
+        // Send the AJAX call
+        xhr.open("POST", this.url, true);
+        xhr.send(formData);
+    },
+
+    /**
+     * Show a dialog box to gather the name of the resource / activity to be created
+     * from the uploaded content
+     * @param type the details of the type of content
+     * @param contents the contents to be uploaded
+     * @section the DOM element for the section being uploaded to
+     * @sectionnumber the number of the section being uploaded to
+     */
+    handle_item: function(type, contents, section, sectionnumber) {
+        if (type.handlers.length == 0) {
+            // Nothing to handle this - should not have got here
+            return;
+        }
+
+        if (this.uploaddialog) {
+            var details = new Object();
+            details.isfile = false;
+            details.type = type;
+            details.contents = contents;
+            details.section = section;
+            details.setcionnumber = sectionnumber;
+            this.uploadqueue.push(details);
+            return;
+        }
+        this.uploaddialog = true;
+
+        var timestamp = new Date().getTime();
+        var uploadid = Math.round(Math.random()*100000)+'-'+timestamp;
+        var nameid = 'dndupload_handler_name'+uploadid;
+        var content = '';
+        content += '<label for="'+nameid+'">'+type.namemessage+'</label>';
+        content += ' <input type="text" id="'+nameid+'" value="" />';
+        if (type.handlers.length > 1) {
+            content += '<div id="dndupload_handlers'+uploadid+'">';
+            var sel = type.handlers[0].module;
+            for (var i=0; i<type.handlers.length; i++) {
+                var id = 'dndupload_handler'+uploadid;
+                var checked = (type.handlers[i].module == sel) ? 'checked="checked" ' : '';
+                content += '<input type="radio" name="handler" value="'+type.handlers[i].module+'" id="'+id+'" '+checked+'/>';
+                content += ' <label for="'+id+'">';
+                content += type.handlers[i].message;
+                content += '</label><br/>';
+            }
+            content += '</div>';
+        }
+
+        var Y = this.Y;
+        var self = this;
+        var panel = new Y.Panel({
+            bodyContent: content,
+            width: 350,
+            zIndex: 5,
+            centered: true,
+            modal: true,
+            visible: true,
+            render: true,
+            buttons: [{
+                value: M.util.get_string('upload', 'moodle'),
+                action: function(e) {
+                    e.preventDefault();
+                    var name = Y.one('#dndupload_handler_name'+uploadid).get('value');
+                    name = name.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); // Trim
+                    if (name == '') {
+                        return;
+                    }
+                    var module = false;
+                    if (type.handlers.length > 1) {
+                        // Find out which module was selected
+                        var div = Y.one('#dndupload_handlers'+uploadid);
+                        div.all('input').each(function(input) {
+                            if (input.get('checked')) {
+                                module = input.get('value');
+                            }
+                        });
+                        if (!module) {
+                            return;
+                        }
+                    } else {
+                        module = type.handlers[0].module;
+                    }
+                    panel.hide();
+                    // Do the upload
+                    self.upload_item(name, type.type, contents, section, sectionnumber, module);
+                },
+                section: Y.WidgetStdMod.FOOTER
+            },{
+                value: M.util.get_string('cancel', 'moodle'),
+                action: function(e) {
+                    e.preventDefault();
+                    panel.hide();
+                },
+                section: Y.WidgetStdMod.FOOTER
+            }]
+        });
+        // When the panel is hidden - destroy it and then check for other pending uploads
+        panel.after("visibleChange", function(e) {
+            if (!panel.get('visible')) {
+                panel.destroy(true);
+                self.check_upload_queue();
+            }
+        });
+        // Focus on the 'name' box
+        Y.one('#'+nameid).focus();
+    },
+
+    /**
+     * Upload any data types that are not files: display a dummy resource element, send
+     * the data to the server, update the progress bar for the file, then replace the
+     * dummy element with the real information once the AJAX call completes
+     * @param name the display name for the resource / activity to create
+     * @param type the details of the data type found in the drop event
+     * @param contents the actual data that was dropped
+     * @param section the DOM element representing the selected course section
+     * @param sectionnumber the number of the selected course section
+     */
+    upload_item: function(name, type, contents, section, sectionnumber, module) {
+
+        // This would be an ideal place to use the Y.io function
+        // however, this does not support data encoded using the
+        // FormData object, which is needed to transfer data from
+        // the DataTransfer object into an XMLHTTPRequest
+        // This can be converted when the YUI issue has been integrated:
+        // http://yuilibrary.com/projects/yui3/ticket/2531274
+        var xhr = new XMLHttpRequest();
+        var self = this;
+
+        // Add the item to the display
+        var resel = this.add_resource_element(name, section);
+
+        // Wait for the AJAX call to complete, then update the
+        // dummy element with the returned details
+        xhr.onreadystatechange = function() {
+            if (xhr.readyState == 4) {
+                if (xhr.status == 200) {
+                    var result = JSON.parse(xhr.responseText);
+                    if (result) {
+                        if (result.error == 0) {
+                            // All OK - update the dummy element
+                            resel.icon.src = result.icon;
+                            resel.a.href = result.link;
+                            resel.namespan.innerHTML = result.name;
+                            resel.div.removeChild(resel.progressouter);
+                            resel.li.id = result.elementid;
+                            resel.div.innerHTML += result.commands;
+                            if (result.onclick) {
+                                resel.a.onclick = result.onclick;
+                            }
+                            self.add_editing(result.elementid, sectionnumber);
+                        } else {
+                            // Error - remove the dummy element
+                            resel.parent.removeChild(resel.li);
+                            alert(result.error);
+                        }
+                    }
+                } else {
+                    alert(M.util.get_string('servererror', 'moodle'));
+                }
+            }
+        };
+
+        // Prepare the data to send
+        var formData = new FormData();
+        formData.append('contents', contents);
+        formData.append('displayname', name);
+        formData.append('sesskey', M.cfg.sesskey);
+        formData.append('course', this.courseid);
+        formData.append('section', sectionnumber);
+        formData.append('type', type);
+        formData.append('module', module);
+
+        // Send the data
+        xhr.open("POST", this.url, true);
+        xhr.send(formData);
+    },
+
+    /**
+     * Call the AJAX course editing initialisation to add the editing tools
+     * to the newly-created resource link
+     * @param elementid the id of the DOM element containing the new resource link
+     * @param sectionnumber the number of the selected course section
+     */
+    add_editing: function(elementid) {
+        YUI().use('moodle-course-coursebase', function(Y) {
+            M.course.coursebase.invoke_function('setup_for_resource', '#' + elementid);
+        });
+    }
+};
diff --git a/course/dndupload.php b/course/dndupload.php
new file mode 100644 (file)
index 0000000..c4df5a8
--- /dev/null
@@ -0,0 +1,39 @@
+<?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/>.
+
+/**
+ * Starting point for drag and drop course uploads
+ *
+ * @package    core
+ * @subpackage lib
+ * @copyright  2012 Davo smith
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('AJAX_SCRIPT', true);
+
+require_once(dirname(dirname(__FILE__)).'/config.php');
+require_once($CFG->dirroot.'/course/dnduploadlib.php');
+
+$courseid = required_param('course', PARAM_INT);
+$section = required_param('section', PARAM_INT);
+$type = required_param('type', PARAM_TEXT);
+$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.
+
+$dndproc = new dndupload_ajax_processor($courseid, $section, $type, $modulename);
+$dndproc->process($displayname, $contents);
diff --git a/course/dnduploadlib.php b/course/dnduploadlib.php
new file mode 100644 (file)
index 0000000..7c7756c
--- /dev/null
@@ -0,0 +1,655 @@
+<?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/>.
+
+/**
+ * Library to handle drag and drop course uploads
+ *
+ * @package    core
+ * @subpackage lib
+ * @copyright  2012 Davo smith
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/repository/lib.php');
+require_once($CFG->dirroot.'/repository/upload/lib.php');
+require_once($CFG->dirroot.'/course/lib.php');
+
+/**
+ * Add the Javascript to enable drag and drop upload to a course page
+ *
+ * @param object $course The currently displayed course
+ * @param array $modnames The list of enabled (visible) modules on this site
+ * @return void
+ */
+function dndupload_add_to_course($course, $modnames) {
+    global $CFG, $PAGE;
+
+    // Get all handlers.
+    $handler = new dndupload_handler($course, $modnames);
+    $jsdata = $handler->get_js_data();
+    if (empty($jsdata->types) && empty($jsdata->filehandlers)) {
+        return; // No valid handlers - don't enable drag and drop.
+    }
+
+    // Add the javascript to the page.
+    $jsmodule = array(
+        'name' => 'coursedndupload',
+        'fullpath' => new moodle_url('/course/dndupload.js'),
+        'strings' => array(
+            array('addfilehere', 'moodle'),
+            array('dndworking', 'moodle'),
+            array('filetoolarge', 'moodle'),
+            array('actionchoice', 'moodle'),
+            array('servererror', 'moodle'),
+            array('upload', 'moodle'),
+            array('cancel', 'moodle')
+        ),
+        'requires' => array('node', 'event', 'panel', 'json')
+    );
+    $vars = array(
+        array('courseid' => $course->id,
+              'maxbytes' => get_max_upload_file_size($CFG->maxbytes, $course->maxbytes),
+              'handlers' => $handler->get_js_data())
+    );
+
+    $PAGE->requires->js_init_call('M.course_dndupload.init', $vars, true, $jsmodule);
+}
+
+
+/**
+ * Stores all the information about the available dndupload handlers
+ *
+ * @package    core
+ * @copyright  2012 Davo Smith
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class dndupload_handler {
+
+    /**
+     * @var array A list of all registered mime types that can be dropped onto a course
+     *            along with the modules that will handle them.
+     */
+    protected $types = array();
+
+    /**
+     * @var array  A list of the different file types (extensions) that different modules
+     *             will handle.
+     */
+    protected $filehandlers = array();
+
+    /**
+     * Gather a list of dndupload handlers from the different mods
+     *
+     * @param object $course The course this is being added to (to check course_allowed_module() )
+     */
+    public function __construct($course, $modnames = null) {
+        global $CFG;
+
+        // Add some default types to handle.
+        // Note: 'Files' type is hard-coded into the Javascript as this needs to be ...
+        // ... treated a little differently.
+        $this->add_type('url', array('url', 'text/uri-list'), get_string('addlinkhere', 'moodle'),
+                        get_string('nameforlink', 'moodle'), 10);
+        $this->add_type('text/html', array('text/html'), get_string('addpagehere', 'moodle'),
+                        get_string('nameforpage', 'moodle'), 20);
+        $this->add_type('text', array('text', 'text/plain'), get_string('addpagehere', 'moodle'),
+                        get_string('nameforpage', 'moodle'), 30);
+
+        // Loop through all modules to find handlers.
+        $mods = get_plugin_list_with_function('mod', 'dndupload_register');
+        foreach ($mods as $component => $funcname) {
+            list($modtype, $modname) = normalize_component($component);
+            if ($modnames && !array_key_exists($modname, $modnames)) {
+                continue; // Module is deactivated (hidden) at the site level.
+            }
+            if (!course_allowed_module($course, $modname)) {
+                continue; // User does not have permission to add this module to the course.
+            }
+            $resp = $funcname();
+            if (!$resp) {
+                continue;
+            }
+            if (isset($resp['files'])) {
+                foreach ($resp['files'] as $file) {
+                    $this->add_file_handler($file['extension'], $modname, $file['message']);
+                }
+            }
+            if (isset($resp['addtypes'])) {
+                foreach ($resp['addtypes'] as $type) {
+                    if (isset($type['priority'])) {
+                        $priority = $type['priority'];
+                    } else {
+                        $priority = 100;
+                    }
+                    $this->add_type($type['identifier'], $type['datatransfertypes'],
+                                    $type['addmessage'], $type['namemessage'], $priority);
+                }
+            }
+            if (isset($resp['types'])) {
+                foreach ($resp['types'] as $type) {
+                    $this->add_type_handler($type['identifier'], $modname, $type['message']);
+                }
+            }
+        }
+    }
+
+    /**
+     * Used to add a new mime type that can be drag and dropped onto a
+     * course displayed in a browser window
+     *
+     * @param string $identifier The name that this type will be known as
+     * @param array $datatransfertypes An array of the different types in the browser
+     *                                 'dataTransfer.types' object that will map to this type
+     * @param string $addmessage The message to display in the browser when this type is being
+     *                           dragged onto the page
+     * @param string $namemessage The message to pop up when asking for the name to give the
+     *                            course module instance when it is created
+     * @param int $priority Controls the order in which types are checked by the browser (mainly
+     *                      needed to check for 'text' last as that is usually given as fallback)
+     */
+    public function add_type($identifier, $datatransfertypes, $addmessage, $namemessage, $priority=100) {
+        if ($this->is_known_type($identifier)) {
+            throw new coding_exception("Type $identifier is already registered");
+        }
+
+        $add = new stdClass;
+        $add->identifier = $identifier;
+        $add->datatransfertypes = $datatransfertypes;
+        $add->addmessage = $addmessage;
+        $add->namemessage = $namemessage;
+        $add->priority = $priority;
+        $add->handlers = array();
+
+        $this->types[$identifier] = $add;
+    }
+
+    /**
+     * Used to declare that a particular module will handle a particular type
+     * of dropped data
+     *
+     * @param string $type The name of the type (as declared in add_type)
+     * @param string $module The name of the module to handle this type
+     * @param string $message The message to show the user if more than one handler is registered
+     *                        for a type and the user needs to make a choice between them
+     */
+    public function add_type_handler($type, $module, $message) {
+        if (!$this->is_known_type($type)) {
+            throw new coding_exception("Trying to add handler for unknown type $type");
+        }
+
+        $add = new stdClass;
+        $add->type = $type;
+        $add->module = $module;
+        $add->message = $message;
+
+        $this->types[$type]->handlers[] = $add;
+    }
+
+    /**
+     * Used to declare that a particular module will handle a particular type
+     * of dropped file
+     *
+     * @param string $extension The file extension to handle ('*' for all types)
+     * @param string $module The name of the module to handle this type
+     * @param string $message The message to show the user if more than one handler is registered
+     *                        for a type and the user needs to make a choice between them
+     */
+    public function add_file_handler($extension, $module, $message) {
+        $extension = strtolower($extension);
+
+        $add = new stdClass;
+        $add->extension = $extension;
+        $add->module = $module;
+        $add->message = $message;
+
+        $this->filehandlers[] = $add;
+    }
+
+    /**
+     * Check to see if the type has been registered
+     *
+     * @param string $type The identifier of the type you are interested in
+     * @return bool True if the type is registered
+     */
+    public function is_known_type($type) {
+        return array_key_exists($type, $this->types);
+    }
+
+    /**
+     * Check to see if the module in question has registered to handle the
+     * type given
+     *
+     * @param string $module The name of the module
+     * @param string $type The identifier of the type
+     * @return bool True if the module has registered to handle that type
+     */
+    public function has_type_handler($module, $type) {
+        if (!$this->is_known_type($type)) {
+            throw new coding_exception("Checking for handler for unknown type $type");
+        }
+        foreach ($this->types[$type]->handlers as $handler) {
+            if ($handler->module == $module) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Check to see if the module in question has registered to handle files
+     * with the given extension (or to handle all file types)
+     *
+     * @param string $module The name of the module
+     * @param string $extension The extension of the uploaded file
+     * @return bool True if the module has registered to handle files with
+     *              that extension (or to handle all file types)
+     */
+    public function has_file_handler($module, $extension) {
+        foreach ($this->filehandlers as $handler) {
+            if ($handler->module == $module) {
+                if ($handler->extension == '*' || $handler->extension == $extension) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Gets a list of the file types that are handled by a particular module
+     *
+     * @param string $module The name of the module to check
+     * @return array of file extensions or string '*'
+     */
+    public function get_handled_file_types($module) {
+        $types = array();
+        foreach ($this->filehandlers as $handler) {
+            if ($handler->module == $module) {
+                if ($handler->extension == '*') {
+                    return '*';
+                } else {
+                    // Prepending '.' as otherwise mimeinfo fails.
+                    $types[] = '.'.$handler->extension;
+                }
+            }
+        }
+        return $types;
+    }
+
+    /**
+     * Returns an object to pass onto the javascript code with data about all the
+     * registered file / type handlers
+     *
+     * @return object Data to pass on to Javascript code
+     */
+    public function get_js_data() {
+        $ret = new stdClass;
+
+        // Sort the types by priority.
+        uasort($this->types, array($this, 'type_compare'));
+
+        $ret->types = array();
+        foreach ($this->types as $type) {
+            if (empty($type->handlers)) {
+                continue; // Skip any types without registered handlers.
+            }
+            $ret->types[] = $type;
+        }
+
+        $ret->filehandlers = $this->filehandlers;
+        $uploadrepo = repository::get_instances(array('type' => 'upload'));
+        if (empty($uploadrepo)) {
+            $ret->filehandlers = array(); // No upload repo => no file handlers.
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Comparison function used when sorting types by priority
+     * @param object $type1 first type to compare
+     * @param object $type2 second type to compare
+     * @return integer -1 for $type1 < $type2; 1 for $type1 > $type2; 0 for equal
+     */
+    protected function type_compare($type1, $type2) {
+        if ($type1->priority < $type2->priority) {
+            return -1;
+        }
+        if ($type1->priority > $type2->priority) {
+            return 1;
+        }
+        return 0;
+    }
+
+}
+
+/**
+ * Processes the upload, creating the course module and returning the result
+ *
+ * @package    core
+ * @copyright  2012 Davo Smith
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class dndupload_ajax_processor {
+
+    /** Returned when no error has occurred */
+    const ERROR_OK = 0;
+
+    /** @var object The course that we are uploading to */
+    protected $course = null;
+
+    /** @var context_course The course context for capability checking */
+    protected $context = null;
+
+    /** @var int The section number we are uploading to */
+    protected $section = null;
+
+    /** @var string The type of upload (e.g. 'Files', 'text/plain') */
+    protected $type = null;
+
+    /** @var object The details of the module type that will be created */
+    protected $module= null;
+
+    /** @var object The course module that has been created */
+    protected $cm = null;
+
+    /** @var dndupload_handler used to check the allowed file types */
+    protected $dnduploadhandler = null;
+
+    /** @var string The name to give the new activity instance */
+    protected $displayname = null;
+
+    /**
+     * Set up some basic information needed to handle the upload
+     *
+     * @param int $courseid The ID of the course we are uploading to
+     * @param int $section The section number we are uploading to
+     * @param string $type The type of upload (as reported by the browser)
+     * @param string $modulename The name of the module requested to handle this upload
+     */
+    public function __construct($courseid, $section, $type, $modulename) {
+        global $DB;
+
+        if (!defined('AJAX_SCRIPT')) {
+            throw new coding_exception('dndupload_ajax_processor should only be used within AJAX requests');
+        }
+
+        $this->course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
+
+        require_login($this->course, false);
+        $this->context = context_course::instance($this->course->id);
+
+        if (!is_number($section) || $section < 0) {
+            throw new coding_exception("Invalid section number $section");
+        }
+        $this->section = $section;
+        $this->type = $type;
+
+        if (!$this->module = $DB->get_record('modules', array('name' => $modulename))) {
+            throw new coding_exception("Module $modulename does not exist");
+        }
+
+        $this->dnduploadhandler = new dndupload_handler($this->course);
+    }
+
+    /**
+     * Check if this upload is a 'file' upload
+     *
+     * @return bool true if it is a 'file' upload, false otherwise
+     */
+    protected function is_file_upload() {
+        return ($this->type == 'Files');
+    }
+
+    /**
+     * Process the upload - creating the module in the course and returning the result to the browser
+     *
+     * @param string $displayname optional the name (from the browser) to give the course module instance
+     * @param string $content optional the content of the upload (for non-file uploads)
+     */
+    public function process($displayname = null, $content = null) {
+        require_capability('moodle/course:manageactivities', $this->context);
+
+        if ($this->is_file_upload()) {
+            require_capability('moodle/course:managefiles', $this->context);
+            if ($content != null) {
+                throw new moodle_exception('fileuploadwithcontent', 'moodle');
+            }
+        }
+
+        require_sesskey();
+
+        $this->displayname = $displayname;
+
+        if ($this->is_file_upload()) {
+            $this->handle_file_upload();
+        } else {
+            $this->handle_other_upload($content);
+        }
+    }
+
+    /**
+     * Handle uploads containing files - create the course module, ask the upload repository
+     * to process the file, ask the mod to set itself up, then return the result to the browser
+     */
+    protected function handle_file_upload() {
+        global $CFG;
+
+        // Add the file to a draft file area.
+        $draftitemid = file_get_unused_draft_itemid();
+        $maxbytes = get_max_upload_file_size($CFG->maxbytes, $this->course->maxbytes);
+        $types = $this->dnduploadhandler->get_handled_file_types($this->module->name);
+        $repo = repository::get_instances(array('type' => 'upload'));
+        if (empty($repo)) {
+            throw new moodle_exception('errornouploadrepo', 'moodle');
+        }
+        $repo = reset($repo); // Get the first (and only) upload repo.
+        $details = $repo->process_upload(null, $maxbytes, $types, '/', $draftitemid);
+        if (empty($this->displayname)) {
+            $this->displayname = $this->display_name_from_file($details['file']);
+        }
+
+        // Create a course module to hold the new instance.
+        $this->create_course_module();
+
+        // Ask the module to set itself up.
+        $moduledata = $this->prepare_module_data($draftitemid);
+        $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction');
+        if ($instanceid === 'invalidfunction') {
+            throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function");
+        }
+
+        // Finish setting up the course module.
+        $this->finish_setup_course_module($instanceid);
+    }
+
+    /**
+     * Handle uploads not containing file - create the course module, ask the mod to
+     * set itself up, then return the result to the browser
+     *
+     * @param string $content the content uploaded to the browser
+     */
+    protected function handle_other_upload($content) {
+        // Check this plugin is registered to handle this type of upload
+        if (!$this->dnduploadhandler->has_type_handler($this->module->name, $this->type)) {
+            $info = (object)array('modname' => $this->module->name, 'type' => $this->type);
+            throw new moodle_exception('moddoesnotsupporttype', 'moodle', $info);
+        }
+
+        // Create a course module to hold the new instance.
+        $this->create_course_module();
+
+        // Ask the module to set itself up.
+        $moduledata = $this->prepare_module_data(null, $content);
+        $instanceid = plugin_callback('mod', $this->module->name, 'dndupload', 'handle', array($moduledata), 'invalidfunction');
+        if ($instanceid === 'invalidfunction') {
+            throw new coding_exception("{$this->module->name} does not support drag and drop upload (missing {$this->module->name}_dndupload_handle function");
+        }
+
+        // Finish setting up the course module.
+        $this->finish_setup_course_module($instanceid);
+    }
+
+    /**
+     * Generate the name of the mod instance from the name of the file
+     * (remove the extension and convert underscore => space
+     *
+     * @param string $filename the filename of the uploaded file
+     * @return string the display name to use
+     */
+    protected function display_name_from_file($filename) {
+        $pos = textlib::strrpos($filename, '.');
+        if ($pos) { // Want to skip if $pos === 0 OR $pos === false.
+            $filename = textlib::substr($filename, 0, $pos);
+        }
+        return str_replace('_', ' ', $filename);
+    }
+
+    /**
+     * Create the coursemodule to hold the file/content that has been uploaded
+     */
+    protected function create_course_module() {
+        if (!course_allowed_module($this->course, $this->module->name)) {
+            throw new coding_exception("The module {$this->module->name} is not allowed to be added to this course");
+        }
+
+        $this->cm = new stdClass();
+        $this->cm->course = $this->course->id;
+        $this->cm->section = $this->section;
+        $this->cm->module = $this->module->id;
+        $this->cm->modulename = $this->module->name;
+        $this->cm->instance = 0; // This will be filled in after we create the instance.
+        $this->cm->visible = 1;
+        $this->cm->groupmode = $this->course->groupmode;
+        $this->cm->groupingid = $this->course->defaultgroupingid;
+
+        if (!$this->cm->id = add_course_module($this->cm)) {
+            throw new coding_exception("Unable to create the course module");
+        }
+        // The following are used inside some few core functions, so may as well set them here.
+        $this->cm->coursemodule = $this->cm->id;
+        $this->cm->groupmodelink = (!$this->course->groupmodeforce);
+    }
+
+    /**
+     * Gather together all the details to pass on to the mod, so that it can initialise it's
+     * own database tables
+     *
+     * @param int $draftitemid optional the id of the draft area containing the file (for file uploads)
+     * @param string $content optional the content dropped onto the course (for non-file uploads)
+     * @return object data to pass on to the mod, containing:
+     *              string $type the 'type' as registered with dndupload_handler (or 'Files')
+     *              object $course the course the upload was for
+     *              int $draftitemid optional the id of the draft area containing the files
+     *              int $coursemodule id of the course module that has already been created
+     *              string $displayname the name to use for this activity (can be overriden by the mod)
+     */
+    protected function prepare_module_data($draftitemid = null, $content = null) {
+        $data = new stdClass();
+        $data->type = $this->type;
+        $data->course = $this->course;
+        if ($draftitemid) {
+            $data->draftitemid = $draftitemid;
+        } else if ($content) {
+            $data->content = $content;
+        }
+        $data->coursemodule = $this->cm->id;
+        $data->displayname = $this->displayname;
+        return $data;
+    }
+
+    /**
+     * Called after the mod has set itself up, to finish off any course module settings
+     * (set instance id, add to correct section, set visibility, etc.) and send the response
+     *
+     * @param int $instanceid id returned by the mod when it was created
+     */
+    protected function finish_setup_course_module($instanceid) {
+        global $DB, $USER;
+
+        if (!$instanceid) {
+            // Something has gone wrong - undo everything we can.
+            delete_course_module($this->cm->id);
+            throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name);
+        }
+
+        $DB->set_field('course_modules', 'instance', $instanceid, array('id' => $this->cm->id));
+
+        $sectionid = add_mod_to_section($this->cm);
+        $DB->set_field('course_modules', 'section', $sectionid, array('id' => $this->cm->id));
+
+        set_coursemodule_visible($this->cm->id, true);
+
+        // Rebuild the course cache and retrieve the final info about this module.
+        rebuild_course_cache($this->course->id, true);
+        $this->course->modinfo = null; // Otherwise we will just get the old version back again.
+        $info = get_fast_modinfo($this->course);
+        if (!isset($info->cms[$this->cm->id])) {
+            // The course module has not been properly created in the course - undo everything.
+            delete_course_module($this->cm->id);
+            throw new moodle_exception('errorcreatingactivity', 'moodle', '', $this->module->name);
+        }
+        $mod = $info->cms[$this->cm->id];
+
+        // Trigger mod_created event with information about this module.
+        $eventdata = new stdClass();
+        $eventdata->modulename = $mod->modname;
+        $eventdata->name       = $mod->name;
+        $eventdata->cmid       = $mod->id;
+        $eventdata->courseid   = $this->course->id;
+        $eventdata->userid     = $USER->id;
+        events_trigger('mod_created', $eventdata);
+
+        add_to_log($this->course->id, "course", "add mod",
+                   "../mod/{$mod->modname}/view.php?id=$mod->id",
+                   "{$mod->modname} $instanceid");
+        add_to_log($this->course->id, $mod->modname, "add",
+                   "view.php?id=$mod->id",
+                   "$instanceid", $mod->id);
+
+        if ($this->cm->groupmodelink && plugin_supports('mod', $mod->modname, FEATURE_GROUPS, 0)) {
+            $mod->groupmodelink = $this->cm->groupmodelink;
+        } else {
+            $mod->groupmodelink = false;
+        }
+
+        $this->send_response($mod);
+    }
+
+    /**
+     * Send the details of the newly created activity back to the client browser
+     *
+     * @param cm_info $mod details of the mod just created
+     */
+    protected function send_response($mod) {
+        global $OUTPUT;
+
+        $resp = new stdClass();
+        $resp->error = self::ERROR_OK;
+        $resp->icon = $mod->get_icon_url()->out();
+        $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);
+        $resp->onclick = $mod->get_on_click();
+
+        echo $OUTPUT->header();
+        echo json_encode($resp);
+        die();
+    }
+}
\ No newline at end of file
index 174872c..fc42d0a 100644 (file)
@@ -126,6 +126,7 @@ if ($editform->is_cancelled()) {
         // Save any changes to the files used in the editor
         update_course($data, $editoroptions);
     }
+    rebuild_course_cache($course->id);
 
     switch ($returnto) {
         case 'category':
index 97f791a..0a9dc89 100644 (file)
@@ -128,18 +128,25 @@ if ($mform->is_cancelled()) {
     redirect('category.php?id='.$newcategory->id.'&categoryedit=on');
 }
 
-// Unfortunately the navigation never generates correctly for this page because technically
-// this page doesn't actually exist on the navigation you get here through the course
-// management page.
-try {
-    // First up we'll try to make the course management page active seeing as that is
-    // where the user thinks they are.
-    // The big prolem here is that the course management page is a common page
-    // for both editing users and common users.
-    $PAGE->settingsnav->get('root')->get('courses')->get('coursemgmt')->make_active();
-} catch (Exception $ex) {
+// Unfortunately the navigation never generates correctly for this page because technically this page doesn't actually
+// exist on the navigation; you get here through the course management page.
+// First up we'll try to make the course management page active seeing as that is where the user thinks they are.
+// The big prolem here is that the course management page is a common page for both editing users and common users and
+// is only added to the admin tree if the user has permission to edit at the system level.
+$node = $PAGE->settingsnav->get('root');
+if ($node) {
+    $node = $node->get('courses');
+    if ($node) {
+        $node = $node->get('coursemgmt');
+    }
+}
+if ($node) {
+    // The course management page exists so make that active.
+    $node->make_active();
+} else {
     // Failing that we'll override the URL, not as accurate and chances are things
     // won't be 100% correct all the time but should work most times.
+    // A common reason to arrive here is having the management capability within only a particular category (not at system level).
     navigation_node::override_active_url(new moodle_url('/course/index.php', array('categoryedit' => 'on')));
 }
 
index 3bce208..ecd905b 100644 (file)
 require_once("../config.php");
 require_once("lib.php");
 require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->libdir . '/gradelib.php');
+require_once($CFG->libdir . '/completionlib.php');
+require_once($CFG->libdir . '/conditionlib.php');
+
 require_once('editsection_form.php');
 
 $id = required_param('id',PARAM_INT);    // Week/topic ID
@@ -43,7 +47,17 @@ require_capability('moodle/course:update', $context);
 $editoroptions = array('context'=>$context ,'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes'=>$CFG->maxbytes, 'trusttext'=>false, 'noclean'=>true);
 $section = file_prepare_standard_editor($section, 'summary', $editoroptions, $context, 'course', 'section', $section->id);
 $section->usedefaultname = (is_null($section->name));
-$mform = new editsection_form($PAGE->url, array('course'=>$course, 'editoroptions'=>$editoroptions));
+
+if (!empty($CFG->enableavailability)) {
+    // Get section availability conditions from sectioncache.
+    $modinfo = get_fast_modinfo($course);
+    $sectioninfo = $modinfo->get_section_info($section->section);
+    $section->conditionsgrade = $sectioninfo->conditionsgrade;
+    $section->conditionscompletion = $sectioninfo->conditionscompletion;
+}
+
+$mform = new editsection_form($PAGE->url, array('course' => $course, 'editoroptions' => $editoroptions,
+        'cs' => $section, 'showavailability' => $section->showavailability));
 $mform->set_data($section); // set current value
 
 if ($sectionreturn) {
@@ -65,7 +79,21 @@ if ($mform->is_cancelled()){
     $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'section', $section->id);
     $section->summary = $data->summary;
     $section->summaryformat = $data->summaryformat;
+    if (!empty($CFG->enableavailability)) {
+        $section->availablefrom = $data->availablefrom;
+        $section->availableuntil = $data->availableuntil;
+        if (!empty($data->groupingid)) {
+            $section->groupingid = $data->groupingid;
+        }
+        $section->showavailability = $data->showavailability;
+    }
     $DB->update_record('course_sections', $section);
+    if (!empty($CFG->enableavailability)) {
+        // Update grade and completion conditions
+        condition_info_section::update_section_from_form($section, $data);
+    }
+    rebuild_course_cache($course->id);
+
     add_to_log($course->id, "course", "editsection", "editsection.php?id=$section->id", "$section->section");
     $PAGE->navigation->clear_cache();
     redirect($returnurl);
index ab26049..9f33f83 100644 (file)
@@ -9,11 +9,9 @@ require_once($CFG->libdir.'/formslib.php');
 class editsection_form extends moodleform {
 
     function definition() {
-        global $CFG, $DB;
 
         $mform  = $this->_form;
         $course = $this->_customdata['course'];
-
         $mform->addElement('checkbox', 'usedefaultname', get_string('sectionusedefaultname'));
         $mform->setDefault('usedefaultname', true);
 
@@ -30,8 +28,156 @@ class editsection_form extends moodleform {
         $mform->addElement('hidden', 'id');
         $mform->setType('id', PARAM_INT);
 
-//--------------------------------------------------------------------------------
+        $mform->_registerCancelButton('cancel');
+    }
+
+    public function definition_after_data() {
+        global $CFG, $DB;
+
+        $mform  = $this->_form;
+        $course = $this->_customdata['course'];
+
+        if (!empty($CFG->enableavailability)) {
+            // Grouping conditions - only if grouping is enabled at site level
+            if (!empty($CFG->enablegroupmembersonly)) {
+                $options = array();
+                $options[0] = get_string('none');
+                if ($groupings = $DB->get_records('groupings', array('courseid' => $course->id))) {
+                    foreach ($groupings as $grouping) {
+                        $context = context_course::instance($course->id);
+                        $options[$grouping->id] = format_string(
+                                $grouping->name, true, array('context' => $context));
+                    }
+                }
+                $mform->addElement('header', '', get_string('availabilityconditions', 'condition'));
+                $mform->addElement('select', 'groupingid', get_string('groupingsection', 'group'), $options);
+                $mform->addHelpButton('groupingid', 'groupingsection', 'group');
+            }
+
+            // Date and time conditions
+            $mform->addElement('date_time_selector', 'availablefrom',
+                    get_string('availablefrom', 'condition'), array('optional' => true));
+            $mform->addElement('date_time_selector', 'availableuntil',
+                    get_string('availableuntil', 'condition'), array('optional' => true));
+
+            // Conditions based on grades
+            $gradeoptions = array();
+            $items = grade_item::fetch_all(array('courseid' => $course->id));
+            $items = $items ? $items : array();
+            foreach ($items as $id => $item) {
+                $gradeoptions[$id] = $item->get_name();
+            }
+            asort($gradeoptions);
+            $gradeoptions = array(0 => get_string('none', 'condition')) + $gradeoptions;
+
+            $grouparray = array();
+            $grouparray[] = $mform->createElement('select', 'conditiongradeitemid', '', $gradeoptions);
+            $grouparray[] = $mform->createElement('static', '', '',
+                    ' ' . get_string('grade_atleast', 'condition').' ');
+            $grouparray[] = $mform->createElement('text', 'conditiongrademin', '', array('size' => 3));
+            $grouparray[] = $mform->createElement('static', '', '',
+                    '% ' . get_string('grade_upto', 'condition') . ' ');
+            $grouparray[] = $mform->createElement('text', 'conditiongrademax', '', array('size' => 3));
+            $grouparray[] = $mform->createElement('static', '', '', '%');
+            $group = $mform->createElement('group', 'conditiongradegroup',
+                    get_string('gradecondition', 'condition'), $grouparray);
+
+            // Get full version (including condition info) of section object
+            $ci = new condition_info_section($this->_customdata['cs']);
+            $fullcs = $ci->get_full_section();
+            $count = count($fullcs->conditionsgrade) + 1;
+
+            // Grade conditions
+            $this->repeat_elements(array($group), $count, array(), 'conditiongraderepeats',
+                    'conditiongradeadds', 2, get_string('addgrades', 'condition'), true);
+            $mform->addHelpButton('conditiongradegroup[0]', 'gradecondition', 'condition');
+
+            // Conditions based on completion
+            $completion = new completion_info($course);
+            if ($completion->is_enabled()) {
+                $completionoptions = array();
+                $modinfo = get_fast_modinfo($course);
+                foreach ($modinfo->cms as $id => $cm) {
+                    // Add each course-module if it:
+                    // (a) has completion turned on
+                    // (b) does not belong to current course-section
+                    if ($cm->completion && ($fullcs->id != $cm->section)) {
+                        $completionoptions[$id] = $cm->name;
+                    }
+                }
+                asort($completionoptions);
+                $completionoptions = array(0 => get_string('none', 'condition')) +
+                        $completionoptions;
+
+                $completionvalues = array(
+                    COMPLETION_COMPLETE => get_string('completion_complete', 'condition'),
+                    COMPLETION_INCOMPLETE => get_string('completion_incomplete', 'condition'),
+                    COMPLETION_COMPLETE_PASS => get_string('completion_pass', 'condition'),
+                    COMPLETION_COMPLETE_FAIL => get_string('completion_fail', 'condition'));
+
+                $grouparray = array();
+                $grouparray[] = $mform->createElement('select', 'conditionsourcecmid', '',
+                        $completionoptions);
+                $grouparray[] = $mform->createElement('select', 'conditionrequiredcompletion', '',
+                        $completionvalues);
+                $group = $mform->createElement('group', 'conditioncompletiongroup',
+                        get_string('completioncondition', 'condition'), $grouparray);
+
+                $count = count($fullcs->conditionscompletion) + 1;
+                $this->repeat_elements(array($group), $count, array(),
+                        'conditioncompletionrepeats', 'conditioncompletionadds', 2,
+                        get_string('addcompletions', 'condition'), true);
+                $mform->addHelpButton('conditioncompletiongroup[0]',
+                        'completionconditionsection', 'condition');
+            }
+
+            // Availability conditions - set up form values
+            if (!empty($CFG->enableavailability)) {
+                $num = 0;
+                foreach ($fullcs->conditionsgrade as $gradeitemid => $minmax) {
+                    $groupelements = $mform->getElement(
+                            'conditiongradegroup[' . $num . ']')->getElements();
+                    $groupelements[0]->setValue($gradeitemid);
+                    $groupelements[2]->setValue(is_null($minmax->min) ? '' :
+                            format_float($minmax->min, 5, true, true));
+                    $groupelements[4]->setValue(is_null($minmax->max) ? '' :
+                            format_float($minmax->max, 5, true, true));
+                    $num++;
+                }
+
+                if ($completion->is_enabled()) {
+                    $num = 0;
+                    foreach ($fullcs->conditionscompletion as $othercmid => $state) {
+                        $groupelements = $mform->getElement('conditioncompletiongroup[' . $num . ']')->getElements();
+                        $groupelements[0]->setValue($othercmid);
+                        $groupelements[1]->setValue($state);
+                        $num++;
+                    }
+                }
+            }
+
+            // Do we display availability info to students?
+            $showhide = array(
+                CONDITION_STUDENTVIEW_SHOW => get_string('showavailabilitysection_show', 'condition'),
+                CONDITION_STUDENTVIEW_HIDE => get_string('showavailabilitysection_hide', 'condition'));
+            $mform->addElement('select', 'showavailability',
+                    get_string('showavailabilitysection', 'condition'), $showhide);
+
+            $mform->setDefault('showavailability', $this->_customdata['showavailability']);
+        }
+
         $this->add_action_buttons();
+    }
+
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        // Conditions: Don't let them set dates which make no sense
+        if (array_key_exists('availablefrom', $data) &&
+                $data['availablefrom'] && $data['availableuntil'] &&
+                $data['availablefrom'] > $data['availableuntil']) {
+            $errors['availablefrom'] = get_string('badavailabledates', 'condition');
+        }
 
+        return $errors;
     }
 }
index fef3647..98a68d6 100644 (file)
@@ -252,7 +252,7 @@ class core_course_external extends external_api {
      * Returns description of method parameters
      *
      * @return external_function_parameters
-     * @since Moodle 2.2
+     * @since Moodle 2.3
      */
     public static function get_courses_parameters() {
         return new external_function_parameters(
@@ -501,7 +501,6 @@ class core_course_external extends external_api {
         require_once($CFG->dirroot . "/course/lib.php");
         require_once($CFG->libdir . '/completionlib.php');
 
-
         $params = self::validate_parameters(self::create_courses_parameters(),
                         array('courses' => $courses));
 
@@ -595,7 +594,9 @@ class core_course_external extends external_api {
 
     /**
      * Returns description of method parameters
+     *
      * @return external_function_parameters
+     * @since Moodle 2.2
      */
     public static function delete_courses_parameters() {
         return new external_function_parameters(
@@ -607,7 +608,9 @@ class core_course_external extends external_api {
 
     /**
      * Delete courses
+     *
      * @param array $courseids A list of course ids
+     * @since Moodle 2.2
      */
     public static function delete_courses($courseids) {
         global $CFG, $DB;
@@ -627,7 +630,8 @@ class core_course_external extends external_api {
 
             // Check if the current user has enought permissions.
             if (!can_delete_course($courseid)) {
-                throw new moodle_exception('cannotdeletecategorycourse', 'error', '', format_string($course->fullname)." (id: $courseid)");
+                throw new moodle_exception('cannotdeletecategorycourse', 'error',
+                    '', format_string($course->fullname)." (id: $courseid)");
             }
 
             delete_course($course, false);
@@ -640,12 +644,845 @@ class core_course_external extends external_api {
 
     /**
      * Returns description of method result value
+     *
      * @return external_description
+     * @since Moodle 2.2
      */
     public static function delete_courses_returns() {
         return null;
     }
 
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.3
+     */
+    public static function duplicate_course_parameters() {
+        return new external_function_parameters(
+            array(
+                'courseid' => new external_value(PARAM_INT, 'course to duplicate id'),
+                'fullname' => new external_value(PARAM_TEXT, 'duplicated course full name'),
+                'shortname' => new external_value(PARAM_TEXT, 'duplicated course short name'),
+                'categoryid' => new external_value(PARAM_INT, 'duplicated course category parent'),
+                'visible' => new external_value(PARAM_INT, 'duplicated course visible, default to yes', VALUE_DEFAULT, 1),
+                'options' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                                'name' => new external_value(PARAM_ALPHA, 'The backup option name:
+                                            "activities" (int) Include course activites (default to 1 that is equal to yes),
+                                            "blocks" (int) Include course blocks (default to 1 that is equal to yes),
+                                            "filters" (int) Include course filters  (default to 1 that is equal to yes),
+                                            "users" (int) Include users (default to 0 that is equal to no),
+                                            "role_assignments" (int) Include role assignments  (default to 0 that is equal to no),
+                                            "user_files" (int) Include user files  (default to 0 that is equal to no),
+                                            "comments" (int) Include user comments  (default to 0 that is equal to no),
+                                            "completion_information" (int) Include user course completion information  (default to 0 that is equal to no),
+                                            "logs" (int) Include course logs  (default to 0 that is equal to no),
+                                            "histories" (int) Include histories  (default to 0 that is equal to no)'
+                                            ),
+                                'value' => new external_value(PARAM_RAW, 'the value for the option 1 (yes) or 0 (no)'
+                            )
+                        )
+                    ), VALUE_DEFAULT, array()
+                ),
+            )
+        );
+    }
+
+    /**
+     * Duplicate a course
+     *
+     * @param int $courseid
+     * @param string $fullname Duplicated course fullname
+     * @param string $shortname Duplicated course shortname
+     * @param int $categoryid Duplicated course parent category id
+     * @param int $visible Duplicated course availability
+     * @param array $options List of backup options
+     * @return array New course info
+     * @since Moodle 2.3
+     */
+    public static function duplicate_course($courseid, $fullname, $shortname, $categoryid, $visible, $options) {
+        global $CFG, $USER, $DB;
+        require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+        require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+        // Parameter validation.
+        $params = self::validate_parameters(
+                self::duplicate_course_parameters(),
+                array(
+                      'courseid' => $courseid,
+                      'fullname' => $fullname,
+                      'shortname' => $shortname,
+                      'categoryid' => $categoryid,
+                      'visible' => $visible,
+                      'options' => $options
+                )
+        );
+
+        // Context validation.
+
+        if (! ($course = $DB->get_record('course', array('id'=>$params['courseid'])))) {
+            throw new moodle_exception('invalidcourseid', 'error', '', $params['courseid']);
+        }
+
+        // Category where duplicated course is going to be created.
+        $categorycontext = context_coursecat::instance($params['categoryid']);
+        self::validate_context($categorycontext);
+
+        // Course to be duplicated.
+        $coursecontext = context_course::instance($course->id);
+        self::validate_context($coursecontext);
+
+        $backupdefaults = array(
+            'activities' => 1,
+            'blocks' => 1,
+            'filters' => 1,
+            'users' => 0,
+            'role_assignments' => 0,
+            'user_files' => 0,
+            'comments' => 0,
+            'completion_information' => 0,
+            'logs' => 0,
+            'histories' => 0
+        );
+
+        $backupsettings = array();
+        // Check for backup and restore options.
+        if (!empty($params['options'])) {
+            foreach ($params['options'] as $option) {
+
+                // Strict check for a correct value (allways 1 or 0, true or false).
+                $value = clean_param($option['value'], PARAM_INT);
+
+                if ($value !== 0 and $value !== 1) {
+                    throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']);
+                }
+
+                if (!isset($backupdefaults[$option['name']])) {
+                    throw new moodle_exception('invalidextparam', 'webservice', '', $option['name']);
+                }
+
+                $backupsettings[$option['name']] = $value;
+            }
+        }
+
+        // Capability checking.
+
+        // The backup controller check for this currently, this may be redundant.
+        require_capability('moodle/course:create', $categorycontext);
+        require_capability('moodle/restore:restorecourse', $categorycontext);
+        require_capability('moodle/backup:backupcourse', $coursecontext);
+
+        if (!empty($backupsettings['users'])) {
+            require_capability('moodle/backup:userinfo', $coursecontext);
+            require_capability('moodle/restore:userinfo', $categorycontext);
+        }
+
+        // Check if the shortname is used.
+        if ($foundcourses = $DB->get_records('course', array('shortname'=>$shortname))) {
+            foreach ($foundcourses as $foundcourse) {
+                $foundcoursenames[] = $foundcourse->fullname;
+            }
+
+            $foundcoursenamestring = implode(',', $foundcoursenames);
+            throw new moodle_exception('shortnametaken', '', '', $foundcoursenamestring);
+        }
+
+        // Backup the course.
+
+        $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
+        backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id);
+
+        foreach ($backupsettings as $name => $value) {
+            $bc->get_plan()->get_setting($name)->set_value($value);
+        }
+
+        $backupid       = $bc->get_backupid();
+        $backupbasepath = $bc->get_plan()->get_basepath();
+
+        $bc->execute_plan();
+        $results = $bc->get_results();
+        $file = $results['backup_destination'];
+
+        $bc->destroy();
+
+        // Restore the backup immediately.
+
+        // Check if we need to unzip the file because the backup temp dir does not contains backup files.
+        if (!file_exists($backupbasepath . "/moodle_backup.xml")) {
+            $file->extract_to_pathname(get_file_packer(), $backupbasepath);
+        }
+
+        // Create new course.
+        $newcourseid = restore_dbops::create_new_course($params['fullname'], $params['shortname'], $params['categoryid']);
+
+        $rc = new restore_controller($backupid, $newcourseid,
+                backup::INTERACTIVE_NO, backup::MODE_SAMESITE, $USER->id, backup::TARGET_NEW_COURSE);
+
+        foreach ($backupsettings as $name => $value) {
+            $setting = $rc->get_plan()->get_setting($name);
+            if ($setting->get_status() == backup_setting::NOT_LOCKED) {
+                $setting->set_value($value);
+            }
+        }
+
+        if (!$rc->execute_precheck()) {
+            $precheckresults = $rc->get_precheck_results();
+            if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
+                if (empty($CFG->keeptempdirectoriesonbackup)) {
+                    fulldelete($backupbasepath);
+                }
+
+                $errorinfo = '';
+
+                foreach ($precheckresults['errors'] as $error) {
+                    $errorinfo .= $error;
+                }
+
+                if (array_key_exists('warnings', $precheckresults)) {
+                    foreach ($precheckresults['warnings'] as $warning) {
+                        $errorinfo .= $warning;
+                    }
+                }
+
+                throw new moodle_exception('backupprecheckerrors', 'webservice', '', $errorinfo);
+            }
+        }
+
+        $rc->execute_plan();
+        $rc->destroy();
+
+        $course = $DB->get_record('course', array('id' => $newcourseid), '*', MUST_EXIST);
+        $course->fullname = $params['fullname'];
+        $course->shortname = $params['shortname'];
+        $course->visible = $params['visible'];
+
+        // Set shortname and fullname back.
+        $DB->update_record('course', $course);
+
+        if (empty($CFG->keeptempdirectoriesonbackup)) {
+            fulldelete($backupbasepath);
+        }
+
+        // Delete the course backup file created by this WebService. Originally located in the course backups area.
+        $file->delete();
+
+        return array('id' => $course->id, 'shortname' => $course->shortname);
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.3
+     */
+    public static function duplicate_course_returns() {
+        return new external_single_structure(
+            array(
+                'id'       => new external_value(PARAM_INT, 'course id'),
+                'shortname' => new external_value(PARAM_TEXT, 'short name'),
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.3
+     */
+    public static function get_categories_parameters() {
+        return new external_function_parameters(
+            array(
+                'criteria' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'key' => new external_value(PARAM_ALPHA,
+                                         'The category column to search, expected keys (value format) are:'.
+                                         '"id" (int) the category id,'.
+                                         '"name" (string) the category name,'.
+                                         '"parent" (int) the parent category id,'.
+                                         '"idnumber" (string) category idnumber'.
+                                         ' - user must have \'moodle/category:manage\' to search on idnumber,'.
+                                         '"visible" (int) whether the category is visible or not'.
+                                         ' - user must have \'moodle/category:manage\' or \'moodle/category:viewhiddencategories\' to search on visible,'.
+                                         '"theme" (string) category theme'.
+                                         ' - user must have \'moodle/category:manage\' to search on theme'),
+                            'value' => new external_value(PARAM_RAW, 'the value to match')
+                        )
+                    ), VALUE_DEFAULT, array()
+                ),
+                'addsubcategories' => new external_value(PARAM_BOOL, 'return the sub categories infos
+                                          (1 - default) otherwise only the category info (0)', VALUE_DEFAULT, 1)
+            )
+        );
+    }
+
+    /**
+     * Get categories
+     *
+     * @param array $criteria Criteria to match the results
+     * @param booln $addsubcategories obtain only the category (false) or its subcategories (true - default)
+     * @return array list of categories
+     * @since Moodle 2.3
+     */
+    public static function get_categories($criteria = array(), $addsubcategories = true) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/course/lib.php");
+
+        // Validate parameters.
+        $params = self::validate_parameters(self::get_categories_parameters(),
+                array('criteria' => $criteria, 'addsubcategories' => $addsubcategories));
+
+        // Retrieve the categories.
+        $categories = array();
+        if (!empty($params['criteria'])) {
+
+            $conditions = array();
+            $wheres = array();
+            foreach ($params['criteria'] as $crit) {
+                $key = trim($crit['key']);
+
+                // Trying to avoid duplicate keys.
+                if (!isset($conditions[$key])) {
+
+                    $context = context_system::instance();
+                    $value = null;
+                    switch ($key) {
+                        case 'id':
+                            $value = clean_param($crit['value'], PARAM_INT);
+                            break;
+
+                        case 'idnumber':
+                            if (has_capability('moodle/category:manage', $context)) {
+                                $value = clean_param($crit['value'], PARAM_RAW);
+                            } else {
+                                // We must throw an exception.
+                                // Otherwise the dev client would think no idnumber exists.
+                                throw new moodle_exception('criteriaerror',
+                                        'webservice', '', null,
+                                        'You don\'t have the permissions to search on the "idnumber" field.');
+                            }
+                            break;
+
+                        case 'name':
+                            $value = clean_param($crit['value'], PARAM_TEXT);
+                            break;
+
+                        case 'parent':
+                            $value = clean_param($crit['value'], PARAM_INT);
+                            break;
+
+                        case 'visible':
+                            if (has_capability('moodle/category:manage', $context)
+                                or has_capability('moodle/category:viewhiddencategories',
+                                        context_system::instance())) {
+                                $value = clean_param($crit['value'], PARAM_INT);
+                            } else {
+                                throw new moodle_exception('criteriaerror',
+                                        'webservice', '', null,
+                                        'You don\'t have the permissions to search on the "visible" field.');
+                            }
+                            break;
+
+                        case 'theme':
+                            if (has_capability('moodle/category:manage', $context)) {
+                                $value = clean_param($crit['value'], PARAM_THEME);
+                            } else {
+                                throw new moodle_exception('criteriaerror',
+                                        'webservice', '', null,
+                                        'You don\'t have the permissions to search on the "theme" field.');
+                            }
+                            break;
+
+                        default:
+                            throw new moodle_exception('criteriaerror',
+                                    'webservice', '', null,
+                                    'You can not search on this criteria: ' . $key);
+                    }
+
+                    if (isset($value)) {
+                        $conditions[$key] = $crit['value'];
+                        $wheres[] = $key . " = :" . $key;
+                    }
+                }
+            }
+
+            if (!empty($wheres)) {
+                $wheres = implode(" AND ", $wheres);
+
+                $categories = $DB->get_records_select('course_categories', $wheres, $conditions);
+
+                // Retrieve its sub subcategories (all levels).
+                if ($categories and !empty($params['addsubcategories'])) {
+                    $newcategories = array();
+
+                    foreach ($categories as $category) {
+                        $sqllike = $DB->sql_like('path', ':path');
+                        $sqlparams = array('path' => $category->path.'/%'); // It will NOT include the specified category.
+                        $subcategories = $DB->get_records_select('course_categories', $sqllike, $sqlparams);
+                        $newcategories = $newcategories + $subcategories;   // Both arrays have integer as keys.
+                    }
+                    $categories = $categories + $newcategories;
+                }
+            }
+
+        } else {
+            // Retrieve all categories in the database.
+            $categories = $DB->get_records('course_categories');
+        }
+
+        // The not returned categories. key => category id, value => reason of exclusion.
+        $excludedcats = array();
+
+        // The returned categories.
+        $categoriesinfo = array();
+
+        // We need to sort the categories by path.
+        // The parent cats need to be checked by the algo first.
+        usort($categories, "core_course_external::compare_categories_by_path");
+
+        foreach ($categories as $category) {
+
+            // Check if the category is a child of an excluded category, if yes exclude it too (excluded => do not return).
+            $parents = explode('/', $category->path);
+            unset($parents[0]); // First key is always empty because path start with / => /1/2/4.
+            foreach ($parents as $parentid) {
+                // Note: when the parent exclusion was due to the context,
+                // the sub category could still be returned.
+                if (isset($excludedcats[$parentid]) and $excludedcats[$parentid] != 'context') {
+                    $excludedcats[$category->id] = 'parent';
+                }
+            }
+
+            // Check category depth is <= maxdepth (do not check for user who can manage categories).
+            if ((!empty($CFG->maxcategorydepth) && count($parents) > $CFG->maxcategorydepth)
+                    and !has_capability('moodle/category:manage', $context)) {
+                $excludedcats[$category->id] = 'depth';
+            }
+
+            // Check the user can use the category context.
+            $context = context_coursecat::instance($category->id);
+            try {
+                self::validate_context($context);
+            } catch (Exception $e) {
+                $excludedcats[$category->id] = 'context';
+
+                // If it was the requested category then throw an exception.
+                if (isset($params['categoryid']) && $category->id == $params['categoryid']) {
+                    $exceptionparam = new stdClass();
+                    $exceptionparam->message = $e->getMessage();
+                    $exceptionparam->catid = $category->id;
+                    throw new moodle_exception('errorcatcontextnotvalid', 'webservice', '', $exceptionparam);
+                }
+            }
+
+            // Return the category information.
+            if (!isset($excludedcats[$category->id])) {
+
+                // Final check to see if the category is visible to the user.
+                if ($category->visible
+                        or has_capability('moodle/category:viewhiddencategories', context_system::instance())
+                        or has_capability('moodle/category:manage', $context)) {
+
+                    $categoryinfo = array();
+                    $categoryinfo['id'] = $category->id;
+                    $categoryinfo['name'] = $category->name;
+                    $categoryinfo['description'] = file_rewrite_pluginfile_urls($category->description,
+                            'webservice/pluginfile.php', $context->id, 'coursecat', 'description', null);
+                    $options = new stdClass;
+                    $options->noclean = true;
+                    $options->para = false;
+                    $categoryinfo['description'] = format_text($categoryinfo['description'],
+                            $category->descriptionformat, $options);
+                    $categoryinfo['parent'] = $category->parent;
+                    $categoryinfo['sortorder'] = $category->sortorder;
+                    $categoryinfo['coursecount'] = $category->coursecount;
+                    $categoryinfo['depth'] = $category->depth;
+                    $categoryinfo['path'] = $category->path;
+
+                    // Some fields only returned for admin.
+                    if (has_capability('moodle/category:manage', $context)) {
+                        $categoryinfo['idnumber'] = $category->idnumber;
+                        $categoryinfo['visible'] = $category->visible;
+                        $categoryinfo['visibleold'] = $category->visibleold;
+                        $categoryinfo['timemodified'] = $category->timemodified;
+                        $categoryinfo['theme'] = $category->theme;
+                    }
+
+                    $categoriesinfo[] = $categoryinfo;
+                } else {
+                    $excludedcats[$category->id] = 'visibility';
+                }
+            }
+        }
+
+        // Sorting the resulting array so it looks a bit better for the client developer.
+        usort($categoriesinfo, "core_course_external::compare_categories_by_sortorder");
+
+        return $categoriesinfo;
+    }
+
+    /**
+     * Sort categories array by path
+     * private function: only used by get_categories
+     *
+     * @param array $category1
+     * @param array $category2
+     * @return int result of strcmp
+     * @since Moodle 2.3
+     */
+    private static function compare_categories_by_path($category1, $category2) {
+        return strcmp($category1->path, $category2->path);
+    }
+
+    /**
+     * Sort categories array by sortorder
+     * private function: only used by get_categories
+     *
+     * @param array $category1
+     * @param array $category2
+     * @return int result of strcmp
+     * @since Moodle 2.3
+     */
+    private static function compare_categories_by_sortorder($category1, $category2) {
+        return strcmp($category1['sortorder'], $category2['sortorder']);
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.3
+     */
+    public static function get_categories_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_INT, 'category id'),
+                    'name' => new external_value(PARAM_TEXT, 'category name'),
+                    'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL),
+                    'description' => new external_value(PARAM_RAW, 'category description'),
+                    'parent' => new external_value(PARAM_INT, 'parent category id'),
+                    'sortorder' => new external_value(PARAM_INT, 'category sorting order'),
+                    'coursecount' => new external_value(PARAM_INT, 'number of courses in this category'),
+                    'visible' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL),
+                    'visibleold' => new external_value(PARAM_INT, '1: available, 0:not available', VALUE_OPTIONAL),
+                    'timemodified' => new external_value(PARAM_INT, 'timestamp', VALUE_OPTIONAL),
+                    'depth' => new external_value(PARAM_INT, 'category depth'),
+                    'path' => new external_value(PARAM_TEXT, 'category path'),
+                    'theme' => new external_value(PARAM_THEME, 'category theme', VALUE_OPTIONAL),
+                ), 'List of categories'
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.3
+     */
+    public static function create_categories_parameters() {
+        return new external_function_parameters(
+            array(
+                'categories' => new external_multiple_structure(
+                        new external_single_structure(
+                            array(
+                                'name' => new external_value(PARAM_TEXT, 'new category name'),
+                                'parent' => new external_value(PARAM_INT,
+                                        'the parent category id inside which the new category will be created'),
+                                'idnumber' => new external_value(PARAM_RAW,
+                                        'the new category idnumber', VALUE_OPTIONAL),
+                                'description' => new external_value(PARAM_RAW,
+                                        'the new category description', VALUE_OPTIONAL),
+                                'theme' => new external_value(PARAM_THEME,
+                                        'the new category theme. This option must be enabled on moodle',
+                                        VALUE_OPTIONAL),
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Create categories
+     *
+     * @param array $categories - see create_categories_parameters() for the array structure
+     * @return array - see create_categories_returns() for the array structure
+     * @since Moodle 2.3
+     */
+    public static function create_categories($categories) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/course/lib.php");
+
+        $params = self::validate_parameters(self::create_categories_parameters(),
+                        array('categories' => $categories));
+
+        $transaction = $DB->start_delegated_transaction();
+
+        $createdcategories = array();
+        foreach ($params['categories'] as $category) {
+            if ($category['parent']) {
+                if (!$DB->record_exists('course_categories', array('id' => $category['parent']))) {
+                    throw new moodle_exception('unknowcategory');
+                }
+                $context = context_coursecat::instance($category['parent']);
+            } else {
+                $context = context_system::instance();
+            }
+            self::validate_context($context);
+            require_capability('moodle/category:manage', $context);
+
+            // Check id number.
+            if (!empty($category['idnumber'])) { // Same as in course/editcategory_form.php .
+                if (textlib::strlen($category['idnumber'])>100) {
+                    throw new moodle_exception('idnumbertoolong');
+                }
+                if ($existing = $DB->get_record('course_categories', array('idnumber' => $category['idnumber']))) {
+                    if ($existing->id) {
+                        throw new moodle_exception('idnumbertaken');
+                    }
+                }
+            }
+            // Check name.
+            if (textlib::strlen($category['name'])>255) {
+                throw new moodle_exception('categorytoolong');
+            }
+
+            $newcategory = new stdClass();
+            $newcategory->name = $category['name'];
+            $newcategory->parent = $category['parent'];
+            $newcategory->idnumber = $category['idnumber'];
+            $newcategory->sortorder = 999; // Same as in the course/editcategory.php .
+            // Format the description.
+            if (!empty($category['description'])) {
+                $newcategory->description = $category['description'];
+            }
+            $newcategory->descriptionformat = FORMAT_HTML;
+            if (isset($category['theme']) and !empty($CFG->allowcategorythemes)) {
+                $newcategory->theme = $category['theme'];
+            }
+
+            $newcategory = create_course_category($newcategory);
+            // Populate special fields.
+            fix_course_sortorder();
+
+            $createdcategories[] = array('id' => $newcategory->id, 'name' => $newcategory->name);
+        }
+
+        $transaction->allow_commit();
+
+        return $createdcategories;
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.3
+     */
+    public static function create_categories_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_INT, 'new category id'),
+                    'name' => new external_value(PARAM_TEXT, 'new category name'),
+                )
+            )
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.3
+     */
+    public static function update_categories_parameters() {
+        return new external_function_parameters(
+            array(
+                'categories' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id'       => new external_value(PARAM_INT, 'course id'),
+                            'name' => new external_value(PARAM_TEXT, 'category name', VALUE_OPTIONAL),
+                            'idnumber' => new external_value(PARAM_RAW, 'category id number', VALUE_OPTIONAL),
+                            'parent' => new external_value(PARAM_INT, 'parent category id', VALUE_OPTIONAL),
+                            'description' => new external_value(PARAM_RAW, 'category description', VALUE_OPTIONAL),
+                            'theme' => new external_value(PARAM_THEME,
+                                    'the category theme. This option must be enabled on moodle', VALUE_OPTIONAL),
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Update categories
+     *
+     * @param array $categories The list of categories to update
+     * @return null
+     * @since Moodle 2.3
+     */
+    public static function update_categories($categories) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/course/lib.php");
+
+        // Validate parameters.
+        $params = self::validate_parameters(self::update_categories_parameters(), array('categories' => $categories));
+
+        $transaction = $DB->start_delegated_transaction();
+
+        foreach ($params['categories'] as $cat) {
+            if (!$category = $DB->get_record('course_categories', array('id' => $cat['id']))) {
+                throw new moodle_exception('unknowcategory');
+            }
+
+            $categorycontext = context_coursecat::instance($cat['id']);
+            self::validate_context($categorycontext);
+            require_capability('moodle/category:manage', $categorycontext);
+
+            if (!empty($cat['name'])) {
+                if (textlib::strlen($cat['name'])>255) {
+                     throw new moodle_exception('categorytoolong');
+                }
+                $category->name = $cat['name'];
+            }
+            if (!empty($cat['idnumber'])) {
+                if (textlib::strlen($cat['idnumber'])>100) {
+                    throw new moodle_exception('idnumbertoolong');
+                }
+                $category->idnumber = $cat['idnumber'];
+            }
+            if (!empty($cat['description'])) {
+                $category->description = $cat['description'];
+                $category->descriptionformat = FORMAT_HTML;
+            }
+            if (!empty($cat['theme'])) {
+                $category->theme = $cat['theme'];
+            }
+            if (!empty($cat['parent']) && ($category->parent != $cat['parent'])) {
+                // First check if parent exists.
+                if (!$parent_cat = $DB->get_record('course_categories', array('id' => $cat['parent']))) {
+                    throw new moodle_exception('unknowcategory');
+                }
+                // Then check if we have capability.
+                self::validate_context(get_category_or_system_context((int)$cat['parent']));
+                require_capability('moodle/category:manage', get_category_or_system_context((int)$cat['parent']));
+                // Finally move the category.
+                move_category($category, $parent_cat);
+                $category->parent = $cat['parent'];
+            }
+            $DB->update_record('course_categories', $category);
+        }
+
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.3
+     */
+    public static function update_categories_returns() {
+        return null;
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.3
+     */
+    public static function delete_categories_parameters() {
+        return new external_function_parameters(
+            array(
+                'categories' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'category id to delete'),
+                            'newparent' => new external_value(PARAM_INT,
+                                'the parent category to move the contents to, if specified', VALUE_OPTIONAL),
+                            'recursive' => new external_value(PARAM_BOOL, '1: recursively delete all contents inside this
+                                category, 0 (default): move contents to newparent or current parent category (except if parent is root)', VALUE_DEFAULT, 0)
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Delete categories
+     *
+     * @param array $categories A list of category ids
+     * @return array
+     * @since Moodle 2.3
+     */
+    public static function delete_categories($categories) {
+        global $CFG, $DB;
+        require_once($CFG->dirroot . "/course/lib.php");
+
+        // Validate parameters.
+        $params = self::validate_parameters(self::delete_categories_parameters(), array('categories' => $categories));
+
+        $transaction = $DB->start_delegated_transaction();
+
+        foreach ($params['categories'] as $category) {
+            if (!$deletecat = $DB->get_record('course_categories', array('id' => $category['id']))) {
+                throw new moodle_exception('unknowcategory');
+            }
+            $context = context_coursecat::instance($deletecat->id);
+            require_capability('moodle/category:manage', $context);
+            self::validate_context($context);
+            self::validate_context(get_category_or_system_context($deletecat->parent));
+
+            if ($category['recursive']) {
+                // If recursive was specified, then we recursively delete the category's contents.
+                category_delete_full($deletecat, false);
+            } else {
+                // In this situation, we don't delete the category's contents, we either move it to newparent or parent.
+                // If the parent is the root, moving is not supported (because a course must always be inside a category).
+                // We must move to an existing category.
+                if (!empty($category['newparent'])) {
+                    if (!$DB->record_exists('course_categories', array('id' => $category['newparent']))) {
+                        throw new moodle_exception('unknowcategory');
+                    }
+                    $newparent = $category['newparent'];
+                } else {
+                    $newparent = $deletecat->parent;
+                }
+
+                // This operation is not allowed. We must move contents to an existing category.
+                if ($newparent == 0) {
+                    throw new moodle_exception('movecatcontentstoroot');
+                }
+
+                $parentcontext = get_category_or_system_context($newparent);
+                require_capability('moodle/category:manage', $parentcontext);
+                self::validate_context($parentcontext);
+                category_delete_move($deletecat, $newparent, false);
+            }
+        }
+
+        $transaction->allow_commit();
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.3
+     */
+    public static function delete_categories_returns() {
+        return null;
+    }
+
 }
 
 /**
index 61670a5..45977d4 100644 (file)
@@ -162,6 +162,8 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         }
         $o.= html_writer::end_tag('div');
 
+        $o .= $this->section_availability_message($section);
+
         return $o;
     }
 
@@ -272,11 +274,35 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $o.= html_writer::start_tag('div', array('class' => 'summarytext'));
         $o.= $this->format_summary_text($section);
         $o.= html_writer::end_tag('div');
+
+        $o .= $this->section_availability_message($section);
+
         $o.= html_writer::end_tag('li');
 
         return $o;
     }
 
+    /**
+     * If section is not visible to current user, display the message about that
+     * ('Not available until...', that sort of thing). Otherwise, returns blank.
+     *
+     * @param stdClass $section The course_section entry from DB
+     * @return string HTML to output
+     */
+    protected function section_availability_message($section) {
+        $o = '';
+        if (!$section->uservisible || $section->availableinfo) {
+            $o .= html_writer::start_tag('div', array('class' => 'availabilityinfo'));
+            if (!empty($section->availableinfo)) {
+                $o .= $section->availableinfo;
+            } else {
+                $o .= get_string('notavailable');
+            }
+            $o .= html_writer::end_tag('div');
+        }
+        return $o;
+    }
+
     /**
      * Show if something is on on the course clipboard (moving around)
      *
@@ -314,7 +340,7 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
      * @param stdClass $course The course entry from DB
      * @param array $sections The course_sections entries from the DB
      * @param int $sectionno The section number in the coruse which is being dsiplayed
-     * @return string HTML to output.
+     * @return array associative array with previous and next section link
      */
     protected function get_nav_links($course, $sections, $sectionno) {
         // FIXME: This is really evil and should by using the navigation API.
@@ -325,8 +351,9 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $back = $sectionno - 1;
         while ($back > 0 and empty($links['previous'])) {
             if ($canviewhidden || $sections[$back]->visible) {
-                $links['previous'] = html_writer::link(course_get_url($course, $back),
-                    $this->output->larrow().$this->output->spacer().get_section_name($course, $sections[$back]));
+                $previouslink = html_writer::tag('span', $this->output->larrow(), array('class' => 'larrow'));
+                $previouslink .= get_section_name($course, $sections[$back]);
+                $links['previous'] = html_writer::link(course_get_url($course, $back), $previouslink);
             }
             $back--;
         }
@@ -334,19 +361,14 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
         $forward = $sectionno + 1;
         while ($forward <= $course->numsections and empty($links['next'])) {
             if ($canviewhidden || $sections[$forward]->visible) {
-                $links['next'] = html_writer::link(course_get_url($course, $forward),
-                    get_section_name($course, $sections[$forward]).$this->output->spacer().$this->output->rarrow());
+                $nextlink = get_section_name($course, $sections[$forward]);
+                $nextlink .= html_writer::tag('span', $this->output->rarrow(), array('class' => 'rarrow'));
+                $links['next'] = html_writer::link(course_get_url($course, $forward), $nextlink);
             }
             $forward++;
         }
 
-        $o = '';
-        $o.= html_writer::start_tag('div', array('class' => 'section-navigation yui3-g'));
-        $o.= html_writer::tag('div', $links['previous'], array('class' => 'yui3-u'));
-        $o.= html_writer::tag('div', $links['next'], array('class' => 'right yui3-u'));
-        $o.= html_writer::end_tag('div');
-
-        return $o;
+        return $links;
     }
 
     /**
@@ -441,16 +463,19 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             echo $this->end_section_list();
         }
 
-        // Section next/previous links.
+        // Title with section navigation links.
         $sectionnavlinks = $this->get_nav_links($course, $sections, $displaysection);
-        echo $sectionnavlinks;
-
-
-        // Title with completion help icon.
+        $sectiontitle = '';
+        $sectiontitle .= html_writer::start_tag('div', array('class' => 'section-navigation headingblock header'));
+        $sectiontitle .= html_writer::tag('span', $sectionnavlinks['previous'], array('class' => 'mdl-left'));
+        $sectiontitle .= html_writer::tag('span', $sectionnavlinks['next'], array('class' => 'mdl-right'));
+        $sectiontitle .= html_writer::tag('div', get_section_name($course, $sections[$displaysection]), array('class' => 'mdl-align'));
+        $sectiontitle .= html_writer::end_tag('div');
+        echo $sectiontitle;
+
+        // Show completion help icon.
         $completioninfo = new completion_info($course);
         echo $completioninfo->display_help_icon();
-        $title = get_section_name($course, $sections[$displaysection]);
-        echo $this->output->heading($title, 2, 'headingblock header outline');
 
         // Copy activity clipboard..
         echo $this->course_activity_clipboard($course, $displaysection);
@@ -466,8 +491,17 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             print_section_add_menus($course, $displaysection, $modnames);
         }
         echo $this->section_footer();
-        echo $sectionnavlinks;
         echo $this->end_section_list();
+
+        // Display section bottom navigation.
+        $courselink = html_writer::link(course_get_url($course), get_string('returntomaincoursepage'));
+        $sectionbottomnav = '';
+        $sectionbottomnav .= html_writer::start_tag('div', array('class' => 'section-navigation mdl-bottom'));
+        $sectionbottomnav .= html_writer::tag('span', $sectionnavlinks['previous'], array('class' => 'mdl-left'));
+        $sectionbottomnav .= html_writer::tag('span', $sectionnavlinks['next'], array('class' => 'mdl-right'));
+        $sectionbottomnav .= html_writer::tag('div', $courselink, array('class' => 'mdl-align'));
+        $sectionbottomnav .= html_writer::end_tag('div');
+        echo $sectionbottomnav;
     }
 
     /**
@@ -513,10 +547,20 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
             } else {
                 // This will create a course section if it doesn't exist..
                 $thissection = get_course_section($section, $course->id);
+
+                // The returned section is only a bare database object rather than
+                // a section_info object - we will need at least the uservisible
+                // field in it.
+                $thissection->uservisible = true;
             }
-            $showsection = ($canviewhidden or $thissection->visible or !$course->hiddensections);
-            if (!$thissection->visible && !$canviewhidden) {
-                if (!$course->hiddensections) {
+            // Show the section if the user is permitted to access it, OR if it's not available
+            // but showavailability is turned on
+            $showsection = $thissection->uservisible ||
+                    ($thissection->visible && !$thissection->available && $thissection->showavailability);
+            if (!$showsection) {
+                // Hidden section message is overridden by 'unavailable' control
+                // (showavailability option).
+                if (!$course->hiddensections && $thissection->available) {
                     echo $this->section_hidden($section);
                 }
 
@@ -529,9 +573,11 @@ abstract class format_section_renderer_base extends plugin_renderer_base {
                 echo $this->section_summary($thissection, $course);
             } else {
                 echo $this->section_header($thissection, $course, false);
-                print_section($course, $thissection, $mods, $modnamesused);
-                if ($PAGE->user_is_editing()) {
-                    print_section_add_menus($course, $section, $modnames);
+                if ($thissection->uservisible) {
+                    print_section($course, $thissection, $mods, $modnamesused);
+                    if ($PAGE->user_is_editing()) {
+                        print_section_add_menus($course, $section, $modnames);
+                    }
                 }
                 echo $this->section_footer();
             }
index 26f2801..85aec6d 100644 (file)
@@ -26,15 +26,12 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     var CSS = {
         COURSECONTENT : 'course-content',
         LEFT : 'left',
-        RIGHT : 'right',
-        SECTIONADDMENUS : 'section_add_menus',
+        SECTIONADDMENUS : 'section_add_menus'
     };
 
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
     // Swap left block
     sectionlist.item(node1).one('.'+CSS.LEFT).swap(sectionlist.item(node2).one('.'+CSS.LEFT));
-    // Swap right block
-    sectionlist.item(node1).one('.'+CSS.RIGHT).swap(sectionlist.item(node2).one('.'+CSS.RIGHT));
     // Swap menus
     sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
 }
index d1b5430..78c2063 100644 (file)
@@ -26,7 +26,6 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     var CSS = {
         COURSECONTENT : 'course-content',
         LEFT : 'left',
-        RIGHT : 'right',
         SECTIONADDMENUS : 'section_add_menus',
         WEEKDATES: 'weekdates'
     };
@@ -34,8 +33,6 @@ M.course.format.swap_sections = function(Y, node1, node2) {
     var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
     // Swap left block
     sectionlist.item(node1).one('.'+CSS.LEFT).swap(sectionlist.item(node2).one('.'+CSS.LEFT));
-    // Swap right block
-    sectionlist.item(node1).one('.'+CSS.RIGHT).swap(sectionlist.item(node2).one('.'+CSS.RIGHT));
     // Swap menus
     sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
     // Swap week dates
index 262582f..9a64052 100644 (file)
@@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die;
 
 require_once($CFG->libdir.'/completionlib.php');
 require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->dirroot.'/course/dnduploadlib.php');
 
 define('COURSE_MAX_LOGS_PER_PAGE', 1000);       // records
 define('COURSE_MAX_RECENT_PERIOD', 172800);     // Two days, in seconds
@@ -566,7 +567,7 @@ function print_log_csv($course, $user, $date, $order='l.time DESC', $modname,
     $filename = 'logs_'.userdate(time(),get_string('backupnameformat', 'langconfig'),99,false);
     $filename .= '.txt';
     header("Content-Type: application/download\n");
-    header("Content-Disposition: attachment; filename=$filename");
+    header("Content-Disposition: attachment; filename=\"$filename\"");
     header("Expires: 0");
     header("Cache-Control: must-revalidate,post-check=0,pre-check=0");
     header("Pragma: public");
@@ -1262,6 +1263,9 @@ function get_all_mods($courseid, &$mods, &$modnames, &$modnamesplural, &$modname
  * of subsequent requests. This is used all over + in some standard libs and course
  * format callbacks so subsequent requests are a reality.
  *
+ * Note: Since Moodle 2.3, it is more efficient to get this data by calling
+ * get_fast_modinfo, then using $modinfo->get_section_info or get_section_info_all.
+ *
  * @staticvar array $coursesections
  * @param int $courseid
  * @return array Array of sections
@@ -1271,7 +1275,8 @@ function get_all_sections($courseid) {
     static $coursesections = array();
     if (!array_key_exists($courseid, $coursesections)) {
         $coursesections[$courseid] = $DB->get_records("course_sections", array("course"=>"$courseid"), "section",
-                           "section, id, course, name, summary, summaryformat, sequence, visible");
+                'section, id, course, name, summary, summaryformat, sequence, visible, ' .
+                'availablefrom, availableuntil, showavailability, groupingid');
     }
     return $coursesections[$courseid];
 }
@@ -1384,7 +1389,6 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false,
     static $strmovehere;
     static $strmovefull;
     static $strunreadpostsone;
-    static $groupings;
     static $modulenames;
 
     if (!isset($initialised)) {
@@ -1586,9 +1590,7 @@ function print_section($course, $section, $mods, $modnamesused, $absolute=false,
                 }
 
                 if (!empty($mod->groupingid) && has_capability('moodle/course:managegroups', get_context_instance(CONTEXT_COURSE, $course->id))) {
-                    if (!isset($groupings)) {
-                        $groupings = groups_get_all_groupings($course->id);
-                    }
+                    $groupings = groups_get_all_groupings($course->id);
                     echo " <span class=\"groupinglabel\">(".format_string($groupings[$mod->groupingid]->name).')</span>';
                 }
             } else {
@@ -1825,10 +1827,10 @@ function print_section_add_menus($course, $section, $modnames, $vertical=false,
     $straddactivity = get_string('addactivity');
     $straddresource = get_string('addresource');
 
-    $output  = '<div class="section_add_menus">';
+    $output = html_writer::start_tag('div', array('class' => 'section_add_menus', 'id' => 'add_menus-section-' . $section));
 
     if (!$vertical) {
-        $output .= '<div class="horizontal">';
+        $output .= html_writer::start_tag('div', array('class' => 'horizontal'));
     }
 
     if (!empty($resources)) {
@@ -1844,11 +1846,34 @@ function print_section_add_menus($course, $section, $modnames, $vertical=false,
     }
 
     if (!$vertical) {
-        $output .= '</div>';
+        $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', $straddeither);
+        $span = html_writer::tag('span', $straddeither, array('class' => 'section-modchooser-text'));
+        $modchooser.= html_writer::link('#', $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', 1);
+        if ($usemodchooser) {
+            $output = html_writer::tag('div', $output, array('class' => 'hiddenifjs addresourcedropdown'));
+            $modchooser = html_writer::tag('div', $modchooser, array('class' => 'visibleifjs addresourcemodchooser'));
+        } else {
+            $output = html_writer::tag('div', $output, array('class' => 'visibleifjs addresourcedropdown'));
+            $modchooser = html_writer::tag('div', $modchooser, array('class' => 'hiddenifjs addresourcemodchooser'));
+        }
+        $output = $modchooser . $output;
     }
 
-    $output .= '</div>';
-
     if ($return) {
         return $output;
     } else {
@@ -1934,8 +1959,14 @@ function get_module_metadata($course, $modnames) {
             $module->name = $modname;
             $module->link = $urlbase . $modname;
             $module->icon = $OUTPUT->pix_icon('icon', '', $module->name, array('class' => 'icon'));
-            if (get_string_manager()->string_exists('modulename_help', $modname)) {
+            $sm = get_string_manager();
+            if ($sm->string_exists('modulename_help', $modname)) {
                 $module->help = get_string('modulename_help', $modname);
+                if ($sm->string_exists('modulename_link', $modname)) {  // Link to further info in Moodle docs
+                    $link = get_string('modulename_link', $modname);
+                    $linktext = get_string('morehelp');
+                    $module->help .= html_writer::tag('div', $OUTPUT->doc_link($link, $linktext), array('class' => 'helpdoclink'));
+                }
             }
             $module->archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
             $modlist[$course->id][$modname] = $module;
@@ -2750,8 +2781,10 @@ function get_course_section($section, $courseid) {
     $cw->summaryformat = FORMAT_HTML;
     $cw->sequence = "";
     $id = $DB->insert_record("course_sections", $cw);
+    rebuild_course_cache($courseid, true);
     return $DB->get_record("course_sections", array("id"=>$id));
 }
+
 /**
  * Given a full mod object with section and course already defined, adds this module to that section.
  *
@@ -3026,11 +3059,11 @@ function move_section_to($course, $section, $destination) {
     // If we move the highlighted section itself, then just highlight the destination.
     // Adjust the higlighted section location if we move something over it either direction.
     if ($section == $course->marker) {
-        course_set_marker($course, $destination);
+        course_set_marker($course->id, $destination);
     } elseif ($moveup && $section > $course->marker && $course->marker >= $destination) {
-        course_set_marker($course, $course->marker+1);
+        course_set_marker($course->id, $course->marker+1);
     } elseif (!$moveup && $section < $course->marker && $course->marker <= $destination) {
-        course_set_marker($course, $course->marker-1);
+        course_set_marker($course->id, $course->marker-1);
     }
 
     $transaction->allow_commit();
@@ -3155,7 +3188,7 @@ function moveto_module($mod, $section, $beforemod=NULL) {
  * @return string XHTML for the editing buttons
  */
 function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $moveselect = true, $indent=-1, $section=-1) {
-    global $CFG, $OUTPUT;
+    global $CFG, $OUTPUT, $COURSE;
 
     static $str;
 
@@ -3191,6 +3224,7 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
         $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()));
@@ -3200,6 +3234,16 @@ function make_editing_buttons(stdClass $mod, $absolute_ignored = true, $movesele
     }
     $actions = array();
 
+    // AJAX edit title
+    if ($mod->modname !== 'label' && $hasmanageactivities && course_ajax_enabled($COURSE)) {
+        $actions[] = new action_link(
+            new moodle_url($baseurl, array('update' => $mod->id)),
+            new pix_icon('t/editstring', $str->edittitle, 'moodle', array('class' => 'iconsmall visibleifjs')),
+            null,
+            array('class' => 'editing_title', 'title' => $str->edittitle)
+        );
+    }
+
     // leftright
     if ($hasmanageactivities) {
         if (right_to_left()) {   // Exchange arrows on RTL
@@ -3478,10 +3522,14 @@ function category_delete_move($category, $newparentid, $showfeedback=true) {
 
     if ($courses = $DB->get_records('course', array('category'=>$category->id), 'sortorder ASC', 'id')) {
         if (!move_courses(array_keys($courses), $newparentid)) {
-            echo $OUTPUT->notification("Error moving courses");
+            if ($showfeedback) {
+                echo $OUTPUT->notification("Error moving courses");
+            }
             return false;
         }
-        echo $OUTPUT->notification(get_string('coursesmovedout', '', format_string($category->name)), 'notifysuccess');
+        if ($showfeedback) {
+            echo $OUTPUT->notification(get_string('coursesmovedout', '', format_string($category->name)), 'notifysuccess');
+        }
     }
 
     // move or delete cohorts in this context
@@ -3490,7 +3538,9 @@ function category_delete_move($category, $newparentid, $showfeedback=true) {
     // now delete anything that may depend on course category context
     grade_course_category_delete($category->id, $newparentid, $showfeedback);
     if (!question_delete_course_category($category, $newparentcat, $showfeedback)) {
-        echo $OUTPUT->notification(get_string('errordeletingquestionsfromcategory', 'question', $category), 'notifysuccess');
+        if ($showfeedback) {
+            echo $OUTPUT->notification(get_string('errordeletingquestionsfromcategory', 'question', $category), 'notifysuccess');
+        }
         return false;
     }
 
@@ -3500,8 +3550,9 @@ function category_delete_move($category, $newparentid, $showfeedback=true) {
 
     events_trigger('course_category_deleted', $category);
 
-    echo $OUTPUT->notification(get_string('coursecategorydeleted', '', format_string($category->name)), 'notifysuccess');
-
+    if ($showfeedback) {
+        echo $OUTPUT->notification(get_string('coursecategorydeleted', '', format_string($category->name)), 'notifysuccess');
+    }
     return true;
 }
 
@@ -4396,30 +4447,62 @@ function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
     }
 }
 
+/**
+ * Determine whether course ajax should be enabled for the specified course
+ *
+ * @param stdClass $course The course to test against
+ * @return boolean Whether course ajax is enabled or note
+ */
+function course_ajax_enabled($course) {
+    global $CFG, $PAGE, $SITE;
+
+    // Ajax must be enabled globally
+    if (!$CFG->enableajax) {
+        return false;
+    }
+
+    // The user must be editing for AJAX to be included
+    if (!$PAGE->user_is_editing()) {
+        return false;
+    }
+
+    // Check that the theme suports
+    if (!$PAGE->theme->enablecourseajax) {
+        return false;
+    }
+
+    // Check that the course format supports ajax functionality
+    // The site 'format' doesn't have information on course format support
+    if ($SITE->id !== $course->id) {
+        $courseformatajaxsupport = course_format_ajax_support($course->format);
+        if (!$courseformatajaxsupport->capable) {
+            return false;
+        }
+    }
+
+    // All conditions have been met so course ajax should be enabled
+    return true;
+}
+
 /**
  * Include the relevant javascript and language strings for the resource
  * toolbox YUI module
  *
  * @param integer $id The ID of the course being applied to
- * @param array $modules An array containing the names of the modules in
- *                       use on the page
- * @param object $config An object containing configuration parameters for ajax modules including:
+ * @param array $usedmodules An array containing the names of the modules in use on the page
+ * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site
+ * @param stdClass $config An object containing configuration parameters for ajax modules including:
  *          * resourceurl   The URL to post changes to for resource changes
  *          * sectionurl    The URL to post changes to for section changes
  *          * pageparams    Additional parameters to pass through in the post
- * @return void
+ * @return bool
  */
-function include_course_ajax($course, $modules = array(), $config = null) {
-    global $PAGE, $CFG, $USER;
+function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) {
+    global $PAGE, $SITE;
 
     // Ensure that ajax should be included
-    $courseformatajaxsupport = course_format_ajax_support($course->format);
-    if (!$PAGE->theme->enablecourseajax
-        || !$CFG->enableajax
-        || empty($USER->editing)
-        || !$PAGE->user_is_editing()
-        || ($course->id != SITEID && !$courseformatajaxsupport->capable)) {
-        return;
+    if (!course_ajax_enabled($course)) {
+        return false;
     }
 
     if (!$config) {
@@ -4461,7 +4544,7 @@ function include_course_ajax($course, $modules = array(), $config = null) {
     );
 
     // Include course dragdrop
-    if ($course->id != SITEID) {
+    if ($course->id != $SITE->id) {
         $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop',
             array(array(
                 'courseid' => $course->id,
@@ -4491,6 +4574,8 @@ function include_course_ajax($course, $modules = array(), $config = null) {
             'moveleft',
             'deletechecktype',
             'deletechecktypename',
+            'edittitle',
+            'edittitleinstructions',
             'show',
             'hide',
             'groupsnone',
@@ -4504,7 +4589,7 @@ function include_course_ajax($course, $modules = array(), $config = null) {
         ), 'moodle');
 
     // Include format-specific strings
-    if ($course->id != SITEID) {
+    if ($course->id != $SITE->id) {
         $PAGE->requires->strings_for_js(array(
                 'showfromothers',
                 'hidefromothers',
@@ -4512,9 +4597,25 @@ function include_course_ajax($course, $modules = array(), $config = null) {
     }
 
     // For confirming resource deletion we need the name of the module in question
-    foreach ($modules as $module => $modname) {
+    foreach ($usedmodules as $module => $modname) {
         $PAGE->requires->string_for_js('pluginname', $module);
     }
+
+    // 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))
+    );
+    $PAGE->requires->strings_for_js(array(
+            'addresourceoractivity',
+            'modchooserenable',
+            'modchooserdisable',
+    ), 'moodle');
+
+    return true;
 }
 
 /**
index dd32419..a9a3b53 100644 (file)
@@ -252,10 +252,10 @@ abstract class moodleform_mod extends moodleform {
             foreach($fullcm->conditionsgrade as $gradeitemid=>$minmax) {
                 $groupelements=$mform->getElement('conditiongradegroup['.$num.']')->getElements();
                 $groupelements[0]->setValue($gradeitemid);
-                // These numbers are always in the format 0.00000 - the rtrims remove any final zeros and,
-                // if it is a whole number, the decimal place.
-                $groupelements[2]->setValue(is_null($minmax->min)?'':rtrim(rtrim($minmax->min,'0'),'.'));
-                $groupelements[4]->setValue(is_null($minmax->max)?'':rtrim(rtrim($minmax->max,'0'),'.'));
+                $groupelements[2]->setValue(is_null($minmax->min) ? '' :
+                        format_float($minmax->min, 5, true, true));
+                $groupelements[4]->setValue(is_null($minmax->max) ? '' :
+                        format_float($minmax->max, 5, true, true));
                 $num++;
             }
 
@@ -321,16 +321,18 @@ abstract class moodleform_mod extends moodleform {
         // Conditions: Verify that the grade conditions are numbers, and make sense.
         if (array_key_exists('conditiongradegroup', $data)) {
             foreach ($data['conditiongradegroup'] as $i => $gradedata) {
-                if ($gradedata['conditiongrademin'] !== '' && !is_numeric($gradedata['conditiongrademin'])) {
+                if ($gradedata['conditiongrademin'] !== '' &&
+                        !is_numeric(unformat_float($gradedata['conditiongrademin']))) {
                     $errors["conditiongradegroup[{$i}]"] = get_string('gradesmustbenumeric', 'condition');
                     continue;
                 }
-                if ($gradedata['conditiongrademax'] !== '' && !is_numeric($gradedata['conditiongrademax'])) {
+                if ($gradedata['conditiongrademax'] !== '' &&
+                        !is_numeric(unformat_float($gradedata['conditiongrademax']))) {
                     $errors["conditiongradegroup[{$i}]"] = get_string('gradesmustbenumeric', 'condition');
                     continue;
                 }
                 if ($gradedata['conditiongrademin'] !== '' && $gradedata['conditiongrademax'] !== '' &&
-                        $gradedata['conditiongrademax'] < $gradedata['conditiongrademin']) {
+                        unformat_float($gradedata['conditiongrademax']) < unformat_float($gradedata['conditiongrademin'])) {
                     $errors["conditiongradegroup[{$i}]"] = get_string('badgradelimits', 'condition');
                     continue;
                 }
index 1e1d7ad..2831fed 100644 (file)
@@ -154,4 +154,155 @@ class core_course_renderer extends plugin_renderer_base {
         $content .= html_writer::end_tag('div');
         return $content;
     }
+
+    /**
+     * Build the HTML for the module chooser javascript popup
+     *
+     * @param array $modules A set of modules as returned form @see
+     * get_module_metadata
+     * @param object $course The course that will be displayed
+     * @return string The composed HTML for the module
+     */
+    public function course_modchooser($modules, $course) {
+        global $OUTPUT;
+
+        // Add the header
+        $header = html_writer::tag('div', get_string('addresourceoractivity', 'moodle'),
+                array('id' => 'choosertitle', 'class' => 'hd'));
+
+        $formcontent = html_writer::start_tag('form', array('action' => new moodle_url('/course/jumpto.php'),
+                'id' => 'chooserform', 'method' => 'post'));
+        $formcontent .= html_writer::start_tag('div', array('id' => 'typeformdiv'));
+        $formcontent .= html_writer::tag('input', '', array('type' => 'hidden', 'id' => 'course',
+                'name' => 'course', 'value' => $course->id));
+        $formcontent .= html_writer::tag('input', '',
+                array('type' => 'hidden', 'id' => 'jump', 'name' => 'jump', 'value' => ''));
+        $formcontent .= html_writer::tag('input', '', array('type' => 'hidden', 'name' => 'sesskey',
+                'value' => sesskey()));
+        $formcontent .= html_writer::end_tag('div');
+
+        // Put everything into one tag 'options'
+        $formcontent .= html_writer::start_tag('div', array('class' => 'options'));
+        $formcontent .= html_writer::tag('div', get_string('selectmoduletoviewhelp', 'moodle'),
+                array('class' => 'instruction'));
+        // Put all options into one tag 'alloptions' to allow us to handle scrolling
+        $formcontent .= html_writer::start_tag('div', array('class' => 'alloptions'));
+
+        // First display Resources
+        $resources = array_filter($modules,
+                create_function('$mod', 'return ($mod->archetype === MOD_CLASS_RESOURCE);'));
+        if (count($resources)) {
+            $formcontent .= $this->course_modchooser_title('resources');
+            $formcontent .= $this->course_modchooser_module_types($resources);
+        }
+
+        // Then activities
+        $activities = array_filter($modules,
+                create_function('$mod', 'return ($mod->archetype !== MOD_CLASS_RESOURCE);'));
+        if (count($activities)) {
+            $formcontent .= $this->course_modchooser_title('activities');
+            $formcontent .= $this->course_modchooser_module_types($activities);
+        }
+
+        $formcontent .= html_writer::end_tag('div'); // modoptions
+        $formcontent .= html_writer::end_tag('div'); // types
+
+        $formcontent .= html_writer::start_tag('div', array('class' => 'submitbuttons'));
+        $formcontent .= html_writer::tag('input', '',
+                array('type' => 'submit', 'name' => 'addcancel', 'id' => 'addcancel', 'value' => get_string('cancel')));
+        $formcontent .= html_writer::tag('input', '',
+                array('type' => 'submit', 'name' => 'submitbutton', 'id' => 'submitbutton', 'value' => get_string('add')));
+        $formcontent .= html_writer::end_tag('div');
+        $formcontent .= html_writer::end_tag('form');
+
+        // Wrap the whole form in a div
+        $formcontent = html_writer::tag('div', $formcontent, array('id' => 'chooseform'));
+
+        // Put all of the content together
+        $content = $formcontent;
+
+        $content = html_writer::tag('div', $content, array('id' => 'choosercontainer'));
+        return $header . html_writer::tag('div', $content, array('id' => 'chooserdialogue'));
+    }
+
+    /**
+     * Build the HTML for a specified set of modules
+     *
+     * @param array $modules A set of modules as used by the
+     * course_modchooser_module function
+     * @return string The composed HTML for the module
+     */
+    protected function course_modchooser_module_types($modules) {
+        $return = '';
+        foreach ($modules as $module) {
+            if (!isset($module->types)) {
+                $return .= $this->course_modchooser_module($module);
+            } else {
+                $return .= $this->course_modchooser_module($module, array('nonoption'));
+                foreach ($module->types as $type) {
+                    $return .= $this->course_modchooser_module($type, array('option', 'subtype'));
+                }
+            }
+        }
+        return $return;
+    }
+
+    /**
+     * Return the HTML for the specified module adding any required classes
+     *
+     * @param object $module An object containing the title, and link. An
+     * icon, and help text may optionally be specified. If the module
+     * contains subtypes in the types option, then these will also be
+     * displayed.
+     * @param array $classes Additional classes to add to the encompassing
+     * div element
+     * @return string The composed HTML for the module
+     */
+    protected function course_modchooser_module($module, $classes = array('option')) {
+        $output = '';
+        $output .= html_writer::start_tag('div', array('class' => implode(' ', $classes)));
+        $output .= html_writer::start_tag('label', array('for' => 'module_' . $module->name));
+        if (!isset($module->types)) {
+            $output .= html_writer::tag('input', '', array('type' => 'radio',
+                    'name' => 'jumplink', 'id' => 'module_' . $module->name, 'value' => $module->link));
+        }
+
+        $output .= html_writer::start_tag('span', array('class' => 'modicon'));
+        if (isset($module->icon)) {
+            // Add an icon if we have one
+            $output .= $module->icon;
+        }
+        $output .= html_writer::end_tag('span');
+
+        $output .= html_writer::tag('span', $module->title, array('class' => 'typename'));
+        if (!isset($module->help)) {
+            // Add help if found
+            $module->help = get_string('nohelpforactivityorresource', 'moodle');
+        }
+
+        // Format the help text using markdown with the following options
+        $options = new stdClass();
+        $options->trusted = false;
+        $options->noclean = false;
+        $options->smiley = false;
+        $options->filter = false;
+        $options->para = true;
+        $options->newlines = false;
+        $options->overflowdiv = false;
+        $module->help = format_text($module->help, FORMAT_MARKDOWN, $options);
+        $output .= html_writer::tag('span', $module->help, array('class' => 'typesummary'));
+        $output .= html_writer::end_tag('label');
+        $output .= html_writer::end_tag('div');
+
+        return $output;
+    }
+
+    protected function course_modchooser_title($title, $identifier = null) {
+        $module = new stdClass();
+        $module->name = $title;
+        $module->types = array();
+        $module->title = get_string($title, $identifier);
+        $module->help = '';
+        return $this->course_modchooser_module($module, array('moduletypetitle'));
+    }
 }
index 8ec14e6..5152563 100644 (file)
@@ -43,6 +43,7 @@ $summary    = optional_param('summary', '', PARAM_RAW);
 $sequence   = optional_param('sequence', '', PARAM_SEQUENCE);
 $visible    = optional_param('visible', 0, PARAM_INT);
 $pageaction = optional_param('action', '', PARAM_ALPHA); // Used to simulate a DELETE command
+$title      = optional_param('title', '', PARAM_TEXT);
 
 $PAGE->set_url('/course/rest.php', array('courseId'=>$courseid,'class'=>$class));
 
@@ -56,8 +57,8 @@ if (in_array($class, array('resource'))) {
     $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
 } else {
     require_login($course);
-    $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
 }
+$coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
 require_sesskey();
 
 echo $OUTPUT->header(); // send headers
@@ -128,6 +129,39 @@ switch($requestmethod) {
 
                         moveto_module($cm, $section, $beforemod);
                         break;
+                    case 'gettitle':
+                        require_capability('moodle/course:manageactivities', $modcontext);
+                        $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST);
+                        $module = new stdClass();
+                        $module->id = $cm->instance;
+
+                        // Don't pass edit strings through multilang filters - we need the entire string
+                        echo json_encode(array('instancename' => $cm->name));
+                        break;
+                    case 'updatetitle':
+                        require_capability('moodle/course:manageactivities', $modcontext);
+                        $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST);
+                        $module = new stdClass();
+                        $module->id = $cm->instance;
+
+                        // Escape strings as they would be by mform
+                        if (!empty($CFG->formatstringstriptags)) {
+                            $module->name = clean_param($title, PARAM_TEXT);
+                        } else {
+                            $module->name = clean_param($title, PARAM_CLEANHTML);
+                        }
+
+                        if (!empty($module->name)) {
+                            $DB->update_record($cm->modname, $module);
+                        } else {
+                            $module->name = $cm->name;
+                        }
+
+                        // We need to return strings after they've been through filters for multilang
+                        $stringoptions = new stdClass;
+                        $stringoptions->context = $coursecontext;
+                        echo json_encode(array('instancename' => format_string($module->name, true,  $stringoptions)));
+                        break;
                 }
                 rebuild_course_cache($course->id);
                 break;
diff --git a/course/style.css b/course/style.css
new file mode 100644 (file)
index 0000000..e69de29
index 7a4d7aa..a7b1784 100644 (file)
@@ -5,6 +5,7 @@
     require_once('../config.php');
     require_once('lib.php');
     require_once($CFG->dirroot.'/mod/forum/lib.php');
+    require_once($CFG->libdir.'/conditionlib.php');
     require_once($CFG->libdir.'/completionlib.php');
 
     $id          = optional_param('id', 0, PARAM_INT);
@@ -17,6 +18,7 @@
     $move        = optional_param('move', 0, PARAM_INT);
     $marker      = optional_param('marker',-1 , PARAM_INT);
     $switchrole  = optional_param('switchrole',-1, PARAM_INT);
+    $modchooser  = optional_param('modchooser', -1, PARAM_BOOL);
 
     $params = array();
     if (!empty($name)) {
@@ -38,6 +40,9 @@
 
     $PAGE->set_url('/course/view.php', $urlparams); // Defined here to avoid notices on errors etc
 
+    // Prevent caching of this page to stop confusion when changing page after making AJAX changes
+    $PAGE->set_cacheable(false);
+
     preload_course_contexts($course->id);
     $context = context_course::instance($course->id, MUST_EXIST);
 
                 redirect($PAGE->url);
             }
         }
+        if (($modchooser == 1) && confirm_sesskey()) {
+            set_user_preference('usemodchooser', $modchooser);
+        } else if (($modchooser == 0) && confirm_sesskey()) {
+            set_user_preference('usemodchooser', $modchooser);
+        }
 
         if (has_capability('moodle/course:update', $context)) {
             if ($hide && confirm_sesskey()) {
         }
     }
 
-    if (! $sections = get_all_sections($course->id)) {   // No sections found
-        // Double-check to be extra sure
-        if (! $section = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>0))) {
-            $section->course = $course->id;   // Create a default section.
-            $section->section = 0;
-            $section->visible = 1;
-            $section->summaryformat = FORMAT_HTML;
-            $section->id = $DB->insert_record('course_sections', $section);
-        }
-        if (! $sections = get_all_sections($course->id) ) {      // Try again
+    if (!$sections = $modinfo->get_section_info_all()) {   // No sections found
+        $section = new stdClass;
+        $section->course = $course->id;   // Create a default section.
+        $section->section = 0;
+        $section->visible = 1;
+        $section->summaryformat = FORMAT_HTML;
+        $section->id = $DB->insert_record('course_sections', $section);
+        rebuild_course_cache($course->id);
+        $modinfo = get_fast_modinfo($COURSE);
+        if (!$sections = $modinfo->get_section_info_all()) {      // Try again
             print_error('cannotcreateorfindstructs', 'error');
         }
     }
 
     echo html_writer::end_tag('div');
 
-    // Include the command toolbox YUI module
-    include_course_ajax($course, $modnamesused);
+    // 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), $course);
+    }
 
     echo $OUTPUT->footer();
diff --git a/course/yui/modchooser/modchooser.js b/course/yui/modchooser/modchooser.js
new file mode 100644 (file)
index 0000000..5d95a02
--- /dev/null
@@ -0,0 +1,185 @@
+YUI.add('moodle-course-modchooser', function(Y) {
+    var CSS = {
+        PAGECONTENT : 'div#page-content',
+        SECTION : 'li.section',
+        SECTIONMODCHOOSER : 'a.section-modchooser-link',
+        SITEMENU : 'div.block_site_main_menu',
+        SITETOPIC : 'div.sitetopic'
+    };
+
+    var MODCHOOSERNAME = 'course-modchooser';
+
+    var MODCHOOSER = function() {
+        MODCHOOSER.superclass.constructor.apply(this, arguments);
+    }
+
+    Y.extend(MODCHOOSER, M.core.chooserdialogue, {
+        // The current section ID
+        sectionid : null,
+
+        // The hidden element holding the jump param
+        jumplink : null,
+
+        initializer : function(config) {
+            var dialogue = Y.one('#chooserdialogue');
+            var header = Y.one('#choosertitle');
+            var params = {
+                width: '540px'
+            };
+            this.setup_chooser_dialogue(dialogue, header, params);
+            this.overlay.get('boundingBox').addClass('modchooser');
+
+            this.jumplink = this.container.one('#jump');
+
+            // Initialize existing sections and register for dynamically created sections
+            this.setup_for_section();
+            M.course.coursebase.register_module(this);
+
+            // Catch the page toggle
+            Y.all('.block_settings #settingsnav .type_course .modchoosertoggle a').on('click', this.toggle_mod_chooser, this);
+
+            // Ensure that help links are opened in an appropriate popup
+            this.container.all('div.helpdoclink a').on('click', function(e) {
+                var anchor = e.target.ancestor('a', true);
+
+                var args = {
+                    'name'          : 'popup',
+                    'url'           : anchor.getAttribute('href'),
+                    'option'        : ''
+                };
+                var options = [
+                    'height=600',
+                    'width=800',
+                    'top=0',
+                    'left=0',
+                    'menubar=0',
+                    'location=0',
+                    'scrollbars',
+                    'resizable',
+                    'toolbar',
+                    'status',
+                    'directories=0',
+                    'fullscreen=0',
+                    'dependent'
+                ]
+                args.options = options.join(',');
+
+                // Note: openpopup is provided by lib/javascript-static.js
+                openpopup(e, args);
+            });
+        },
+        /**
+         * Update any section areas within the scope of the specified
+         * selector with AJAX equivalents
+         *
+         * @param baseselector The selector to limit scope to
+         * @return void
+         */
+        setup_for_section : function(baseselector) {
+            if (!baseselector) {
+                var baseselector = CSS.PAGECONTENT;
+            }
+
+            // Setup for site topics
+            Y.one(baseselector).all(CSS.SITETOPIC).each(function(section) {
+                // The site topic has a sectionid of 1
+                this._setup_for_section(section, 1);
+            }, this);
+
+            // Setup for the site menu
+            Y.one(baseselector).all(CSS.SITEMENU).each(function(section) {
+                // The site menu has a sectionid of 0
+                this._setup_for_section(section, 0);
+            }, this);
+
+            // Setup for standard course topics
+            Y.one(baseselector).all(CSS.SECTION).each(function(section) {
+                // Determine the sectionid for this section
+                var sectionid = section.get('id').replace('section-', '');
+                this._setup_for_section(section, sectionid);
+            }, this);
+        },
+        _setup_for_section : function(section, sectionid) {
+            var chooserlink = section.one(CSS.SECTIONMODCHOOSER);
+            chooserlink.on('click', this.display_mod_chooser, this, sectionid);
+        },
+        /**
+         * Display the module chooser
+         *
+         * @param e Event Triggering Event
+         * @param secitonid integer The ID of the section triggering the dialogue
+         * @return void
+         */
+        display_mod_chooser : function (e, sectionid) {
+            // Set the section for this version of the dialogue
+            this.sectionid = sectionid;
+            this.display_chooser(e);
+        },
+        toggle_mod_chooser : function(e) {
+            // Get the add section link
+            var modchooserlinks = Y.all('div.addresourcemodchooser');
+
+            // Get the dropdowns
+            var dropdowns = Y.all('div.addresourcedropdown');
+
+            if (modchooserlinks.size() == 0) {
+                // Continue with non-js action if there are no modchoosers to add
+                return;
+            }
+
+            // We need to update the text and link
+            var togglelink = Y.one('.block_settings #settingsnav .type_course .modchoosertoggle a');
+
+            // The actual text is in the last child
+            var toggletext = togglelink.get('lastChild');
+
+            var usemodchooser;
+            // Determine whether they're currently hidden
+            if (modchooserlinks.item(0).hasClass('visibleifjs')) {
+                // The modchooser is currently visible, hide it
+                usemodchooser = 0;
+                modchooserlinks
+                    .removeClass('visibleifjs')
+                    .addClass('hiddenifjs');
+                dropdowns
+                    .addClass('visibleifjs')
+                    .removeClass('hiddenifjs');
+                toggletext.set('data', M.util.get_string('modchooserenable', 'moodle'));
+                togglelink.set('href', togglelink.get('href').replace('off', 'on'));
+            } else {
+                // The modchooser is currently not visible, show it
+                usemodchooser = 1;
+                modchooserlinks
+                    .addClass('visibleifjs')
+                    .removeClass('hiddenifjs');
+                dropdowns
+                    .removeClass('visibleifjs')
+                    .addClass('hiddenifjs');
+                toggletext.set('data', M.util.get_string('modchooserdisable', 'moodle'));
+                togglelink.set('href', togglelink.get('href').replace('on', 'off'));
+            }
+
+            M.util.set_user_preference('usemodchooser', usemodchooser);
+
+            // Prevent the page from reloading
+            e.preventDefault();
+        },
+        option_selected : function(thisoption) {
+            // Add the sectionid to the URL
+            this.jumplink.set('value', thisoption.get('value') + '&section=' + this.sectionid);
+        }
+    },
+    {
+        NAME : MODCHOOSERNAME,
+        ATTRS : {
+        }
+    });
+    M.course = M.course || {};
+    M.course.init_chooser = function(config) {
+        return new MODCHOOSER(config);
+    }
+},
+'@VERSION@', {
+    requires:['base', 'overlay', 'moodle-core-chooserdialogue', 'transition']
+}
+);
index 562307a..6dfcce0 100644 (file)
@@ -3,12 +3,13 @@ YUI.add('moodle-course-toolboxes', function(Y) {
     // The CSS selectors we use
     var CSS = {
         ACTIVITYLI : 'li.activity',
-        COMMANDSPAN : 'li.activity span.commands',
+        COMMANDSPAN : 'span.commands',
         SPINNERCOMMANDSPAN : 'span.commands',
         CONTENTAFTERLINK : 'div.contentafterlink',
         DELETE : 'a.editing_delete',
         DIMCLASS : 'dimmed',
         DIMMEDTEXT : 'dimmed_text',
+        EDITTITLE : 'a.editing_title',
         EDITTITLECLASS : 'edittitle',
         GENERICICONCLASS : 'iconsmall',
         GROUPSNONE : 'a.editing_groupsnone',
@@ -251,12 +252,15 @@ YUI.add('moodle-course-toolboxes', function(Y) {
          */
         setup_for_resource : function(baseselector) {
             if (!baseselector) {
-                var baseselector = CSS.PAGECONTENT;
+                var baseselector = CSS.PAGECONTENT + ' ' + CSS.ACTIVITYLI;;
             }
 
             Y.all(baseselector).each(this._setup_for_resource, this);
         },
         _setup_for_resource : function(toolboxtarget) {
+            // Edit Title
+            this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.EDITTITLE, this.edit_resource_title);
+
             // Move left and right
             this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.MOVELEFT, this.move_left);
             this.replace_button(toolboxtarget, CSS.COMMANDSPAN + ' ' + CSS.MOVERIGHT, this.move_right);
@@ -482,6 +486,126 @@ YUI.add('moodle-course-toolboxes', function(Y) {
             anchor.appendChild(newicon);
             anchor.on('click', this.move_left, this);
             moveright.insert(anchor, 'before');
+        },
+        /**
+         * Edit the title for the resource
+         */
+        edit_resource_title : function(e) {
+            // Get the element we're working on
+            var element = e.target.ancestor(CSS.ACTIVITYLI);
+            var instancename  = element.one(CSS.INSTANCENAME);
+            var currenttitle = instancename.get('firstChild');
+            var oldtitle = currenttitle.get('data');
+            var titletext = oldtitle;
+            var editbutton = element.one('a.' + CSS.EDITTITLECLASS + ' img');
+
+            // Handle events for edit_resource_title
+            var listenevents = [];
+            var thisevent;
+
+            // Grab the anchor so that we can swap it with the edit form
+            var anchor = instancename.ancestor('a');
+
+            var data = {
+                'class'   : 'resource',
+                'field'   : 'gettitle',
+                'id'      : this.get_element_id(element)
+            };
+
+            // Try to retrieve the existing string from the server
+            var response = this.send_request(data, editbutton);
+            if (response.instancename) {
+                titletext = response.instancename;
+            }
+
+            // Create the editor and submit button
+            var editor = Y.Node.create('<input />')
+                .setAttrs({
+                    'name'  : 'title',
+                    'value' : titletext,
+                    'autocomplete' : 'off'
+                })
+                .addClass('titleeditor');
+            var editform = Y.Node.create('<form />')
+                .setStyle('padding', '0')
+                .setStyle('display', 'inline')
+                .setAttribute('action', '#');
+
+            var editinstructions = Y.Node.create('<span />')
+                .addClass('editinstructions')
+                .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
+
+            // Clear the existing content and put the editor in
+            currenttitle.set('data', '');
+            editform.appendChild(editor);
+            anchor.replace(editform);
+            element.appendChild(editinstructions);
+            e.preventDefault();
+
+            // Focus and select the editor text
+            editor.focus().select();
+
+            // Handle removal of the editor
+            var clear_edittitle = function() {
+                // Detach all listen events to prevent duplicate triggers
+                var thisevent;
+                while (thisevent = listenevents.shift()) {
+                    thisevent.detach();
+                }
+
+                if (editinstructions) {
+                    // Convert back to anchor and remove instructions
+                    editform.replace(anchor);
+                    editinstructions.remove();
+                    editinstructions = null;
+                }
+            }
+
+            // Handle cancellation of the editor
+            var cancel_edittitle = function(e) {
+                clear_edittitle();
+
+                // Set the title and anchor back to their previous settings
+                currenttitle.set('data', oldtitle);
+            };
+
+            // Cancel the edit if we lose focus or the escape key is pressed
+            thisevent = editor.on('blur', cancel_edittitle);
+            listenevents.push(thisevent);
+            thisevent = Y.one('document').on('keyup', function(e) {
+                if (e.keyCode == 27) {
+                    cancel_edittitle(e);
+                }
+            });
+            listenevents.push(thisevent);
+
+            // Handle form submission
+            thisevent = editform.on('submit', function(e) {
+                // We don't actually want to submit anything
+                e.preventDefault();
+
+                // Clear the edit title boxes
+                clear_edittitle();
+
+                // We only accept strings which have valid content
+                var newtitle = Y.Lang.trim(editor.get('value'));
+                if (newtitle != null && newtitle != "" && newtitle != titletext) {
+                    var data = {
+                        'class'   : 'resource',
+                        'field'   : 'updatetitle',
+                        'title'   : newtitle,
+                        'id'      : this.get_element_id(element)
+                    };
+                    var response = this.send_request(data, editbutton);
+                    if (response.instancename) {
+                        currenttitle.set('data', response.instancename);
+                    }
+                } else {
+                    // Invalid content. Set the title back to it's original contents
+                    currenttitle.set('data', oldtitle);
+                }
+            }, this);
+            listenevents.push(thisevent);
         }
     }, {
         NAME : 'course-resource-toolbox',
index a5c02c6..daccfa8 100644 (file)
@@ -37,6 +37,10 @@ class grade_export_form extends moodleform {
         $mform->addElement('advcheckbox', 'export_feedback', get_string('exportfeedback', 'grades'));
         $mform->setDefault('export_feedback', 0);
 
+        $mform->addElement('advcheckbox', 'export_onlyactive', get_string('exportonlyactive', 'grades'));
+        $mform->setDefault('export_onlyactive', 0);
+        $mform->addHelpButton('export_onlyactive', 'exportonlyactive', 'grades');
+
         $options = array('10'=>10, '20'=>20, '100'=>100, '1000'=>1000, '100000'=>100000);
         $mform->addElement('select', 'previewrows', get_string('previewrows', 'grades'), $options);
 
index b71a7b3..1530d2c 100644 (file)
@@ -39,6 +39,7 @@ abstract class grade_export {
     public $updatedgradesonly; // only export updated grades
     public $displaytype; // display type (e.g. real, percentages, letter) for exports
     public $decimalpoints; // number of decimal points for exports
+    public $onlyactive; // only include users with an active enrolment
     /**
      * Constructor should set up all the private variables ready to be pulled
      * @access public
@@ -49,7 +50,7 @@ abstract class grade_export {
      * @param boolean $export_letters
      * @note Exporting as letters will lead to data loss if that exported set it re-imported.
      */
-    public function grade_export($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2) {
+    public function grade_export($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $onlyactive = false) {
         $this->course = $course;
         $this->groupid = $groupid;
         $this->grade_items = grade_item::fetch_all(array('courseid'=>$this->course->id));
@@ -83,6 +84,7 @@ abstract class grade_export {
 
         $this->displaytype = $displaytype;
         $this->decimalpoints = $decimalpoints;
+        $this->onlyactive = $onlyactive;
     }
 
     /**
@@ -125,6 +127,10 @@ abstract class grade_export {
             $this->export_feedback = $formdata->export_feedback;
         }
 
+        if (isset($formdata->export_onlyactive)) {
+            $this->onlyactive = $formdata->export_onlyactive;
+        }
+
         if (isset($formdata->previewrows)) {
             $this->previewrows = $formdata->previewrows;
         }
@@ -222,6 +228,7 @@ abstract class grade_export {
 
         $i = 0;
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             // number of preview rows
@@ -290,7 +297,8 @@ abstract class grade_export {
                         'export_feedback'   =>$this->export_feedback,
                         'updatedgradesonly' =>$this->updatedgradesonly,
                         'displaytype'       =>$this->displaytype,
-                        'decimalpoints'     =>$this->decimalpoints);
+                        'decimalpoints'     =>$this->decimalpoints,
+                        'export_onlyactive' =>$this->onlyactive);
 
         return $params;
     }
index f81f1d6..04a00e8 100644 (file)
@@ -26,6 +26,7 @@ $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
 $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive        = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -44,7 +45,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 
 // print all the exported data here
-$export = new grade_export_ods($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_ods($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
 $export->print_grades();
 
 
index e9ff095..e366131 100644 (file)
@@ -64,6 +64,7 @@ class grade_export_ods extends grade_export {
         $i = 0;
         $geub = new grade_export_update_buffer();
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $i++;
index f0d4b3c..78cd116 100644 (file)
@@ -51,7 +51,7 @@ if ($groupmode == SEPARATEGROUPS and !$currentgroup and !has_capability('moodle/
 
 // process post information
 if ($data = $mform->get_data()) {
-    $export = new grade_export_ods($course, $currentgroup, '', false, false, $data->display, $data->decimals);
+    $export = new grade_export_ods($course, $currentgroup, '', false, false, $data->display, $data->decimals, $data->export_onlyactive);
 
     // print the grades on screen for feedbacks
     $export->process_form($data);
index ab7a03b..e5e572c 100644 (file)
@@ -27,6 +27,7 @@ $separator         = optional_param('separator', 'comma', PARAM_ALPHA);
 $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive        = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -45,7 +46,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 
 // print all the exported data here
-$export = new grade_export_txt($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $separator);
+$export = new grade_export_txt($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $separator, $onlyactive);
 $export->print_grades();
 
 
index 77f804b..0be9589 100644 (file)
@@ -23,13 +23,8 @@ class grade_export_txt extends grade_export {
 
     public $separator; // default separator
 
-    public function grade_export_txt($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator='comma') {
-        $this->grade_export($course, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
-        $this->separator = $separator;
-    }
-
-    public function __construct($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator='comma') {
-        parent::__construct($course, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+    public function __construct($course, $groupid=0, $itemlist='', $export_feedback=false, $updatedgradesonly = false, $displaytype = GRADE_DISPLAY_TYPE_REAL, $decimalpoints = 2, $separator = 'comma', $onlyactive = false) {
+        parent::__construct($course, $groupid, $itemlist, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
         $this->separator = $separator;
     }
 
@@ -91,6 +86,7 @@ class grade_export_txt extends grade_export {
 /// Print all the lines of data.
         $geub = new grade_export_update_buffer();
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
 
index 47de3e7..7f5f842 100644 (file)
@@ -51,7 +51,7 @@ if ($groupmode == SEPARATEGROUPS and !$currentgroup and !has_capability('moodle/
 
 // process post information
 if ($data = $mform->get_data()) {
-    $export = new grade_export_txt($course, $currentgroup, '', false, false, $data->display, $data->decimals, $data->separator);
+    $export = new grade_export_txt($course, $currentgroup, '', false, false, $data->display, $data->decimals, $data->separator, $data->export_onlyactive);
 
     // print the grades on screen for feedback
 
index 4df1fec..8526f3c 100644 (file)
@@ -26,6 +26,7 @@ $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
 $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive        = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -44,7 +45,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 
 // print all the exported data here
-$export = new grade_export_xls($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_xls($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
 $export->print_grades();
 
 
index f1efc6e..a89c910 100644 (file)
@@ -63,6 +63,7 @@ class grade_export_xls extends grade_export {
         $i = 0;
         $geub = new grade_export_update_buffer();
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $i++;
index fc509bc..68d7f67 100644 (file)
@@ -51,7 +51,7 @@ if ($groupmode == SEPARATEGROUPS and !$currentgroup and !has_capability('moodle/
 
 // process post information
 if ($data = $mform->get_data()) {
-    $export = new grade_export_xls($course, $currentgroup, '', false, false, $data->display, $data->decimals);
+    $export = new grade_export_xls($course, $currentgroup, '', false, false, $data->display, $data->decimals, $data->export_onlyactive);
 
     // print the grades on screen for feedbacks
     $export->process_form($data);
index 7807993..1424fe8 100644 (file)
@@ -26,6 +26,7 @@ $export_feedback   = optional_param('export_feedback', 0, PARAM_BOOL);
 $updatedgradesonly = optional_param('updatedgradesonly', false, PARAM_BOOL);
 $displaytype       = optional_param('displaytype', $CFG->grade_export_displaytype, PARAM_INT);
 $decimalpoints     = optional_param('decimalpoints', $CFG->grade_export_decimalpoints, PARAM_INT);
+$onlyactive        = optional_param('export_onlyactive', 0, PARAM_BOOL);
 
 if (!$course = $DB->get_record('course', array('id'=>$id))) {
     print_error('nocourseid');
@@ -44,7 +45,7 @@ if (groups_get_course_groupmode($COURSE) == SEPARATEGROUPS and !has_capability('
 }
 
 // print all the exported data here
-$export = new grade_export_xml($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints);
+$export = new grade_export_xml($course, $groupid, $itemids, $export_feedback, $updatedgradesonly, $displaytype, $decimalpoints, $onlyactive);
 $export->print_grades();
 
 
index b349b89..396a044 100644 (file)
@@ -54,6 +54,7 @@ class grade_export_xml extends grade_export {
 
         $geub = new grade_export_update_buffer();
         $gui = new graded_users_iterator($this->course, $this->columns, $this->groupid);
+        $gui->require_active_enrolment($this->onlyactive);
         $gui->init();
         while ($userdata = $gui->next_user()) {
             $user = $userdata->user;
index bc4d925..3903846 100644 (file)
@@ -52,7 +52,7 @@ if ($groupmode == SEPARATEGROUPS and !$currentgroup and !has_capability('moodle/
 
 // process post information
 if ($data = $mform->get_data()) {
-    $export = new grade_export_xml($course, $currentgroup, '', false, $data->updatedgradesonly, $data->display, $data->decimals);
+    $export = new grade_export_xml($course, $currentgroup, '', false, $data->updatedgradesonly, $data->display, $data->decimals, $data->export_onlyactive);
 
     // print the grades on screen for feedbacks
     $export->process_form($data);
index 8c969f2..185fed5 100644 (file)
@@ -43,6 +43,11 @@ class graded_users_iterator {
     public $sortfield2;
     public $sortorder2;
 
+    /**
+     * Should users whose enrolment has been suspended be ignored?
+     */
+    protected $onlyactive = false;
+
     /**
      * Constructor
      *
@@ -89,9 +94,7 @@ class graded_users_iterator {
 
         list($gradebookroles_sql, $params) =
             $DB->get_in_or_equal(explode(',', $CFG->gradebookroles), SQL_PARAMS_NAMED, 'grbr');
-
-        //limit to users with an active enrolment
-        list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext);
+        list($enrolledsql, $enrolledparams) = get_enrolled_sql($coursecontext, '', 0, $this->onlyactive);
 
         $params = array_merge($params, $enrolledparams);
 
@@ -215,6 +218,9 @@ class graded_users_iterator {
 
         if (!empty($this->grade_items)) {
             foreach ($this->grade_items as $grade_item) {
+                if (!isset($feedbacks[$grade_item->id])) {
+                    $feedbacks[$grade_item->id] = new stdClass();
+                }
                 if (array_key_exists($grade_item->id, $grade_records)) {
                     $feedbacks[$grade_item->id]->feedback       = $grade_records[$grade_item->id]->feedback;
                     $feedbacks[$grade_item->id]->feedbackformat = $grade_records[$grade_item->id]->feedbackformat;
@@ -253,6 +259,18 @@ class graded_users_iterator {
         $this->gradestack = array();
     }
 
+    /**
+     * Should all enrolled users be exported or just those with an active enrolment?
+     *
+     * @param bool $onlyactive True to limit the export to users with an active enrolment
+     */
+    public function require_active_enrolment($onlyactive = true) {
+        if (!empty($this->users_rs)) {
+            debugging('Calling require_active_enrolment() has no effect unless you call init() again', DEBUG_DEVELOPER);
+        }
+        $this->onlyactive  = $onlyactive;
+    }
+
 
     /**
      * _push
index 9d03a57..83d3d6c 100644 (file)
@@ -391,8 +391,7 @@ class grade_report_grader extends grade_report {
         list($enrolledsql, $enrolledparams) = get_enrolled_sql($this->context);
 
         //fields we need from the user table
-        $userfields = user_picture::fields('u');
-        $userfields .= get_extra_user_fields_sql($this->context);
+        $userfields = user_picture::fields('u', get_extra_user_fields($this->context));
 
         $sortjoin = $sort = $params = null;
 
index a1750df..158b8eb 100644 (file)
@@ -40,6 +40,7 @@ if (!$course = $DB->get_record('course', array('id' => $courseid))) {
 require_login($course);
 $context = get_context_instance(CONTEXT_COURSE, $course->id);
 require_capability('moodle/course:managegroups', $context);
+$changeidnumber = has_capability('moodle/course:changeidnumber', $context);
 
 // Make sure all groups are OK and belong to course
 $groupidarray = explode(',',$groupids);
@@ -48,6 +49,9 @@ foreach($groupidarray as $groupid) {
     if (!$group = $DB->get_record('groups', array('id' => $groupid))) {
         print_error('invalidgroupid');
     }
+    if (!empty($group->idnumber) && !$changeidnumber) {
+        print_error('grouphasidnumber', '', '', $group->name);
+    }
     if ($courseid != $group->courseid) {
         print_error('groupunknown', '', '', $group->courseid);
     }
index 128ad9e..b0e7ce9 100644 (file)
@@ -550,6 +550,560 @@ class core_group_external extends external_api {
         return null;
     }
 
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 2.3
+     */
+    public static function create_groupings_parameters() {
+        return new external_function_parameters(
+            array(
+                'groupings' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'courseid' => new external_value(PARAM_INT, 'id of course'),
+                            'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
+                            'description' => new external_value(PARAM_RAW, 'grouping description text')
+                        )
+                    ), 'List of grouping object. A grouping has a courseid, a name and a description.'
+                )
+            )
+        );
+    }
+
+    /**
+     * Create groupings
+     *
+     * @param array $groupings array of grouping description arrays (with keys groupname and courseid)
+     * @return array of newly created groupings
+     * @since Moodle 2.3
+     */
+    public static function create_groupings($groupings) {
+        global $CFG, $DB;
+        require_once("$CFG->dirroot/group/lib.php");
+
+        $params = self::validate_parameters(self::create_groupings_parameters(), array('groupings'=>$groupings));
+
+        $transaction = $DB->start_delegated_transaction();
+
+        $groupings = array();
+
+        foreach ($params['groupings'] as $grouping) {
+            $grouping = (object)$grouping;
+
+            if (trim($grouping->name) == '') {
+                throw new invalid_parameter_exception('Invalid grouping name');
+            }
+            if ($DB->count_records('groupings', array('courseid'=>$grouping->courseid, 'name'=>$grouping->name))) {
+                throw new invalid_parameter_exception('Grouping with the same name already exists in the course');
+            }
+
+            // Now security checks            .
+            $context = context_course::instance($grouping->courseid);
+            try {
+                self::validate_context($context);
+            } catch (Exception $e) {
+                $exceptionparam = new stdClass();
+                $exceptionparam->message = $e->getMessage();
+                $exceptionparam->courseid = $grouping->courseid;
+                throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam);
+            }
+            require_capability('moodle/course:managegroups', $context);
+
+            // We must force allways FORMAT_HTML.
+            $grouping->descriptionformat = FORMAT_HTML;
+
+            // Finally create the grouping.
+            $grouping->id = groups_create_grouping($grouping);
+            $groupings[] = (array)$grouping;
+        }
+
+        $transaction->allow_commit();
+
+        return $groupings;
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     * @since Moodle 2.3
+     */
+    public static function create_groupings_returns() {
+        return new external_multiple_structure(
+            new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_INT, 'grouping record id'),
+                    'courseid' => new external_value(PARAM_INT, 'id of course'),
+                    'name' => new external_value(PARAM_TEXT, 'multilang compatible name, course unique'),
+                    'description' => new external_value(PARAM_CLEANHTML, 'grouping description text')
+                )
+            ), 'List of grouping object. A grouping has an id, a courseid, a name and a description.'
+        );
+    }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+&nb