Merge branch 'MDL-40544_master' of https://github.com/nadavkav/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 23 Jul 2013 20:39:13 +0000 (22:39 +0200)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 23 Jul 2013 20:39:13 +0000 (22:39 +0200)
Conflicts:
theme/bootstrapbase/style/moodle.css

466 files changed:
admin/cli/alternative_component_cache.php [new file with mode: 0644]
admin/cli/install.php
admin/environment.xml
admin/handlevirus.php
admin/index.php
admin/localplugins.php
admin/mnet/peer_forms.php
admin/oacleanup.php [deleted file]
admin/plagiarism.php
admin/portfolio.php
admin/qbehaviours.php
admin/reports.php
admin/repository.php
admin/roles/assign.php
admin/roles/classes/capability_table_base.php
admin/settings/appearance.php
admin/settings/grades.php
admin/settings/plugins.php
admin/tests/behat/behat_admin.php
admin/tests/behat/display_short_names.feature
admin/tool/behat/cli/init.php
admin/tool/behat/cli/util.php
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/renderer.php
admin/tool/behat/tests/behat/basic_actions.feature
admin/tool/behat/tests/behat/data_generators.feature
admin/tool/customlang/filter_form.php
admin/tool/customlang/locallib.php
admin/tool/dbtransfer/locallib.php
admin/tool/installaddon/classes/installer.php
admin/tool/installaddon/classes/validator.php
admin/tool/phpunit/cli/init.php
admin/tool/phpunit/cli/util.php
admin/tool/profiling/index.php
admin/tool/profiling/lang/en/tool_profiling.php
admin/tool/qeupgradehelper/extracttestcase_form.php
admin/tool/uploadcourse/classes/base_form.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/course.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/helper.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/processor.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/step1_form.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/step2_form.php [new file with mode: 0644]
admin/tool/uploadcourse/classes/tracker.php [new file with mode: 0644]
admin/tool/uploadcourse/cli/uploadcourse.php [new file with mode: 0644]
admin/tool/uploadcourse/db/caches.php [moved from enrol/authorize/db/messages.php with 79% similarity]
admin/tool/uploadcourse/index.php [new file with mode: 0644]
admin/tool/uploadcourse/lang/en/tool_uploadcourse.php [new file with mode: 0644]
admin/tool/uploadcourse/settings.php [moved from enrol/authorize/db/upgrade.php with 59% similarity]
admin/tool/uploadcourse/tests/behat/create.feature [new file with mode: 0644]
admin/tool/uploadcourse/tests/behat/update.feature [new file with mode: 0644]
admin/tool/uploadcourse/tests/course_test.php [new file with mode: 0644]
admin/tool/uploadcourse/tests/fixtures/backup.mbz [new file with mode: 0644]
admin/tool/uploadcourse/tests/fixtures/courses.csv [new file with mode: 0644]
admin/tool/uploadcourse/tests/helper_test.php [new file with mode: 0644]
admin/tool/uploadcourse/tests/processor_test.php [new file with mode: 0644]
admin/tool/uploadcourse/version.php [new file with mode: 0644]
admin/tool/uploaduser/index.php
admin/tool/uploaduser/user_form.php
admin/tool/xmldb/actions/create_xml_file/create_xml_file.class.php
admin/tools.php
admin/webservice/forms.php
admin/webservice/protocols.php
admin/webservice/testclient.php
auth/classes/event/user_loggedin.php [new file with mode: 0644]
auth/tests/auth_test.php [new file with mode: 0644]
auth/tests/behat/behat_auth.php
backup/backupfilesedit_form.php
backup/converter/moodle1/handlerlib.php
backup/moodle2/backup_plan_builder.class.php
backup/moodle2/backup_qtype_plugin.class.php
backup/moodle2/backup_stepslib.php
backup/moodle2/backup_xml_transformer.class.php
backup/moodle2/restore_plan_builder.class.php
backup/moodle2/restore_plugin.class.php
backup/moodle2/restore_stepslib.php
backup/util/dbops/restore_dbops.class.php
backup/util/helper/backup_general_helper.class.php
backup/util/helper/restore_decode_processor.class.php
backup/util/helper/restore_logs_processor.class.php
backup/util/plan/backup_structure_step.class.php
backup/util/plan/restore_structure_step.class.php
backup/util/ui/restore_ui_components.php
backup/util/ui/tests/behat/backup_courses.feature
backup/util/ui/tests/behat/behat_backup.php
backup/util/xml/parser/processors/grouped_parser_processor.class.php
badges/backpack.js
badges/badge.php
badges/mybadges.php
badges/newbadge.php
badges/renderer.php
badges/tests/badgeslib_test.php
blocks/comments/tests/behat/behat_block_comments.php
blocks/community/renderer.php
blocks/course_overview/renderer.php
blocks/dock.js [deleted file]
blocks/moodleblock.class.php
blocks/navigation/block_navigation.php
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-debug.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation-min.js
blocks/navigation/yui/build/moodle-block_navigation-navigation/moodle-block_navigation-navigation.js
blocks/navigation/yui/src/navigation/js/navigation.js
blocks/navigation/yui/src/navigation/meta/navigation.json
blocks/recent_activity/block_recent_activity.php
blocks/section_links/block_section_links.php
blocks/settings/block_settings.php
blocks/tests/behat/behat_blocks.php
blocks/tests/behat/configure_block_throughout_site.feature
cache/classes/loaders.php
cache/locallib.php
cache/stores/memcache/addinstanceform.php
cache/stores/memcache/lang/en/cachestore_memcache.php
cache/stores/memcache/lib.php
cache/stores/memcache/tests/memcache_test.php
cache/stores/memcached/addinstanceform.php
cache/stores/memcached/lang/en/cachestore_memcached.php
cache/stores/memcached/lib.php
cache/stores/memcached/tests/memcached_test.php
cache/stores/session/lib.php
cache/stores/session/tests/session_test.php
cache/stores/static/lib.php
cache/stores/static/tests/static_test.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
cache/tests/fixtures/stores.php
calendar/export.php
calendar/lib.php
cohort/externallib.php
cohort/tests/behat/behat_cohort.php
cohort/tests/behat/upload_cohort_users.feature
cohort/tests/externallib_test.php
cohort/upgrade.txt [new file with mode: 0644]
comment/lib.php
completion/tests/behat/behat_completion.php
config-dist.php
course/dnduploadlib.php
course/edit_form.php
course/externallib.php
course/format/lib.php
course/format/singleactivity/format.php [new file with mode: 0644]
course/format/singleactivity/lang/en/format_singleactivity.php [new file with mode: 0644]
course/format/singleactivity/lib.php [new file with mode: 0644]
course/format/singleactivity/renderer.php [new file with mode: 0644]
course/format/singleactivity/settings.php [new file with mode: 0644]
course/format/singleactivity/settingslib.php [new file with mode: 0644]
course/format/singleactivity/styles.css [new file with mode: 0644]
course/format/singleactivity/version.php [moved from enrol/authorize/version.php with 67% similarity]
course/lib.php
course/modlib.php
course/moodleform_mod.php
course/report.php
course/tests/behat/activities_group_icons.feature
course/tests/behat/activities_indentation.feature
course/tests/behat/behat_course.php
course/tests/behat/force_group_mode.feature
course/tests/courselib_test.php
course/tests/externallib_test.php
enrol/authorize/authorize.js [deleted file]
enrol/authorize/authorizenet.class.php [deleted file]
enrol/authorize/const.php [deleted file]
enrol/authorize/db/access.php [deleted file]
enrol/authorize/db/install.xml [deleted file]
enrol/authorize/enrol_form.php [deleted file]
enrol/authorize/index.php [deleted file]
enrol/authorize/lang/en/enrol_authorize.php [deleted file]
enrol/authorize/localfuncs.php [deleted file]
enrol/authorize/locallib.php [deleted file]
enrol/authorize/uploadcsv.php [deleted file]
enrol/category/classes/observer.php [new file with mode: 0644]
enrol/category/db/events.php
enrol/category/locallib.php
enrol/category/tests/plugin_test.php [moved from enrol/category/tests/sync_test.php with 98% similarity]
enrol/externallib.php
enrol/guest/lib.php
enrol/meta/addinstance_form.php
enrol/tests/behat/behat_enrol.php
enrol/upgrade.txt
filter/activitynames/filter.php
filter/data/filter.php
filter/glossary/filter.php
filter/manage.php
filter/urltolink/filter.php
filter/urltolink/tests/filter_test.php
grade/edit/settings/form.php
grade/grading/form/guide/lib.php
grade/grading/form/guide/version.php
grade/grading/form/lib.php
grade/grading/form/rubric/lib.php
grade/grading/form/rubric/styles.css
grade/grading/form/rubric/version.php
grade/grading/form/upgrade.txt [new file with mode: 0644]
grade/grading/lib.php
grade/import/lib.php
grade/lib.php
grade/report/grader/lib.php
grade/report/index.php
grade/report/lib.php
grade/report/user/lib.php
group/lib.php
group/tests/behat/behat_groups.php
install.php
install/lang/gl/moodle.php
install/lang/ko/error.php
lang/en/auth.php
lang/en/badges.php
lang/en/cache.php
lib/accesslib.php
lib/adminlib.php
lib/authlib.php
lib/badgeslib.php
lib/behat/behat_files.php
lib/behat/classes/behat_command.php
lib/behat/form_field/behat_form_select.php
lib/behat/lib.php
lib/blocklib.php
lib/classes/component.php
lib/classes/event/base.php [new file with mode: 0644]
lib/classes/event/manager.php [new file with mode: 0644]
lib/classes/event/role_assigned.php [new file with mode: 0644]
lib/classes/event/role_unassigned.php [new file with mode: 0644]
lib/coursecatlib.php
lib/cronlib.php
lib/csslib.php
lib/datalib.php
lib/db/caches.php
lib/db/events.php
lib/db/install.xml
lib/db/upgrade.php
lib/deprecatedlib.php
lib/dml/moodle_database.php
lib/editor/tinymce/adminlib.php
lib/editor/tinymce/all_strings.php
lib/editor/tinymce/classes/plugin.php
lib/editor/tinymce/plugins/dragmath/lib.php
lib/editor/tinymce/plugins/managefiles/lang/en/tinymce_managefiles.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/lib.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/manage.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/manage_form.php [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/module.js [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/pix/icon.gif [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/styles.css [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/tinymce/editor_plugin.js [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/tinymce/img/managefiles.png [new file with mode: 0644]
lib/editor/tinymce/plugins/managefiles/version.php [new file with mode: 0644]
lib/editor/tinymce/plugins/moodleemoticon/lib.php
lib/editor/tinymce/plugins/moodlemedia/lib.php
lib/editor/tinymce/plugins/moodlenolink/lib.php
lib/editor/tinymce/plugins/pdw/lib.php
lib/editor/tinymce/plugins/spellchecker/config.php
lib/editor/tinymce/plugins/spellchecker/lib.php
lib/editor/tinymce/plugins/spellchecker/settings.php
lib/editor/tinymce/subplugins.php
lib/editor/tinymce/tests/editor_test.php
lib/editor/tinymce/upgrade.txt
lib/editorlib.php
lib/enrollib.php
lib/eventslib.php
lib/externallib.php
lib/filebrowser/file_info_context_module.php
lib/filelib.php
lib/filterlib.php
lib/form/form.js
lib/grade/grade_item.php
lib/gradelib.php
lib/javascript.php
lib/messagelib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputfactories.php
lib/outputlib.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/pagelib.php
lib/phpunit/bootstrap.php
lib/phpunit/classes/advanced_testcase.php
lib/phpunit/classes/event_sink.php [new file with mode: 0644]
lib/phpunit/classes/util.php
lib/phpunit/lib.php
lib/plagiarismlib.php
lib/pluginlib.php
lib/portfoliolib.php
lib/questionlib.php
lib/setup.php
lib/setuplib.php
lib/tablelib.php
lib/testing/classes/tests_finder.php
lib/testing/classes/util.php
lib/testing/generator/data_generator.php
lib/tests/accesslib_test.php
lib/tests/behat/behat_data_generators.php
lib/tests/behat/behat_forms.php
lib/tests/behat/behat_general.php
lib/tests/behat/behat_navigation.php
lib/tests/behat/behat_permissions.php
lib/tests/component_test.php
lib/tests/event_test.php [new file with mode: 0644]
lib/tests/eventslib_test.php
lib/tests/fixtures/event_fixtures.php [new file with mode: 0644]
lib/tests/fixtures/events.php
lib/tests/setuplib_test.php
lib/tests/weblib_test.php
lib/upgrade.txt
lib/upgradelib.php
lib/weblib.php
lib/xhprof/xhprof_moodle.php
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-dock/moodle-core-dock.js [new file with mode: 0644]
lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-debug.js [new file with mode: 0644]
lib/yui/build/moodle-core-dockloader/moodle-core-dockloader-min.js [new file with mode: 0644]
lib/yui/build/moodle-core-dockloader/moodle-core-dockloader.js [new file with mode: 0644]
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-debug.js
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip-min.js
lib/yui/build/moodle-core-tooltip/moodle-core-tooltip.js
lib/yui/config/moodle.js [new file with mode: 0644]
lib/yui/config/yui2.js [new file with mode: 0644]
lib/yui/src/dock/build.json [new file with mode: 0644]
lib/yui/src/dock/js/actionkey.js [new file with mode: 0644]
lib/yui/src/dock/js/block.js [new file with mode: 0644]
lib/yui/src/dock/js/dock.js [new file with mode: 0644]
lib/yui/src/dock/js/dockeditem.js [new file with mode: 0644]
lib/yui/src/dock/js/dockloader.js [new file with mode: 0644]
lib/yui/src/dock/js/panel.js [new file with mode: 0644]
lib/yui/src/dock/js/tabheightmanager.js [new file with mode: 0644]
lib/yui/src/dock/meta/dock.json [new file with mode: 0644]
lib/yui/src/tooltip/meta/tooltip.json
login/index.php
message/tests/behat/behat_message.php
mnet/xmlrpc/serverlib.php
mod/assign/adminlib.php
mod/assign/assignmentplugin.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/settings.php
mod/assign/tests/locallib_test.php
mod/assignment/backup/moodle1/lib.php
mod/assignment/lib.php
mod/book/lib.php
mod/chat/gui_ajax/module.js
mod/chat/gui_ajax/theme/bubble/chat.css
mod/chat/styles.css
mod/choice/tests/behat/behat_mod_choice.php
mod/data/field.php
mod/data/lib.php
mod/folder/lang/en/folder.php
mod/folder/renderer.php
mod/forum/lib.php
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/completion_condition_number_discussions.feature
mod/glossary/tests/behat/behat_mod_glossary.php
mod/glossary/tests/behat/print_friendly_version.feature
mod/lesson/format.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/tests/behat/lesson_navigation.feature
mod/quiz/accessmanager.php
mod/quiz/attemptlib.php
mod/quiz/db/upgrade.php
mod/quiz/locallib.php
mod/quiz/report/reportlib.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/startattempt.php
mod/quiz/styles.css
mod/quiz/tests/attempt_walkthrough_from_csv_test.php [new file with mode: 0644]
mod/quiz/tests/attempt_walkthrough_test.php [new file with mode: 0644]
mod/quiz/tests/attempts_test.php
mod/quiz/tests/fixtures/questions00.csv [new file with mode: 0644]
mod/quiz/tests/fixtures/results00.csv [new file with mode: 0644]
mod/quiz/tests/fixtures/steps00.csv [new file with mode: 0644]
mod/scorm/module.js
mod/scorm/player.php
mod/scorm/report/objectives/lang/en/scormreport_objectives.php [new file with mode: 0644]
mod/scorm/report/objectives/report.php [new file with mode: 0644]
mod/scorm/report/objectives/responsessettings_form.php [new file with mode: 0644]
mod/scorm/report/objectives/version.php [new file with mode: 0644]
mod/scorm/report/reportlib.php
mod/wiki/locallib.php
mod/wiki/pagelib.php
mod/workshop/allocation/manual/lib.php
mod/workshop/backup/moodle1/lib.php
mod/workshop/lib.php
mod/workshop/locallib.php
mod/workshop/settings.php
phpunit.xml.dist
pluginfile.php
question/engine/bank.php
question/engine/lib.php
question/engine/questionusage.php
question/engine/tests/helpers.php
question/tests/behat/behat_question.php
question/type/match/question.php
question/type/match/tests/helper.php
question/type/multianswer/question.php
question/type/multichoice/question.php
question/type/multichoice/tests/helper.php
question/type/multichoice/tests/questiontype_test.php
question/type/questionbase.php
question/type/random/questiontype.php
question/type/random/tests/questiontype_test.php
rating/lib.php
report/backups/index.php
report/participation/index.php
report/questioninstances/index.php
repository/areafiles/db/access.php [new file with mode: 0644]
repository/areafiles/db/install.php [moved from enrol/authorize/import_form.php with 58% similarity]
repository/areafiles/lang/en/repository_areafiles.php [new file with mode: 0644]
repository/areafiles/lib.php [new file with mode: 0644]
repository/areafiles/pix/icon.gif [new file with mode: 0644]
repository/areafiles/version.php [new file with mode: 0644]
repository/coursefiles/lib.php
repository/recent/tests/behat/behat_repository_recent.php
repository/skydrive/db/access.php [new file with mode: 0644]
repository/skydrive/db/caches.php [new file with mode: 0644]
repository/skydrive/lang/en/repository_skydrive.php [new file with mode: 0644]
repository/skydrive/lib.php [new file with mode: 0644]
repository/skydrive/microsoftliveapi.php [new file with mode: 0644]
repository/skydrive/pix/icon.png [new file with mode: 0644]
repository/skydrive/version.php [new file with mode: 0644]
repository/tests/behat/behat_filepicker.php
repository/tests/behat/cancel_add_file.feature
repository/upload/tests/behat/behat_repository_upload.php
rss/file.php
theme/bootstrapbase/layout/columns1.php
theme/bootstrapbase/layout/columns2.php
theme/bootstrapbase/layout/columns3.php
theme/bootstrapbase/less/moodle.less
theme/bootstrapbase/less/moodle/admin.less
theme/bootstrapbase/less/moodle/bootstrapoverride.less [new file with mode: 0644]
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/course.less
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/less/moodle/forms.less
theme/bootstrapbase/less/moodle/grade.less
theme/bootstrapbase/less/moodle/question.less
theme/bootstrapbase/less/moodle/responsive.less
theme/bootstrapbase/style/moodle.css
theme/clean/layout/columns1.php
theme/clean/layout/columns2.php
theme/clean/layout/columns3.php
theme/font.php [new file with mode: 0644]
theme/formal_white/lib.php
theme/formal_white/style/block.css
theme/formal_white/style/calendar.css
theme/formal_white/style/course.css
theme/image.php
theme/index.php
theme/javascript.php
theme/jquery.php
theme/mymobile/renderers.php
theme/styles.php
theme/styles_debug.php
theme/yui_combo.php
theme/yui_image.php
user/default/README.txt [deleted file]
user/default/f1.jpg [deleted file]
user/default/f2.jpg [deleted file]
user/editadvanced_form.php
user/externallib.php
user/filters/lib.php
user/index.php
user/profile.php
user/profile/definelib.php
user/selector/lib.php
version.php
webservice/externallib.php
webservice/tests/externallib_test.php

diff --git a/admin/cli/alternative_component_cache.php b/admin/cli/alternative_component_cache.php
new file mode 100644 (file)
index 0000000..6b3d454
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This hack is intended for clustered sites that do not want
+ * to use shared cachedir for component cache.
+ *
+ * This file needs to be called after any change in PHP files in dataroot,
+ * that is before upgrade and install.
+ *
+ * @package   core
+ * @copyright 2013 Petr Skoda (skodak)  {@link http://skodak.org}
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+define('CLI_SCRIPT', true);
+define('ABORT_AFTER_CONFIG', true); // We need just the values from config.php.
+define('CACHE_DISABLE_ALL', true); // This prevents reading of existing caches.
+define('IGNORE_COMPONENT_CACHE', true);
+
+require(__DIR__.'/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(
+    array(
+        'file'    => false,
+        'rebuild' => false,
+        'print'   => false,
+        'help'    => false
+    ),
+    array(
+        'h' => 'help'
+    )
+);
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized), 2);
+}
+
+if (!$options['rebuild'] and !$options['file'] and !$options['print']) {
+    $help =
+"Create alternative component cache file
+
+Options:
+-h, --help            Print out this help
+--rebuild             Rebuild \$CFG->alternative_component_cache file
+--file=filepath       Save component cache to file
+--print               Print component cache file content
+
+Example:
+\$ php admin/cli/rebuild_alternative_component_cache.php --rebuild
+";
+
+    echo $help;
+    exit(0);
+}
+
+error_reporting(E_ALL | E_STRICT);
+ini_set('display_errors', 1);
+
+$content = core_component::get_cache_content();
+
+if ($options['print']) {
+    echo $content;
+    exit(0);
+}
+
+if ($options['rebuild']) {
+    if (empty($CFG->alternative_component_cache)) {
+        fwrite(STDERR, 'config.php does not contain $CFG->alternative_component_cache setting');
+        fwrite(STDERR, "\n");
+        exit(2);
+    }
+    $target = $CFG->alternative_component_cache;
+} else {
+    $target = $options['file'];
+}
+
+if (!$target) {
+    fwrite(STDERR, "Invalid target file $target");
+    fwrite(STDERR, "\n");
+    exit(1);
+}
+
+$bytes = file_put_contents($target, $content);
+
+if (!$bytes) {
+    fwrite(STDERR, "Error writing to $target");
+    fwrite(STDERR, "\n");
+    exit(1);
+}
+
+// Success.
+echo "File $target was updated\n";
+exit(0);
index f0bee31..2ab997e 100644 (file)
@@ -139,6 +139,8 @@ define('CACHE_DISABLE_ALL', true);
 
 define('PHPUNIT_TEST', false);
 
+define('IGNORE_COMPONENT_CACHE', true);
+
 // Check that PHP is of a sufficient version
 if (version_compare(phpversion(), "5.3.3") < 0) {
     $phpversion = phpversion();
@@ -392,8 +394,9 @@ if ($interactive) {
         cli_error(get_string('pathserrcreatedataroot', 'install', $a));
     }
 }
-$CFG->tempdir  = $CFG->dataroot.'/temp';
-$CFG->cachedir = $CFG->dataroot.'/cache';
+$CFG->tempdir       = $CFG->dataroot.'/temp';
+$CFG->cachedir      = $CFG->dataroot.'/cache';
+$CFG->localcachedir = $CFG->dataroot.'/localcache';
 
 // download required lang packs
 if ($CFG->lang !== 'en') {
index 19997a4..66e4337 100644 (file)
           <ON_ERROR message="ziprequired" />
         </FEEDBACK>
       </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="optional">
+      </PHP_EXTENSION>
       <PHP_EXTENSION name="gd" level="required">
         <FEEDBACK>
           <ON_ERROR message="gdrequired" />
index b29392d..36cc0e3 100644 (file)
@@ -39,9 +39,10 @@ while(!feof($fd)) {
     $action = clam_handle_infected_file($file,$log->userid,true);
     clam_replace_infected_file($file);
 
-    list($ctxselect, $ctxjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
+    $ctxselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
+    $ctxjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)";
     $sql = "SELECT c.id, c.fullname $ctxselect FROM {course} c $ctxjoin WHERE c.id = :courseid";
-    $course = $DB->get_record_sql($sql, array('courseid' => $log->course));
+    $course = $DB->get_record_sql($sql, array('courseid' => $log->course, 'contextlevel' => CONTEXT_COURSE));
     context_helper::preload_from_record($course);
 
     $user = $DB->get_record("user", array("id"=>$log->userid));
index 9acf551..1e9e810 100644 (file)
@@ -47,10 +47,11 @@ if (!function_exists('iconv')) {
 
 define('NO_OUTPUT_BUFFERING', true);
 
-if (empty($_GET['cache']) and empty($_POST['cache'])) {
+if (empty($_GET['cache']) and empty($_POST['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey'])) {
     // Prevent caching at all cost when visiting this page directly,
     // we redirect to self once we known no upgrades are necessary.
     // Note: $_GET and $_POST are used here intentionally because our param cleaning is not loaded yet.
+    // Note2: the sesskey is present in all block editing hacks, we can not redirect there, so enable caching.
     define('CACHE_DISABLE_ALL', true);
 
     // Force OPcache reset if used, we do not want any stale caches
@@ -449,7 +450,7 @@ if (during_initial_install()) {
 
 // Now we can be sure everything was upgraded and caches work fine,
 // redirect if necessary to make sure caching is enabled.
-if (!$cache) {
+if (!$cache and !optional_param('sesskey', '', PARAM_RAW)) {
     redirect(new moodle_url($PAGE->url, array('cache' => 1)));
 }
 
index 12672b2..145f65a 100644 (file)
@@ -47,7 +47,7 @@ $table->set_attribute('class', 'admintable generaltable');
 $table->setup();
 
 $plugins = array();
-foreach (get_plugin_list('local') as $plugin => $plugindir) {
+foreach (core_component::get_plugin_list('local') as $plugin => $plugindir) {
     if (get_string_manager()->string_exists('pluginname', 'local_' . $plugin)) {
         $strpluginname = get_string('pluginname', 'local_' . $plugin);
     } else {
index 1ff7293..ce1c1c0 100644 (file)
@@ -95,7 +95,7 @@ class mnet_review_host_form extends moodleform {
         $mform->addRule('wwwroot', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
 
         $themes = array('' => get_string('forceno'));
-        foreach (array_keys(get_plugin_list('theme')) as $themename) {
+        foreach (array_keys(core_component::get_plugin_list('theme')) as $themename) {
             $themes[$themename] = get_string('pluginname', 'theme_'.$themename);
         }
         $mform->addElement('select', 'theme', get_string('forcetheme'), $themes);
diff --git a/admin/oacleanup.php b/admin/oacleanup.php
deleted file mode 100644 (file)
index faf55f5..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-<?php
-
-if (!isset($CFG)) {
-
-    require('../config.php');
-    require_once($CFG->libdir.'/adminlib.php');
-
-    admin_externalpage_setup('oacleanup');
-
-    echo $OUTPUT->header();
-    online_assignment_cleanup(true);
-    echo $OUTPUT->footer();
-
-}
-
-
-
-function online_assignment_cleanup($output=false) {
-    global $CFG, $DB, $OUTPUT;
-
-    if ($output) {
-        echo $OUTPUT->heading('Online Assignment Cleanup');
-        echo '<center>';
-    }
-
-
-    /// We don't want to run this code if we are doing an upgrade from an assignment
-    /// version earlier than 2005041400
-    /// because the assignment type field will not exist
-    $amv = $DB->get_field('modules', 'version', array('name'=>'assignment'));
-    if ((int)$amv < 2005041400) {
-        if ($output) {
-            echo '</center>';
-        }
-        return;
-    }
-
-
-    /// get the module id for assignments from db
-    $arecord = $DB->get_record('modules', array('name', 'assignment'));
-    $aid = $arecord->id;
-
-
-    /// get a list of all courses on this site
-    list($ctxselect, $ctxjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
-    $sql = "SELECT c.* $ctxselect FROM {course} c $ctxjoin";
-    $courses = $DB->get_records_sql($sql);
-
-    /// cycle through each course
-    foreach ($courses as $course) {
-        context_helper::preload_from_record($course);
-        $context = context_course::instance($course->id);
-
-        if (empty($course->fullname)) {
-            $fullname = get_string('course').': '.$course->id;
-        } else {
-            $fullname = format_string($course->fullname, true, array('context' => $context));
-        }
-        if ($output) echo $OUTPUT->heading($fullname);
-
-        /// retrieve a list of sections beyond what is currently being shown
-        $courseformatoptions = course_get_format($course)->get_format_options();
-        if (!isset($courseformatoptions['numsections'])) {
-            // Course format does not use numsections
-            if ($output) {
-                echo 'No extra sections<br />';
-            }
-            continue;
-        }
-        $sql = "SELECT *
-                  FROM {course_sections}
-                 WHERE course=? AND section>?
-              ORDER BY section ASC";
-        $params = array($course->id, $courseformatoptions['numsections']);
-        if (!($xsections = $DB->get_records_sql($sql, $params))) {
-            if ($output) echo 'No extra sections<br />';
-            continue;
-        }
-
-        /// cycle through each of the xtra sections
-        foreach ($xsections as $xsection) {
-
-            if ($output) echo 'Checking Section: '.$xsection->section.'<br />';
-
-            /// grab any module instances from the sequence field
-            if (!empty($xsection->sequence)) {
-                $instances = explode(',', $xsection->sequence);
-
-                /// cycle through the instances
-                foreach ($instances as $instance) {
-                    /// is this an instance of an online assignment
-                    $sql = "SELECT a.id
-                              FROM  {course_modules} cm, {assignment} a
-                             WHERE cm.id = ? AND cm.module = ? AND
-                                   cm.instance = a.id AND a.assignmenttype = 'online'";
-                    $params = array($instance, $aid);
-
-                    /// if record exists then we need to move instance to it's correct section
-                    if ($DB->record_exists_sql($sql, $params)) {
-
-                        /// check the new section id
-                        /// the journal update erroneously stored it in course_sections->section
-                        $newsection = $xsection->section;
-                        /// double check the new section
-                        if ($newsection > $courseformatoptions['numsections']) {
-                            /// get the record for section 0 for this course
-                            if (!($zerosection = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>'0')))) {
-                                continue;
-                            }
-                            $newsection = $zerosection->id;
-                        }
-
-                        /// grab the section record
-                        if (!($section = $DB->get_record('course_sections', array('id'=>$newsection)))) {
-                            if ($output) {
-                                echo 'Serious error: Cannot retrieve section: '.$newsection.' for course: '. $fullname .'<br />';
-                            }
-                            continue;
-                        }
-
-                        /// explode the sequence
-                        if  (($sequence = explode(',', $section->sequence)) === false) {
-                            $sequence = array();
-                        }
-
-                        /// add instance to correct section
-                        array_push($sequence, $instance);
-
-                        /// implode the sequence
-                        $section->sequence = implode(',', $sequence);
-
-                        $DB->set_field('course_sections', 'sequence', $section->sequence, array('id'=>$section->id));
-
-                        /// now we need to remove the instance from the old sequence
-
-                        /// grab the old section record
-                        if (!($section = $DB->get_record('course_sections', array('id'=>$xsection->id)))) {
-                            if ($output) echo 'Serious error: Cannot retrieve old section: '.$xsection->id.' for course: '.$fullname.'<br />';
-                            continue;
-                        }
-
-                        /// explode the sequence
-                        if  (($sequence = explode(',', $section->sequence)) === false) {
-                            $sequence = array();
-                        }
-
-                        /// remove the old value from the array
-                        $key = array_search($instance, $sequence);
-                        unset($sequence[$key]);
-
-                        /// implode the sequence
-                        $section->sequence = implode(',', $sequence);
-
-                        $DB->set_field('course_sections', 'sequence', $section->sequence, array('id'=>$section->id));
-
-
-                        if ($output) echo 'Online Assignment (instance '.$instance.') moved from section '.$section->id.': to section '.$newsection.'<br />';
-
-                    }
-                }
-            }
-
-            /// if the summary and sequence are empty then remove this section
-            if (empty($xsection->summary) and empty($xsection->sequence)) {
-                $DB->delete_records('course_sections', array('id'=>$xsection->id));
-                if ($output) echo 'Deleting empty section '.$xsection->section.'<br />';
-            }
-        }
-    }
-
-    echo '</center>';
-}
-
-
index db2cd2f..ace710f 100644 (file)
@@ -39,7 +39,7 @@ echo $OUTPUT->header();
 $txt = get_strings(array('settings', 'name', 'version'));
 $txt->uninstall = get_string('uninstallplugin', 'core_admin');
 
-$plagiarismplugins = get_plugin_list('plagiarism');
+$plagiarismplugins = core_component::get_plugin_list('plagiarism');
 if (empty($plagiarismplugins)) {
     echo $OUTPUT->notification(get_string('nopluginsinstalled', 'plagiarism'));
     echo $OUTPUT->footer();
index 1811f5f..f594053 100644 (file)
@@ -163,7 +163,7 @@ if (($action == 'edit') || ($action == 'new')) {
 
     $output = $OUTPUT->box_start('generalbox');
 
-    $plugins = get_plugin_list('portfolio');
+    $plugins = core_component::get_plugin_list('portfolio');
     $plugins = array_keys($plugins);
     $instances = portfolio_instances(false, false);
     $usedplugins = array();
index 3dd14a0..cb59976 100644 (file)
@@ -39,7 +39,7 @@ require_capability('moodle/question:config', $systemcontext);
 admin_externalpage_setup('manageqbehaviours');
 $thispageurl = new moodle_url('/admin/qbehaviours.php');
 
-$behaviours = get_plugin_list('qbehaviour');
+$behaviours = core_component::get_plugin_list('qbehaviour');
 $pluginmanager = plugin_manager::instance();
 
 // Get some data we will need - question counts and which types are needed.
@@ -186,14 +186,14 @@ if (($delete = optional_param('delete', '', PARAM_PLUGIN)) && confirm_sesskey())
     }
 
     // Then the tables themselves
-    drop_plugin_tables($delete, get_plugin_directory('qbehaviour', $delete) . '/db/install.xml', false);
+    drop_plugin_tables($delete, core_component::get_plugin_directory('qbehaviour', $delete) . '/db/install.xml', false);
 
     // Remove event handlers and dequeue pending events
     events_uninstall('qbehaviour_' . $delete);
 
     $a = new stdClass();
     $a->behaviour = $behaviourname;
-    $a->directory = get_plugin_directory('qbehaviour', $delete);
+    $a->directory = core_component::get_plugin_directory('qbehaviour', $delete);
     echo $OUTPUT->box(get_string('qbehaviourdeletefiles', 'question', $a), 'generalbox', 'notice');
     echo $OUTPUT->continue_button($thispageurl);
     echo $OUTPUT->footer();
index 19fafdb..70ec952 100644 (file)
@@ -49,7 +49,7 @@ $table->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthw
 $table->setup();
 
 $plugins = array();
-foreach (get_plugin_list('report') as $plugin => $plugindir) {
+foreach (core_component::get_plugin_list('report') as $plugin => $plugindir) {
     if (get_string_manager()->string_exists('pluginname', 'report_' . $plugin)) {
         $strpluginname = get_string('pluginname', 'report_' . $plugin);
     } else {
index aa77af9..50a08ee 100644 (file)
@@ -390,7 +390,7 @@ if (($action == 'edit') || ($action == 'new')) {
     }
 
     // Get all the plugins that exist on disk
-    $plugins = get_plugin_list('repository');
+    $plugins = core_component::get_plugin_list('repository');
     if (!empty($plugins)) {
         foreach ($plugins as $plugin => $dir) {
             // Check that it has not already been listed
index ee212b1..8346640 100644 (file)
@@ -67,7 +67,7 @@ if ($roleid && !isset($assignableroles[$roleid])) {
     $a = new stdClass;
     $a->roleid = $roleid;
     $a->context = $contextname;
-    print_error('cannotassignrolehere', '', get_context_url($context), $a);
+    print_error('cannotassignrolehere', '', $context->get_url(), $a);
 }
 
 // Work out an appropriate page title.
index 9c9d7f9..11cadee 100644 (file)
@@ -36,7 +36,7 @@ abstract class core_role_capability_table_base {
     /** The context this table relates to. */
     protected $context;
 
-    /** The capabilities to display. Initialised as fetch_context_capabilities($context). */
+    /** The capabilities to display. Initialised as $context->get_capabilities(). */
     protected $capabilities = array();
 
     /** Added as an id="" attribute to the table on output. */
index f9ca6d4..1367452 100644 (file)
@@ -24,7 +24,7 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page
     $ADMIN->add('themes', new admin_externalpage('themeselector', new lang_string('themeselector','admin'), $CFG->wwwroot . '/theme/index.php'));
 
     // settings for each theme
-    foreach (get_plugin_list('theme') as $theme => $themedir) {
+    foreach (core_component::get_plugin_list('theme') as $theme => $themedir) {
         $settings_path = "$themedir/settings.php";
         if (file_exists($settings_path)) {
             $settings = new admin_settingpage('themesetting'.$theme, new lang_string('pluginname', 'theme_'.$theme));
index d2cc43e..b80bb43 100644 (file)
@@ -178,7 +178,7 @@ if (has_capability('moodle/grade:manage', $systemcontext)
 
     // Reports
     $ADMIN->add('grades', new admin_category('gradereports', new lang_string('reportsettings', 'grades')));
-    foreach (get_plugin_list('gradereport') as $plugin => $plugindir) {
+    foreach (core_component::get_plugin_list('gradereport') as $plugin => $plugindir) {
      // Include all the settings commands for this plugin if there are any
         if (file_exists($plugindir.'/settings.php')) {
             $settings = new admin_settingpage('gradereport'.$plugin, new lang_string('pluginname', 'gradereport_'.$plugin), 'moodle/grade:manage');
@@ -191,7 +191,7 @@ if (has_capability('moodle/grade:manage', $systemcontext)
 
     // Imports
     $ADMIN->add('grades', new admin_category('gradeimports', new lang_string('importsettings', 'grades')));
-    foreach (get_plugin_list('gradeimport') as $plugin => $plugindir) {
+    foreach (core_component::get_plugin_list('gradeimport') as $plugin => $plugindir) {
 
      // Include all the settings commands for this plugin if there are any
         if (file_exists($plugindir.'/settings.php')) {
@@ -206,7 +206,7 @@ if (has_capability('moodle/grade:manage', $systemcontext)
 
     // Exports
     $ADMIN->add('grades', new admin_category('gradeexports', new lang_string('exportsettings', 'grades')));
-    foreach (get_plugin_list('gradeexport') as $plugin => $plugindir) {
+    foreach (core_component::get_plugin_list('gradeexport') as $plugin => $plugindir) {
      // Include all the settings commands for this plugin if there are any
         if (file_exists($plugindir.'/settings.php')) {
             $settings = new admin_settingpage('gradeexport'.$plugin, new lang_string('pluginname', 'gradeexport_'.$plugin), 'moodle/grade:manage');
index a11cbfa..502d5e6 100644 (file)
@@ -17,9 +17,6 @@ if ($hassiteconfig) {
         $module->load_settings($ADMIN, 'modsettings', $hassiteconfig);
     }
 
-    // hidden script for converting journals to online assignments (or something like that) linked from elsewhere
-    $ADMIN->add('modsettings', new admin_externalpage('oacleanup', 'Online Assignment Cleanup', $CFG->wwwroot.'/'.$CFG->admin.'/oacleanup.php', 'moodle/site:config', true));
-
     // course formats
     $ADMIN->add('modules', new admin_category('formatsettings', new lang_string('courseformats')));
     $temp = new admin_settingpage('manageformats', new lang_string('manageformats', 'core_admin'));
@@ -385,7 +382,7 @@ $ADMIN->add('reports', new admin_externalpage('comments', new lang_string('comme
 // Course reports settings
 if ($hassiteconfig) {
     $pages = array();
-    foreach (get_plugin_list('coursereport') as $report => $path) {
+    foreach (core_component::get_plugin_list('coursereport') as $report => $path) {
         $file = $CFG->dirroot . '/course/report/' . $report . '/settings.php';
         if (file_exists($file)) {
             $settings = new admin_settingpage('coursereport' . $report,
@@ -408,7 +405,7 @@ if ($hassiteconfig) {
 
 // Now add reports
 $pages = array();
-foreach (get_plugin_list('report') as $report => $plugindir) {
+foreach (core_component::get_plugin_list('report') as $report => $plugindir) {
     $settings_path = "$plugindir/settings.php";
     if (file_exists($settings_path)) {
         $settings = new admin_settingpage('report' . $report,
@@ -434,7 +431,7 @@ if ($hassiteconfig) {
 }
 
 // Now add various admin tools
-foreach (get_plugin_list('tool') as $plugin => $plugindir) {
+foreach (core_component::get_plugin_list('tool') as $plugin => $plugindir) {
     $settings_path = "$plugindir/settings.php";
     if (file_exists($settings_path)) {
         include($settings_path);
@@ -447,7 +444,7 @@ if ($hassiteconfig) {
     $ADMIN->add('cache', new admin_externalpage('cacheconfig', new lang_string('cacheconfig', 'cache'), $CFG->wwwroot .'/cache/admin.php'));
     $ADMIN->add('cache', new admin_externalpage('cachetestperformance', new lang_string('testperformance', 'cache'), $CFG->wwwroot . '/cache/testperformance.php'));
     $ADMIN->add('cache', new admin_category('cachestores', new lang_string('cachestores', 'cache')));
-    foreach (get_plugin_list('cachestore') as $plugin => $path) {
+    foreach (core_component::get_plugin_list('cachestore') as $plugin => $path) {
         $settingspath = $path.'/settings.php';
         if (file_exists($settingspath)) {
             $settings = new admin_settingpage('cachestore_'.$plugin.'_settings', new lang_string('pluginname', 'cachestore_'.$plugin), 'moodle/site:config');
@@ -466,7 +463,7 @@ if ($hassiteconfig) {
 
 // extend settings for each local plugin. Note that their settings may be in any part of the
 // settings tree and may be visible not only for administrators. We can not use $allplugins here
-foreach (get_plugin_list('local') as $plugin => $plugindir) {
+foreach (core_component::get_plugin_list('local') as $plugin => $plugindir) {
     $settings_path = "$plugindir/settings.php";
     if (file_exists($settings_path)) {
         include($settings_path);
index 0cd2306..6b4f88f 100644 (file)
@@ -62,7 +62,7 @@ class behat_admin extends behat_base {
             }
 
             // Search by label.
-            $searchbox = $this->find_field('Search in settings');
+            $searchbox = $this->find_field(get_string('searchinsettings', 'admin'));
             $searchbox->setValue($label);
             $submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]');
             $submitsearch->press();
@@ -72,8 +72,12 @@ class behat_admin extends behat_base {
             // Admin settings does not use the same DOM structure than other moodle forms
             // but we also need to use lib/behat/form_field/* to deal with the different moodle form elements.
             $exception = new ElementNotFoundException($this->getSession(), '"' . $label . '" administration setting ');
+
+            // The argument should be converted to an xpath literal.
+            $label = $this->getSession()->getSelectorsHandler()->xpathLiteral($label);
+
             $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" .
-                "[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]";
+                "[@id=//label[contains(normalize-space(.), $label)]/@for]";
             $fieldnode = $this->find('xpath', $fieldxpath, $exception);
             $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" .
                 "/child::div[contains(concat(' ', @class, ' '),  ' form-')]/child::*/parent::div");
@@ -90,7 +94,7 @@ class behat_admin extends behat_base {
             $field = behat_field_manager::get_field_instance($type, $fieldnode, $this->getSession());
             $field->set_value($value);
 
-            $this->find_button('Save changes')->press();
+            $this->find_button(get_string('savechanges'))->press();
         }
     }
 
index b6c8258..f17f092 100644 (file)
@@ -15,7 +15,7 @@ Feature: Display extended course names
     And I should not see "C_shortname Course fullname"
 
   Scenario: Courses list with extended course names
-    Given I click on "Courses" "link" in the "//div[@id='settingsnav']//descendant::li[contains(concat(' ', @class, ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
+    Given I click on "Courses" "link" in the "//div[@id='settingsnav']/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element"
     And I check "Display extended course names"
     When I press "Save changes"
     And I am on homepage
index 9ac6931..5547b56 100644 (file)
@@ -34,6 +34,7 @@ if (function_exists('opcache_reset')) {
 
 // Is not really necessary but adding it as is a CLI_SCRIPT.
 define('CLI_SCRIPT', true);
+define('CACHE_DISABLE_ALL', true);
 
 // Basic functions.
 require_once(__DIR__ . '/../../../../lib/clilib.php');
index b28f4fc..ab41e3d 100644 (file)
@@ -84,6 +84,7 @@ define('BEHAT_UTIL', true);
 define('CLI_SCRIPT', true);
 define('ABORT_AFTER_CONFIG', true);
 define('NO_OUTPUT_BUFFERING', true);
+define('IGNORE_COMPONENT_CACHE', true);
 
 error_reporting(E_ALL | E_STRICT);
 ini_set('display_errors', '1');
@@ -142,6 +143,9 @@ foreach ($vars as $var) {
     $CFG->{$var} = $CFG->{'behat_' . $var};
 }
 
+// Clean $CFG extra values before performing any action.
+behat_clean_init_config();
+
 $CFG->noemailever = true;
 $CFG->passwordsaltmain = 'moodle';
 
@@ -150,6 +154,7 @@ $CFG->jsrev = 1;
 
 // Unset cache and temp directories to reset them again with the new $CFG->dataroot.
 unset($CFG->cachedir);
+unset($CFG->localcachedir);
 unset($CFG->tempdir);
 
 // Continues setup.
index 6c4f296..9f426cc 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically.';
 $string['allavailablesteps'] = 'All the available steps definitions';
 $string['giveninfo'] = 'Given. Processes to set up the environment';
+$string['infoheading'] = 'Info';
 $string['installinfo'] = 'Read {$a} for installation and tests execution info';
 $string['moreinfoin'] = 'More info in {$a}';
 $string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
@@ -39,6 +41,5 @@ $string['theninfo'] = 'Then. Checkings to ensure the outcomes are the expected o
 $string['viewsteps'] = 'Filter';
 $string['wheninfo'] = 'When. Actions that provokes an event';
 $string['wrongbehatsetup'] = 'Something is wrong with behat setup, ensure:<ul>
-<li>You ran "curl http://getcomposer.org/installer | php"</li>
-<li>You ran "php composer.phar install --dev"</li>
+<li>You ran "php admin/tool/behat/cli/init.php" from your moodle root directory</li>
 <li>vendor/bin/behat file has execution permissions</li></ul>';
index 503bc09..db94c85 100644 (file)
@@ -64,9 +64,10 @@ class tool_behat_renderer extends plugin_renderer_base {
             get_string('newstepsinfo', 'tool_behat', $writestepslink)
         );
 
-        // List of steps
+        // List of steps.
         $html .= $this->output->box_start();
-        $html .= html_writer::tag('h1', 'Info');
+        $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
+        $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
         $html .= html_writer::empty_tag('div');
         $html .= html_writer::empty_tag('ul');
         $html .= html_writer::empty_tag('li');
index c2f829b..637086f 100644 (file)
@@ -37,7 +37,7 @@ Feature: Page contents assertions
     And I follow "Course 1"
     When I click on "Move this to the dock" "button" in the ".block_settings" "css_element"
     Then I should not see "Question bank"
-    And I click on "//div[@id='dock']/descendant::*[contains(., 'Administration')]/h2" "xpath_element"
+    And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element"
 
   @javascript
   Scenario: Locators inside specific DOM nodes using XPath
@@ -45,5 +45,5 @@ Feature: Page contents assertions
       | fullname | shortname | category |
       | Course 1 | C1 | 0 |
     And I log in as "admin"
-    When I click on "Move this to the dock" "button" in the "//*[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
+    When I click on "Move this to the dock" "button" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element"
     Then I should not see "Turn editing on"
index a98adcf..e37f9bc 100644 (file)
@@ -30,17 +30,17 @@ Feature: Set up contextual data for tests
     Then I should see "Course 1"
     And I should see "Course 2"
     And I should see "Course 3"
-    When I go to the courses management page
+    And I go to the courses management page
     And I follow "Cat 1"
-    Then I should see "Cat 2"
+    And I should see "Cat 2"
     And I should see "Cat 3"
-    When I follow "Cat 3"
-    Then I should see "Course 1"
+    And I follow "Cat 3"
+    And I should see "Course 1"
     And I should see "Course 2"
-    When I select "Cat 2" from "Course categories:"
-    Then I should see "No courses in this category"
-    When I select "Miscellaneous" from "Course categories:"
-    Then I should see "Course 3"
+    And I select "Cat 1 / Cat 2" from "Course categories:"
+    And I should see "No courses in this category"
+    And I select "Miscellaneous" from "Course categories:"
+    And I should see "Course 3"
 
   @javascript
   Scenario: Add a bunch of groups and groupings
@@ -79,6 +79,49 @@ Feature: Set up contextual data for tests
     And I follow "Course 1"
     Then I should see "Topic 1"
 
+  Scenario: Add role assigns
+    Given the following "users" exists:
+      | username | firstname | lastname | email |
+      | user1 | User | 1 | user1@moodlemoodle.com |
+      | user2 | User | 2 | user2@moodlemoodle.com |
+      | user3 | User | 3 | user3@moodlemoodle.com |
+    And the following "categories" exists:
+      | name | category | idnumber |
+      | Cat 1 | 0 | CAT1 |
+    And the following "courses" exists:
+      | fullname | shortname | category |
+      | Course 1 | C1 | CAT1 |
+    And the following "role assigns" exists:
+      | user  | role           | contextlevel | reference |
+      | user1 | manager        | System       |           |
+      | user2 | editingteacher | Category     | CAT1      |
+      | user3 | editingteacher | Course       | C1        |
+    When I log in as "user1"
+    Then I should see "Front page settings"
+    And I log out
+    And I log in as "user2"
+    And I follow "Course 1"
+    And I should see "Turn editing on"
+    And I log out
+    And I log in as "user3"
+    And I follow "Course 1"
+    And I should see "Turn editing on"
+
+  Scenario: Add modules
+    Given the following "courses" exists:
+      | fullname | shortname |
+      | Course 1 | C1 |
+    And the following "activities" exists:
+      | activity | name | intro | course | idnumber |
+      | assign   | Test assignment name | Test assignment description | C1 | assign1 |
+      | data     | Test database name | Test database description | C1 | data1 |
+    When I log in as "admin"
+    And I follow "Course 1"
+    Then I should see "Test assignment name"
+    And I should see "Test database name"
+    And I follow "Test assignment name"
+    And I should see "Test assignment description"
+
   @javascript
   Scenario: Add relations between users and groups
     Given the following "users" exists:
index 547a8a4..19d74c1 100644 (file)
@@ -39,7 +39,7 @@ class tool_customlang_filter_form extends moodleform {
         // Component
         $options = array();
         foreach (tool_customlang_utils::list_components() as $component => $normalized) {
-            list($type, $plugin) = normalize_component($normalized);
+            list($type, $plugin) = core_component::normalize_component($normalized);
             if ($type == 'core' and is_null($plugin)) {
                 $plugin = 'moodle';
             }
index f209785..2492730 100644 (file)
@@ -267,7 +267,7 @@ class tool_customlang_utils {
         if ($filename !== clean_param($filename, PARAM_FILE)) {
             throw new coding_exception('Incorrect file name '.s($filename));
         }
-        list($package, $subpackage) = normalize_component($component);
+        list($package, $subpackage) = core_component::normalize_component($component);
         $packageinfo = " * @package    $package";
         if (!is_null($subpackage)) {
             $packageinfo .= "\n * @subpackage $subpackage";
index 8b0b062..190d86f 100644 (file)
@@ -105,9 +105,9 @@ function tool_dbtransfer_rebuild_target_log_actions(moodle_database $target, pro
     try {
         $DB->delete_records('log_display', array('component'=>'moodle'));
         log_update_descriptions('moodle');
-        $plugintypes = get_plugin_types();
+        $plugintypes = core_component::get_plugin_types();
         foreach ($plugintypes as $type => $location) {
-            $plugs = get_plugin_list($type);
+            $plugs = core_component::get_plugin_list($type);
             foreach ($plugs as $plug => $fullplug) {
                 $component = $type.'_'.$plug;
                 $DB->delete_records('log_display', array('component'=>$component));
index 0b888ba..37c3600 100644 (file)
@@ -182,7 +182,7 @@ class tool_installaddon_installer {
     public function get_plugintype_root($plugintype) {
 
         $plugintypepath = null;
-        foreach (get_plugin_types() as $type => $fullpath) {
+        foreach (core_component::get_plugin_types() as $type => $fullpath) {
             if ($type === $plugintype) {
                 $plugintypepath = $fullpath;
                 break;
@@ -250,7 +250,7 @@ class tool_installaddon_installer {
             exit();
         }
 
-        list($plugintype, $pluginname) = normalize_component($data->component);
+        list($plugintype, $pluginname) = core_component::normalize_component($data->component);
 
         $plugintypepath = $this->get_plugintype_root($plugintype);
 
index 557fbc1..cafd9b9 100644 (file)
@@ -332,7 +332,7 @@ class tool_installaddon_validator {
 
         if (isset($info[$type.'->component'])) {
             $this->versionphp['component'] = $info[$type.'->component'];
-            list($reqtype, $reqname) = normalize_component($this->versionphp['component']);
+            list($reqtype, $reqname) = core_component::normalize_component($this->versionphp['component']);
             if ($reqtype !== $this->assertions['plugintype']) {
                 $this->add_message(self::ERROR, 'componentmismatchtype', array(
                     'expected' => $this->assertions['plugintype'],
@@ -563,7 +563,7 @@ class tool_installaddon_validator {
 
         $plugintypepath = null;
 
-        foreach (get_plugin_types() as $type => $fullpath) {
+        foreach (core_component::get_plugin_types() as $type => $fullpath) {
             if ($type === $plugintype) {
                 $plugintypepath = $fullpath;
                 break;
index a3d875d..eb807ee 100644 (file)
@@ -32,6 +32,8 @@ if (function_exists('opcache_reset')) {
     opcache_reset();
 }
 
+define('IGNORE_COMPONENT_CACHE', true);
+
 require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
 require_once(__DIR__.'/../../../../lib/testing/lib.php');
index 035ce7b..be593a2 100644 (file)
@@ -28,6 +28,8 @@ if (isset($_SERVER['REMOTE_ADDR'])) {
     die; // no access from web!
 }
 
+define('IGNORE_COMPONENT_CACHE', true);
+
 require_once(__DIR__.'/../../../../lib/clilib.php');
 require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
 require_once(__DIR__.'/../../../../lib/testing/lib.php');
index a5e21d3..e602e44 100644 (file)
@@ -74,12 +74,11 @@ if (isset($script)) {
     $prevreferences = $DB->get_records_select('profiling',
                                               'url = ? AND runreference = 1 AND timecreated < ?',
                                               array($run->url, $run->timecreated),
-                                              'timecreated DESC', 'runid', 0, 1);
-    $prevrunid = $prevreferences ? reset($prevreferences)->runid : false;
+                                              'timecreated DESC', 'runid, runcomment, timecreated', 0, 10);
     echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
     $header = get_string('lastrunof', 'tool_profiling', $script);
     echo $OUTPUT->heading($header);
-    $table = profiling_print_run($run, $prevrunid);
+    $table = profiling_print_run($run, $prevreferences);
     echo $table;
     echo $OUTPUT->box_end();
 
@@ -126,12 +125,11 @@ if (isset($script)) {
     $prevreferences = $DB->get_records_select('profiling',
                                               'url = ? AND runreference = 1 AND timecreated < ?',
                                               array($run->url, $run->timecreated),
-                                              'timecreated DESC', 'runid', 0, 1);
-    $prevrunid = $prevreferences ? reset($prevreferences)->runid : false;
+                                              'timecreated DESC', 'runid, runcomment, timecreated', 0, 10);
     echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter');
     $header = get_string('summaryof', 'tool_profiling', $run->url);
     echo $OUTPUT->heading($header);
-    $table = profiling_print_run($run, $prevrunid);
+    $table = profiling_print_run($run, $prevreferences);
     echo $table;
     echo $OUTPUT->box_end();
 
index 4a687f8..7c80c84 100644 (file)
@@ -51,5 +51,5 @@ $string['referencerun'] = 'Reference run/comment';
 $string['runid'] = 'Run ID';
 $string['summaryof'] = 'Summary of {$a}';
 $string['viewdetails'] = 'View profiling details';
-$string['viewdiff'] = 'View profiling differences with last reference run';
+$string['viewdiff'] = 'View profiling differences with:';
 $string['viewdiffdetails'] = 'View profiling diff details';
index d9c568b..0e60dc2 100644 (file)
@@ -44,7 +44,7 @@ class tool_qeupgradehelper_extract_options_form extends moodleform {
             'adaptivenopenalty' => 'Adaptive (no penalties)',
         );
 
-        $qtypes = get_plugin_list('qtype');
+        $qtypes = core_component::get_plugin_list('qtype');
         foreach ($qtypes as $qtype => $notused) {
             $qtypes[$qtype] = get_string($qtype, 'qtype_' . $qtype);
         }
diff --git a/admin/tool/uploadcourse/classes/base_form.php b/admin/tool/uploadcourse/classes/base_form.php
new file mode 100644 (file)
index 0000000..fc0ddf9
--- /dev/null
@@ -0,0 +1,93 @@
+<?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/>.
+
+/**
+ * File containing the base import form.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Base import form.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_base_form extends moodleform {
+
+    /**
+     * Empty definition.
+     *
+     * @return void
+     */
+    public function definition() {
+    }
+
+    /**
+     * Adds the import settings part.
+     *
+     * @return void
+     */
+    public function add_import_options() {
+        $mform = $this->_form;
+
+        // Upload settings and file.
+        $mform->addElement('header', 'importoptionshdr', get_string('importoptions', 'tool_uploadcourse'));
+        $mform->setExpanded('importoptionshdr', true);
+
+        $choices = array(
+            tool_uploadcourse_processor::MODE_CREATE_NEW => get_string('createnew', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('createall', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('createorupdate', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('updateonly', 'tool_uploadcourse')
+        );
+        $mform->addElement('select', 'options[mode]', get_string('mode', 'tool_uploadcourse'), $choices);
+
+        $choices = array(
+            tool_uploadcourse_processor::UPDATE_NOTHING => get_string('nochanges', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY => get_string('updatewithdataonly', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS =>
+                get_string('updatewithdataordefaults', 'tool_uploadcourse'),
+            tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('updatemissing', 'tool_uploadcourse')
+        );
+        $mform->addElement('select', 'options[updatemode]', get_string('updatemode', 'tool_uploadcourse'), $choices);
+        $mform->setDefault('options[updatemode]', tool_uploadcourse_processor::UPDATE_NOTHING);
+        $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+        $mform->addElement('selectyesno', 'options[allowdeletes]', get_string('allowdeletes', 'tool_uploadcourse'));
+        $mform->setDefault('options[allowdeletes]', 0);
+        $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+        $mform->addElement('selectyesno', 'options[allowrenames]', get_string('allowrenames', 'tool_uploadcourse'));
+        $mform->setDefault('options[allowrenames]', 0);
+        $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+        $mform->addElement('selectyesno', 'options[allowresets]', get_string('allowresets', 'tool_uploadcourse'));
+        $mform->setDefault('options[allowresets]', 0);
+        $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+    }
+
+}
diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php
new file mode 100644 (file)
index 0000000..629e498
--- /dev/null
@@ -0,0 +1,898 @@
+<?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/>.
+
+/**
+ * File containing the course class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * Course class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_course {
+
+    /** Outcome of the process: creating the course */
+    const DO_CREATE = 1;
+
+    /** Outcome of the process: updating the course */
+    const DO_UPDATE = 2;
+
+    /** Outcome of the process: deleting the course */
+    const DO_DELETE = 3;
+
+    /** @var array final import data. */
+    protected $data = array();
+
+    /** @var array default values. */
+    protected $defaults = array();
+
+    /** @var array enrolment data. */
+    protected $enrolmentdata;
+
+    /** @var array errors. */
+    protected $errors = array();
+
+    /** @var int the ID of the course that had been processed. */
+    protected $id;
+
+    /** @var array containing options passed from the processor. */
+    protected $importoptions = array();
+
+    /** @var int import mode. Matches tool_uploadcourse_processor::MODE_* */
+    protected $mode;
+
+    /** @var array course import options. */
+    protected $options = array();
+
+    /** @var int constant value of self::DO_*, what to do with that course */
+    protected $do;
+
+    /** @var bool set to true once we have prepared the course */
+    protected $prepared = false;
+
+    /** @var bool set to true once we have started the process of the course */
+    protected $processstarted = false;
+
+    /** @var array course import data. */
+    protected $rawdata = array();
+
+    /** @var array restore directory. */
+    protected $restoredata;
+
+    /** @var string course shortname. */
+    protected $shortname;
+
+    /** @var array errors. */
+    protected $statuses = array();
+
+    /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */
+    protected $updatemode;
+
+    /** @var array fields allowed as course data. */
+    static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate',
+        'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes',
+        'groupmode', 'groupmodeforce', 'groupmodeforce', 'enablecompletion');
+
+    /** @var array fields required on course creation. */
+    static protected $mandatoryfields = array('fullname', 'category');
+
+    /** @var array fields which are considered as options. */
+    static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null,
+        'templatecourse' => null, 'reset' => false);
+
+    /** @var array options determining what can or cannot be done at an import level. */
+    static protected $importoptionsdefaults = array('canrename' => false, 'candelete' => false, 'canreset' => false,
+        'reset' => false, 'restoredir' => null, 'shortnametemplate' => null);
+
+    /**
+     * Constructor
+     *
+     * @param int $mode import mode, constant matching tool_uploadcourse_processor::MODE_*
+     * @param int $updatemode update mode, constant matching tool_uploadcourse_processor::UPDATE_*
+     * @param array $rawdata raw course data.
+     * @param array $defaults default course data.
+     * @param array $importoptions import options.
+     */
+    public function __construct($mode, $updatemode, $rawdata, $defaults = array(), $importoptions = array()) {
+
+        if ($mode !== tool_uploadcourse_processor::MODE_CREATE_NEW &&
+                $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL &&
+                $mode !== tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE &&
+                $mode !== tool_uploadcourse_processor::MODE_UPDATE_ONLY) {
+            throw new coding_exception('Incorrect mode.');
+        } else if ($updatemode !== tool_uploadcourse_processor::UPDATE_NOTHING &&
+                $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY &&
+                $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS &&
+                $updatemode !== tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS) {
+            throw new coding_exception('Incorrect update mode.');
+        }
+
+        $this->mode = $mode;
+        $this->updatemode = $updatemode;
+
+        if (isset($rawdata['shortname'])) {
+            $this->shortname = $rawdata['shortname'];
+        }
+        $this->rawdata = $rawdata;
+        $this->defaults = $defaults;
+
+        // Extract course options.
+        foreach (self::$optionfields as $option => $default) {
+            $this->options[$option] = isset($rawdata[$option]) ? $rawdata[$option] : $default;
+        }
+
+        // Import options.
+        foreach (self::$importoptionsdefaults as $option => $default) {
+            $this->importoptions[$option] = isset($importoptions[$option]) ? $importoptions[$option] : $default;
+        }
+    }
+
+    /**
+     * Does the mode allow for course creation?
+     *
+     * @return bool
+     */
+    public function can_create() {
+        return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL,
+            tool_uploadcourse_processor::MODE_CREATE_NEW,
+            tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)
+        );
+    }
+
+    /**
+     * Does the mode allow for course deletion?
+     *
+     * @return bool
+     */
+    public function can_delete() {
+        return $this->importoptions['candelete'];
+    }
+
+    /**
+     * Does the mode only allow for course creation?
+     *
+     * @return bool
+     */
+    public function can_only_create() {
+        return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL,
+            tool_uploadcourse_processor::MODE_CREATE_NEW));
+    }
+
+    /**
+     * Does the mode allow for course rename?
+     *
+     * @return bool
+     */
+    public function can_rename() {
+        return $this->importoptions['canrename'];
+    }
+
+    /**
+     * Does the mode allow for course reset?
+     *
+     * @return bool
+     */
+    public function can_reset() {
+        return $this->importoptions['canreset'];
+    }
+
+    /**
+     * Does the mode allow for course update?
+     *
+     * @return bool
+     */
+    public function can_update() {
+        return in_array($this->mode,
+                array(
+                    tool_uploadcourse_processor::MODE_UPDATE_ONLY,
+                    tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)
+                ) && $this->updatemode != tool_uploadcourse_processor::UPDATE_NOTHING;
+    }
+
+    /**
+     * Can we use default values?
+     *
+     * @return bool
+     */
+    public function can_use_defaults() {
+        return in_array($this->updatemode, array(tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS,
+            tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS));
+    }
+
+    /**
+     * Delete the current course.
+     *
+     * @return bool
+     */
+    protected function delete() {
+        global $DB;
+        $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname',
+            array('shortname' => $this->shortname), MUST_EXIST);
+        return delete_course($this->id, false);
+    }
+
+    /**
+     * Log an error
+     *
+     * @param string $code error code.
+     * @param lang_string $message error message.
+     * @return void
+     */
+    protected function error($code, lang_string $message) {
+        if (array_key_exists($code, $this->errors)) {
+            throw new coding_exception('Error code already defined');
+        }
+        $this->errors[$code] = $message;
+    }
+
+    /**
+     * Return whether the course exists or not.
+     *
+     * @param string $shortname the shortname to use to check if the course exists. Falls back on $this->shortname if empty.
+     * @return bool
+     */
+    protected function exists($shortname = null) {
+        global $DB;
+        if (is_null($shortname)) {
+            $shortname = $this->shortname;
+        }
+        if (!empty($shortname) || is_numeric($shortname)) {
+            return $DB->record_exists('course', array('shortname' => $shortname));
+        }
+        return false;
+    }
+
+    /**
+     * Return the data that will be used upon saving.
+     *
+     * @return null|array
+     */
+    public function get_data() {
+        return $this->data;
+    }
+
+    /**
+     * Return the errors found during preparation.
+     *
+     * @return array
+     */
+    public function get_errors() {
+        return $this->errors;
+    }
+
+    /**
+     * Assemble the course data based on defaults.
+     *
+     * This returns the final data to be passed to create_course().
+     *
+     * @param array $data current data.
+     * @return array
+     */
+    protected function get_final_create_data($data) {
+        foreach (self::$validfields as $field) {
+            if (!isset($data[$field]) && isset($this->defaults[$field])) {
+                $data[$field] = $this->defaults[$field];
+            }
+        }
+        $data['shortname'] = $this->shortname;
+        return $data;
+    }
+
+    /**
+     * Assemble the course data based on defaults.
+     *
+     * This returns the final data to be passed to update_course().
+     *
+     * @param array $data current data.
+     * @param bool $usedefaults are defaults allowed?
+     * @param bool $missingonly ignore fields which are already set.
+     * @return array
+     */
+    protected function get_final_update_data($data, $usedefaults = false, $missingonly = false) {
+        global $DB;
+        $newdata = array();
+        $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
+        foreach (self::$validfields as $field) {
+            if ($missingonly) {
+                if (!is_null($existingdata->$field) and $existingdata->$field !== '') {
+                    continue;
+                }
+            }
+            if (isset($data[$field])) {
+                $newdata[$field] = $data[$field];
+            } else if ($usedefaults && isset($this->defaults[$field])) {
+                $newdata[$field] = $this->defaults[$field];
+            }
+        }
+        $newdata['id'] =  $existingdata->id;
+        return $newdata;
+    }
+
+    /**
+     * Return the ID of the processed course.
+     *
+     * @return int|null
+     */
+    public function get_id() {
+        if (!$this->processstarted) {
+            throw new coding_exception('The course has not been processed yet!');
+        }
+        return $this->id;
+    }
+
+    /**
+     * Get the directory of the object to restore.
+     *
+     * @return string|false subdirectory in $CFG->tempdir/backup/...
+     */
+    protected function get_restore_content_dir() {
+        $backupfile = null;
+        $shortname = null;
+
+        if (!empty($this->options['backupfile'])) {
+            $backupfile = $this->options['backupfile'];
+        } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) {
+            $shortname = $this->options['templatecourse'];
+        }
+
+        $errors = array();
+        $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname, $errors);
+        if (!empty($errors)) {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+            return false;
+        }
+
+        if (empty($dir) && !empty($this->importoptions['restoredir'])) {
+            $dir = $this->importoptions['restoredir'];
+        }
+
+        return $dir;
+    }
+
+    /**
+     * Return the errors found during preparation.
+     *
+     * @return array
+     */
+    public function get_statuses() {
+        return $this->statuses;
+    }
+
+    /**
+     * Return whether there were errors with this course.
+     *
+     * @return boolean
+     */
+    public function has_errors() {
+        return !empty($this->errors);
+    }
+
+    /**
+     * Validates and prepares the data.
+     *
+     * @return bool false is any error occured.
+     */
+    public function prepare() {
+        global $DB;
+        $this->prepared = true;
+
+        // Validate the shortname.
+        if (!empty($this->shortname) || is_numeric($this->shortname)) {
+            if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) {
+                $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
+                return false;
+            }
+        }
+
+        $exists = $this->exists();
+
+        // Do we want to delete the course?
+        if ($this->options['delete']) {
+            if (!$exists) {
+                $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse'));
+                return false;
+            } else if (!$this->can_delete()) {
+                $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse'));
+                return false;
+            }
+
+            $this->do = self::DO_DELETE;
+            return true;
+        }
+
+        // Can we create/update the course under those conditions?
+        if ($exists) {
+            if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
+                $this->error('courseexistsanduploadnotallowed',
+                    new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
+                return false;
+            }
+        } else {
+            if (!$this->can_create()) {
+                $this->error('coursedoesnotexistandcreatenotallowed',
+                    new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
+                return false;
+            }
+        }
+
+        // Basic data.
+        $coursedata = array();
+        foreach ($this->rawdata as $field => $value) {
+            if (!in_array($field, self::$validfields)) {
+                continue;
+            } else if ($field == 'shortname') {
+                // Let's leave it apart from now, use $this->shortname only.
+                continue;
+            }
+            $coursedata[$field] = $value;
+        }
+
+        $mode = $this->mode;
+        $updatemode = $this->updatemode;
+        $usedefaults = $this->can_use_defaults();
+
+        // Resolve the category, and fail if not found.
+        $errors = array();
+        $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors);
+        if (empty($errors)) {
+            $coursedata['category'] = $catid;
+        } else {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+            return false;
+        }
+
+        // If the course does not exist, or will be forced created.
+        if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
+
+            // Mandatory fields upon creation.
+            $errors = array();
+            foreach (self::$mandatoryfields as $field) {
+                if ((!isset($coursedata[$field]) || $coursedata[$field] === '') &&
+                        (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) {
+                    $errors[] = $field;
+                }
+            }
+            if (!empty($errors)) {
+                $this->error('missingmandatoryfields', new lang_string('missingmandatoryfields', 'tool_uploadcourse',
+                    implode(', ', $errors)));
+                return false;
+            }
+        }
+
+        // Should the course be renamed?
+        if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) {
+            if (!$this->can_update()) {
+                $this->error('canonlyrenameinupdatemode', new lang_string('canonlyrenameinupdatemode', 'tool_uploadcourse'));
+                return false;
+            } else if (!$exists) {
+                $this->error('cannotrenamecoursenotexist', new lang_string('cannotrenamecoursenotexist', 'tool_uploadcourse'));
+                return false;
+            } else if (!$this->can_rename()) {
+                $this->error('courserenamingnotallowed', new lang_string('courserenamingnotallowed', 'tool_uploadcourse'));
+                return false;
+            } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) {
+                $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
+                return false;
+            } else if ($this->exists($this->options['rename'])) {
+                $this->error('cannotrenameshortnamealreadyinuse',
+                    new lang_string('cannotrenameshortnamealreadyinuse', 'tool_uploadcourse'));
+                return false;
+            } else if (isset($coursedata['idnumber']) &&
+                    $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn',
+                    array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) {
+                $this->error('cannotrenameidnumberconflict', new lang_string('cannotrenameidnumberconflict', 'tool_uploadcourse'));
+                return false;
+            }
+            $coursedata['shortname'] = $this->options['rename'];
+            $this->status('courserenamed', new lang_string('courserenamed', 'tool_uploadcourse',
+                array('from' => $this->shortname, 'to' => $coursedata['shortname'])));
+        }
+
+        // Should we generate a shortname?
+        if (empty($this->shortname) && !is_numeric($this->shortname)) {
+            if (empty($this->importoptions['shortnametemplate'])) {
+                $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse'));
+                return false;
+            } else if (!$this->can_only_create()) {
+                $this->error('cannotgenerateshortnameupdatemode',
+                    new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse'));
+                return false;
+            } else {
+                $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata,
+                    $this->importoptions['shortnametemplate']);
+                if (is_null($newshortname)) {
+                    $this->error('generatedshortnameinvalid', new lang_string('generatedshortnameinvalid', 'tool_uploadcourse'));
+                    return false;
+                } else if ($this->exists($newshortname)) {
+                    if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
+                        $this->error('generatedshortnamealreadyinuse',
+                            new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse'));
+                        return false;
+                    }
+                    $exists = true;
+                }
+                $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse',
+                    $newshortname));
+                $this->shortname = $newshortname;
+            }
+        }
+
+        // If exists, but we only want to create courses, increment the shortname.
+        if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
+            $original = $this->shortname;
+            $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname);
+            $exists = false;
+            if ($this->shortname != $original) {
+                $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse',
+                    array('from' => $original, 'to' => $this->shortname)));
+                if (isset($coursedata['idnumber'])) {
+                    $originalidn = $coursedata['idnumber'];
+                    $coursedata['idnumber'] = tool_uploadcourse_helper::increment_idnumber($coursedata['idnumber']);
+                    if ($originalidn != $coursedata['idnumber']) {
+                        $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse',
+                            array('from' => $originalidn, 'to' => $coursedata['idnumber'])));
+                    }
+                }
+            }
+        }
+
+        // If the course does not exist, ensure that the ID number is not taken.
+        if (!$exists && isset($coursedata['idnumber'])) {
+            if ($DB->count_records_select('course', 'idnumber = :idn', array('idn' => $coursedata['idnumber'])) > 0) {
+                $this->error('idnumberalreadyinuse', new lang_string('idnumberalreadyinuse', 'tool_uploadcourse'));
+                return false;
+            }
+        }
+
+        // Ultimate check mode vs. existence.
+        switch ($mode) {
+            case tool_uploadcourse_processor::MODE_CREATE_NEW:
+            case tool_uploadcourse_processor::MODE_CREATE_ALL:
+                if ($exists) {
+                    $this->error('courseexistsanduploadnotallowed',
+                        new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
+                    return false;
+                }
+                break;
+            case tool_uploadcourse_processor::MODE_UPDATE_ONLY:
+                if (!$exists) {
+                    $this->error('coursedoesnotexistandcreatenotallowed',
+                        new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
+                    return false;
+                }
+                // No break!
+            case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE:
+                if ($exists) {
+                    if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) {
+                        $this->error('updatemodedoessettonothing',
+                            new lang_string('updatemodedoessettonothing', 'tool_uploadcourse'));
+                        return false;
+                    }
+                }
+                break;
+            default:
+                // O_o Huh?! This should really never happen here!
+                $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse'));
+                return false;
+        }
+
+        // Get final data.
+        if ($exists) {
+            $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS);
+            $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly);
+            $this->do = self::DO_UPDATE;
+        } else {
+            $coursedata = $this->get_final_create_data($coursedata);
+            $this->do = self::DO_CREATE;
+        }
+
+        // Course start date.
+        if (!empty($coursedata['startdate'])) {
+            $coursedata['startdate'] = strtotime($coursedata['startdate']);
+        }
+
+        // Add role renaming.
+        $errors = array();
+        $rolenames = tool_uploadcourse_helper::get_role_names($this->rawdata, $errors);
+        if (!empty($errors)) {
+            foreach ($errors as $key => $message) {
+                $this->error($key, $message);
+            }
+            return false;
+        }
+        foreach ($rolenames as $rolekey => $rolename) {
+            $coursedata[$rolekey] = $rolename;
+        }
+
+        // Some validation.
+        if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
+            $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
+            return false;
+        }
+
+        // Saving data.
+        $this->data = $coursedata;
+        $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
+
+        // Restore data.
+        // TODO log warnings.
+        $this->restoredata = $this->get_restore_content_dir();
+
+        // We can only reset courses when allowed and we are updating the course.
+        if ($this->importoptions['reset'] || $this->options['reset']) {
+            if ($this->do !== self::DO_UPDATE) {
+                $this->error('canonlyresetcourseinupdatemode',
+                    new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse'));
+                return false;
+            } else if (!$this->can_reset()) {
+                $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse'));
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Proceed with the import of the course.
+     *
+     * @return void
+     */
+    public function proceed() {
+        global $CFG, $USER;
+
+        if (!$this->prepared) {
+            throw new coding_exception('The course has not been prepared.');
+        } else if ($this->has_errors()) {
+            throw new moodle_exception('Cannot proceed, errors were detected.');
+        } else if ($this->processstarted) {
+            throw new coding_exception('The process has already been started.');
+        }
+        $this->processstarted = true;
+
+        if ($this->do === self::DO_DELETE) {
+            if ($this->delete()) {
+                $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse'));
+            } else {
+                $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse'));
+            }
+            return true;
+        } else if ($this->do === self::DO_CREATE) {
+            $course = create_course((object) $this->data);
+            $this->id = $course->id;
+            $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse'));
+        } else if ($this->do === self::DO_UPDATE) {
+            $course = (object) $this->data;
+            update_course($course);
+            $this->id = $course->id;
+            $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse'));
+        } else {
+            // Strangely the outcome has not been defined, or is unknown!
+            throw new coding_exception('Unknown outcome!');
+        }
+
+        // Restore a course.
+        if (!empty($this->restoredata)) {
+            $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO,
+                backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
+
+            // Check if the format conversion must happen first.
+            if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) {
+                $rc->convert();
+            }
+            if ($rc->execute_precheck()) {
+                $rc->execute_plan();
+                $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse'));
+            } else {
+                $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
+            }
+            $rc->destroy();
+        }
+
+        // Proceed with enrolment data.
+        $this->process_enrolment_data($course);
+
+        // Reset the course.
+        if ($this->importoptions['reset'] || $this->options['reset']) {
+            if ($this->do === self::DO_UPDATE && $this->can_reset()) {
+                $this->reset($course);
+                $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse'));
+            }
+        }
+
+        // Mark context as dirty.
+        $context = context_course::instance($course->id);
+        $context->mark_dirty();
+    }
+
+    /**
+     * Add the enrolment data for the course.
+     *
+     * @param object $course course record.
+     * @return void
+     */
+    protected function process_enrolment_data($course) {
+        global $DB;
+
+        $enrolmentdata = $this->enrolmentdata;
+        if (empty($enrolmentdata)) {
+            return;
+        }
+
+        $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
+        $instances = enrol_get_instances($course->id, false);
+        foreach ($enrolmentdata as $enrolmethod => $method) {
+
+            $instance = null;
+            foreach ($instances as $i) {
+                if ($i->enrol == $enrolmethod) {
+                    $instance = $i;
+                    break;
+                }
+            }
+
+            $todelete = isset($method['delete']) && $method['delete'];
+            $todisable = isset($method['disable']) && $method['disable'];
+            unset($method['delete']);
+            unset($method['disable']);
+
+            if (!empty($instance) && $todelete) {
+                // Remove the enrolment method.
+                foreach ($instances as $instance) {
+                    if ($instance->enrol == $enrolmethod) {
+                        $plugin = $enrolmentplugins[$instance->enrol];
+                        $plugin->delete_instance($instance);
+                        break;
+                    }
+                }
+            } else if (!empty($instance) && $todisable) {
+                // Disable the enrolment.
+                foreach ($instances as $instance) {
+                    if ($instance->enrol == $enrolmethod) {
+                        $plugin = $enrolmentplugins[$instance->enrol];
+                        $plugin->update_status($instance, ENROL_INSTANCE_DISABLED);
+                        $enrol_updated = true;
+                        break;
+                    }
+                }
+            } else {
+                $plugin = null;
+                if (empty($instance)) {
+                    $plugin = $enrolmentplugins[$enrolmethod];
+                    $instance = new stdClass();
+                    $instance->id = $plugin->add_default_instance($course);
+                    $instance->roleid = $plugin->get_config('roleid');
+                    $instance->status = ENROL_INSTANCE_ENABLED;
+                } else {
+                    $plugin = $enrolmentplugins[$instance->enrol];
+                    $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
+                }
+
+                // Now update values.
+                foreach ($method as $k => $v) {
+                    $instance->{$k} = $v;
+                }
+
+                // Sort out the start, end and date.
+                $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0);
+                $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0);
+
+                // Is the enrolment period set?
+                if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) {
+                    if (preg_match('/^\d+$/', $method['enrolperiod'])) {
+                        $method['enrolperiod'] = (int) $method['enrolperiod'];
+                    } else {
+                        // Try and convert period to seconds.
+                        $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']);
+                    }
+                    $instance->enrolperiod = $method['enrolperiod'];
+                }
+                if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) {
+                    $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod'];
+                }
+                if ($instance->enrolenddate > 0) {
+                    $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate;
+                }
+                if ($instance->enrolenddate < $instance->enrolstartdate) {
+                    $instance->enrolenddate = $instance->enrolstartdate;
+                }
+
+                // Sort out the given role. This does not filter the roles allowed in the course.
+                if (isset($method['role'])) {
+                    $roleids = tool_uploadcourse_helper::get_role_ids();
+                    if (isset($roleids[$method['role']])) {
+                        $instance->roleid = $roleids[$method['role']];
+                    }
+                }
+
+                $instance->timemodified = time();
+                $DB->update_record('enrol', $instance);
+            }
+        }
+    }
+
+    /**
+     * Reset the current course.
+     *
+     * This does not reset any of the content of the activities.
+     *
+     * @param stdClass $course the course object of the course to reset.
+     * @return array status array of array component, item, error.
+     */
+    protected function reset($course) {
+        global $DB;
+
+        $resetdata = new stdClass();
+        $resetdata->id = $course->id;
+        $resetdata->reset_start_date = time();
+        $resetdata->reset_logs = true;
+        $resetdata->reset_events = true;
+        $resetdata->reset_notes = true;
+        $resetdata->delete_blog_associations = true;
+        $resetdata->reset_completion = true;
+        $resetdata->reset_roles_overrides = true;
+        $resetdata->reset_roles_local = true;
+        $resetdata->reset_groups_members = true;
+        $resetdata->reset_groups_remove = true;
+        $resetdata->reset_groupings_members = true;
+        $resetdata->reset_groupings_remove = true;
+        $resetdata->reset_gradebook_items = true;
+        $resetdata->reset_gradebook_grades = true;
+        $resetdata->reset_comments = true;
+
+        if (empty($course->startdate)) {
+            $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id));
+        }
+        $resetdata->reset_start_date_old = $course->startdate;
+
+        // Add roles.
+        $roles = tool_uploadcourse_helper::get_role_ids();
+        $resetdata->unenrol_users = array_values($roles);
+        $resetdata->unenrol_users[] = 0;    // Enrolled without role.
+
+        return reset_course_userdata($resetdata);
+    }
+
+    /**
+     * Log a status
+     *
+     * @param string $code status code.
+     * @param lang_string $message status message.
+     * @return void
+     */
+    protected function status($code, lang_string $message) {
+        if (array_key_exists($code, $this->statuses)) {
+            throw new coding_exception('Status code already defined');
+        }
+        $this->statuses[$code] = $message;
+    }
+
+}
diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php
new file mode 100644 (file)
index 0000000..b328a42
--- /dev/null
@@ -0,0 +1,486 @@
+<?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/>.
+
+/**
+ * File containing the helper class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->dirroot . '/cache/lib.php');
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+/**
+ * Class containing a set of helpers.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_helper {
+
+    /**
+     * Remove the restore content from disk and cache.
+     *
+     * @return void
+     */
+    public static function clean_restore_content() {
+        global $CFG;
+        if (!empty($CFG->keeptempdirectoriesonbackup)) {
+            $cache = cache::make('tool_uploadcourse', 'helper');
+            $backupids = (array) $cache->get('backupids');
+            foreach ($backupids as $cachekey => $backupid) {
+                $cache->delete($cachekey);
+                fulldelete("$CFG->tempdir/backup/$backupid/");
+            }
+            $cache->delete('backupids');
+        }
+    }
+
+    /**
+     * Generate a shortname based on a template.
+     *
+     * @param array|object $data course data.
+     * @param string $templateshortname template of shortname.
+     * @return null|string shortname based on the template, or null when an error occured.
+     */
+    public static function generate_shortname($data, $templateshortname) {
+        if (empty($templateshortname) && !is_numeric($templateshortname)) {
+            return null;
+        }
+        if (strpos($templateshortname, '%') === false) {
+            return $templateshortname;
+        }
+
+        $course = (object) $data;
+        $fullname   = isset($course->fullname) ? $course->fullname : '';
+        $idnumber   = isset($course->idnumber) ? $course->idnumber  : '';
+
+        $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber);
+        $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname);
+
+        if (!is_null($result)) {
+            $result = clean_param($result, PARAM_TEXT);
+        }
+
+        if (empty($result) && !is_numeric($result)) {
+            $result = null;
+        }
+
+        return $result;
+    }
+
+    /**
+     * Callback used when generating a shortname based on a template.
+     *
+     * @param string $fullname full name.
+     * @param string $idnumber ID number.
+     * @param array $block result from preg_replace_callback.
+     * @return string
+     */
+    public static function generate_shortname_callback($fullname, $idnumber, $block) {
+        switch ($block[3]) {
+            case 'f':
+                $repl = $fullname;
+                break;
+            case 'i':
+                $repl = $idnumber;
+                break;
+            default:
+                return $block[0];
+        }
+
+        switch ($block[1]) {
+            case '+':
+                $repl = textlib::strtoupper($repl);
+                break;
+            case '-':
+                $repl = textlib::strtolower($repl);
+                break;
+            case '~':
+                $repl = textlib::strtotitle($repl);
+                break;
+        }
+
+        if (!empty($block[2])) {
+            $repl = textlib::substr($repl, 0, $block[2]);
+        }
+
+        return $repl;
+    }
+
+    /**
+     * Return the available course formats.
+     *
+     * @return array
+     */
+    public static function get_course_formats() {
+        return array_keys(core_component::get_plugin_list('format'));
+    }
+
+    /**
+     * Extract enrolment data from passed data.
+     *
+     * Constructs an array of methods, and their options:
+     * array(
+     *     'method1' => array(
+     *         'option1' => value,
+     *         'option2' => value
+     *     ),
+     *     'method2' => array(
+     *         'option1' => value,
+     *         'option2' => value
+     *     )
+     * )
+     *
+     * @param array $data data to extract the enrolment data from.
+     * @return array
+     */
+    public static function get_enrolment_data($data) {
+        $enrolmethods = array();
+        $enroloptions = array();
+        foreach ($data as $field => $value) {
+
+            // Enrolmnent data.
+            $matches = array();
+            if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) {
+                $key = $matches[1];
+                if (!isset($enroloptions[$key])) {
+                    $enroloptions[$key] = array();
+                }
+                if (empty($matches[3])) {
+                    $enrolmethods[$key] = $value;
+                } else {
+                    $enroloptions[$key][$matches[3]] = $value;
+                }
+            }
+        }
+
+        // Combining enrolment methods and their options in a single array.
+        $enrolmentdata = array();
+        if (!empty($enrolmethods)) {
+            $enrolmentplugins = self::get_enrolment_plugins();
+            foreach ($enrolmethods as $key => $method) {
+                if (!array_key_exists($method, $enrolmentplugins)) {
+                    // Error!
+                    continue;
+                }
+                $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
+            }
+        }
+        return $enrolmentdata;
+    }
+
+    /**
+     * Return the enrolment plugins.
+     *
+     * The result is cached for faster execution.
+     *
+     * @return array
+     */
+    public static function get_enrolment_plugins() {
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        if (($enrol = $cache->get('enrol')) === false) {
+            $enrol = enrol_get_plugins(false);
+            $cache->set('enrol', $enrol);
+        }
+        return $enrol;
+    }
+
+    /**
+     * Get the restore content tempdir.
+     *
+     * The tempdir is the sub directory in which the backup has been extracted.
+     * This caches the result for better performance.
+     *
+     * @param string $backupfile path to a backup file.
+     * @param string $shortname shortname of a course.
+     * @param array $errors will be populated with errors found.
+     * @return string|false false when the backup couldn't retrieved.
+     */
+    public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
+        global $CFG, $DB, $USER;
+
+        $cachekey = null;
+        if (!empty($backupfile)) {
+            $backupfile = realpath($backupfile);
+            $cachekey = 'backup_path:' . $backupfile;
+        } else if (!empty($shortname) || is_numeric($shortname)) {
+            $cachekey = 'backup_sn:' . $shortname;
+        }
+
+        if (empty($cachekey)) {
+            return false;
+        }
+
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        if (($backupid = $cache->get($cachekey)) === false) {
+            // Use false instead of null because it would consider that the cache
+            // key has not been set.
+            $backupid = false;
+            if (!empty($backupfile)) {
+                if (!is_readable($backupfile)) {
+                    $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
+                } else {
+                    // Extracting the backup file.
+                    $packer = get_file_packer('application/vnd.moodle.backup');
+                    $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
+                    $path = "$CFG->tempdir/backup/$backupid/";
+                    $result = $packer->extract_to_pathname($backupfile, $path);
+                    if (!$result) {
+                        $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
+                    }
+                }
+            } else if (!empty($shortname) || is_numeric($shortname)) {
+                // Creating restore from an existing course.
+                $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING);
+                if (!empty($courseid)) {
+                    $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE,
+                        backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
+                    $bc->execute_plan();
+                    $backupid = $bc->get_backupid();
+                    $bc->destroy();
+                } else {
+                    $errors['coursetorestorefromdoesnotexist'] =
+                        new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
+                }
+            }
+            $cache->set($cachekey, $backupid);
+
+            // Store all the directories to be able to remove them in self::clean_restore_content().
+            $backupids = (array) $cache->get('backupids');
+            $backupids[$cachekey] = $backupid;
+            $cache->set('backupids', $backupids);
+        }
+
+        return $backupid;
+    }
+
+    /**
+     * Return the role IDs.
+     *
+     * The result is cached for faster execution.
+     *
+     * @return array
+     */
+    public static function get_role_ids() {
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        if (($roles = $cache->get('roles')) === false) {
+            $roles = array();
+            $rolesraw = get_all_roles();
+            foreach ($rolesraw as $role) {
+                $roles[$role->shortname] = $role->id;
+            }
+            $cache->set('roles', $roles);
+        }
+        return $roles;
+    }
+
+    /**
+     * Get the role renaming data from the passed data.
+     *
+     * @param array $data data to extract the names from.
+     * @param array $errors will be populated with errors found.
+     * @return array where the key is the role_<id>, the value is the new name.
+     */
+    public static function get_role_names($data, &$errors = array()) {
+        $rolenames = array();
+        $rolesids = self::get_role_ids();
+        $invalidroles = array();
+        foreach ($data as $field => $value) {
+
+            $matches = array();
+            if (preg_match('/^role_(.+)?$/', $field, $matches)) {
+                if (!isset($rolesids[$matches[1]])) {
+                    $invalidroles[] = $matches[1];
+                    continue;
+                }
+                $rolenames['role_' . $rolesids[$matches[1]]] = $value;
+            }
+
+        }
+
+        if (!empty($invalidroles)) {
+            $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
+        }
+
+        // Roles names.
+        return $rolenames;
+    }
+
+    /**
+     * Helper to increment an ID number.
+     *
+     * This first checks if the ID number is in use.
+     *
+     * @param string $idnumber ID number to increment.
+     * @return string new ID number.
+     */
+    public static function increment_idnumber($idnumber) {
+        global $DB;
+        while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
+            $matches = array();
+            if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
+                $newidnumber = $idnumber . '_2';
+            } else {
+                $newidnumber = $matches[1] . ((int) $matches[2] + 1);
+            }
+            $idnumber = $newidnumber;
+        }
+        return $idnumber;
+    }
+
+    /**
+     * Helper to increment a shortname.
+     *
+     * This considers that the shortname passed has to be incremented.
+     *
+     * @param string $shortname shortname to increment.
+     * @return string new shortname.
+     */
+    public static function increment_shortname($shortname) {
+        global $DB;
+        do {
+            $matches = array();
+            if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
+                $newshortname = $shortname . '_2';
+            } else {
+                $newshortname = $matches[1] . ($matches[2]+1);
+            }
+            $shortname = $newshortname;
+        } while ($DB->record_exists('course', array('shortname' => $shortname)));
+        return $shortname;
+    }
+
+    /**
+     * Resolve a category based on the data passed.
+     *
+     * Key accepted are:
+     * - category, which is supposed to be a category ID.
+     * - category_idnumber
+     * - category_path, array of categories from parent to child.
+     *
+     * @param array $data to resolve the category from.
+     * @param array $errors will be populated with errors found.
+     * @return int category ID.
+     */
+    public static function resolve_category($data, &$errors = array()) {
+        $catid = null;
+
+        if (!empty($data['category'])) {
+            $category = coursecat::get((int) $data['category'], IGNORE_MISSING);
+            if (!empty($category) && !empty($category->id)) {
+                $catid = $category->id;
+            } else {
+                $errors['couldnotresolvecatgorybyid'] =
+                    new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
+            }
+        }
+
+        if (empty($catid) && !empty($data['category_idnumber'])) {
+            $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
+            if (empty($catid)) {
+                $errors['couldnotresolvecatgorybyidnumber'] =
+                    new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
+            }
+        }
+        if (empty($catid) && !empty($data['category_path'])) {
+            $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
+            if (empty($catid)) {
+                $errors['couldnotresolvecatgorybypath'] =
+                    new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
+            }
+        }
+
+        return $catid;
+    }
+
+    /**
+     * Resolve a category by ID number.
+     *
+     * @param string $idnumber category ID number.
+     * @return int category ID.
+     */
+    public static function resolve_category_by_idnumber($idnumber) {
+        global $DB;
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        $cachekey = 'cat_idn_' . $idnumber;
+        if (($id = $cache->get($cachekey)) === false) {
+            $params = array('idnumber' => $idnumber);
+            $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
+
+            // Little hack to be able to differenciate between the cache not set and a category not found.
+            if ($id === false) {
+                $id = -1;
+            }
+
+            $cache->set($cachekey, $id);
+        }
+
+        // Little hack to be able to differenciate between the cache not set and a category not found.
+        if ($id == -1) {
+            $id = false;
+        }
+
+        return $id;
+    }
+
+    /**
+     * Resolve a category by path.
+     *
+     * @param array $path category names indexed from parent to children.
+     * @return int category ID.
+     */
+    public static function resolve_category_by_path(array $path) {
+        global $DB;
+        $cache = cache::make('tool_uploadcourse', 'helper');
+        $cachekey = 'cat_path_' . serialize($path);
+        if (($id = $cache->get($cachekey)) === false) {
+            $parent = 0;
+            $sql = 'name = :name AND parent = :parent';
+            while ($name = array_shift($path)) {
+                $params = array('name' => $name, 'parent' => $parent);
+                if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
+                    if (count($records) > 1) {
+                        // Too many records with the same name!
+                        $id = -1;
+                        break;
+                    }
+                    $record = reset($records);
+                    $id = $record->id;
+                    $parent = $record->id;
+                } else {
+                    // Not found.
+                    $id = -1;
+                    break;
+                }
+            }
+            $cache->set($cachekey, $id);
+        }
+
+        // We save -1 when the category has not been found to be able to know if the cache was set.
+        if ($id == -1) {
+            $id = false;
+        }
+        return $id;
+    }
+
+}
diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php
new file mode 100644 (file)
index 0000000..94ab8a7
--- /dev/null
@@ -0,0 +1,390 @@
+<?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/>.
+
+/**
+ * File containing processor class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/csvlib.class.php');
+
+/**
+ * Processor class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_processor {
+
+    /**
+     * Create courses that do not exist yet.
+     */
+    const MODE_CREATE_NEW = 1;
+
+    /**
+     * Create all courses, appending a suffix to the shortname if the course exists.
+     */
+    const MODE_CREATE_ALL = 2;
+
+    /**
+     * Create courses, and update the ones that already exist.
+     */
+    const MODE_CREATE_OR_UPDATE = 3;
+
+    /**
+     * Only update existing courses.
+     */
+    const MODE_UPDATE_ONLY = 4;
+
+    /**
+     * During update, do not update anything... O_o Huh?!
+     */
+    const UPDATE_NOTHING = 0;
+
+    /**
+     * During update, only use data passed from the CSV.
+     */
+    const UPDATE_ALL_WITH_DATA_ONLY = 1;
+
+    /**
+     * During update, use either data from the CSV, or defaults.
+     */
+    const UPDATE_ALL_WITH_DATA_OR_DEFAUTLS = 2;
+
+    /**
+     * During update, update missing values from either data from the CSV, or defaults.
+     */
+    const UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS = 3;
+
+    /** @var int processor mode. */
+    protected $mode;
+
+    /** @var int upload mode. */
+    protected $updatemode;
+
+    /** @var bool are renames allowed. */
+    protected $allowrenames = false;
+
+    /** @var bool are deletes allowed. */
+    protected $allowdeletes = false;
+
+    /** @var bool are resets allowed. */
+    protected $allowresets = false;
+
+    /** @var string path to a restore file. */
+    protected $restorefile;
+
+    /** @var string shortname of the course to be restored. */
+    protected $templatecourse;
+
+    /** @var string reset courses after processing them. */
+    protected $reset;
+
+    /** @var string template to generate a course shortname. */
+    protected $shortnametemplate;
+
+    /** @var csv_import_reader */
+    protected $cir;
+
+    /** @var array default values. */
+    protected $defaults = array();
+
+    /** @var array CSV columns. */
+    protected $columns = array();
+
+    /** @var array of errors where the key is the line number. */
+    protected $errors = array();
+
+    /** @var int line number. */
+    protected $linenb = 0;
+
+    /** @var bool whether the process has been started or not. */
+    protected $processstarted = false;
+
+    /**
+     * Constructor
+     *
+     * @param csv_import_reader $cir import reader object
+     * @param array $options options of the process
+     * @param array $defaults default data value
+     */
+    public function __construct(csv_import_reader $cir, array $options, array $defaults = array()) {
+
+        if (!isset($options['mode']) || !in_array($options['mode'], array(self::MODE_CREATE_NEW, self::MODE_CREATE_ALL,
+                self::MODE_CREATE_OR_UPDATE, self::MODE_UPDATE_ONLY))) {
+            throw new coding_exception('Unknown process mode');
+        }
+
+        // Force int to make sure === comparison work as expected.
+        $this->mode = (int) $options['mode'];
+
+        $this->updatemode = self::UPDATE_NOTHING;
+        if (isset($options['updatemode'])) {
+            // Force int to make sure === comparison work as expected.
+            $this->updatemode = (int) $options['updatemode'];
+        }
+        if (isset($options['allowrenames'])) {
+            $this->allowrenames = $options['allowrenames'];
+        }
+        if (isset($options['allowdeletes'])) {
+            $this->allowdeletes = $options['allowdeletes'];
+        }
+        if (isset($options['allowresets'])) {
+            $this->allowresets = $options['allowresets'];
+        }
+
+        if (isset($options['restorefile'])) {
+            $this->restorefile = $options['restorefile'];
+        }
+        if (isset($options['templatecourse'])) {
+            $this->templatecourse = $options['templatecourse'];
+        }
+        if (isset($options['reset'])) {
+            $this->reset = $options['reset'];
+        }
+        if (isset($options['shortnametemplate'])) {
+            $this->shortnametemplate = $options['shortnametemplate'];
+        }
+
+        $this->cir = $cir;
+        $this->columns = $cir->get_columns();
+        $this->defaults = $defaults;
+        $this->validate();
+        $this->reset();
+    }
+
+    /**
+     * Execute the process.
+     *
+     * @param object $tracker the output tracker to use.
+     * @return void
+     */
+    public function execute($tracker = null) {
+        if ($this->processstarted) {
+            throw new coding_exception('Process has already been started');
+        }
+        $this->processstarted = true;
+
+        if (empty($tracker)) {
+            $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
+        }
+        $tracker->start();
+
+        $total = 0;
+        $created = 0;
+        $updated = 0;
+        $deleted = 0;
+        $errors = 0;
+
+        // Loop over the CSV lines.
+        while ($line = $this->cir->next()) {
+            $this->linenb++;
+            $total++;
+
+            $data = $this->parse_line($line);
+            $course = $this->get_course($data);
+            if ($course->prepare()) {
+                $course->proceed();
+
+                $status = $course->get_statuses();
+                if (array_key_exists('coursecreated', $status)) {
+                    $created++;
+                } else if (array_key_exists('courseupdated', $status)) {
+                    $updated++;
+                } else if (array_key_exists('coursedeleted', $status)) {
+                    $deleted++;
+                }
+
+                $data = array_merge($data, $course->get_data(), array('id' => $course->get_id()));
+                $tracker->output($this->linenb, true, $status, $data);
+            } else {
+                $errors++;
+                $tracker->output($this->linenb, false, $course->get_errors(), $data);
+            }
+        }
+
+        $tracker->finish();
+        $tracker->results($total, $created, $updated, $deleted, $errors);
+
+        $this->remove_restore_content();
+    }
+
+    /**
+     * Return a course import object.
+     *
+     * @param array $data data to import the course with.
+     * @return tool_uploadcourse_course
+     */
+    protected function get_course($data) {
+        $importoptions = array(
+            'candelete' => $this->allowdeletes,
+            'canrename' => $this->allowrenames,
+            'canreset' => $this->allowresets,
+            'reset' => $this->reset,
+            'restoredir' => $this->get_restore_content_dir(),
+            'shortnametemplate' => $this->shortnametemplate
+        );
+        return new tool_uploadcourse_course($this->mode, $this->updatemode, $data, $this->defaults, $importoptions);
+    }
+
+    /**
+     * Return the errors.
+     *
+     * @return array
+     */
+    public function get_errors() {
+        return $this->errors;
+    }
+
+    /**
+     * Get the directory of the object to restore.
+     *
+     * @return string subdirectory in $CFG->tempdir/backup/...
+     */
+    protected function get_restore_content_dir() {
+        $backupfile = null;
+        $shortname = null;
+
+        if (!empty($this->restorefile)) {
+            $backupfile = $this->restorefile;
+        } else if (!empty($this->templatecourse) || is_numeric($this->templatecourse)) {
+            $shortname = $this->templatecourse;
+        }
+
+        $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname);
+        return $dir;
+    }
+
+    /**
+     * Log errors on the current line.
+     *
+     * @param array $errors array of errors
+     * @return void
+     */
+    protected function log_error($errors) {
+        if (empty($errors)) {
+            return;
+        }
+
+        foreach ($errors as $code => $langstring) {
+            if (!isset($this->errors[$this->linenb])) {
+                $this->errors[$this->linenb] = array();
+            }
+            $this->errors[$this->linenb][$code] = $langstring;
+        }
+    }
+
+    /**
+     * Parse a line to return an array(column => value)
+     *
+     * @param array $line returned by csv_import_reader
+     * @return array
+     */
+    protected function parse_line($line) {
+        $data = array();
+        foreach ($line as $keynum => $value) {
+            if (!isset($this->columns[$keynum])) {
+                // This should not happen.
+                continue;
+            }
+
+            $key = $this->columns[$keynum];
+            $data[$key] = $value;
+        }
+        return $data;
+    }
+
+    /**
+     * Return a preview of the import.
+     *
+     * This only returns passed data, along with the errors.
+     *
+     * @param integer $rows number of rows to preview.
+     * @param object $tracker the output tracker to use.
+     * @return array of preview data.
+     */
+    public function preview($rows = 10, $tracker = null) {
+        if ($this->processstarted) {
+            throw new coding_exception('Process has already been started');
+        }
+        $this->processstarted = true;
+
+        if (empty($tracker)) {
+            $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
+        }
+        $tracker->start();
+
+        // Loop over the CSV lines.
+        $preview = array();
+        while (($line = $this->cir->next()) && $rows > $this->linenb) {
+            $this->linenb++;
+            $data = $this->parse_line($line);
+            $course = $this->get_course($data);
+            $result = $course->prepare();
+            if (!$result) {
+                $tracker->output($this->linenb, $result, $course->get_errors(), $data);
+            } else {
+                $tracker->output($this->linenb, $result, $course->get_statuses(), $data);
+            }
+            $row = $data;
+            $preview[$this->linenb] = $row;
+        }
+
+        $tracker->finish();
+        $this->remove_restore_content();
+
+        return $preview;
+    }
+
+    /**
+     * Delete the restore object.
+     *
+     * @return void
+     */
+    protected function remove_restore_content() {
+        tool_uploadcourse_helper::clean_restore_content();
+    }
+
+    /**
+     * Reset the current process.
+     *
+     * @return void.
+     */
+    public function reset() {
+        $this->processstarted = false;
+        $this->linenb = 0;
+        $this->cir->init();
+        $this->errors = array();
+    }
+
+    /**
+     * Validation.
+     *
+     * @return void
+     */
+    protected function validate() {
+        if (empty($this->columns)) {
+            throw new moodle_exception('cannotreadtmpfile', 'error');
+        } else if (count($this->columns) < 2) {
+            throw new moodle_exception('csvfewcolumns', 'error');
+        }
+    }
+}
diff --git a/admin/tool/uploadcourse/classes/step1_form.php b/admin/tool/uploadcourse/classes/step1_form.php
new file mode 100644 (file)
index 0000000..cd0abc0
--- /dev/null
@@ -0,0 +1,75 @@
+<?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/>.
+
+/**
+ * File containing the step 1 of the upload form.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Upload a file CVS file with course information.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_step1_form extends tool_uploadcourse_base_form {
+
+    /**
+     * The standard form definiton.
+     * @return void
+     */
+    public function definition () {
+        $mform = $this->_form;
+
+        $mform->addElement('header', 'generalhdr', get_string('general'));
+
+        $mform->addElement('filepicker', 'coursefile', get_string('file'));
+        $mform->addRule('coursefile', null, 'required');
+
+        $choices = csv_import_reader::get_delimiter_list();
+        $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'tool_uploadcourse'), $choices);
+        if (array_key_exists('cfg', $choices)) {
+            $mform->setDefault('delimiter_name', 'cfg');
+        } else if (get_string('listsep', 'langconfig') == ';') {
+            $mform->setDefault('delimiter_name', 'semicolon');
+        } else {
+            $mform->setDefault('delimiter_name', 'comma');
+        }
+
+        $choices = textlib::get_encodings();
+        $mform->addElement('select', 'encoding', get_string('encoding', 'tool_uploadcourse'), $choices);
+        $mform->setDefault('encoding', 'UTF-8');
+
+        $choices = array('10' => 10, '20' => 20, '100' => 100, '1000' => 1000, '100000' => 100000);
+        $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'tool_uploadcourse'), $choices);
+        $mform->setType('previewrows', PARAM_INT);
+
+        $this->add_import_options();
+
+        $mform->addElement('hidden', 'showpreview', 1);
+        $mform->setType('showpreview', PARAM_INT);
+
+        $this->add_action_buttons(false, get_string('preview', 'tool_uploadcourse'));
+    }
+}
diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php
new file mode 100644 (file)
index 0000000..7464e4e
--- /dev/null
@@ -0,0 +1,220 @@
+<?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/>.
+
+/**
+ * Bulk course upload step 2.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Specify course upload details.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
+
+    /**
+     * The standard form definiton.
+     * @return void.
+     */
+    public function definition () {
+        global $CFG;
+
+        $mform   = $this->_form;
+        $data    = $this->_customdata['data'];
+        $courseconfig = get_config('moodlecourse');
+
+        // Import options.
+        $this->add_import_options();
+
+        // Course options.
+        $mform->addElement('header', 'courseoptionshdr', get_string('courseprocess', 'tool_uploadcourse'));
+        $mform->setExpanded('courseoptionshdr', true);
+
+        $mform->addElement('text', 'options[shortnametemplate]', get_string('shortnametemplate', 'tool_uploadcourse'),
+            'maxlength="100" size="20"');
+        $mform->setType('options[shortnametemplate]', PARAM_RAW);
+        $mform->addHelpButton('options[shortnametemplate]', 'shortnametemplate', 'tool_uploadcourse');
+        $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE);
+        $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY);
+
+        $contextid = $this->_customdata['contextid'];
+        $mform->addElement('hidden', 'contextid', $contextid);
+        $mform->setType('contextid', PARAM_INT);
+        $mform->addElement('filepicker', 'options[restorefile]', get_string('templatefile', 'tool_uploadcourse'));
+        $mform->addHelpButton('options[restorefile]', 'templatefile', 'tool_uploadcourse');
+
+        $mform->addElement('text', 'options[templatecourse]', get_string('coursetemplatename', 'tool_uploadcourse'));
+        $mform->setType('options[templatecourse]', PARAM_TEXT);
+        $mform->addHelpButton('options[templatecourse]', 'coursetemplatename', 'tool_uploadcourse');
+
+        $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse'));
+        $mform->setDefault('options[reset]', 0);
+        $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->disabledIf('options[reset]', 'options[allowresets]', 'eq', 0);
+
+        // Default values.
+        $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse'));
+        $mform->setExpanded('defaultheader', true);
+
+        $displaylist = coursecat::make_categories_list('moodle/course:create');
+        $mform->addElement('select', 'defaults[category]', get_string('coursecategory'), $displaylist);
+        $mform->addHelpButton('defaults[category]', 'coursecategory');
+
+        $choices = array();
+        $choices['0'] = get_string('hide');
+        $choices['1'] = get_string('show');
+        $mform->addElement('select', 'defaults[visible]', get_string('visible'), $choices);
+        $mform->addHelpButton('defaults[visible]', 'visible');
+        $mform->setDefault('defaults[visible]', $courseconfig->visible);
+
+        $mform->addElement('date_selector', 'defaults[startdate]', get_string('startdate'));
+        $mform->addHelpButton('defaults[startdate]', 'startdate');
+        $mform->setDefault('defaults[startdate]', time() + 3600 * 24);
+
+        $courseformats = get_sorted_course_formats(true);
+        $formcourseformats = array();
+        foreach ($courseformats as $courseformat) {
+            $formcourseformats[$courseformat] = get_string('pluginname', "format_$courseformat");
+        }
+        $mform->addElement('select', 'defaults[format]', get_string('format'), $formcourseformats);
+        $mform->addHelpButton('defaults[format]', 'format');
+        $mform->setDefault('defaults[format]', $courseconfig->format);
+
+        if (!empty($CFG->allowcoursethemes)) {
+            $themeobjects = get_list_of_themes();
+            $themes=array();
+            $themes[''] = get_string('forceno');
+            foreach ($themeobjects as $key => $theme) {
+                if (empty($theme->hidefromselector)) {
+                    $themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
+                }
+            }
+            $mform->addElement('select', 'defaults[theme]', get_string('forcetheme'), $themes);
+        }
+
+        $languages = array();
+        $languages[''] = get_string('forceno');
+        $languages += get_string_manager()->get_list_of_translations();
+        $mform->addElement('select', 'defaults[lang]', get_string('forcelanguage'), $languages);
+        $mform->setDefault('defaults[lang]', $courseconfig->lang);
+
+        $options = range(0, 10);
+        $mform->addElement('select', 'defaults[newsitems]', get_string('newsitemsnumber'), $options);
+        $mform->addHelpButton('defaults[newsitems]', 'newsitemsnumber');
+        $mform->setDefault('defaults[newsitems]', $courseconfig->newsitems);
+
+        $mform->addElement('selectyesno', 'defaults[showgrades]', get_string('showgrades'));
+        $mform->addHelpButton('defaults[showgrades]', 'showgrades');
+        $mform->setDefault('defaults[showgrades]', $courseconfig->showgrades);
+
+        $mform->addElement('selectyesno', 'defaults[showreports]', get_string('showreports'));
+        $mform->addHelpButton('defaults[showreports]', 'showreports');
+        $mform->setDefault('defaults[showreports]', $courseconfig->showreports);
+
+        if (!empty($CFG->legacyfilesinnewcourses)) {
+            $mform->addElement('select', 'defaults[legacyfiles]', get_string('courselegacyfiles'), $choices);
+            $mform->addHelpButton('defaults[legacyfiles]', 'courselegacyfiles');
+            if (!isset($courseconfig->legacyfiles)) {
+                $courseconfig->legacyfiles = 0;
+            }
+            $mform->setDefault('defaults[legacyfiles]', $courseconfig->legacyfiles);
+        }
+
+        $choices = get_max_upload_sizes($CFG->maxbytes);
+        $mform->addElement('select', 'defaults[maxbytes]', get_string('maximumupload'), $choices);
+        $mform->addHelpButton('defaults[maxbytes]', 'maximumupload');
+        $mform->setDefault('defaults[maxbytes]', $courseconfig->maxbytes);
+
+        $choices = array();
+        $choices[NOGROUPS] = get_string('groupsnone', 'group');
+        $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group');
+        $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group');
+        $mform->addElement('select', 'defaults[groupmode]', get_string('groupmode', 'group'), $choices);
+        $mform->addHelpButton('defaults[groupmode]', 'groupmode', 'group');
+        $mform->setDefault('defaults[groupmode]', $courseconfig->groupmode);
+
+        $mform->addElement('selectyesno', 'defaults[groupmodeforce]', get_string('groupmodeforce', 'group'));
+        $mform->addHelpButton('defaults[groupmodeforce]', 'groupmodeforce', 'group');
+        $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce);
+
+        // Hidden fields.
+        $mform->addElement('hidden', 'importid');
+        $mform->setType('importid', PARAM_INT);
+
+        $mform->addElement('hidden', 'previewrows');
+        $mform->setType('previewrows', PARAM_INT);
+
+        $this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse'));
+
+        $this->set_data($data);
+    }
+
+    /**
+     * Add actopm buttons.
+     *
+     * @param bool $cancel whether to show cancel button, default true
+     * @param string $submitlabel label for submit button, defaults to get_string('savechanges')
+     * @return void
+     */
+    public function add_action_buttons($cancel = true, $submitlabel = null) {
+        $mform =& $this->_form;
+        $buttonarray = array();
+        $buttonarray[] = &$mform->createElement('submit', 'showpreview', get_string('preview', 'tool_uploadcourse'));
+        $buttonarray[] = &$mform->createElement('submit', 'submitbutton', $submitlabel);
+        $buttonarray[] = &$mform->createElement('cancel');
+        $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+        $mform->closeHeaderBefore('buttonar');
+    }
+
+    /**
+     * Server side validation.
+     * @param array $data - form data
+     * @param object $files  - form files
+     * @return array $errors - form errors
+     */
+    public function validation($data, $files) {
+        $errors = parent::validation($data, $files);
+        $columns = $this->_customdata['columns'];
+        $optype  = $data['options']['mode'];
+
+        // Look for other required data.
+        if ($optype != tool_uploadcourse_processor::MODE_UPDATE_ONLY) {
+            if (!in_array('fullname', $columns)) {
+                if (isset($errors['mode'])) {
+                    $errors['mode'] .= ' ';
+                }
+                $errors['mode'] .= get_string('missingfield', 'error', 'fullname');
+            }
+            if (!in_array('summary', $columns)) {
+                if (isset($errors['mode'])) {
+                    $errors['mode'] .= ' ';
+                }
+                $errors['mode'] .= get_string('missingfield', 'error', 'summary');
+            }
+        }
+
+        return $errors;
+    }
+}
diff --git a/admin/tool/uploadcourse/classes/tracker.php b/admin/tool/uploadcourse/classes/tracker.php
new file mode 100644 (file)
index 0000000..0daa845
--- /dev/null
@@ -0,0 +1,220 @@
+<?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/>.
+
+/**
+ * Output tracker.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/weblib.php');
+
+/**
+ * Class output tracker.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_tracker {
+
+    /**
+     * Constant to output nothing.
+     */
+    const NO_OUTPUT = 0;
+
+    /**
+     * Constant to output HTML.
+     */
+    const OUTPUT_HTML = 1;
+
+    /**
+     * Constant to output plain text.
+     */
+    const OUTPUT_PLAIN = 2;
+
+    /**
+     * @var array columns to display.
+     */
+    protected $columns = array('line', 'result', 'id', 'shortname', 'fullname', 'idnumber', 'status');
+
+    /**
+     * @var int row number.
+     */
+    protected $rownb = 0;
+
+    /**
+     * @var int chosen output mode.
+     */
+    protected $outputmode;
+
+    /**
+     * @var object output buffer.
+     */
+    protected $buffer;
+
+    /**
+     * Constructor.
+     *
+     * @param int $outputmode desired output mode.
+     */
+    public function __construct($outputmode = self::NO_OUTPUT) {
+        $this->outputmode = $outputmode;
+        if ($this->outputmode == self::OUTPUT_PLAIN) {
+            $this->buffer = new progress_trace_buffer(new text_progress_trace());
+        }
+    }
+
+    /**
+     * Finish the output.
+     *
+     * @return void
+     */
+    public function finish() {
+        if ($this->outputmode == self::NO_OUTPUT) {
+            return;
+        }
+
+        if ($this->outputmode == self::OUTPUT_HTML) {
+            echo html_writer::end_tag('table');
+        }
+    }
+
+    /**
+     * Output the results.
+     *
+     * @param int $total total courses.
+     * @param int $created count of courses created.
+     * @param int $updated count of courses updated.
+     * @param int $deleted count of courses deleted.
+     * @param int $errors count of errors.
+     * @return void
+     */
+    public function results($total, $created, $updated, $deleted, $errors) {
+        if ($this->outputmode == self::NO_OUTPUT) {
+            return;
+        }
+
+        $message = array(
+            get_string('coursestotal', 'tool_uploadcourse', $total),
+            get_string('coursescreated', 'tool_uploadcourse', $created),
+            get_string('coursesupdated', 'tool_uploadcourse', $updated),
+            get_string('coursesdeleted', 'tool_uploadcourse', $deleted),
+            get_string('courseserrors', 'tool_uploadcourse', $errors)
+        );
+
+        if ($this->outputmode == self::OUTPUT_PLAIN) {
+            foreach ($message as $msg) {
+                $this->buffer->output($msg);
+            }
+        } else if ($this->outputmode == self::OUTPUT_HTML) {
+            $buffer = new progress_trace_buffer(new html_list_progress_trace());
+            foreach ($message as $msg) {
+                $buffer->output($msg);
+            }
+            $buffer->finished();
+        }
+    }
+
+    /**
+     * Output one more line.
+     *
+     * @param int $line line number.
+     * @param bool $outcome success or not?
+     * @param array $status array of statuses.
+     * @param array $data extra data to display.
+     * @return void
+     */
+    public function output($line, $outcome, $status, $data) {
+        global $OUTPUT;
+        if ($this->outputmode == self::NO_OUTPUT) {
+            return;
+        }
+
+        if ($this->outputmode == self::OUTPUT_PLAIN) {
+            $message = array(
+                $line,
+                $outcome ? 'OK' : 'NOK',
+                isset($data['id']) ? $data['id'] : '',
+                isset($data['shortname']) ? $data['shortname'] : '',
+                isset($data['fullname']) ? $data['fullname'] : '',
+                isset($data['idnumber']) ? $data['idnumber'] : ''
+            );
+            $this->buffer->output(implode("\t", $message));
+            if (!empty($status)) {
+                foreach ($status as $st) {
+                    $this->buffer->output($st, 1);
+                }
+            }
+        } else if ($this->outputmode == self::OUTPUT_HTML) {
+            $ci = 0;
+            $this->rownb++;
+            if (is_array($status)) {
+                $status = implode(html_writer::empty_tag('br'), $status);
+            }
+            if ($outcome) {
+                $outcome = $OUTPUT->pix_icon('i/valid', '');
+            } else {
+                $outcome = $OUTPUT->pix_icon('i/invalid', '');
+            }
+            echo html_writer::start_tag('tr', array('class' => 'r' . $this->rownb % 2));
+            echo html_writer::tag('td', $line, array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', $outcome, array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', isset($data['id']) ? $data['id'] : '', array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', isset($data['shortname']) ? $data['shortname'] : '', array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', isset($data['fullname']) ? $data['fullname'] : '', array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', isset($data['idnumber']) ? $data['idnumber'] : '', array('class' => 'c' . $ci++));
+            echo html_writer::tag('td', $status, array('class' => 'c' . $ci++));
+            echo html_writer::end_tag('tr');
+        }
+    }
+
+    /**
+     * Start the output.
+     *
+     * @return void
+     */
+    public function start() {
+        if ($this->outputmode == self::NO_OUTPUT) {
+            return;
+        }
+
+        if ($this->outputmode == self::OUTPUT_PLAIN) {
+            $columns = array_flip($this->columns);
+            unset($columns['status']);
+            $columns = array_flip($columns);
+            $this->buffer->output(implode("\t", $columns));
+        } else if ($this->outputmode == self::OUTPUT_HTML) {
+            $ci = 0;
+            echo html_writer::start_tag('table', array('class' => 'generaltable boxaligncenter flexible-wrap',
+                'summary' => get_string('uploadcoursesresult', 'tool_uploadcourse')));
+            echo html_writer::start_tag('tr', array('class' => 'heading r' . $this->rownb));
+            echo html_writer::tag('th', get_string('csvline', 'tool_uploadcourse'),
+                array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('result', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('id', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('shortname'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('fullname'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('idnumber'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::tag('th', get_string('status'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+            echo html_writer::end_tag('tr');
+        }
+    }
+
+}
diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php
new file mode 100644 (file)
index 0000000..1aa4613
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * CLI Bulk course registration script from a comma separated file.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2012 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir . '/clilib.php');
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+
+$courseconfig = get_config('moodlecourse');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(array(
+    'help' => false,
+    'mode' => '',
+    'updatemode' => 'nothing',
+    'file' => '',
+    'delimiter' => 'comma',
+    'encoding' => 'UTF-8',
+    'shortnametemplate' => '',
+    'templatecourse' => false,
+    'restorefile' => false,
+    'allowdeletes' => false,
+    'allowrenames' => false,
+    'allowresets' => false,
+    'reset' => false,
+    'category' => coursecat::get_default()->id,
+),
+array(
+    'h' => 'help',
+    'm' => 'mode',
+    'u' => 'updatemode',
+    'f' => 'file',
+    'd' => 'delimiter',
+    'e' => 'encoding',
+    't' => 'templatecourse',
+    'r' => 'restorefile',
+    'g' => 'format',
+));
+
+if ($unrecognized) {
+    $unrecognized = implode("\n  ", $unrecognized);
+    cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+$help =
+"Execute Course Upload.
+
+Options:
+-h, --help                 Print out this help
+-m, --mode                 Import mode: createnew, createall, createorupdate, update
+-u, --updatemode           Update mode: nothing, dataonly, dataordefaults¸ missingonly
+-f, --file                 CSV file
+-d, --delimiter            CSV delimiter: colon, semicolon, tab, cfg, comma
+-e, --encoding             CSV file encoding: utf8, ... etc
+-t, --templatecourse       Shortname of the course to restore after import
+-r, --restorefile          Backup file to restore after import
+--reset                    Run the course reset after each course import
+--allowdeletes             Allow courses to be deleted
+--allowrenames             Allow courses to be renamed
+--allowresets              Allow courses to be reset
+--shortnametemplate        Template to generate the shortname from
+--category                 ID of default category (--updatemode dataordefaults will use this value)
+
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/tool/uploadcourse/cli/uploadcourse.php --mode=createnew \\
+       --updatemode=dataonly --file=./courses.csv --delimiter=comma
+";
+
+if ($options['help']) {
+    echo $help;
+    die();
+}
+echo "Moodle course uploader running ...\n";
+
+$processoroptions = array(
+    'allowdeletes' => $options['allowdeletes'],
+    'allowrenames' => $options['allowrenames'],
+    'allowresets' => $options['allowresets'],
+    'reset' => $options['reset'],
+    'shortnametemplate' => $options['shortnametemplate']
+);
+
+// Confirm that the mode is valid.
+$modes = array(
+    'createnew' => tool_uploadcourse_processor::MODE_CREATE_NEW,
+    'createall' => tool_uploadcourse_processor::MODE_CREATE_ALL,
+    'createorupdate' => tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE,
+    'update' => tool_uploadcourse_processor::MODE_UPDATE_ONLY
+);
+if (!isset($options['mode']) || !isset($modes[$options['mode']])) {
+    echo get_string('invalidmode', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+$processoroptions['mode'] = $modes[$options['mode']];
+
+// Check that the update mode is valid.
+$updatemodes = array(
+    'nothing' => tool_uploadcourse_processor::UPDATE_NOTHING,
+    'dataonly' => tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY,
+    'dataordefaults' => tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS,
+    'missingonly' => tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS
+);
+if (($processoroptions['mode'] === tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE ||
+        $processoroptions['mode'] === tool_uploadcourse_processor::MODE_UPDATE_ONLY)
+        && (!isset($options['updatemode']) || !isset($updatemodes[$options['updatemode']]))) {
+    echo get_string('invalideupdatemode', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+$processoroptions['updatemode'] = $updatemodes[$options['updatemode']];
+
+// File.
+if (!empty($options['file'])) {
+    $options['file'] = realpath($options['file']);
+}
+if (!file_exists($options['file'])) {
+    echo get_string('invalidcsvfile', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+
+// Encoding.
+$encodings = textlib::get_encodings();
+if (!isset($encodings[$options['encoding']])) {
+    echo get_string('invalidencoding', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+
+// Default values.
+$defaults = array();
+$defaults['category'] = $options['category'];
+$defaults['startdate'] = time() + 3600 * 24;
+$defaults['newsitems'] = $courseconfig->newsitems;
+$defaults['showgrades'] = $courseconfig->showgrades;
+$defaults['showreports'] = $courseconfig->showreports;
+$defaults['maxbytes'] = $courseconfig->maxbytes;
+$defaults['legacyfiles'] = $CFG->legacyfilesinnewcourses;
+$defaults['groupmode'] = $courseconfig->groupmode;
+$defaults['groupmodeforce'] = $courseconfig->groupmodeforce;
+$defaults['visible'] = $courseconfig->visible;
+$defaults['lang'] =  $courseconfig->lang;
+
+// Course template.
+if (isset($options['templatecourse'])) {
+    $processoroptions['templatecourse'] = $options['templatecourse'];
+}
+
+// Restore file.
+if ($options['restorefile']) {
+    $options['restorefile'] = realpath($options['restorefile']);
+}
+if ($options['restorefile'] && !file_exists($options['restorefile'])) {
+    echo get_string('invalidrestorefile', 'tool_uploadcourse')."\n";
+    echo $help;
+    die();
+}
+$processoroptions['restorefile'] = $options['restorefile'];
+
+// Emulate normal session.
+cron_setup_user();
+
+// Let's get started!
+$content = file_get_contents($options['file']);
+$importid = csv_import_reader::get_new_iid('uploadcourse');
+$cir = new csv_import_reader($importid, 'uploadcourse');
+$readcount = $cir->load_csv_content($content, $options['encoding'], $options['delimiter']);
+unset($content);
+if ($readcount === false) {
+    print_error('csvfileerror', 'tool_uploadcourse', '', $cir->get_error());
+} else if ($readcount == 0) {
+    print_error('csvemptyfile', 'error', '', $cir->get_error());
+}
+$processor = new tool_uploadcourse_processor($cir, $processoroptions, $defaults);
+$processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_PLAIN));
similarity index 79%
rename from enrol/authorize/db/messages.php
rename to admin/tool/uploadcourse/db/caches.php
index 8d3df01..7c0c7c0 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Defines message providers (types of message sent) for the PayPal enrolment plugin.
+ * Cache definitions.
  *
- * @package    enrol_authorize
- * @copyright  2012 Andrew Davis
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 defined('MOODLE_INTERNAL') || die();
 
-$messageproviders = array(
-    'authorize_enrolment' => array(),
+$definitions = array(
+    'helper' => array(
+        'mode' => cache_store::MODE_REQUEST,
+    )
 );
diff --git a/admin/tool/uploadcourse/index.php b/admin/tool/uploadcourse/index.php
new file mode 100644 (file)
index 0000000..667551e
--- /dev/null
@@ -0,0 +1,102 @@
+<?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/>.
+
+/**
+ * Bulk course registration script from a comma separated file.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+
+admin_externalpage_setup('tooluploadcourse');
+
+$importid         = optional_param('importid', '', PARAM_INT);
+$previewrows = optional_param('previewrows', 10, PARAM_INT);
+
+$returnurl = new moodle_url('/admin/tool/uploadcourse/index.php');
+
+if (empty($importid)) {
+    $mform1 = new tool_uploadcourse_step1_form();
+    if ($form1data = $mform1->get_data()) {
+        $importid = csv_import_reader::get_new_iid('uploadcourse');
+        $cir = new csv_import_reader($importid, 'uploadcourse');
+        $content = $mform1->get_file_content('coursefile');
+        $readcount = $cir->load_csv_content($content, $form1data->encoding, $form1data->delimiter_name);
+        unset($content);
+        if ($readcount === false) {
+            print_error('csvfileerror', 'tool_uploadcourse', $returnurl, $cir->get_error());
+        } else if ($readcount == 0) {
+            print_error('csvemptyfile', 'error', $returnurl, $cir->get_error());
+        }
+    } else {
+        echo $OUTPUT->header();
+        echo $OUTPUT->heading_with_help(get_string('uploadcourses', 'tool_uploadcourse'), 'uploadcourses', 'tool_uploadcourse');
+        $mform1->display();
+        echo $OUTPUT->footer();
+        die();
+    }
+} else {
+    $cir = new csv_import_reader($importid, 'uploadcourse');
+}
+
+// Data to set in the form.
+$data = array('importid' => $importid, 'previewrows' => $previewrows);
+if (!empty($form1data)) {
+    // Get options from the first form to pass it onto the second.
+    foreach ($form1data->options as $key => $value) {
+        $data["options[$key]"] = $value;
+    }
+}
+$context = context_system::instance();
+$mform2 = new tool_uploadcourse_step2_form(null, array('contextid' => $context->id, 'columns' => $cir->get_columns(),
+    'data' => $data));
+
+// If a file has been uploaded, then process it.
+if ($form2data = $mform2->is_cancelled()) {
+    $cir->cleanup(true);
+    redirect($returnurl);
+} else if ($form2data = $mform2->get_data()) {
+
+    $options = (array) $form2data->options;
+    $defaults = (array) $form2data->defaults;
+    $processor = new tool_uploadcourse_processor($cir, $options, $defaults);
+
+    echo $OUTPUT->header();
+    if (isset($form2data->showpreview)) {
+        echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse'));
+        $processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+        $mform2->display();
+    } else {
+        echo $OUTPUT->heading(get_string('uploadcoursesresult', 'tool_uploadcourse'));
+        $processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+        echo $OUTPUT->continue_button($returnurl);
+    }
+
+} else {
+    $processor = new tool_uploadcourse_processor($cir, $form1data->options, array());
+    echo $OUTPUT->header();
+    echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse'));
+    $processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+    $mform2->display();
+}
+
+echo $OUTPUT->footer();
diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php
new file mode 100644 (file)
index 0000000..83138d3
--- /dev/null
@@ -0,0 +1,115 @@
+<?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/>.
+
+/**
+ * Strings for component 'tool_uploadcourse'.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['allowdeletes'] = 'Allow deletes';
+$string['allowrenames'] = 'Allow renames';
+$string['allowresets'] = 'Allow resets';
+$string['cachedef_helper'] = 'Helper caching';
+$string['cannotdeletecoursenotexist'] = 'Cannot delete a course that does not exist';
+$string['cannotgenerateshortnameupdatemode'] = 'Cannot generate a shortname when updates are allowed';
+$string['cannotreadbackupfile'] = 'Cannot read the backup file';
+$string['cannotrenamecoursenotexist'] = 'Cannot rename a course that does not exist';
+$string['cannotrenameidnumberconflict'] = 'Cannot rename the course, the ID number conflicts with an existing course';
+$string['cannotrenameshortnamealreadyinuse'] = 'Cannot rename the course, the shortname is already used';
+$string['canonlyrenameinupdatemode'] = 'Can only rename a course when update is allowed';
+$string['canonlyresetcourseinupdatemode'] = 'Can only reset a course in update mode';
+$string['couldnotresolvecatgorybyid'] = 'Could not resolve category by ID';
+$string['couldnotresolvecatgorybyidnumber'] = 'Could not resolve category by ID number';
+$string['couldnotresolvecatgorybypath'] = 'Could not resolve category by path';
+$string['coursecreated'] = 'Course created';
+$string['coursedeleted'] = 'Course deleted';
+$string['coursedeletionnotallowed'] = 'Course deletion is not allowed';
+$string['coursedoesnotexistandcreatenotallowed'] = 'The course does not exist and creating course is not allowed';
+$string['courseexistsanduploadnotallowed'] = 'The course exists and update is not allowed';
+$string['courseidnumberincremented'] = 'Course ID number incremented {$a->from} -> {$a->to}';
+$string['courseprocess'] = 'Course process';
+$string['courserenamed'] = 'Course renamed';
+$string['courserenamingnotallowed'] = 'Course renaming is not allowed';
+$string['coursereset'] = 'Course reset';
+$string['courseresetnotallowed'] = 'Course reset now allowed';
+$string['courserestored'] = 'Course restored';
+$string['coursestotal'] = 'Courses total: {$a}';
+$string['coursescreated'] = 'Courses created: {$a}';
+$string['coursesupdated'] = 'Courses updated: {$a}';
+$string['coursesdeleted'] = 'Courses deleted: {$a}';
+$string['courseserrors'] = 'Courses errors: {$a}';
+$string['courseshortnameincremented'] = 'Course shortname incremented {$a->from} -> {$a->to}';
+$string['courseshortnamegenerated'] = 'Course shortname generated: {$a}';
+$string['coursetemplatename'] = 'Restore from this course after upload';
+$string['coursetemplatename_help'] = 'Enter an existing course shortname to use as a template for the creation of all courses.';
+$string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist';
+$string['courseupdated'] = 'Course updated';
+$string['createall'] = 'Create all, increment shortname if needed';
+$string['createnew'] = 'Create new courses only, skip existing ones';
+$string['createorupdate'] = 'Create new courses, or update existing ones';
+$string['csvdelimiter'] = 'CSV delimiter';
+$string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
+$string['csvline'] = 'Line';
+$string['defaultvalues'] = 'Default course values';
+$string['encoding'] = 'Encoding';
+$string['errorwhilerestoringcourse'] = 'Error while restoring the course';
+$string['errorwhiledeletingcourse'] = 'Error while deleting the course';
+$string['generatedshortnameinvalid'] = 'The generated shortname is invalid';
+$string['generatedshortnamealreadyinuse'] = 'The generated shortname is already in use';
+$string['id'] = 'ID';
+$string['importoptions'] = 'Import options';
+$string['idnumberalreadyinuse'] = 'ID number already used by a course';
+$string['invalidbackupfile'] = 'Invalid backup file';
+$string['invalidcourseformat'] = 'Invalid course format';
+$string['invalidcsvfile'] = 'Invalid input CSV file';
+$string['invalidencoding'] = 'Invalid encoding';
+$string['invalidmode'] = 'Invalid mode selected';
+$string['invalideupdatemode'] = 'Invalid update mode selected';
+$string['invalidroles'] = 'Invalid role names: {$a}';
+$string['invalidshortname'] = 'Invalid shortname';
+$string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}';
+$string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set';
+$string['mode'] = 'Upload mode';
+$string['nochanges'] = 'No changes';
+$string['pluginname'] = 'Course upload';
+$string['preview'] = 'Preview';
+$string['reset'] = 'Reset course after upload';
+$string['result'] = 'Result';
+$string['restoreafterimport'] = 'Restore after import';
+$string['rowpreviewnum'] = 'Preview rows';
+$string['shortnametemplate'] = 'Template to generate a shortname';
+$string['shortnametemplate_help'] = 'The short name of the course is displayed in the navigation. You may use template syntax here (%f = fullname, %i = idnumber), or enter an initial value that is incremented.';
+$string['templatefile'] = 'Restore from this file after upload';
+$string['templatefile_help'] = 'Select a file to use as a template for the creation of all courses.';
+$string['unknownimportmode'] = 'Unknown import mode';
+$string['updatemissing'] = 'Fill in missing from CSV data and defaults';
+$string['updatemode'] = 'Update mode';
+$string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated';
+$string['updateonly'] = 'Only update existing courses';
+$string['updatewithdataordefaults'] = 'Update with CSV data and defaults';
+$string['updatewithdataonly'] = 'Update with CSV data only';
+$string['uploadcourses'] = 'Upload courses';
+$string['uploadcourses_help'] = 'Courses may be uploaded via text file. The format of the file should be as follows:
+
+* Each line of the file contains one record
+* Each record is a series of data separated by commas (or other delimiters)
+* The first record contains a list of fieldnames defining the format of the rest of the file
+* Required fieldnames are shortname, fullname, summary and category';
+$string['uploadcoursespreview'] = 'Upload courses preview';
+$string['uploadcoursesresult'] = 'Upload courses results';
similarity index 59%
rename from enrol/authorize/db/upgrade.php
rename to admin/tool/uploadcourse/settings.php
index adbb27d..9b485d9 100644 (file)
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Authorize.Net enrolment plugin upgrades.
+ * Link to CSV course upload.
  *
- * @package    enrol_authorize
- * @copyright  2006 Eugene Venter
- * @author     Eugene Venter
+ * @package    tool_uploadcourse
+ * @copyright  2011 Piers Harding
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-function xmldb_enrol_authorize_upgrade($oldversion) {
-    global $CFG, $DB;
+defined('MOODLE_INTERNAL') || die();
 
-    $dbman = $DB->get_manager();
-
-
-    // Moodle v2.3.0 release upgrade line
-    // Put any upgrade step following this
-
-
-    // Moodle v2.4.0 release upgrade line
-    // Put any upgrade step following this
-
-
-    // Moodle v2.5.0 release upgrade line.
-    // Put any upgrade step following this.
-
-
-    return true;
+if ($hassiteconfig) {
+    $ADMIN->add('courses', new admin_externalpage('tooluploadcourse',
+        get_string('uploadcourses', 'tool_uploadcourse'), "/admin/tool/uploadcourse/index.php"));
 }
diff --git a/admin/tool/uploadcourse/tests/behat/create.feature b/admin/tool/uploadcourse/tests/behat/create.feature
new file mode 100644 (file)
index 0000000..ee95ca1
--- /dev/null
@@ -0,0 +1,46 @@
+@tool @tool_uploadcourse @_only_local
+Feature: An admin can create courses using a CSV file
+  In order to create courses using a CSV file
+  As an admin
+  I need to be able to upload a CSV file and navigate through the import process
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | First course | C1 | 0 |
+    And I log in as "admin"
+    And I expand "Site administration" node
+    And I expand "Courses" node
+    And I follow "Upload courses"
+
+  @javascript
+  Scenario: Creation of unexisting courses
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "The course exists and update is not allowed"
+    And I should see "Course created"
+    And I should see "Courses total: 3"
+    And I should see "Courses created: 2"
+    And I should see "Courses errors: 1"
+    And I follow "Home"
+    And I should see "Course 2"
+    And I should see "Course 3"
+
+  @javascript
+  Scenario: Creation of existing courses
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    And I select "Create all, increment shortname if needed" from "Upload mode"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course created"
+    And I should see "Course shortname incremented C1 -> C2"
+    And I should see "Course shortname incremented C2 -> C3"
+    And I should see "Course shortname incremented C3 -> C4"
+    And I should see "Courses total: 3"
+    And I should see "Courses created: 3"
+    And I should see "Courses errors: 0"
+    And I follow "Home"
+    And I should see "Course 1"
+    And I should see "Course 2"
+    And I should see "Course 3"
diff --git a/admin/tool/uploadcourse/tests/behat/update.feature b/admin/tool/uploadcourse/tests/behat/update.feature
new file mode 100644 (file)
index 0000000..3a06d16
--- /dev/null
@@ -0,0 +1,32 @@
+@tool @tool_uploadcourse @_only_local
+Feature: An admin can update courses using a CSV file
+  In order to update courses using a CSV file
+  As an admin
+  I need to be able to upload a CSV file and navigate through the import process
+
+  Background:
+    Given the following "courses" exists:
+      | fullname | shortname | category |
+      | Some random name | C1 | 0 |
+    And I log in as "admin"
+    And I expand "Site administration" node
+    And I expand "Courses" node
+    And I follow "Upload courses"
+
+  @javascript
+  Scenario: Updating a course fullname
+    Given I upload "admin/tool/uploadcourse/tests/fixtures/courses.csv" file to "File" filepicker
+    And I select "Only update existing courses" from "Upload mode"
+    And I select "Update with CSV data only" from "Update mode"
+    And I click on "Preview" "button"
+    When I click on "Upload courses" "button"
+    Then I should see "Course updated"
+    And I should see "The course does not exist and creating course is not allowed"
+    And I should see "Courses total: 3"
+    And I should see "Courses updated: 1"
+    And I should see "Courses created: 0"
+    And I should see "Courses errors: 2"
+    And I follow "Home"
+    And I should see "Course 1"
+    And I should not see "Course 2"
+    And I should not see "Course 3"
diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php
new file mode 100644 (file)
index 0000000..f04a2ea
--- /dev/null
@@ -0,0 +1,923 @@
+<?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/>.
+
+/**
+ * File containing tests for the course class.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Course test case.
+ *
+ * @package    tool_uploadcourse
+ * @copyright  2013 Frédéric Massart
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
+ */
+class tool_uploadcourse_course_testcase extends advanced_testcase {
+
+    public function test_proceed_without_prepare() {
+        $this->resetAfterTest(true);
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array();
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->setExpectedException('coding_exception');
+        $co->proceed();
+    }
+
+    public function test_proceed_when_prepare_failed() {
+        $this->resetAfterTest(true);
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array();
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->setExpectedException('moodle_exception');
+        $co->proceed();
+    }
+
+    public function test_proceed_when_already_started() {
+        $this->resetAfterTest(true);
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('shortname' => 'test', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->setExpectedException('coding_exception');
+        $co->proceed();
+    }
+
+    public function test_invalid_shortname() {
+        $this->resetAfterTest(true);
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('shortname' => '<invalid>', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('invalidshortname', $co->get_errors());
+    }
+
+    public function test_create() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Existing course.
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1', 'summary' => 'Yay!'));
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+
+        // Try to add a new course.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'newcourse', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'newcourse')));
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'newcourse')));
+
+        // Try to add a new course, that already exists.
+        $coursecount = $DB->count_records('course', array());
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $data = array('shortname' => 'c1', 'fullname' => 'C1FN', 'summary' => 'C1', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('courseexistsanduploadnotallowed', $co->get_errors());
+        $this->assertEquals($coursecount, $DB->count_records('course', array()));
+        $this->assertNotEquals('C1', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+        // Try to add new with shortname incrementation.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+        $data = array('shortname' => 'c1', 'fullname' => 'C1FN', 'summary' => 'C1', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
+    }
+
+    public function test_delete() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c2 = $this->getDataGenerator()->create_course();
+
+        $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'DoesNotExist')));
+
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+
+        // Try delete when option not available.
+        $importoptions = array('candelete' => false);
+        $data = array('shortname' => $c1->shortname, 'delete' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('coursedeletionnotallowed', $co->get_errors());
+        $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+
+        // Try delete when not requested.
+        $importoptions = array('candelete' => true);
+        $data = array('shortname' => $c1->shortname, 'delete' => 0);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+
+        // Try delete when requested.
+        $importoptions = array('candelete' => true);
+        $data = array('shortname' => $c1->shortname, 'delete' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertFalse($DB->record_exists('course', array('shortname' => $c1->shortname)));
+        $this->assertTrue($DB->record_exists('course', array('shortname' => $c2->shortname)));
+
+        // Try deleting non-existing record, this should not fail.
+        $data = array('shortname' => 'DoesNotExist', 'delete' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotdeletecoursenotexist', $co->get_errors());
+    }
+
+    public function test_update() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1'));
+
+        // Try to update with existing shortnames, not allowing creation, and updating nothing.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array('shortname' => 'c1', 'fullname' => 'New fullname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('updatemodedoessettonothing', $co->get_errors());
+
+        // Try to update with non-existing shortnames.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'DoesNotExist', 'fullname' => 'New fullname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('coursedoesnotexistandcreatenotallowed', $co->get_errors());
+
+        // Try a proper update.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'fullname' => 'New fullname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('New fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1')));
+
+        // Try a proper update with defaults.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+        $data = array('shortname' => 'c1', 'fullname' => 'Another fullname');
+        $defaults = array('fullname' => 'Not this one', 'summary' => 'Awesome summary');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('Another fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1')));
+        $this->assertEquals('Awesome summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+        // Try a proper update missing only.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS;
+        $DB->set_field('course', 'summary', '', array('shortname' => 'c1'));
+        $this->assertEquals('', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+        $data = array('shortname' => 'c1', 'summary' => 'Fill in summary');
+        $defaults = array('summary' => 'Do not use this summary');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('Fill in summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+        // Try a proper update missing only using defaults.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS;
+        $DB->set_field('course', 'summary', '', array('shortname' => 'c1'));
+        $this->assertEquals('', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+        $data = array('shortname' => 'c1');
+        $defaults = array('summary' => 'Use this summary');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('Use this summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+    }
+
+    public function test_data_saved() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Create.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array(
+            'shortname' => 'c1',
+            'fullname' => 'Fullname',
+            'category' => '1',
+            'visible' => '0',
+            'startdate' => '8 June 1990',
+            'idnumber' => '123abc',
+            'summary' => 'Summary',
+            'format' => 'weeks',
+            'theme' => 'afterburner',
+            'lang' => 'en',
+            'newsitems' => '7',
+            'showgrades' => '0',
+            'showreports' => '1',
+            'legacyfiles' => '1',
+            'maxbytes' => '1234',
+            'groupmode' => '2',
+            'groupmodeforce' => '1',
+            'enablecompletion' => '1',
+
+            'role_teacher' => 'Knight',
+            'role_manager' => 'Jedi',
+
+            'enrolment_1' => 'guest',
+            'enrolment_2' => 'self',
+            'enrolment_2_roleid' => '1',
+            'enrolment_3' => 'manual',
+            'enrolment_3_disable' => '1',
+        );
+
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1')));
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $course = $DB->get_record('course', array('shortname' => 'c1'));
+        $ctx = context_course::instance($course->id);
+
+        $this->assertEquals($data['fullname'], $course->fullname);
+        $this->assertEquals($data['category'], $course->category);
+        $this->assertEquals($data['visible'], $course->visible);
+        $this->assertEquals(mktime(0, 0, 0, 6, 8, 1990), $course->startdate);
+        $this->assertEquals($data['idnumber'], $course->idnumber);
+        $this->assertEquals($data['summary'], $course->summary);
+        $this->assertEquals($data['format'], $course->format);
+        $this->assertEquals($data['theme'], $course->theme);
+        $this->assertEquals($data['lang'], $course->lang);
+        $this->assertEquals($data['newsitems'], $course->newsitems);
+        $this->assertEquals($data['showgrades'], $course->showgrades);
+        $this->assertEquals($data['showreports'], $course->showreports);
+        $this->assertEquals($data['legacyfiles'], $course->legacyfiles);
+        $this->assertEquals($data['maxbytes'], $course->maxbytes);
+        $this->assertEquals($data['groupmode'], $course->groupmode);
+        $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce);
+        $this->assertEquals($data['enablecompletion'], $course->enablecompletion);
+
+        // Roles.
+        $roleids = array();
+        $roles = get_all_roles();
+        foreach ($roles as $role) {
+            $roleids[$role->shortname] = $role->id;
+        }
+        $this->assertEquals('Knight', $DB->get_field_select('role_names', 'name',
+            'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['teacher'])));
+        $this->assertEquals('Jedi', $DB->get_field_select('role_names', 'name',
+            'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['manager'])));
+
+        // Enrolment methods.
+        $enroldata = array();
+        $instances = enrol_get_instances($course->id, false);
+        $this->assertCount(3, $instances);
+        foreach ($instances as $instance) {
+            $enroldata[$instance->enrol] = $instance;
+        }
+
+        $this->assertNotEmpty($enroldata['guest']);
+        $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['guest']->status);
+        $this->assertNotEmpty($enroldata['self']);
+        $this->assertEquals($data['enrolment_2_roleid'], $enroldata['self']->roleid);
+        $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['self']->status);
+        $this->assertNotEmpty($enroldata['manual']);
+        $this->assertEquals(ENROL_INSTANCE_DISABLED, $enroldata['manual']->status);
+
+        // Update existing course.
+        $cat = $this->getDataGenerator()->create_category();
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array(
+            'shortname' => 'c1',
+            'fullname' => 'Fullname 2',
+            'category' => $cat->id,
+            'visible' => '1',
+            'startdate' => '11 June 1984',
+            'idnumber' => 'changeidn',
+            'summary' => 'Summary 2',
+            'format' => 'topics',
+            'theme' => 'clean',
+            'lang' => '',
+            'newsitems' => '2',
+            'showgrades' => '1',
+            'showreports' => '0',
+            'legacyfiles' => '0',
+            'maxbytes' => '4321',
+            'groupmode' => '1',
+            'groupmodeforce' => '0',
+            'enablecompletion' => '0',
+
+            'role_teacher' => 'Teacher',
+            'role_manager' => 'Manager',
+
+            'enrolment_1' => 'guest',
+            'enrolment_1_disable' => '1',
+            'enrolment_2' => 'self',
+            'enrolment_2_roleid' => '2',
+            'enrolment_3' => 'manual',
+            'enrolment_3_delete' => '1',
+        );
+
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'c1'));
+        $ctx = context_course::instance($course->id);
+
+        $this->assertEquals($data['fullname'], $course->fullname);
+        $this->assertEquals($data['category'], $course->category);
+        $this->assertEquals($data['visible'], $course->visible);
+        $this->assertEquals(mktime(0, 0, 0, 6, 11, 1984), $course->startdate);
+        $this->assertEquals($data['idnumber'], $course->idnumber);
+        $this->assertEquals($data['summary'], $course->summary);
+        $this->assertEquals($data['format'], $course->format);
+        $this->assertEquals($data['theme'], $course->theme);
+        $this->assertEquals($data['lang'], $course->lang);
+        $this->assertEquals($data['newsitems'], $course->newsitems);
+        $this->assertEquals($data['showgrades'], $course->showgrades);
+        $this->assertEquals($data['showreports'], $course->showreports);
+        $this->assertEquals($data['legacyfiles'], $course->legacyfiles);
+        $this->assertEquals($data['maxbytes'], $course->maxbytes);
+        $this->assertEquals($data['groupmode'], $course->groupmode);
+        $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce);
+        $this->assertEquals($data['enablecompletion'], $course->enablecompletion);
+
+        // Roles.
+        $roleids = array();
+        $roles = get_all_roles();
+        foreach ($roles as $role) {
+            $roleids[$role->shortname] = $role->id;
+        }
+        $this->assertEquals('Teacher', $DB->get_field_select('role_names', 'name',
+            'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['teacher'])));
+        $this->assertEquals('Manager', $DB->get_field_select('role_names', 'name',
+            'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['manager'])));
+
+        // Enrolment methods.
+        $enroldata = array();
+        $instances = enrol_get_instances($course->id, false);
+        $this->assertCount(2, $instances);
+        foreach ($instances as $instance) {
+            $enroldata[$instance->enrol] = $instance;
+        }
+
+        $this->assertNotEmpty($enroldata['guest']);
+        $this->assertEquals(ENROL_INSTANCE_DISABLED, $enroldata['guest']->status);
+        $this->assertNotEmpty($enroldata['self']);
+        $this->assertEquals($data['enrolment_2_roleid'], $enroldata['self']->roleid);
+        $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['self']->status);
+    }
+
+    public function test_default_data_saved() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        // Create.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+        $data = array(
+            'shortname' => 'c1',
+        );
+        $defaultdata = array(
+            'fullname' => 'Fullname',
+            'category' => '1',
+            'visible' => '0',
+            'startdate' => '8 June 1990',
+            'idnumber' => '123abc',
+            'summary' => 'Summary',
+            'format' => 'weeks',
+            'theme' => 'afterburner',
+            'lang' => 'en',
+            'newsitems' => '7',
+            'showgrades' => '0',
+            'showreports' => '1',
+            'legacyfiles' => '1',
+            'maxbytes' => '1234',
+            'groupmode' => '2',
+            'groupmodeforce' => '1',
+            'enablecompletion' => '1',
+        );
+
+        $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1')));
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $course = $DB->get_record('course', array('shortname' => 'c1'));
+        $ctx = context_course::instance($course->id);
+
+        $this->assertEquals($defaultdata['fullname'], $course->fullname);
+        $this->assertEquals($defaultdata['category'], $course->category);
+        $this->assertEquals($defaultdata['visible'], $course->visible);
+        $this->assertEquals(mktime(0, 0, 0, 6, 8, 1990), $course->startdate);
+        $this->assertEquals($defaultdata['idnumber'], $course->idnumber);
+        $this->assertEquals($defaultdata['summary'], $course->summary);
+        $this->assertEquals($defaultdata['format'], $course->format);
+        $this->assertEquals($defaultdata['theme'], $course->theme);
+        $this->assertEquals($defaultdata['lang'], $course->lang);
+        $this->assertEquals($defaultdata['newsitems'], $course->newsitems);
+        $this->assertEquals($defaultdata['showgrades'], $course->showgrades);
+        $this->assertEquals($defaultdata['showreports'], $course->showreports);
+        $this->assertEquals($defaultdata['legacyfiles'], $course->legacyfiles);
+        $this->assertEquals($defaultdata['maxbytes'], $course->maxbytes);
+        $this->assertEquals($defaultdata['groupmode'], $course->groupmode);
+        $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce);
+        $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion);
+
+        // Update.
+        $cat = $this->getDataGenerator()->create_category();
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+        $data = array(
+            'shortname' => 'c1',
+        );
+        $defaultdata = array(
+            'fullname' => 'Fullname 2',
+            'category' => $cat->id,
+            'visible' => '1',
+            'startdate' => '11 June 1984',
+            'idnumber' => 'changedid',
+            'summary' => 'Summary 2',
+            'format' => 'topics',
+            'theme' => 'clean',
+            'lang' => '',
+            'newsitems' => '2',
+            'showgrades' => '1',
+            'showreports' => '0',
+            'legacyfiles' => '0',
+            'maxbytes' => '1111',
+            'groupmode' => '1',
+            'groupmodeforce' => '0',
+            'enablecompletion' => '0',
+        );
+
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+        $course = $DB->get_record('course', array('shortname' => 'c1'));
+        $ctx = context_course::instance($course->id);
+
+        $this->assertEquals($defaultdata['fullname'], $course->fullname);
+        $this->assertEquals($defaultdata['category'], $course->category);
+        $this->assertEquals($defaultdata['visible'], $course->visible);
+        $this->assertEquals(mktime(0, 0, 0, 6, 11, 1984), $course->startdate);
+        $this->assertEquals($defaultdata['idnumber'], $course->idnumber);
+        $this->assertEquals($defaultdata['summary'], $course->summary);
+        $this->assertEquals($defaultdata['format'], $course->format);
+        $this->assertEquals($defaultdata['theme'], $course->theme);
+        $this->assertEquals($defaultdata['lang'], $course->lang);
+        $this->assertEquals($defaultdata['newsitems'], $course->newsitems);
+        $this->assertEquals($defaultdata['showgrades'], $course->showgrades);
+        $this->assertEquals($defaultdata['showreports'], $course->showreports);
+        $this->assertEquals($defaultdata['legacyfiles'], $course->legacyfiles);
+        $this->assertEquals($defaultdata['maxbytes'], $course->maxbytes);
+        $this->assertEquals($defaultdata['groupmode'], $course->groupmode);
+        $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce);
+        $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion);
+    }
+
+    public function test_rename() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1'));
+        $c2 = $this->getDataGenerator()->create_course(array('shortname' => 'c2'));
+
+        // Cannot rename when creating.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('courseexistsanduploadnotallowed', $co->get_errors());
+
+        // Cannot rename when creating.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => 'newshortname', 'category' => 1, 'summary' => 'S', 'fullname' => 'F');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('canonlyrenameinupdatemode', $co->get_errors());
+
+        // Error when not allowed to rename the course.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => false);
+        $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('courserenamingnotallowed', $co->get_errors());
+
+        // Can rename when updating.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('newshortname', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+
+        // Can rename when updating.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'newshortname', 'rename' => 'newshortname2');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('newshortname2', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+
+        // Error when course does not exist.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'DoesNotExist', 'rename' => 'c1', 'category' => 1, 'summary' => 'S', 'fullname' => 'F');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotrenamecoursenotexist', $co->get_errors());
+
+        // Renaming still updates the other values.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'newshortname2', 'rename' => 'c1', 'fullname' => 'Another fullname!');
+        $defaultdata = array('summary' => 'New summary!');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata, $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertEquals('c1', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+        $this->assertEquals('New summary!', $DB->get_field_select('course', 'summary', 'id = :id', array('id' => $c1->id)));
+        $this->assertEquals('Another fullname!', $DB->get_field_select('course', 'fullname', 'id = :id', array('id' => $c1->id)));
+
+        // Renaming with invalid shortname.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => '<span>invalid</span>');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('invalidshortname', $co->get_errors());
+
+        // Renaming with invalid shortname.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $importoptions = array('canrename' => true);
+        $data = array('shortname' => 'c1', 'rename' => 'c2');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
+    }
+
+    public function test_restore_course() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id));
+
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'A1', 'templatecourse' => $c1->shortname, 'summary' => 'A', 'category' => 1,
+            'fullname' => 'A1');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'A1'));
+        $modinfo = get_fast_modinfo($course);
+        $found = false;
+        foreach ($modinfo->get_cms() as $cmid => $cm) {
+            if ($cm->modname == 'forum' && $cm->name == $c1f1->name) {
+                $found = true;
+                break;
+            }
+        }
+        $this->assertTrue($found);
+
+        // Restore the time limit to prevent warning.
+        set_time_limit(0);
+    }
+
+    public function test_restore_file() {
+        global $DB;
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id));
+
+        // Restore from a file, checking that the file takes priority over the templatecourse.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'A1', 'backupfile' => __DIR__ . '/fixtures/backup.mbz',
+            'summary' => 'A', 'category' => 1, 'fullname' => 'A1', 'templatecourse' => $c1->shortname);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $course = $DB->get_record('course', array('shortname' => 'A1'));
+        $modinfo = get_fast_modinfo($course);
+        $found = false;
+        foreach ($modinfo->get_cms() as $cmid => $cm) {
+            if ($cm->modname == 'glossary' && $cm->name == 'Imported Glossary') {
+                $found = true;
+            } else if ($cm->modname == 'forum' && $cm->name == $c1f1->name) {
+                // We should not find this!
+                $this->assertTrue(false);
+            }
+        }
+        $this->assertTrue($found);
+
+        // Restore the time limit to prevent warning.
+        set_time_limit(0);
+    }
+
+    /**
+     * Testing the reset on groups, group members and enrolments.
+     */
+    public function test_reset() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course();
+        $c1ctx = context_course::instance($c1->id);
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($u1->id, $c1->id, $studentrole->id);
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        $g1 = $this->getDataGenerator()->create_group(array('courseid' => $c1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $g1->id, 'userid' => $u1->id));
+        $this->assertEquals(1, $DB->count_records('groups', array('courseid' => $c1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+
+        // Wrong mode.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'DoesNotExist', 'reset' => '1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => 1);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('canonlyresetcourseinupdatemode', $co->get_errors());
+        $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        // Reset not allowed.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'reset' => '1');
+        $importoptions = array('canreset' => false);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('courseresetnotallowed', $co->get_errors());
+        $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        // Reset allowed but not requested.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'reset' => '0');
+        $importoptions = array('canreset' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        // Reset passed as a default parameter, should not be taken in account.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname);
+        $importoptions = array('canreset' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array('reset' => 1), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(1, get_enrolled_users($c1ctx));
+
+        // Reset executed from data.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'reset' => 1);
+        $importoptions = array('canreset' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertFalse($DB->record_exists('groups', array('id' => $g1->id)));
+        $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+        $this->assertCount(0, get_enrolled_users($c1ctx));
+
+        // Reset executed from import option.
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'reset' => 0);
+        $importoptions = array('reset' => 1, 'canreset' => true);
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+
+        $g1 = $this->getDataGenerator()->create_group(array('courseid' => $c1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $g1->id, 'userid' => $u1->id));
+        $this->assertEquals(1, $DB->count_records('groups', array('courseid' => $c1->id)));
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+        $this->assertFalse($DB->record_exists('groups', array('id' => $g1->id)));
+    }
+
+    public function test_create_bad_category() {
+        $this->resetAfterTest(true);
+
+        // Ensure fails when category cannot be resolved upon creation.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => 'Wrong cat');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors());
+
+        // Ensure fails when category cannot be resolved upon update.
+        $c1 = $this->getDataGenerator()->create_course();
+        $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => $c1->shortname, 'category' => 'Wrong cat');
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertFalse($co->prepare());
+        $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors());
+    }
+
+    public function test_enrolment_data() {
+        $this->resetAfterTest(true);
+
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+        $data = array('shortname' => 'c1', 'summary' => 'S', 'fullname' => 'FN', 'category' => '1');
+        $data['enrolment_1'] = 'manual';
+        $data['enrolment_1_role'] = 'teacher';
+        $data['enrolment_1_startdate'] = '2nd July 2013';
+        $data['enrolment_1_enddate'] = '2nd August 2013';
+        $data['enrolment_1_enrolperiod'] = '10 days';
+        $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+        $this->assertTrue($co->prepare());
+        $co->proceed();
+
+        // Enrolment methods.
+        $enroldata = array();
+        $instances = enrol_get_instances($co->get_id(), false);
+        foreach ($instances as $instance) {
+            $enroldata[$instance->enrol] = $instance;
+        }
+
+        $this->assertNotEmpty($enroldata['manual']);
+        $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['manual']->status);
+        $this->assertEquals(strtotime($data['enrolment_1_startdate']), $enroldata['manual']->enrolstartdate);
+        $this->assertEquals(strtotime('1970-01-01 GMT + ' . $data['enrolment_1_enrolperiod']), $enroldata['manual']->enrolperiod);
+        $this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate);
+    }
+
+    public function test_idnumber_problems() {
+        $this->resetAfterTest(true);
+
+        $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'sntaken', 'idnumber' => 'taken'));
+        $c2 = $this->getDataGenerator()->create_course();
+
+        // Create with existing ID number.
+        $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+        $updatemode&n