From 3a0fda69d70fc0752adcd6bd1dd306c6c240fffc Mon Sep 17 00:00:00 2001 From: Piers Harding Date: Tue, 18 Jun 2013 15:12:09 +0800 Subject: [PATCH 01/21] MDL-13114 tool_uploadcourse: Initial commit --- admin/tool/uploadcourse/README.txt | 196 +++ admin/tool/uploadcourse/cli/uploadcourse.php | 260 ++++ admin/tool/uploadcourse/course_form.php | 350 +++++ admin/tool/uploadcourse/index.php | 184 +++ .../lang/en/tool_uploadcourse.php | 130 ++ admin/tool/uploadcourse/locallib.php | 1328 +++++++++++++++++ admin/tool/uploadcourse/settings.php | 33 + admin/tool/uploadcourse/version.php | 31 + 8 files changed, 2512 insertions(+) create mode 100644 admin/tool/uploadcourse/README.txt create mode 100644 admin/tool/uploadcourse/cli/uploadcourse.php create mode 100644 admin/tool/uploadcourse/course_form.php create mode 100644 admin/tool/uploadcourse/index.php create mode 100644 admin/tool/uploadcourse/lang/en/tool_uploadcourse.php create mode 100644 admin/tool/uploadcourse/locallib.php create mode 100644 admin/tool/uploadcourse/settings.php create mode 100644 admin/tool/uploadcourse/version.php diff --git a/admin/tool/uploadcourse/README.txt b/admin/tool/uploadcourse/README.txt new file mode 100644 index 00000000000..5ee935491a0 --- /dev/null +++ b/admin/tool/uploadcourse/README.txt @@ -0,0 +1,196 @@ +moodle-tool_uploadcourse +======================== + +Is a Moodle admin/tools plugin for uploading course outlines +in much the same way that admin/tools/uploaduser works for users. +These plugins became available from Moodle 2.2x and onwards, as +this is when the admin/tools framework first appeared. + +https://gitorious.org/moodle-tool_uploadcourse + +There is also a bulk course category upload function available at https://gitorious.org/moodle-tool_uploadcoursecategory + +If you need to manage course enrolments via bulk upload then you should look at +the core user upload facility - http://docs.moodle.org/22/en/Upload_users + +This takes CSV files as input and enables override or augmentation +with default parameter values. + +All the usual add,updated,rename, and delete functions. + + +Thanks to Moshe Golden for help with getting the command line interface going. + + +!!!! NOTE !!!! +=============== + +This plugin used to come with the full directory structure +admin/tool/uploadcourse - this is nolonger the case so the +installation proceedure has changed! + +General installation proceedures are here: +http://docs.moodle.org/20/en/Installing_contributed_modules_or_plugins + +The basic process is: +Download https://gitorious.org/moodle-tool_uploadcourse/moodle-tool_uploadcourse/archive-tarball/master +unpack the file (probably called master) with tar -xzvf master +This will give you a directory called moodle-tool_uploadcourse-moodle-tool_uploadcourse +Move this directory and rename it into it's final position: +mv moodle-tool_uploadcourse-moodle-tool_uploadcourse /admin/tool/uploadcourse + +Alternatively you can use git: +cd /admin/tool +git clone git@gitorious.org:moodle-tool_uploadcourse/moodle-tool_uploadcourse.git uploadcourse + +Be careful about leaving the .git directory in your live environment. + + +CSV File format +=============== + +Possible column names are: +fullname, shortname, category, idnumber, summary, +format, showgrades, newsitems, teacher, editingteacher, student, modinfo, +manager, coursecreator, guest, user, startdate, numsections, maxbytes, visible, groupmode, restrictmodules, +enablecompletion, completionstartonenrol, completionnotify, hiddensections, groupmodeforce, lang, theme, +cost, showreports, notifystudents, expirynotify, expirythreshold, requested, +deleted, // 1 means delete course +oldshortname, // for renaming +backupfile, // for restoring a course template after creation +templatename, // course to use as a template - the shortname +reset, // reset the course contents after upload - this resets everything + - so you loose groups, roles, logs, grades etc. Be Careful!!! + +An example file is: + +fullname,shortname,category,idnumber,summary,backupfile +Computer Science 101,CS101,Cat1,CS101,The first thing you will ever know,/path/to/backup-moodle2-course-cs101-20120213-0748-nu.mbz + +As a general rule, the input values for fields are what you find on the data entry form if you inspect the HTML element. + +Format +====== +The options for the format value are 'scorm', 'social', weeks', and 'topics'. + +Role Names +=========== + 'teacher', 'editingteacher', 'student', 'manager', +'coursecreator', 'guest', 'user' are - where config permitting - you can +substitute your own name for these roles (string value). + +Category +======== +For category you must supply the category name as it is in Moodle and this +field is case sensitive. If Sub Categories are involved then the full +category hierarchy needs to be specified as a '/' delimited string eg: +'Miscellaneous / Sub Cat / Sub Sub Cat'. The delimiter can be escaped with +a back slash eg: 'some\/category'. + +Course Templating +================= +add column backupfile which has the fully qualified path name to a file on +the server that has a a Moodle course backup in it. + +Add a column templatename which is the shortname of an existing course that +will be copied over the top of the new course. + +Course Enrolment Methods +========================= + +Enrolment methods need special CSV columns as there can be many per course, and the fields for each +method are flexible. The following is an example with two enrolment methods - manual, and self - firstly you need +the column identifying the enrolment method enrolmethod_, and then add the corresponding field values subscripted with _. +eg: +fullname,shortname,category,idnumber,summary,enrolmethod_1,status_1,enrolmethod_2,name_2,password_2,customtext1_2 +Parent,Parent,,Parent,Parent,manual,1,self,self1,letmein,this is a custom message 1 +Students,Students,,Students,Students,manual,0,self,self2,letmein,this is a custom message 2 +Teachers,Teachers,,Teachers,Teachers,manual,0,self,self3,letmein,this is a custom message 3 + +add the special columns for: + * delete - delete_ with value 1 + * disable - disable_ with value 1 + +startdate enrol_startdate enrol_enddate +======================================= +For startdate enrolstartdate, and enrolenddate the values should be supplied in the form like 31.01.2012 or +31/01/2012 that can be consumed by strtotime() (http://php.net/manual/en/function.strtotime.php) - check +your PHP locale settings for the fine tuning eg: m/d/y vs d/m/y. + +Enrolment method field 'enrolperiod' must be in seconds. If this is supplied then enrolenddate will be calculated +as enrolstartdate + enrolperiod. + +enrolperiod should be supplied in multiples of enrolment period measurements - 1 hour = 3600, 1 day = 86400 +and so on. OR - you can pass a text string that php strtotime() can recognise eg: '2 weeks' or '10 days' + +Enrolment Method Role +===================== +Default Role for an enrolment method is supplied by adding the 'role_' column. The expected value is the +descriptive label for the given role eg: 'Student', or "Teacher'. + +Enrolment example: +fullname,shortname,category,idnumber,summary,enrolmethod_1,enrolperiod_1,role_1 +a name,short1,Miscellaneous,id1,a summary,manual,864000,Manager + +Update Course: +================= +Make sure you have shortname in the csv. After uploading the file, select: +Upload type: one of the update existing related options +Existing course details: Overide with file +Allow Renames: Yes + +Update example: +fullname,shortname +new full name,short1 + + +Run it in batch mode +===================== +Execute Course Upload in batch mode - this must be run as the www-data user (or the equivalent user that the web server runs under). + +Options: +-v, --verbose Print verbose progress information +-h, --help Print out this help +-a, --action Action to perform - addnew, addupdate, update, forceadd +-m, --mode Mode of execution - delete, rename, nochange, file, filedefaults, missing +-f, --file CSV File +-d, --delimiter delimiter - colon,semicolon,tab,cfg,comma +-e, --encoding File encoding - utf8 etc +-c, --category Course category +-s, --templateshortname Template course by shortname +-t, --template Template course by backup file +-g, --format Course format - weeks,scorm,social,topics +-n, --numsections Number of sections + + +Example: +sudo -u www-data /usr/bin/php admin/tool/uploadcourse/cli/uploadcourse.php --action=addupdate \ + --mode=delete --file=./courses.csv --delimiter=comma + + + +Installation +================= +git clone this repository into /admin/tools/uploadcourse directory. + +Point your browser at Moodle, and login as admin. This should kick off +the upgrade so that Moodle can now recognise the new plugin. + +This was inspired in part by a need for a complimentary function for uploading +courses (as for users) for the the NZ MLE tools for Identity and +Access Managment (synchronising users with the School SMS): +https://gitorious.org/pla-udi +and +https://gitorious.org/pla-udi/mle_ide_tools + +Copyright (C) Piers Harding 2011 and beyond, All rights reserved + +moodle-tool_uploadcourse free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2 of the License, or (at your option) any later version. + +This library 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 +Lesser General Public License for more details. diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php new file mode 100644 index 00000000000..fd5eea3a776 --- /dev/null +++ b/admin/tool/uploadcourse/cli/uploadcourse.php @@ -0,0 +1,260 @@ +. + +/** + * CLI Bulk course registration script from a comma separated file + * + * @package tool_uploadcourse + * @subpackage uploadcourse + * @copyright 2004 onwards Martin Dougiamas (http://dougiamas.com) + * @copyright 2012 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * Notes: + * - it is required to use the web server account when executing PHP CLI scripts + * - you need to change the "www-data" to match the apache user account + * - use "su" if "sudo" not available + * + */ + +define('CLI_SCRIPT', true); + +require(dirname(dirname(dirname(dirname(dirname(__FILE__))))).'/config.php'); + +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/csvlib.class.php'); +require_once($CFG->dirroot.'/course/lib.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->libdir . '/filelib.php'); +require_once($CFG->libdir.'/clilib.php'); +require_once('../locallib.php'); + +$courseconfig = get_config('moodlecourse'); + +// Now get cli options. +list($options, $unrecognized) = cli_get_params( + array('verbose' => false, + 'help' => false, + 'action' => '', + 'mode' => 'nochange', + 'file' => '', + 'delimiter' => 'comma', + 'encoding' => 'UTF-8', + 'category' => false, + 'templateshortname' => false, + 'template' => false, + 'format' => $courseconfig->format, + 'numsections' => $courseconfig->numsections, + 'reset' => false, + ), + array('v' => 'verbose', + 'h' => 'help', + 'a' => 'action', + 'm' => 'mode', + 'f' => 'file', + 'd' => 'delimiter', + 'e' => 'encoding', + 'c' => 'category', + 's' => 'templateshortname', + 't' => 'template', + 'g' => 'format', + 'n' => 'numsections', + 'r' => 'reset', + )); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +$help = +"Execute Course Upload. + +Options: +-v, --verbose Print verbose progress information +-h, --help Print out this help +-a, --action Action to perform - addnew, addupdate, update, forceadd +-m, --mode Mode of execution - delete, rename, nochange, file, filedefaults, missing +-f, --file CSV File +-d, --delimiter delimiter - colon,semicolon,tab,cfg,comma +-e, --encoding File encoding - utf8 etc +-c, --category Course category +-s, --templateshortname Template course by shortname +-t, --template Template course by backup file +-g, --format Course format - weeks,scorm,social,topics +-n, --numsections Number of sections +-r, --reset Run the course reset by default after each course import + + +Example: +\$sudo -u www-data /usr/bin/php admin/tool/uploadcourse/cli/uploadcourse.php --action=addupdate \\ + --mode=delete --file=./courses.csv --delimiter=comma +"; + +if ($options['help']) { + echo $help; + die; +} +echo "Moodle course uploader running ...\n"; + +$actions = array('addnew' => CC_COURSE_ADDNEW, + 'addupdate' => CC_COURSE_ADD_UPDATE, + 'update' => CC_COURSE_UPDATE, + 'forceadd' => CC_COURSE_ADDINC); +if (!isset($options['action']) || + !isset($actions[$options['action']]) || + ($options['action'] != 'addnew' && !isset($options['mode']))) { + echo get_string('invalidinput', 'tool_uploadcourse')."\n"; + echo $help; + die; +} + +if (!empty($options['verbose']) || $CFG->debug) { + define('CC_DEBUG', true); +} + +if (!isset($actions[$options['action']])) { + echo get_string('invalidaction', 'tool_uploadcourse')."\n"; + echo $help; + die; +} +$options['cctype'] = $actions[$options['action']]; + +$updatetype = array('nochange' => CC_UPDATE_NOCHANGES, + 'file' => CC_UPDATE_FILEOVERRIDE, + 'filedefaults' => CC_UPDATE_ALLOVERRIDE, + 'missing' => CC_UPDATE_MISSING); +if ($options['mode'] == 'rename') { + $options['ccallowrenames'] = 1; + unset($options['mode']); +} else if ($options['mode'] == 'delete') { + $options['ccallowdeletes'] = 1; + unset($options['mode']); +} else if (!isset($updatetype[$options['mode']])) { + echo get_string('invalidmode', 'tool_uploadcourse')."\n"; + echo $help; + die; +} +if (isset($options['mode'])) { + $options['ccupdatetype'] = $updatetype[$options['mode']]; +} +$options['ccstandardshortnames'] = 1; +$options['startdate'] = time() + 3600 * 24; +$options['hiddensections'] = $courseconfig->hiddensections; +$options['newsitems'] = $courseconfig->newsitems; +$options['showgrades'] = $courseconfig->showgrades; +$options['showreports'] = $courseconfig->showreports; +$options['maxbytes'] = $courseconfig->maxbytes; +$options['legacyfiles'] = 0; +$options['groupmode'] = $courseconfig->groupmode; +$options['groupmodeforce'] = $courseconfig->groupmodeforce; +$options['visible'] = $courseconfig->visible; +$options['lang'] = $courseconfig->lang; + +if ($options['category']) { + $split = preg_split('|(?get_record('course_categories', array('name'=>trim($cat), 'parent' => $options['category'])); + if (empty($category)) { + echo get_string('invalidcategory', 'tool_uploadcourse')."\n"; + echo $help; + die; + } + $options['category'] = $category->id; + } + $options['cccategory'] = $options['category']; +} else { + $categories = $DB->get_records('course_categories'); + if (empty($categories)) { + echo get_string('invalidcategory', 'tool_uploadcourse')."\n"; + echo $help; + die; + } + $category = array_shift($categories); + $options['cccategory'] = $category->id; +} + +if (isset($options['templateshortname'])) { + $options['ccshortname'] = $options['templateshortname']; +} + +$options['file'] = realpath($options['file']); +if (!file_exists($options['file'])) { + echo get_string('invalidcsvfile', 'tool_uploadcourse')."\n"; + echo $help; + die; +} + +$encodings = textlib::get_encodings(); +if (!isset($encodings[$options['encoding']])) { + echo get_string('invalidencoding', 'tool_uploadcourse')."\n"; + echo $help; + die; +} + + +if ($options['template']) { + $options['template'] = realpath($options['template']); +} +if ($options['template'] && !file_exists($options['template'])) { + echo get_string('invalidtemplatefile', 'tool_uploadcourse')."\n"; + echo $help; + die; +} +$tmpdir = $CFG->tempdir . '/backup'; +if (!check_dir_exists($tmpdir, true, true)) { + throw new restore_controller_exception('cannot_create_backup_temp_dir'); +} +$filename = restore_controller::get_tempdir_name(SITEID, $USER->id); +$restorefile = null; +if ($options['template']) { + $restorefile = $options['template']; +} + +$formdata = (object) $options; + + +$returnurl = new moodle_url('/admin/tool/uploadcourse/index.php'); +$bulknurl = new moodle_url('/admin/tool/uploadcourse/index.php'); +$std_fields = tool_uploadcourse_std_fields(); + +// Emulate normal session. +cron_setup_user(); + +$content = file_get_contents($formdata->file); +$iid = csv_import_reader::get_new_iid('uploadcourse'); +$cir = new csv_import_reader($iid, 'uploadcourse'); +$readcount = $cir->load_csv_content($content, $formdata->encoding, $formdata->delimiter); +$filecolumns = tool_uploadcourse_validate_course_upload_columns($cir, $std_fields, $returnurl); +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()); +} +echo "CSV read count: ".$readcount."\n"; + +$result = tool_uploadcourse_process_course_upload($formdata, $cir, $filecolumns, $restorefile, true); + +exit($result); diff --git a/admin/tool/uploadcourse/course_form.php b/admin/tool/uploadcourse/course_form.php new file mode 100644 index 00000000000..3f17cc79c01 --- /dev/null +++ b/admin/tool/uploadcourse/course_form.php @@ -0,0 +1,350 @@ +. + +/** + * Bulk course upload forms + * + * @package tool_uploadcourse + * @subpackage uploadcourse + * @copyright 2007 Dan Poltawski + * @copyright 2011 Piers Harding + * @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. + * + * @copyright 2007 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_uploadcourse_form1 extends moodleform { + /** + * The standard form definiton + * @return object $form + */ + public function definition () { + $mform = $this->_form; + + $mform->addElement('header', 'settingsheader', get_string('upload')); + + $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_action_buttons(false, get_string('uploadcourses', 'tool_uploadcourse')); + } +} + + +/** + * Specify course upload details + * + * @copyright 2007 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class admin_uploadcourse_form2 extends moodleform { + /** + * The standard form definiton + * @return object $form + */ + public function definition () { + global $CFG, $COURSE, $DB; + + $mform = $this->_form; + $columns = $this->_customdata['columns']; + $data = $this->_customdata['data']; + $courseconfig = get_config('moodlecourse'); + + // I am the template course, why should it be the administrator? we have roles now, other ppl may use this script ;-). + $templatecourse = $COURSE; + + // Upload settings and file. + $mform->addElement('header', 'settingsheader', get_string('settings')); + + $choices = array(CC_COURSE_ADDNEW => get_string('ccoptype_addnew', 'tool_uploadcourse'), + CC_COURSE_ADDINC => get_string('ccoptype_addinc', 'tool_uploadcourse'), + CC_COURSE_ADD_UPDATE => get_string('ccoptype_addupdate', 'tool_uploadcourse'), + CC_COURSE_UPDATE => get_string('ccoptype_update', 'tool_uploadcourse')); + $mform->addElement('select', 'cctype', get_string('ccoptype', 'tool_uploadcourse'), $choices); + + $choices = array(CC_UPDATE_NOCHANGES => get_string('nochanges', 'tool_uploadcourse'), + CC_UPDATE_FILEOVERRIDE => get_string('ccupdatefromfile', 'tool_uploadcourse'), + CC_UPDATE_ALLOVERRIDE => get_string('ccupdateall', 'tool_uploadcourse'), + CC_UPDATE_MISSING => get_string('ccupdatemissing', 'tool_uploadcourse')); + $mform->addElement('select', 'ccupdatetype', get_string('ccupdatetype', 'tool_uploadcourse'), $choices); + $mform->setDefault('ccupdatetype', CC_UPDATE_NOCHANGES); + $mform->disabledIf('ccupdatetype', 'cctype', 'eq', CC_COURSE_ADDNEW); + $mform->disabledIf('ccupdatetype', 'cctype', 'eq', CC_COURSE_ADDINC); + + $mform->addElement('selectyesno', 'ccallowrenames', get_string('allowrenames', 'tool_uploadcourse')); + $mform->setDefault('ccallowrenames', 0); + $mform->disabledIf('ccallowrenames', 'cctype', 'eq', CC_COURSE_ADDNEW); + $mform->disabledIf('ccallowrenames', 'cctype', 'eq', CC_COURSE_ADDINC); + + $mform->addElement('selectyesno', 'ccallowdeletes', get_string('allowdeletes', 'tool_uploadcourse')); + $mform->setDefault('ccallowdeletes', 0); + $mform->disabledIf('ccallowdeletes', 'cctype', 'eq', CC_COURSE_ADDNEW); + $mform->disabledIf('ccallowdeletes', 'cctype', 'eq', CC_COURSE_ADDINC); + + $mform->addElement('selectyesno', 'reset', get_string('reset', 'tool_uploadcourse')); + $mform->setDefault('ccallowdeletes', 0); + $mform->disabledIf('ccallowdeletes', 'cctype', 'eq', CC_COURSE_ADDNEW); + $mform->disabledIf('ccallowdeletes', 'cctype', 'eq', CC_COURSE_ADDINC); + + $mform->addElement('selectyesno', 'ccstandardshortnames', get_string('ccstandardshortnames', 'tool_uploadcourse')); + $mform->setDefault('ccstandardshortnames', 1); + + // Default values. + $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse')); + $displaylist = array(); + $parentlist = array(); + make_categories_list($displaylist, $parentlist, 'moodle/course:create'); + $mform->addElement('select', 'cccategory', get_string('category'), $displaylist); + $mform->addHelpButton('cccategory', 'category'); + + $mform->addElement('text', 'ccshortname', get_string('ccshortnametemplate', 'tool_uploadcourse'), + 'maxlength="100" size="20"'); + $mform->addHelpButton('ccshortname', 'shortnamecourse', 'tool_uploadcourse'); + $mform->disabledIf('ccshortname', 'cctype', 'eq', CC_COURSE_ADD_UPDATE); + $mform->disabledIf('ccshortname', 'cctype', 'eq', CC_COURSE_UPDATE); + + $courseformats = get_plugin_list('format'); + $formcourseformats = array(); + foreach ($courseformats as $courseformat => $formatdir) { + $formcourseformats[$courseformat] = get_string('pluginname', "format_$courseformat"); + } + $mform->addElement('select', 'format', get_string('format'), $formcourseformats); + $mform->addHelpButton('format', 'format'); + $mform->setDefault('format', $courseconfig->format); + + for ($i = 0; $i <= $courseconfig->maxsections; $i++) { + $sectionmenu[$i] = "$i"; + } + $mform->addElement('select', 'numsections', get_string('numberweeks'), $sectionmenu); + $mform->setDefault('numsections', $courseconfig->numsections); + + $mform->addElement('date_selector', 'startdate', get_string('startdate')); + $mform->addHelpButton('startdate', 'startdate'); + $mform->setDefault('startdate', time() + 3600 * 24); + + $choices = array(); + $choices['0'] = get_string('hiddensectionscollapsed'); + $choices['1'] = get_string('hiddensectionsinvisible'); + $mform->addElement('select', 'hiddensections', get_string('hiddensections'), $choices); + $mform->addHelpButton('hiddensections', 'hiddensections'); + $mform->setDefault('hiddensections', $courseconfig->hiddensections); + + $options = range(0, 10); + $mform->addElement('select', 'newsitems', get_string('newsitemsnumber'), $options); + $mform->addHelpButton('newsitems', 'newsitemsnumber'); + $mform->setDefault('newsitems', $courseconfig->newsitems); + + $mform->addElement('selectyesno', 'showgrades', get_string('showgrades')); + $mform->addHelpButton('showgrades', 'showgrades'); + $mform->setDefault('showgrades', $courseconfig->showgrades); + + $mform->addElement('selectyesno', 'showreports', get_string('showreports')); + $mform->addHelpButton('showreports', 'showreports'); + $mform->setDefault('showreports', $courseconfig->showreports); + + $choices = get_max_upload_sizes($CFG->maxbytes); + $mform->addElement('select', 'maxbytes', get_string('maximumupload'), $choices); + $mform->addHelpButton('maxbytes', 'maximumupload'); + $mform->setDefault('maxbytes', $courseconfig->maxbytes); + + if (!empty($course->legacyfiles) or !empty($CFG->legacyfilesinnewcourses)) { + if (empty($course->legacyfiles)) { + // 0 or missing means no legacy files ever used in this course - new course or nobody turned on legacy files yet. + $choices = array('0'=>get_string('no'), '2'=>get_string('yes')); + } else { + $choices = array('1'=>get_string('no'), '2'=>get_string('yes')); + } + $mform->addElement('select', 'legacyfiles', get_string('courselegacyfiles'), $choices); + $mform->addHelpButton('legacyfiles', 'courselegacyfiles'); + if (!isset($courseconfig->legacyfiles)) { + // In case this was not initialised properly due to switching of $CFG->legacyfilesinnewcourses. + $courseconfig->legacyfiles = 0; + } + $mform->setDefault('legacyfiles', $courseconfig->legacyfiles); + } + + 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', 'theme', get_string('forcetheme'), $themes); + } + $courseshortnames = $DB->get_records('course', null, $sort='shortname', 'id,shortname,idnumber'); + $formccourseshortnames = array('none' => get_string('none')); + foreach ($courseshortnames as $course) { + $formccourseshortnames[$course->shortname] = $course->shortname; + } + $mform->addElement('select', 'templatename', get_string('coursetemplatename', 'tool_uploadcourse'), $formccourseshortnames); + $mform->addHelpButton('templatename', 'coursetemplatename', 'tool_uploadcourse'); + $mform->setDefault('templatename', 'none'); + + $contextid = $this->_customdata['contextid']; + $mform->addElement('hidden', 'contextid', $contextid); + $mform->addElement('filepicker', 'restorefile', get_string('templatefile', 'tool_uploadcourse')); + + enrol_course_edit_form($mform, null, get_context_instance(CONTEXT_SYSTEM)); + + $mform->addElement('header', '', get_string('groups', 'group')); + + $choices = array(); + $choices[NOGROUPS] = get_string('groupsnone', 'group'); + $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group'); + $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group'); + $mform->addElement('select', 'groupmode', get_string('groupmode', 'group'), $choices); + $mform->addHelpButton('groupmode', 'groupmode', 'group'); + $mform->setDefault('groupmode', $courseconfig->groupmode); + + $choices = array(); + $choices['0'] = get_string('no'); + $choices['1'] = get_string('yes'); + $mform->addElement('select', 'groupmodeforce', get_string('groupmodeforce', 'group'), $choices); + $mform->addHelpButton('groupmodeforce', 'groupmodeforce', 'group'); + $mform->setDefault('groupmodeforce', $courseconfig->groupmodeforce); + + // Default groupings selector. + $options = array(); + $options[0] = get_string('none'); + $mform->addElement('select', 'defaultgroupingid', get_string('defaultgrouping', 'group'), $options); + + $mform->addElement('header', '', get_string('availability')); + + $choices = array(); + $choices['0'] = get_string('courseavailablenot'); + $choices['1'] = get_string('courseavailable'); + $mform->addElement('select', 'visible', get_string('availability'), $choices); + $mform->addHelpButton('visible', 'availability'); + $mform->setDefault('visible', $courseconfig->visible); + + $mform->addElement('header', '', get_string('language')); + + $languages=array(); + $languages[''] = get_string('forceno'); + $languages += get_string_manager()->get_list_of_translations(); + $mform->addElement('select', 'lang', get_string('forcelanguage'), $languages); + $mform->setDefault('lang', $courseconfig->lang); + + // Hidden fields. + $mform->addElement('hidden', 'iid'); + $mform->setType('iid', 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); + } + + /** + * Form tweaks that depend on current data. + */ + public function definition_after_data() { + $mform = $this->_form; + $columns = $this->_customdata['columns']; + + foreach ($columns as $column) { + if ($mform->elementExists($column)) { + $mform->removeElement($column); + } + } + + } + + /** + * Server side validation. + * @param array $data - form data + * @param object $files - form files + * @return array $errors - form errors + */ + public function validation($data, $files) { + global $DB; + + $errors = parent::validation($data, $files); + $columns = $this->_customdata['columns']; + $optype = $data['cctype']; + + // Look for other required data. + if ($optype != CC_COURSE_UPDATE) { + if (!in_array('fullname', $columns)) { + if (isset($errors['cctype'])) { + $errors['cctype'] .= ' '; + } + $errors['cctype'] .= get_string('missingfield', 'error', 'fullname'); + } + if (!in_array('summary', $columns)) { + if (isset($errors['cctype'])) { + $errors['cctype'] .= ' '; + } + $errors['cctype'] .= get_string('missingfield', 'error', 'summary'); + } + } + if (!empty($data['templatename']) && $data['templatename'] != 'none') { + if (!$template = $DB->get_record('course', array('shortname' => $data['templatename']))) { + $errors['templatename'] = get_string('missingtemplate', 'tool_uploadcourse'); + } + } + + return $errors; + } + + /** + * Used to reformat the data from the editor component + * + * @return stdClass + */ + public function get_data() { + $data = parent::get_data(); + + if ($data !== null and isset($data->description)) { + $data->descriptionformat = $data->description['format']; + $data->description = $data->description['text']; + } + + return $data; + } +} diff --git a/admin/tool/uploadcourse/index.php b/admin/tool/uploadcourse/index.php new file mode 100644 index 00000000000..e92749fa7a9 --- /dev/null +++ b/admin/tool/uploadcourse/index.php @@ -0,0 +1,184 @@ +. + +/** + * Bulk course registration script from a comma separated file + * + * @package tool_uploadcourse + * @copyright 2004 onwards Martin Dougiamas (http://dougiamas.com) + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require('../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/csvlib.class.php'); +require_once($CFG->dirroot.'/course/lib.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->libdir . '/filelib.php'); +require_once('locallib.php'); +require_once('course_form.php'); + +$iid = optional_param('iid', '', PARAM_INT); +$previewrows = optional_param('previewrows', 10, PARAM_INT); +require_login(); +admin_externalpage_setup('tooluploadcourse'); + +$returnurl = new moodle_url('/admin/tool/uploadcourse/index.php'); +$bulknurl = new moodle_url('/admin/tool/uploadcourse/index.php'); +$std_fields = tool_uploadcourse_std_fields(); + + +if (empty($iid)) { + $mform1 = new admin_uploadcourse_form1(); + + if ($formdata = $mform1->get_data()) { + $iid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($iid, 'uploadcourse'); + + $content = $mform1->get_file_content('coursefile'); + + $readcount = $cir->load_csv_content($content, $formdata->encoding, $formdata->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()); + } + // Test if columns ok. + $filecolumns = tool_uploadcourse_validate_course_upload_columns($cir, $std_fields, $returnurl); + // Continue to form2. + + } 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($iid, 'uploadcourse'); + $filecolumns = tool_uploadcourse_validate_course_upload_columns($cir, $std_fields, $returnurl); +} + +$frontpagecontext = context_course::instance(SITEID); +$mform2 = new admin_uploadcourse_form2(null, + array('contextid' => $frontpagecontext->id, + 'columns' => $filecolumns, + 'data' => array('iid'=>$iid, 'previewrows'=>$previewrows))); + +// If a file has been uploaded, then process it. +if ($formdata = $mform2->is_cancelled()) { + $cir->cleanup(true); + redirect($returnurl); +} else if ($formdata = $mform2->get_data()) { + // Print the header. + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('uploadcoursesresult', 'tool_uploadcourse')); + + $tmpdir = $CFG->tempdir . '/backup'; + if (!check_dir_exists($tmpdir, true, true)) { + throw new restore_controller_exception('cannot_create_backup_temp_dir'); + } + $filename = restore_controller::get_tempdir_name(SITEID, $USER->id); + $restorefile = $tmpdir . '/' . $filename; + if (!$mform2->save_file('restorefile', $restorefile)) { + $restorefile = null; + } + $bulk = isset($formdata->ccbulk) ? $formdata->ccbulk : 0; + + tool_uploadcourse_process_course_upload($formdata, $cir, $filecolumns, $restorefile); + + echo $OUTPUT->box_end(); + + if ($bulk) { + echo $OUTPUT->continue_button($bulknurl); + } else { + echo $OUTPUT->continue_button($returnurl); + } + echo $OUTPUT->footer(); + die; +} + +// Print the header. +echo $OUTPUT->header(); + +echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse')); + +// NOTE: this is JUST csv processing preview, we must not prevent import from here if there is something in the file!! +// this was intended for validation of csv formatting and encoding, not filtering the data!!!! +// we definitely must not process the whole file! + +// Preview table data. +$data = array(); +$cir->init(); +$linenum = 1; // Column header is first line. +while ($linenum <= $previewrows and $fields = $cir->next()) { + $linenum++; + $rowcols = array(); + $rowcols['line'] = $linenum; + foreach ($fields as $key => $field) { + $rowcols[$filecolumns[$key]] = s($field); + } + $rowcols['status'] = array(); + + if (isset($rowcols['shortname'])) { + $stdshortname = clean_param($rowcols['shortname'], PARAM_MULTILANG); + if ($rowcols['shortname'] !== $stdshortname) { + $rowcols['status'][] = get_string('invalidshortnameupload'); + } + if ($courseid = $DB->get_field('course', 'id', array('shortname'=>$stdshortname))) { + $rowcols['shortname'] = html_writer::link(new moodle_url('/course/view.php', + array('id' => $courseid)), + $rowcols['shortname']); + } + } else { + $rowcols['status'][] = get_string('missingshortname'); + } + + $rowcols['status'] = implode('
', $rowcols['status']); + $data[] = $rowcols; +} +if ($fields = $cir->next()) { + $data[] = array_fill(0, count($fields) + 2, '...'); +} +$cir->close(); + +$table = new html_table(); +$table->id = "ccpreview"; +$table->attributes['class'] = 'generaltable'; +$table->tablealign = 'center'; +$table->summary = get_string('uploadcoursespreview', 'tool_uploadcourse'); +$table->head = array(); +$table->data = $data; + +$table->head[] = get_string('cccsvline', 'tool_uploadcourse'); +foreach ($filecolumns as $column) { + $table->head[] = $column; +} +$table->head[] = get_string('status'); + +echo html_writer::tag('div', html_writer::table($table), array('class'=>'flexible-wrap')); + +// Print the form. +$mform2->display(); +echo $OUTPUT->footer(); +die; + diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php new file mode 100644 index 00000000000..dad679517aa --- /dev/null +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -0,0 +1,130 @@ +. + +/** + * Strings for component 'tool_uploadcourse', language 'en', branch 'MOODLE_22_STABLE' + * + * @package tool_uploadcourse + * @subpackage uploadcourse + * @copyright 2011 Petr Skoda {@link http://skodak.org} + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$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 (don\t use comma-quoted as Moodle does not support it): {$a}'; +$string['allowdeletes'] = 'Allow deletes'; +$string['allowrenames'] = 'Allow renames'; +$string['csvdelimiter'] = 'CSV delimiter'; +$string['defaultvalues'] = 'Default values'; +$string['deleteerrors'] = 'Delete errors'; +$string['encoding'] = 'Encoding'; +$string['errors'] = 'Errors'; +$string['invalidinput'] = 'You must specify a valid combination of --action and --mode'; +$string['nochanges'] = 'No changes'; +$string['pluginname'] = 'Course upload'; +$string['renameerrors'] = 'Rename errors'; +$string['requiredtemplate'] = 'Required. You may use template syntax here (%l = lastname, %f = firstname, %u = coursename). See help for details and examples.'; +$string['rowpreviewnum'] = 'Preview rows'; +$string['uploadpicture_badcoursefield'] = 'The course attribute specified is not valid. Please, try again.'; +$string['uploadpicture_cannotmovezip'] = 'Cannot move zip file to temporary directory.'; +$string['uploadpicture_cannotprocessdir'] = 'Cannot process unzipped files.'; +$string['uploadpicture_cannotsave'] = 'Cannot save picture for course {$a}. Check original picture file.'; +$string['uploadpicture_cannotunzip'] = 'Cannot unzip pictures file.'; +$string['uploadpicture_invalidfilename'] = 'Picture file {$a} has invalid characters in its name. Skipping.'; +$string['uploadpicture_overwrite'] = 'Overwrite existing course pictures?'; +$string['uploadpicture_coursefield'] = 'Course attribute to use to match pictures:'; +$string['uploadpicture_coursenotfound'] = 'Course with a \'{$a->coursefield}\' value of \'{$a->coursevalue}\' does not exist. Skipping.'; +$string['uploadpicture_courseskipped'] = 'Skipping course {$a} (already has a picture).'; +$string['uploadpicture_courseupdated'] = 'Picture updated for course {$a}.'; +$string['uploadpictures'] = 'Upload course pictures'; +$string['uploadpictures_help'] = 'Course pictures can be uploaded as a zip file of image files. The image files should be named chosen-course-attribute.extension, for example course1234.jpg for a course with coursename course1234.'; +$string['uploadcourses'] = 'Upload courses'; +$string['uploadcourses_help'] = 'Courses may be uploaded (and optionally enrolled in courses) 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 coursename, password, firstname, lastname, email'; +$string['uploadcoursespreview'] = 'Upload courses preview'; +$string['uploadcoursesresult'] = 'Upload courses results'; +$string['courseupdated'] = 'Course updated'; +$string['courseuptodate'] = 'Course up-to-date'; +$string['coursedeleted'] = 'Course deleted'; +$string['courserenamed'] = 'Course renamed'; +$string['coursescreated'] = 'Courses created'; +$string['coursesdeleted'] = 'Courses deleted'; +$string['coursesrenamed'] = 'Courses renamed'; +$string['coursesskipped'] = 'Courses skipped'; +$string['coursesupdated'] = 'Courses updated'; +$string['coursenotadded'] = 'Course not added - already exists'; +$string['coursenotaddederror'] = 'Course not added - error'; +$string['coursenotdeletederror'] = 'Course not deleted - error'; +$string['coursenotdeletedmissing'] = 'Course not deleted - missing'; +$string['coursenotdeletedoff'] = 'Course not deleted - delete off'; +$string['coursenotdeletedadmin'] = 'Course not deleted - no admin access'; +$string['coursenotupdatederror'] = 'Course not updated - error'; +$string['coursenotupdatednotexists'] = 'Course not updated - does not exist'; +$string['coursenotupdatedadmin'] = 'Course not updated - no admin'; +$string['coursenotrenamedexists'] = 'Course not renamed - target exists'; +$string['coursenotrenamedmissing'] = 'Course not renamed - source missing'; +$string['coursenotrenamedoff'] = 'Course not renamed - renaming off'; +$string['coursenotrenamedadmin'] = 'Course not renamed - no admin'; +$string['invalidvalue'] = 'Invalid value for field {$a}'; +$string['shortnamecourse'] = 'Shortname'; +$string['shortnamecourse_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. See help for details and examples.'; +$string['idnumbernotunique'] = 'idnumber is not unique'; +$string['ccbulk'] = 'Select for bulk operations'; +$string['ccbulkall'] = 'All courses'; +$string['ccbulknew'] = 'New courses'; +$string['ccbulkupdated'] = 'Updated courses'; +$string['cccsvline'] = 'CSV line'; +$string['cclegacy1role'] = '(Original Student) typeN=1'; +$string['cclegacy2role'] = '(Original Teacher) typeN=2'; +$string['cclegacy3role'] = '(Original Non-editing teacher) typeN=3'; +$string['ccnoemailduplicates'] = 'Prevent email address duplicates'; +$string['ccoptype'] = 'Upload type'; +$string['ccoptype_addinc'] = 'Add all, append number to shortnames if needed'; +$string['ccoptype_addnew'] = 'Add new only, skip existing courses'; +$string['ccoptype_addupdate'] = 'Add new and update existing courses'; +$string['ccoptype_update'] = 'Update existing courses only'; +$string['ccpasswordcron'] = 'Generated in cron'; +$string['ccpasswordnew'] = 'New course password'; +$string['ccpasswordold'] = 'Existing course password'; +$string['reset'] = 'Reset course after upload'; +$string['ccstandardshortnames'] = 'Standardise shortnames'; +$string['ccupdateall'] = 'Override with file and defaults'; +$string['ccupdatefromfile'] = 'Override with file'; +$string['ccupdatemissing'] = 'Fill in missing from file and defaults'; +$string['ccupdatetype'] = 'Existing course details'; +$string['ccshortnametemplate'] = 'Shortname template'; +$string['ccfullnametemplate'] = 'Fullname template'; +$string['ccidnumbertemplate'] = 'Idnumber template'; +$string['missingtemplate'] = 'Template not found'; +$string['missing'] = 'missing'; +$string['incorrectformat'] = 'Invalid format specified'; +$string['incorrecttemplatefile'] = 'Template file not found'; +$string['invalidenrolmethod'] = 'Invalid enrolment method'; +$string['invalidcsvfile'] = 'Invalid input CSV file'; +$string['invalidaction'] = 'Invalid action selected'; +$string['invalidmode'] = 'Invalid mode selected'; +$string['invalidtemplatefile'] = 'Invalid template file'; +$string['invalidencoding'] = 'Invalid encoding'; +$string['invalidcategory'] = 'Invalid category'; +$string['coursetemplatename'] = 'Course template shortname'; +$string['coursetemplatename_help'] = 'Select an existing course shortname to use as a template for the creation of all courses.'; +$string['templatefile'] = 'Template backup file'; +$string['invalidbackupfile'] = 'Invalid backup file'; + diff --git a/admin/tool/uploadcourse/locallib.php b/admin/tool/uploadcourse/locallib.php new file mode 100644 index 00000000000..79ba08f9791 --- /dev/null +++ b/admin/tool/uploadcourse/locallib.php @@ -0,0 +1,1328 @@ +. + +/** + * Bulk course registration functions + * + * @package tool_uploadcourse + * @subpackage uploadcourse + * @copyright 2004 onwards Martin Dougiamas (http://dougiamas.com) + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +define('CC_COURSE_ADDNEW', 0); +define('CC_COURSE_ADDINC', 1); +define('CC_COURSE_ADD_UPDATE', 2); +define('CC_COURSE_UPDATE', 3); + +define('CC_UPDATE_NOCHANGES', 0); +define('CC_UPDATE_FILEOVERRIDE', 1); +define('CC_UPDATE_ALLOVERRIDE', 2); +define('CC_UPDATE_MISSING', 3); + +define('CC_BULK_NEW', 1); +define('CC_BULK_UPDATED', 2); +define('CC_BULK_ALL', 3); + +define('CC_PWRESET_NONE', 0); +define('CC_PWRESET_WEAK', 1); +define('CC_PWRESET_ALL', 2); +define('CC_BULK_NONE', 0); + + +/** + * Return the list of stad fields the course upload processes + */ +function tool_uploadcourse_std_fields() { + // Array of all valid fields for validation. + return $std_fields = array('fullname', 'shortname', 'category', 'idnumber', 'summary', + 'format', 'showgrades', 'newsitems', 'teacher', 'editingteacher', 'student', 'modinfo', + 'manager', 'coursecreator', 'guest', 'user', 'startdate', 'numsections', + 'maxbytes', 'visible', 'groupmode', 'restrictmodules', + 'enablecompletion', 'completionstartonenrol', 'completionnotify', + 'hiddensections', 'groupmodeforce', 'lang', 'theme', + 'cost', 'showreports', 'notifystudents', 'expirynotify', 'expirythreshold', 'requested', + 'deleted', // 1 means delete course. + 'oldshortname', // For renaming. + 'backupfile', // For restoring a course template after creation. + 'templatename', // Course to use as a template - the shortname. + 'reset', + // There are also the enrolment fields but these are free form as they vary on enrolment type + // eg: enrolmethod_1,status_1,enrolmethod_2,name_2,password_2,customtext1_2 + // manual, 1, self, self1, letmein, this is a custom message 1. + ); +} + +/** + * process the upload + * + * @param object $formdata - object of the form data + * @param object $cir - object of the CSV importer + * @param array $filecolumns - file column definitions + * @param string $restorefile - file to restore from + * @param boolean $plain - plain text output + */ +function tool_uploadcourse_process_course_upload($formdata, $cir, $filecolumns, $restorefile=null, $plain=false) { + global $CFG, $USER, $OUTPUT, $SESSION, $DB; + + $std_fields = tool_uploadcourse_std_fields(); + + @set_time_limit(60*60); // 1 hour should be enough. + raise_memory_limit(MEMORY_HUGE); + + require_capability('moodle/course:create', get_context_instance(CONTEXT_SYSTEM)); + require_capability('moodle/course:update', get_context_instance(CONTEXT_SYSTEM)); + require_capability('moodle/course:delete', get_context_instance(CONTEXT_SYSTEM)); + + $strcourserenamed = get_string('courserenamed', 'tool_uploadcourse'); + $strcoursenotrenamedexists = get_string('coursenotrenamedexists', 'tool_uploadcourse'); + $strcoursenotrenamedmissing = get_string('coursenotrenamedmissing', 'tool_uploadcourse'); + $strcoursenotrenamedoff = get_string('coursenotrenamedoff', 'tool_uploadcourse'); + + $strcourseupdated = get_string('courseupdated', 'tool_uploadcourse'); + $strcoursenotupdated = get_string('coursenotupdatederror', 'tool_uploadcourse'); + $strcoursenotupdatednotexists = get_string('coursenotupdatednotexists', 'tool_uploadcourse'); + + $strcourseuptodate = get_string('courseuptodate', 'tool_uploadcourse'); + + $strcourseadded = get_string('newcourse'); + $strcoursenotadded = get_string('coursenotadded', 'tool_uploadcourse'); + $strcoursenotaddederror = get_string('coursenotaddederror', 'tool_uploadcourse'); + + $strcoursedeleted = get_string('coursedeleted', 'tool_uploadcourse'); + $strcoursenotdeletederror = get_string('coursenotdeletederror', 'tool_uploadcourse'); + $strcoursenotdeletedmissing = get_string('coursenotdeletedmissing', 'tool_uploadcourse'); + $strcoursenotdeletedoff = get_string('coursenotdeletedoff', 'tool_uploadcourse'); + $errorstr = get_string('error'); + + $returnurl = new moodle_url('/admin/tool/uploadcourse/index.php'); + $bulknurl = new moodle_url('/admin/tool/uploadcourse/index.php'); + + $today = time(); + $today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0); + + $optype = $formdata->cctype; + + $updatetype = isset($formdata->ccupdatetype) ? $formdata->ccupdatetype : 0; + $allowrenames = (!empty($formdata->ccallowrenames) and $optype != CC_COURSE_ADDNEW and $optype != CC_COURSE_ADDINC); + $allowdeletes = (!empty($formdata->ccallowdeletes) and $optype != CC_COURSE_ADDNEW and $optype != CC_COURSE_ADDINC); + $bulk = isset($formdata->ccbulk) ? $formdata->ccbulk : 0; + $standardshortnames = $formdata->ccstandardshortnames; + + // Check for the template. + $templatepathname = null; + if (!empty($formdata->templatename) && $formdata->templatename != 'none') { + $template = $DB->get_record('course', array('shortname' => $formdata->templatename)); + + // Backup the course template. + $bc = new backup_controller(backup::TYPE_1COURSE, $template->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); + $backupid = $bc->get_backupid(); + $backupbasepath = $bc->get_plan()->get_basepath(); + $bc->execute_plan(); + $bc->destroy(); + $packer = get_file_packer('application/zip'); + // Check if tmp dir exists. + $tmpdir = $CFG->tempdir . '/backup'; + if (!check_dir_exists($tmpdir, true, true)) { + throw new restore_controller_exception('cannot_create_backup_temp_dir'); + } + $filename = restore_controller::get_tempdir_name(SITEID, $USER->id); + $templatepathname = $tmpdir . '/' . $filename; + // Get the list of files in directory. + $filestemp = get_directory_list($backupbasepath, '', false, true, true); + $files = array(); + foreach ($filestemp as $file) { + // Add zip paths and fs paths to all them. + $files[$file] = $backupbasepath . '/' . $file; + } + $zippacker = get_file_packer('application/zip'); + $zippacker->archive_to_pathname($files, $templatepathname); + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + } + + // Check the uploaded backup file. + if (!empty($formdata->restorefile)) { + // Check if tmp dir exists. + if ($restorefile) { + $filepath = restore_controller::get_tempdir_name(SITEID, $USER->id); + $packer = get_file_packer('application/zip'); + $restorepathname = "$CFG->tempdir/backup/$filepath/"; + $result = $packer->extract_to_pathname($restorefile, $restorepathname); + // If not a backup zip file. + if (!$result) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($restorepathname); + fulldelete($restorefile); + } + throw new moodle_exception('invalidbackupfile', 'tool_uploadcourse'); + } + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($restorepathname); + } + } else { + $restorefile = null; + } + } + + // Verification moved to two places: after upload and into form2. + $coursesnew = 0; + $coursesupdated = 0; + $coursesuptodate = 0; // Not printed yet anywhere. + $courseserrors = 0; + $deletes = 0; + $deleteerrors = 0; + $renames = 0; + $renameerrors = 0; + $coursesskipped = 0; + $enrolmentplugins = enrol_get_plugins(false); + $courseformats = array_keys(get_plugin_list('format')); + + // Clear bulk selection. + if ($bulk) { + $SESSION->bulk_courses = array(); + } + + // Init csv import helper. + $cir->init(); + $linenum = 1; // Column header is first line. + + // Init upload progress tracker. + $upt = new tool_uploadcourse_progress_tracker($plain); + $upt->start(); // Start table. + + while ($line = $cir->next()) { + $upt->flush(); + $linenum++; + + $upt->track('line', $linenum); + + $course = new stdClass(); + + // Add fields to course object. + foreach ($line as $keynum => $value) { + if (!isset($filecolumns[$keynum])) { + // This should not happen. + continue; + } + $key = $filecolumns[$keynum]; + $course->$key = $value; + + if (in_array($key, $upt->columns)) { + // Default value in progress tracking table, can be changed later. + $upt->track($key, s($value), 'normal'); + } + } + // Validate category. + $error = false; + if (!empty($course->category)) { + $split = preg_split('|(?category); + $categories = array(); + foreach ($split as $cat) { + $cat = preg_replace('/\\\/', '', $cat); + $categories[]= $cat; + } + $course->category = 0; + foreach ($categories as $cat) { + // Does the category exist - does the category hierachy make sense. + $category = $DB->get_record('course_categories', array('name'=>trim($cat), 'parent' => $course->category)); + if (empty($category)) { + $upt->track('status', get_string('invalidvalue', 'tool_uploadcourse', 'category').' ('.$cat.' '.get_string('missing', 'tool_uploadcourse').')', 'error'); + $upt->track('category', $errorstr, 'error'); + $error = true; + break; + } + $course->category = $category->id; + } + } + // Check for category errors. + if ($error) { + $courseserrors++; + continue; + } + + if (!isset($course->shortname)) { + // Prevent warnings bellow. + $course->shortname = ''; + } + if (!empty($course->startdate) && $course->startdate != 0) { + $course->startdate = strtotime($course->startdate); + } + if (!empty($course->enrolstartdate) && $course->enrolstartdate != 0) { + $course->enrolstartdate = strtotime($course->enrolstartdate); + } + + // Check for enrolment methods. + $line_fields = (array) $course; + $enrolmethods = array(); + $enrolments = array(); + $error = false; + foreach ($line_fields as $k => $v) { + if (preg_match('/^(\w+)\_(\d+)$/', $k, $matches)) { + if (!isset($enrolments[$matches[2]])) { + $enrolments[$matches[2]] = array(); + } + if ($matches[1] == 'enrolmethod') { + if (!isset($enrolmentplugins[$v])) { + $upt->track('status', get_string('invalidenrolmethod', 'tool_uploadcourse', 'category'), 'error'); + $upt->track($k, $errorstr, 'error'); + $error = true; + } + $enrolmethods[$v] = $matches[2]; + } + $enrolments[$matches[2]][$matches[1]] = $v; + } + } + if ($error) { + continue; + } + foreach ($enrolmethods as $k => $v) { + $enrolmethods[$k] = $enrolments[$v]; + } + + // Roles. + $roles = get_all_roles(); + foreach ($roles as $role) { + if (isset($course->{$role->shortname})) { + if (in_array($role->shortname, array('teacher', 'editingteacher', 'student', + 'manager', 'coursecreator', 'guest', 'user'))) { + $course->{'role_'.$role->id} = $course->{$role->shortname}; + } + } + } + + // What type of operation is this ? + if ($optype == CC_COURSE_ADDNEW or $optype == CC_COURSE_ADDINC) { + // Course creation is a special case - the shortname may be constructed from templates using firstname and lastname + // better never try this in mixed update types. + $error = false; + if (!isset($course->fullname) or $course->fullname === '') { + $upt->track('status', get_string('missingfield', 'error', 'fullname'), 'error'); + $upt->track('fullname', $errorstr, 'error'); + $error = true; + } + if (!isset($course->summary) or $course->summary === '') { + $upt->track('status', get_string('missingfield', 'error', 'summary'), 'error'); + $upt->track('summary', $errorstr, 'error'); + $error = true; + } + if ($error) { + $courseserrors++; + continue; + } + // We require shortname too - we might use template for it though. + if (empty($course->shortname) and !empty($formdata->ccshortname)) { + $course->shortname = tool_uploadcourse_process_template($formdata->ccshortname, $course); + $upt->track('shortname', s($course->shortname)); + } + } + + // Normalize shortname. + $originalshortname = $course->shortname; + if ($standardshortnames) { + $course->shortname = clean_param($course->shortname, PARAM_MULTILANG); + } + + // Make sure we really have shortname. + if (empty($course->shortname)) { + $upt->track('status', get_string('missingfield', 'error', 'shortname'), 'error'); + $upt->track('shortname', $errorstr, 'error'); + $courseserrors++; + continue; + } + + if ($existingcourse = $DB->get_record('course', array('shortname' => $course->shortname))) { + $upt->track('id', $existingcourse->id, 'normal', false); + } + + // Find out in shortname incrementing required. + if ($existingcourse and $optype == CC_COURSE_ADDINC) { + $course->shortname = tool_uploadcourse_increment_shortname($course->shortname); + if (!empty($course->idnumber)) { + $oldidnumber = $course->idnumber; + $course->idnumber = tool_uploadcourse_increment_idnumber($course->idnumber); + if ($course->idnumber !== $oldidnumber) { + $upt->track('idnumber', s($oldidnumber).'-->'.s($course->idnumber), 'info'); + } + } + $existingcourse = false; + } + + // Check duplicate idnumber. + if (!$existingcourse and !empty($course->idnumber)) { + if ($DB->record_exists('course', array('idnumber' => $course->idnumber))) { + $upt->track('status', get_string('idnumbernotunique', 'tool_uploadcourse'), 'error'); + $upt->track('idnumber', $errorstr, 'error'); + $error = true; + } + } + + // Notify about nay shortname changes. + if ($originalshortname !== $course->shortname) { + $upt->track('shortname', '', 'normal', false); // Clear previous. + $upt->track('shortname', s($originalshortname).'-->'.s($course->shortname), 'info'); + } else { + $upt->track('shortname', s($course->shortname), 'normal', false); + } + + // Add default values for remaining fields. + $formdefaults = array(); + foreach ($std_fields as $field) { + if (isset($course->$field)) { + continue; + } + // All validation moved to form2. + if (isset($formdata->$field)) { + $course->$field = $formdata->$field; + $formdefaults[$field] = true; + if (in_array($field, $upt->columns)) { + $upt->track($field, s($course->$field), 'normal'); + } + } else { + // Process templates. + if (isset($formdata->{"cc".$field}) && !empty($formdata->{"cc".$field}) && empty($course->$field)) { + $course->$field = tool_uploadcourse_process_template($formdata->{"cc".$field}, $course); + } + } + } + // Do we run the reset ? + $resetcourse = false; + if ($course->reset) { + $resetcourse = true; + unset($course->reset); + } + + // Proof visible flag. + $course->visible = (int) $course->visible; + + if (empty($course->category)) { + $course->category = $formdata->cccategory; + } + + // Delete course. + if (!empty($course->deleted)) { + if (!$allowdeletes) { + $coursesskipped++; + $upt->track('status', $strcoursenotdeletedoff, 'warning'); + continue; + } + if ($existingcourse) { + if (delete_course($existingcourse->id, false)) { + $upt->track('status', $strcoursedeleted); + $deletes++; + } else { + $upt->track('status', $strcoursenotdeletederror, 'error'); + $deleteerrors++; + } + } else { + $upt->track('status', $strcoursenotdeletedmissing, 'error'); + $deleteerrors++; + } + continue; + } + // We do not need the deleted flag anymore. + unset($course->deleted); + + // Renaming requested? + if (!empty($course->oldshortname) ) { + if (!$allowrenames) { + $coursesskipped++; + $upt->track('status', $strcoursenotrenamedoff, 'warning'); + continue; + } + + if ($existingcourse) { + $upt->track('status', $strcoursenotrenamedexists, 'error'); + $renameerrors++; + continue; + } + + if ($standardshortnames) { + $oldshortname = clean_param($course->oldshortname, PARAM_MULTILANG); + } else { + $oldshortname = $course->oldshortname; + } + + // No guessing when looking for old shortname, it must be exact match. + if ($oldcourse = $DB->get_record('course', array('shortname'=>$oldshortname))) { + $upt->track('id', $oldcourse->id, 'normal', false); + $DB->set_field('course', 'shortname', $course->shortname, array('id'=>$oldcourse->id)); + $upt->track('shortname', '', 'normal', false); // Clear previous. + $upt->track('shortname', s($oldshortname).'-->'.s($course->shortname), 'info'); + $upt->track('status', $strcourserenamed); + $renames++; + } else { + $upt->track('status', $strcoursenotrenamedmissing, 'error'); + $renameerrors++; + continue; + } + $existingcourse = $oldcourse; + $existingcourse->shortname = $course->shortname; + } + + // Can we process with update or insert? + $skip = false; + switch ($optype) { + case CC_COURSE_ADDNEW: + if ($existingcourse) { + $coursesskipped++; + $upt->track('status', $strcoursenotadded, 'warning'); + $skip = true; + } + break; + + case CC_COURSE_ADDINC: + if ($existingcourse) { + // This should not happen! + $upt->track('status', $strcoursenotaddederror, 'error'); + $courseserrors++; + $skip = true; + } + break; + + case CC_COURSE_ADD_UPDATE: + break; + + case CC_COURSE_UPDATE: + if (!$existingcourse) { + $coursesskipped++; + $upt->track('status', $strcoursenotupdatednotexists, 'warning'); + $skip = true; + } + break; + + default: + // Unknown type. + $skip = true; + } + + // Check for the backup file as template. + $backupfile = null; + if (!empty($course->backupfile)) { + if (!is_readable($course->backupfile) || !preg_match('/(\.mbz|\.zip)$/i', $course->backupfile)) { + $upt->track('status', get_string('incorrecttemplatefile', 'tool_uploadcourse'), 'error'); + $courseserrors++; + $skip = true; + } else { + $backupfile = $course->backupfile; + } + } + + if ($skip) { + continue; + } + + // check the format + if (!empty($course->format) && !in_array($course->format, $courseformats)) { + $upt->track('status', get_string('incorrectformat', 'tool_uploadcourse'), 'error'); + $courseserrors++; + continue; + } + + $templatename = null; + if ($existingcourse) { + $course->id = $existingcourse->id; + + $upt->track('shortname', html_writer::link(new moodle_url('/course/view.php', + array('id '=> $existingcourse->id)), + s($existingcourse->shortname)), + 'normal', false); + + $existingcourse->timemodified = time(); + // Do NOT mess with timecreated or firstaccess here! + $doupdate = false; + + if ($updatetype != CC_UPDATE_NOCHANGES) { + foreach ($std_fields as $column) { + if ($column === 'shortname') { + // These can not be changed here. + continue; + } + if (!property_exists($course, $column) or !property_exists($existingcourse, $column)) { + // This should never happen. + continue; + } + // In the case $updatetype == CC_UPDATE_ALLOVERRIDE we override everything. + if ($updatetype == CC_UPDATE_MISSING) { + if (!is_null($existingcourse->$column) and $existingcourse->$column !== '') { + continue; + } + + } else if ($updatetype == CC_UPDATE_FILEOVERRIDE) { + if (!empty($formdefaults[$column])) { + // Do not override with form defaults. + continue; + } + } + if ($existingcourse->$column !== $course->$column) { + if (in_array($column, $upt->columns)) { + $upt->track($column, s($existingcourse->$column).'-->'.s($course->$column), 'info', false); + } + $existingcourse->$column = $course->$column; + $doupdate = true; + } + } + } + + if ($doupdate) { + // We want only courses that were really updated. + update_course($existingcourse); + $upt->track('status', $strcourseupdated); + $coursesupdated++; + + events_trigger('course_updated', $existingcourse); + + if ($bulk == CC_BULK_UPDATED or $bulk == CC_BULK_ALL) { + if (!in_array($course->id, $SESSION->bulk_courses)) { + $SESSION->bulk_courses[] = $course->id; + } + } + + } else { + // No course information changed. + $upt->track('status', $strcourseuptodate); + $coursesuptodate++; + + if ($bulk == CC_BULK_ALL) { + if (!in_array($course->id, $SESSION->bulk_courses)) { + $SESSION->bulk_courses[] = $course->id; + } + } + } + + } else { + // Save the new course to the database. + $course->timemodified = time(); + $course->timecreated = time(); + + // Create course - insert_record ignores any extra properties. + if (isset($course->templatename) && $course->templatename != 'none') { + $templatename = $course->templatename; + } else { + $templatename = null; + } + try { + $course = create_course($course); + } catch (moodle_exception $e) { + $upt->track('status', $e->getMessage(), 'error'); + $courseserrors++; + $skip = true; + continue; + } + $upt->track('shortname', html_writer::link(new moodle_url('/course/view.php', + array('id' => $course->id)), + s($course->shortname)), + 'normal', false); + + $upt->track('status', $strcourseadded); + $upt->track('id', $course->id, 'normal', false); + $coursesnew++; + + // Make sure course context exists. + get_context_instance(CONTEXT_COURSE, $course->id); + + events_trigger('course_created', $course); + + if ($bulk == CC_BULK_NEW or $bulk == CC_BULK_ALL) { + if (!in_array($course->id, $SESSION->bulk_courses)) { + $SESSION->bulk_courses[] = $course->id; + } + } + } + + // After creation/update, do we need to copy from template nominated in the CSV file? + if (!empty($templatename)) { + $coursetemplate = $DB->get_record('course', array('shortname' => $templatename)); + if (empty($coursetemplate)) { + $upt->track('status', get_string('incorrecttemplatefile', 'tool_uploadcourse'), 'error'); + $courseserrors++; + continue; + } + + // Backup the course template. + $bc = new backup_controller(backup::TYPE_1COURSE, $coursetemplate->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); + $backupid = $bc->get_backupid(); + $backupbasepath = $bc->get_plan()->get_basepath(); + $bc->execute_plan(); + $bc->destroy(); + $packer = get_file_packer('application/zip'); + // Check if tmp dir exists. + $tmpdir = $CFG->tempdir . '/backup'; + if (!check_dir_exists($tmpdir, true, true)) { + throw new restore_controller_exception('cannot_create_backup_temp_dir'); + } + $filename = restore_controller::get_tempdir_name(SITEID, $USER->id); + $temppathname = $tmpdir . '/' . $filename; + // Get the list of files in directory. + $filestemp = get_directory_list($backupbasepath, '', false, true, true); + $files = array(); + foreach ($filestemp as $file) { + // Add zip paths and fs paths to all them. + $files[$file] = $backupbasepath . '/' . $file; + } + $zippacker = get_file_packer('application/zip'); + $zippacker->archive_to_pathname($files, $temppathname); + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + + // Check if tmp dir exists. + $tmpdir = $CFG->tempdir . '/backup'; + $filename = restore_controller::get_tempdir_name($course->id, $USER->id); + $pathname = $tmpdir . '/' . $filename; + $packer = get_file_packer('application/zip'); + $packer->extract_to_pathname($temppathname, $pathname); + + // Restore the backup immediately. + $rc = new restore_controller($filename, $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()) { + $precheckresults = $rc->get_precheck_results(); + if (is_array($precheckresults) && !empty($precheckresults['errors'])) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($pathname); + } + echo $output->precheck_notices($precheckresults); + if (!$plain) { + echo $output->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); + echo $output->footer(); + } + die(); + } + } + $rc->execute_plan(); + $rc->destroy(); + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($pathname); + } + } + + // After creation/update, do we need to copy from template? + if (!empty($templatepathname)) { + // Check if tmp dir exists. + $tmpdir = $CFG->tempdir . '/backup'; + $filename = restore_controller::get_tempdir_name($course->id, $USER->id); + $pathname = $tmpdir . '/' . $filename; + $packer = get_file_packer('application/zip'); + $packer->extract_to_pathname($templatepathname, $pathname); + + // Restore the backup immediately. + $rc = new restore_controller($filename, $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()) { + $precheckresults = $rc->get_precheck_results(); + if (is_array($precheckresults) && !empty($precheckresults['errors'])) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($pathname); + } + echo $output->precheck_notices($precheckresults); + if (!$plain) { + echo $output->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); + echo $output->footer(); + } + die(); + } + } + $rc->execute_plan(); + $rc->destroy(); + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($pathname); + } + } + + // After creation/update, do we need to copy from template backup file? + if (!empty($restorefile)) { + // Check if tmp dir exists. + $tmpdir = $CFG->tempdir . '/backup'; + $filename = restore_controller::get_tempdir_name($course->id, $USER->id); + $pathname = $tmpdir . '/' . $filename; + $packer = get_file_packer('application/zip'); + $packer->extract_to_pathname($restorefile, $pathname); + + // Restore the backup immediately. + $rc = new restore_controller($filename, $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()) { + $precheckresults = $rc->get_precheck_results(); + if (is_array($precheckresults) && !empty($precheckresults['errors'])) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($pathname); + } + echo $output->precheck_notices($precheckresults); + if (!$plain) { + echo $output->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); + echo $output->footer(); + } + die(); + } + } + $rc->execute_plan(); + $rc->destroy(); + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($pathname); + } + } + + // After creation/update, do we need to import a Moodle backup? + if (!empty($backupfile)) { + // Check if tmp dir exists. + $tmpdir = $CFG->tempdir . '/backup'; + if (!check_dir_exists($tmpdir, true, true)) { + throw new restore_controller_exception('cannot_create_backup_temp_dir'); + } + $filename = restore_controller::get_tempdir_name($course->id, $USER->id); + $pathname = $tmpdir . '/' . $filename; + $packer = get_file_packer('application/zip'); + $packer->extract_to_pathname($backupfile, $pathname); + + // Restore the backup immediately. + $rc = new restore_controller($filename, $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()) { + $precheckresults = $rc->get_precheck_results(); + if (is_array($precheckresults) && !empty($precheckresults['errors'])) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($pathname); + } + echo $output->precheck_notices($precheckresults); + if (!$plain) { + echo $output->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); + echo $output->footer(); + } + die(); + } + } + $rc->execute_plan(); + $rc->destroy(); + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($pathname); + } + } + + // Handle enrolment methods. + $enrol_updated = false; + $instances = enrol_get_instances($course->id, false); + foreach ($enrolments as $method) { + if (isset($method['delete']) && $method['delete']) { + // Remove the enrolment method. + foreach ($instances as $instance) { + if ($instance->enrol == $method['enrolmethod']) { + $plugin = $enrolmentplugins[$instance->enrol]; + $plugin->delete_instance($instance); + $enrol_updated = true; + break; + } + } + } else if (isset($method['disable']) && $method['disable']) { + // Disable the enrolment. + foreach ($instances as $instance) { + if ($instance->enrol == $method['enrolmethod']) { + $plugin = $enrolmentplugins[$instance->enrol]; + $plugin->update_status($instance, ENROL_INSTANCE_DISABLED); + $enrol_updated = true; + break; + } + } + } else { + // We should have this enrolment method. + $instance = null; + foreach ($instances as $i) { + if ($i->enrol == $method['enrolmethod']) { + $instance = $i; + break; + } + } + $plugin = null; + if (empty($instance)) { + $plugin = $enrolmentplugins[$method['enrolmethod']]; + $instance = new stdClass(); + $instance->id = $plugin->add_default_instance($course); + $instance->roleid = $plugin->get_config('roleid'); + } 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. + if (isset($method['role'])) { + $context = context_course::instance($course->id); + $roles = get_default_enrol_roles($context, $plugin->get_config('roleid')); + if (!empty($roles)) { + $roles = array_flip($roles); + } + if (isset($roles[$method['role']])) { + $instance->roleid = $roles[$method['role']]; + } + } + $instance->status = ENROL_INSTANCE_ENABLED; + $instance->timemodified = time(); + $DB->update_record('enrol', $instance); + $enrol_updated = true; + } + } + + // Do the course reset. + if ($resetcourse) { + $resetdata = new stdClass(); + $resetdata->reset_start_date = time(); + $resetdata->id = $course->id; + $resetdata->reset_events = true; + $resetdata->reset_logs = true; + $resetdata->reset_notes = true; + $resetdata->reset_comments = true; + $resetdata->reset_completion = true; + $resetdata->delete_blog_associations = true; + $roles = get_assignable_roles(context_course::instance($course->id)); + $roles[0] = get_string('noroles', 'role'); + $roles = array_reverse($roles, true); + $resetdata->reset_roles_local = true; + $resetdata->reset_gradebook_items = true; + $resetdata->reset_gradebook_grades = true; + $resetdata->reset_gradebook_items = true; + $resetdata->reset_groups_remove = true; + $resetdata->reset_groups_members = true; + $resetdata->reset_groupings_remove = true; + $resetdata->reset_groupings_members = true; + $resetdata->reset_groups_remove = true; + $resetdata->reset_groups_remove = true; + $resetdata->reset_start_date_old = $course->startdate; + $status = reset_course_userdata($resetdata); + } + + if ($enrol_updated) { + $coursesupdated++; + } + // Invalidate all enrol caches. + $context = context_course::instance($course->id); + $context->mark_dirty(); + } + + // Clean up backup files. + if (!empty($template)) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($backupbasepath); + } + } + if (!empty($restorefile)) { + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($restorefile); + } + } + + $upt->close(); // Close table. + + $cir->close(); + $cir->cleanup(true); + $systemcontext = context_system::instance(); + mark_context_dirty($systemcontext->path); + + if (!$plain) { + echo $OUTPUT->box_start('boxwidthnarrow boxaligncenter generalbox', 'uploadresults'); + echo '

'; + if ($optype != CC_COURSE_UPDATE) { + echo get_string('coursescreated', 'tool_uploadcourse').': '.$coursesnew.'
'; + } + if ($optype == CC_COURSE_UPDATE or $optype == CC_COURSE_ADD_UPDATE) { + echo get_string('coursesupdated', 'tool_uploadcourse').': '.$coursesupdated.'
'; + } + if ($allowdeletes) { + echo get_string('coursesdeleted', 'tool_uploadcourse').': '.$deletes.'
'; + echo get_string('deleteerrors', 'tool_uploadcourse').': '.$deleteerrors.'
'; + } + if ($allowrenames) { + echo get_string('coursesrenamed', 'tool_uploadcourse').': '.$renames.'
'; + echo get_string('renameerrors', 'tool_uploadcourse').': '.$renameerrors.'
'; + } + if ($coursesskipped) { + echo get_string('coursesskipped', 'tool_uploadcourse').': '.$coursesskipped.'
'; + } + echo get_string('errors', 'tool_uploadcourse').': '.$courseserrors.'

'; + } else { + if ($optype != CC_COURSE_UPDATE) { + echo get_string('coursescreated', 'tool_uploadcourse').': '.$coursesnew."\n"; + } + if ($optype == CC_COURSE_UPDATE or $optype == CC_COURSE_ADD_UPDATE) { + echo get_string('coursesupdated', 'tool_uploadcourse').': '.$coursesupdated."\n"; + } + if ($allowdeletes) { + echo get_string('coursesdeleted', 'tool_uploadcourse').': '.$deletes."\n"; + echo get_string('deleteerrors', 'tool_uploadcourse').': '.$deleteerrors."\n"; + } + if ($allowrenames) { + echo get_string('coursesrenamed', 'tool_uploadcourse').': '.$renames."\n"; + echo get_string('renameerrors', 'tool_uploadcourse').': '.$renameerrors."\n"; + } + if ($coursesskipped) { + echo get_string('coursesskipped', 'tool_uploadcourse').': '.$coursesskipped."\n"; + } + echo get_string('errors', 'tool_uploadcourse').': '.$courseserrors."\n"; + echo "The End.\n"; + } +} + + +/** + * Tracking of processed courses. + * + * This class prints course information into a html table. + * + * @copyright 2007 Petr Skoda {@link http://skodak.org} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_progress_tracker { + /** @var int $_row - row marker */ + private $_row; + /** @var array $columns - output columns */ + public $columns = array('status', 'line', 'id', 'fullname', 'shortname', 'category', 'idnumber', 'summary', 'deleted'); + /** @var boolean $_plain - output is text mode */ + private $_plain; + + /** + * Constructor + * + * @param boolean $type - is this plain text output + */ + public function __construct($type = false) { + $this->_plain = $type; + } + + + /** + * Print table header. + * @return void + */ + public function start() { + $ci = 0; + if ($this->_plain) { + echo "\n\t".get_string('status')."\t".get_string('cccsvline', 'tool_uploadcourse')."\tID\t". + get_string('fullname')."\t".get_string('shortname')."\t".get_string('category')."\t". + get_string('idnumber')."\t".get_string('summary')."\t".get_string('delete')."\n"; + } else { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + $this->_row = null; + } + + /** + * Flush previous line and start a new one. + * @return void + */ + public function flush() { + if (empty($this->_row) or empty($this->_row['line']['normal'])) { + // Nothing to print - each line has to have at least number. + $this->_row = array(); + foreach ($this->columns as $col) { + $this->_row[$col] = array('normal'=>'', 'info'=>'', 'warning'=>'', 'error'=>''); + } + return; + } + $ci = 0; + $ri = 1; + echo $this->_plain ? "" : ''; + foreach ($this->_row as $key => $field) { + foreach ($field as $type => $content) { + if ($field[$type] !== '') { + $field[$type] = $this->_plain ? $field[$type] : ''.$field[$type].''; + } else { + unset($field[$type]); + } + } + echo $this->_plain ? "\t" : ''; + } + echo $this->_plain ? "\n" : ''; + foreach ($this->columns as $col) { + $this->_row[$col] = array('normal'=>'', 'info'=>'', 'warning'=>'', 'error'=>''); + } + } + + /** + * Add tracking info + * @param string $col name of column + * @param string $msg message + * @param string $level 'normal', 'warning' or 'error' + * @param bool $merge true means add as new line, false means override all previous text of the same type + * @return void + */ + public function track($col, $msg, $level = 'normal', $merge = true) { + if (empty($this->_row)) { + $this->flush(); // Init arrays. + } + if (!in_array($col, $this->columns)) { + debugging('Incorrect column:'.$col); + return; + } + if ($merge) { + if ($this->_row[$col][$level] != '') { + $this->_row[$col][$level] .= $this->_plain ? '' : '
'; + } + $this->_row[$col][$level] .= $msg; + } else { + $this->_row[$col][$level] = $msg; + } + } + + /** + * Print the table end + * @return void + */ + public function close() { + $this->flush(); + echo $this->_plain ? "\n" : '
'.get_string('status').''.get_string('cccsvline', 'tool_uploadcourse').'ID'.get_string('fullname').''.get_string('shortname').''.get_string('category').''.get_string('idnumber').''.get_string('summary').''.get_string('delete').'
'; + if (!empty($field)) { + echo implode(($this->_plain ? "|" : '
'), $field); + } else { + echo $this->_plain ? '' : ' '; + } + echo $this->_plain ? '' : '
'; + } +} + +/** + * Validation callback function - verified the column line of csv file. + * Converts standard column names to lowercase. + * @param csv_import_reader $cir + * @param array $stdfields standard course fields + * @param moodle_url $returnurl return url in case of any error + * @return array list of fields + */ +function tool_uploadcourse_validate_course_upload_columns(csv_import_reader $cir, $stdfields, moodle_url $returnurl) { + $columns = $cir->get_columns(); + + if (empty($columns)) { + $cir->close(); + $cir->cleanup(); + print_error('cannotreadtmpfile', 'error', $returnurl); + } + if (count($columns) < 2) { + $cir->close(); + $cir->cleanup(); + print_error('csvfewcolumns', 'error', $returnurl); + } + + // Test columns. + $processed = array(); + foreach ($columns as $key => $unused) { + $field = $columns[$key]; + $lcfield = textlib::strtolower($field); + if (in_array($field, $stdfields) or in_array($lcfield, $stdfields)) { + // Standard fields are only lowercase. + $newfield = $lcfield; + + } else if (preg_match('/^\w+\_\d+$/', $lcfield)) { + // Special fields for enrolments. + $newfield = $lcfield; + + } else { + $cir->close(); + $cir->cleanup(); + print_error('invalidfieldname', 'error', $returnurl, $field); + } + if (in_array($newfield, $processed)) { + $cir->close(); + $cir->cleanup(); + print_error('duplicatefieldname', 'error', $returnurl, $newfield); + } + $processed[$key] = $newfield; + } + + return $processed; +} + +/** + * Increments shortname - increments trailing number or adds it if not present. + * Varifies that the new shortname does not exist yet + * @param string $shortname + * @return incremented shortname which does not exist yet + */ +function tool_uploadcourse_increment_shortname($shortname) { + global $DB, $CFG; + + if (!preg_match_all('/(.*?)([0-9]+)$/', $shortname, $matches)) { + $shortname = $shortname.'2'; + } else { + $shortname = $matches[1][0].($matches[2][0]+1); + } + + if ($DB->record_exists('course', array('shortname'=>$shortname))) { + return tool_uploadcourse_increment_shortname($shortname); + } else { + return $shortname; + } +} + +/** + * Increments idnumber - increments trailing number or adds it if not present. + * Varifies that the new idnumber does not exist yet + * @param string $idnumber + * @return incremented idnumber which does not exist yet + */ +function tool_uploadcourse_increment_idnumber($idnumber) { + global $DB, $CFG; + + if (!preg_match_all('/(.*?)([0-9]+)$/', $idnumber, $matches)) { + $idnumber = $idnumber.'2'; + } else { + $idnumber = $matches[1][0].($matches[2][0]+1); + } + + if ($DB->record_exists('course', array('idnumber'=>$idnumber))) { + return tool_uploadcourse_increment_idnumber($idnumber); + } else { + return $idnumber; + } +} + +/** + * Check if default field contains templates and apply them. + * @param string $template - potential tempalte string + * @param object $course - we need coursename, firstname and lastname + * @return object $template - course template + * @return string $result - field value + */ +function tool_uploadcourse_process_template($template, $course) { + if (is_array($template)) { + // Hack for for support of text editors with format. + $t = $template['text']; + } else { + $t = $template; + } + if (strpos($t, '%') === false) { + return $template; + } + + $shortname = isset($course->shortname) ? $course->shortname : ''; + $fullname = isset($course->fullname) ? $course->fullname : ''; + $idnumber = isset($course->idnumber) ? $course->idnumber : ''; + + $callback = partial('tool_uploadcourse_process_template_callback', $shortname, $fullname, $idnumber); + + $result = preg_replace_callback('/(?. + +/** + * Link to CSV course upload + * + * @package tool_uploadcourse + * @subpackage uploadcourse + * @copyright 2010 Petr Skoda {@link http://skodak.org} + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if (has_capability('moodle/restore:restorecourse', $systemcontext)) { + $ADMIN->add('courses', new admin_externalpage('tooluploadcourse', + get_string('uploadcourses', 'tool_uploadcourse'), + "$CFG->wwwroot/$CFG->admin/tool/uploadcourse/index.php", 'moodle/site:uploadusers')); +} diff --git a/admin/tool/uploadcourse/version.php b/admin/tool/uploadcourse/version.php new file mode 100644 index 00000000000..1139fb236ad --- /dev/null +++ b/admin/tool/uploadcourse/version.php @@ -0,0 +1,31 @@ +. + +/** + * Plugin version info + * + * @package tool_uploadcourse + * @subpackage uploadcourse + * @copyright 2011 Petr Skoda {@link http://skodak.org} + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die; + +$plugin->version = 2012112301; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2011091600; // Requires this Moodle version +$plugin->component = 'tool_uploadcourse'; // Full name of the plugin (used for diagnostics). + From 9a7cd639b418d0c00025f66f1f444f93f5e653b6 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Fri, 21 Jun 2013 17:11:29 +0800 Subject: [PATCH 02/21] MDL-13114 tool_uploadcourse: Classes for course importation --- admin/tool/uploadcourse/classes/course.php | 780 ++++++++++++++++++ admin/tool/uploadcourse/classes/helper.php | 440 ++++++++++ admin/tool/uploadcourse/classes/processor.php | 265 ++++++ 3 files changed, 1485 insertions(+) create mode 100644 admin/tool/uploadcourse/classes/course.php create mode 100644 admin/tool/uploadcourse/classes/helper.php create mode 100644 admin/tool/uploadcourse/classes/processor.php diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php new file mode 100644 index 00000000000..b282bc98884 --- /dev/null +++ b/admin/tool/uploadcourse/classes/course.php @@ -0,0 +1,780 @@ +. + +/** + * 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(); + +/** + * 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 OUTCOME_CREATE = 1; + + /** Outcome of the process: updating the course */ + const OUTCOME_UPDATE = 2; + + /** Outcome of the process: deleting the course */ + const OUTCOME_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; + + /** @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 the outcome, what to do with that course */ + protected $outcome; + + /** @var bool set to true once we have prepared the course */ + protected $prepared = false; + + /** @var array course import data. */ + protected $rawdata = array(); + + /** @var array restore directory. */ + protected $restoredata; + + /** @var string course shortname. */ + protected $shortname; + + /** @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', 'summary', '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; + $id = $DB->get_field_select('course', 'id', 'shortname = :shortname', array('shortname' => $this->shortname), MUST_EXIST); + return delete_course($id, false); + } + + /** + * Log an error + * + * @param string $message error message. + * @return void + */ + protected function error($message) { + $this->errors[] = $message; + } + + /** + * Return whether the course exists or not. + * + * @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; + } + + /** + * 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->options['backupfile'])) { + $backupfile = $this->options['backupfile']; + } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) { + $shortname = $this->options['templatecourse']; + } + + $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname); + if (empty($dir) && !empty($this->importoptions['restoredir'])) { + $dir = $this->importoptions['restoredir']; + } + + return $dir; + } + + /** + * 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() { + $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('invalid shortname'); + return false; + } + } + + $exists = $this->exists(); + + // Do we want to delete the course? + if ($this->options['delete']) { + if (!$exists) { + $this->error('cannot delete a course that does not exist'); + return false; + } else if (!$this->can_delete()) { + $this->error('no permission to delete course'); + return false; + } + + $this->outcome = self::OUTCOME_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('course exists and mode does not allow update'); + return false; + } + } else { + if (!$this->can_create()) { + $this->error('course does not exist and mode does not allow creation'); + 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(); + + // 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 = 0; + foreach (self::$mandatoryfields as $field) { + if ((!isset($coursedata[$field]) || $coursedata[$field] === '') && + (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) { + $this->error('missing value for mandatory field: ' . $field); + $errors++; + } + } + if ($errors > 0) { + return false; + } + } + + // Should the course be renamed? + if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) { + if (!$exists) { + $this->error('cannot rename a course that does not exist'); + return false; + } else if (!$this->can_rename()) { + $this->error('no permission to rename the course'); + return false; + } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) { + $this->error('the shortname to rename the course with is invalid'); + return false; + } else if ($this->exists($this->options['rename'])) { + $this->error('the shortname to rename the course with is already used'); + 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('cannot rename the course as its ID number would conflict with another one'); + return false; + } + $coursedata['shortname'] = $this->options['rename']; + } + + // Should we generate a shortname? + if (empty($this->shortname) && !is_numeric($this->shortname)) { + if (empty($this->importoptions['shortnametemplate'])) { + $this->error('missing shortname and could not find a shortname template'); + return false; + } else if (!$this->can_only_create()) { + $this->error('cannot generate a shortname from template when mode allow updating'); + return false; + } else { + $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata, $this->importoptions['shortnametemplate']); + if (is_null($newshortname)) { + $this->error('the generated shortname is invalid'); + return false; + } else if ($this->exists($newshortname)) { + if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { + $this->error('the generated shortname is used by another course'); + return false; + } + $exists = true; + } + $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) { + // TODO Log new shortname. + if (isset($coursedata['idnumber'])) { + $originalidn = $coursedata['idnumber']; + $coursedata['idnumber'] = $this->increment_idnumber($coursedata['idnumber']); + if ($originalidn != $coursedata['idnumber']) { + // TODO Log new idnumber. + } + } + } + } + + // 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('the course exists and mode does not allow for update'); + return false; + } + break; + case tool_uploadcourse_processor::MODE_UPDATE_ONLY: + if (!$exists) { + $this->error('the course does not exist and mode only allow for update'); + return false; + } + // No break! + case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE: + if ($exists) { + if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) { + $this->error('the update mode does not allow for any field to be updated'); + return false; + } + } + break; + default: + // O_o Huh?! This should really never happen here! + $this->error('unknown import mode!'); + return false; + } + + // Resolve the category. + // TODO Log resolve error. + $catid = tool_uploadcourse_helper::resolve_category($this->rawdata); + if (!empty($catid)) { + $coursedata['category'] = $catid; + } + + // 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->outcome = self::OUTCOME_UPDATE; + } else { + $coursedata = $this->get_final_create_data($coursedata); + $this->outcome = self::OUTCOME_CREATE; + } + + // Course start date. + if (!empty($coursedata['startdate'])) { + $coursedata['startdate'] = strtotime($coursedata['startdate']); + } + + // Add role renaming. + // TODO add warning about wrong roles. + foreach (tool_uploadcourse_helper::get_role_names($this->rawdata) as $rolekey => $rolename) { + $coursedata[$rolekey] = $rolename; + } + + // Validation. + if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) { + // Warning, wrong format! + $coursedata['format'] = 'weeks'; + } + + // 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 cannot + if ($this->importoptions['reset'] || $this->options['reset']) { + if ($this->outcome !== self::OUTCOME_UPDATE) { + $this->error('can only reset a course that has been updated'); + return false; + } else if (!$this->can_reset()) { + $this->error('no permission to reset the course'); + 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.'); + } + + if ($this->outcome === self::OUTCOME_DELETE) { + return $this->delete(); + } else if ($this->outcome === self::OUTCOME_CREATE) { + $course = create_course((object) $this->data); + } else if ($this->outcome === self::OUTCOME_UPDATE) { + $course = (object) $this->data; + update_course($course); + } 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(); + } else { + // Error! + // $rc->get_precheck_results()); + } + $rc->destroy(); + } + + // Proceed with enrolment data. + $this->process_enrolment_data($course); + + // Reset the course. + if ($this->importoptions['reset'] || $this->options['reset']) { + if ($this->outcome === self::OUTCOME_UPDATE && $this->can_reset()) { + $this->reset($course); + } + } + + // 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. + * + * @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); + } + +} diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php new file mode 100644 index 00000000000..87f77353852 --- /dev/null +++ b/admin/tool/uploadcourse/classes/helper.php @@ -0,0 +1,440 @@ +. + +/** + * 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(); + +/** + * Class containing a set of helpers. + */ +class tool_uploadcourse_helper { + + + /** @var array category idnumber cache. */ + static protected $categoryidnumbercache = array(); + + /** @var array category path cache. */ + static protected $categorypathcache = array(); + + /** @var array course formats cache. */ + static protected $courseformatscache; + + /** @var array enrolment plugins cache. */ + static protected $enrolpluginscache; + + /** @var string restore content path cache. */ + static protected $restorecontentcache; + + /** @var array roles cache. */ + static protected $rolescache; + + /** + * Remove the restore content from disk and cache. + * + * @return void + */ + public static function clean_restore_content() { + global $CFG; + if (!empty($CFG->keeptempdirectoriesonbackup)) { + foreach (self::$restorecontentcache as $backupid) { + fulldelete("$CFG->tempdir/backup/$backupid/"); + } + } + self::$restorecontentcache = array(); + } + + /** + * 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. + */ + public static function generate_shortname($data, $templateshortname) { + if (is_null($templateshortname)) { + return null; + } + if (strpos($templateshortname, '%') === false) { + return $templateshortname; + } + + $course = (object) $data; + $shortname = isset($course->shortname) ? $course->shortname : ''; + $fullname = isset($course->fullname) ? $course->fullname : ''; + $idnumber = isset($course->idnumber) ? $course->idnumber : ''; + + $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $shortname, $fullname, $idnumber); + $result = preg_replace_callback('/(? 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() { + if (empty(self::$enrolpluginscache)) { + self::$enrolpluginscache = enrol_get_plugins(false); + } + return self::$enrolpluginscache; + } + + /** + * 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. + * @return string|false false when the backup couldn't retrieved. + */ + public static function get_restore_content_dir($backupfile = null, $shortname = null) { + global $CFG, $DB, $USER; + + $cachekey = null; + if (!empty($backupfile)) { + $backupfile = realpath($backupfile); + $cachekey = '[path]:' . $backupfile; + } else if (!empty($shortname) || is_numeric($shortname)) { + $cachekey = '[sn]:' . $shortname; + } + + if (empty($cachekey)) { + return false; + } + + if (!isset(self::$restorecontentcache[$cachekey])) { + // Use false instead of null because it would consider that the cache + // key has not been set. + $backupid = false; + if (!empty($backupfile) && is_readable($backupfile)) { + // 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); + } 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(); + } + } + self::$restorecontentcache[$cachekey] = $backupid; + } + + return self::$restorecontentcache[$cachekey]; + } + + /** + * Return the role IDs. + * + * The result is cached for faster execution. + * + * @param bool $invalidate invalidate the cache. + * @return array + */ + public static function get_role_ids($invalidate = false) { + if (empty(self::$rolescache) || $invalidate) { + $roles = get_all_roles(); + foreach ($roles as $role) { + self::$rolescache[$role->shortname] = $role->id; + } + } + return self::$rolescache; + } + + /** + * Get the role renaming data from the passed data. + * + * @param array $data data to extract the names from. + * @return array where the key is the role_, the value is the new name. + */ + public static function get_role_names($data) { + $rolenames = array(); + $rolesids = self::get_role_ids(); + foreach ($data as $field => $value) { + + $matches = array(); + if (preg_match('/^role_(.+)?$/', $field, $matches)) { + if (!isset($rolesids[$matches[1]])) { + // Error! + continue; + } + $rolenames['role_' . $rolesids[$matches[1]]] = $value; + } + + } + + // 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. + * @return int category ID. + */ + public static function resolve_category($data) { + global $DB; + $catid = null; + + if (!empty($data['category'])) { + $category = coursecat::get($data['category'], IGNORE_MISSING); + if (!empty($category)) { + $catid = $category->id; + } + } + + if (empty($catid) && !empty($data['category_idnumber'])) { + $catid = self::resolve_category_by_idnumber($data['category_idnumber']); + } + + if (empty($catid) && !empty($data['category_path'])) { + $catid = self::resolve_category_by_path(explode(' / ', $data['category_path'])); + } + + 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; + if (!isset(self::$categoryidnumbercache[$idnumber])) { + $params = array('idnumber' => $idnumber); + $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING); + self::$categoryidnumbercache[$idnumber] = $id; + } + return self::$categoryidnumbercache[$idnumber]; + } + + /** + * 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; + $cachekey = serialize($path); + if (!isset(self::$categorypathcache[$cachekey])) { + $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 = false; + break; + } + $record = reset($records); + $id = $record->id; + $parent = $record->id; + } else { + // Not found. + $id = false; + break; + } + } + self::$categorypathcache[$cachekey] = $id; + } + return self::$categorypathcache[$cachekey]; + } + +} diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php new file mode 100644 index 00000000000..9fb6253b839 --- /dev/null +++ b/admin/tool/uploadcourse/classes/processor.php @@ -0,0 +1,265 @@ +. + +/** + * 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(); + +/** + * 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 int line number. */ + protected $linenb = 0; + + /** @var int line data. */ + protected $linedata = array(); + + /** @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; + } + + /** + * Execute the process. + * + * @return void + */ + public function execute() { + if ($this->processstarted) { + throw new coding_exception('Process has already been started'); + } + $this->processstarted = true; + + // Loop over the CSV lines. + while ($line = $this->cir->next()) { + $this->linenb++; + + $data = $this->parse_line($line); + $course = self::get_course($data); + if ($course->prepare()) { + $course->proceed(); + } else { + print_object($course->get_errors()); + } + } + + // $this->remove_restore_content(); + } + + /** + * Return a course import object. + * + * @param array $data data to import the course with. + * @return tool_uploadcourse_course + */ + public 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); + } + + /** + * Get the directory of the object to restore. + * + * @param string $default directory to use if none found. + * @return string subdirectory in $CFG->tempdir/backup/... + */ + public 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; + } + + /** + * 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; + } + + /** + * Delete the restore object. + * + * @return void + */ + protected function remove_restore_content() { + global $CFG; + tool_uploadcourse_helper::clean_restore_content(); + } +} From 509dd69554fcfa4cb24389aab9f969eccf01476c Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Fri, 21 Jun 2013 17:11:41 +0800 Subject: [PATCH 03/21] MDL-13114 tool_uploadcourse: Unit tests for introduced classes --- admin/tool/uploadcourse/tests/course_test.php | 771 ++++++++++++++++++ .../uploadcourse/tests/fixtures/backup.mbz | Bin 0 -> 18941 bytes admin/tool/uploadcourse/tests/helper_test.php | 353 ++++++++ .../uploadcourse/tests/processor_test.php | 170 ++++ 4 files changed, 1294 insertions(+) create mode 100644 admin/tool/uploadcourse/tests/course_test.php create mode 100644 admin/tool/uploadcourse/tests/fixtures/backup.mbz create mode 100644 admin/tool/uploadcourse/tests/helper_test.php create mode 100644 admin/tool/uploadcourse/tests/processor_test.php diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php new file mode 100644 index 00000000000..ebf5c586b01 --- /dev/null +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -0,0 +1,771 @@ +. + +/** + * 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; +require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/processor.php'); +require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/course.php'); +require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/helper.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->libdir . '/csvlib.class.php'); +require_once($CFG->libdir . '/coursecatlib.php'); + +/** + * Course test case. + */ +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_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->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->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()); + } + + public function test_update() { + global $DB; + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1')); + + // Try to add 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()); + + // Try to add 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()); + + // 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->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')); + + // 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()); + + // 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'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + + // 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()); + + // 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_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $importoptions = array('canrename' => true); + $data = array('shortname' => 'DoesNotExist', 'rename' => 'c1'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + + // 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))); + } + + 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' => $c1->shortname, 'reset' => '1'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $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->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()); + $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); + } +} diff --git a/admin/tool/uploadcourse/tests/fixtures/backup.mbz b/admin/tool/uploadcourse/tests/fixtures/backup.mbz new file mode 100644 index 0000000000000000000000000000000000000000..25da1743542a13ba022db01f093a64f8fe7121ab GIT binary patch literal 18941 zcmd^m2RN5+^tX|{_el26-h0o?h>$HIdrP+Lk*%!kBBMw~M#|nH6_JdrL=+*s&(}|V zeZNM2zw7_+y{`AYyPkUDIiKg8`<(mS=Q-!x>Pqlu@L^z(kzwS$2Blzt0SEdE7-+y{ zV&-DyX60gK?#vDkqkin8#E_3#*w{Nen>gJt;^gFlppye$k;hyTXaGE)3|!PF&{;T{ zTs3!Q^RTrke_YzueUVX#mAR*@w_J;BkXv0tp_thxE_+>Ppj z8+2``7iFkSo81n~Gl(VD635bu`z+C%z|u7<8y9n@e>Y*BT}-gdWdK)xWs`XEN?e^9 zR3NT0rP?=cSlyj_&@pbx5o;<8#nB+KOR5|!3=A{;$r`k^zv^mZ4$`1&oVfi94(w)k zFSX5C5o-6A*hf5i37=@x5@1tRlEgJ@$|l8W)u<|_wAkRtdaHjTc%;E-=Hz|9 z9la*fKl^cW8_7ExE3WptOe6E?DS0`g@I;bh5z`+hT|~%}ct~PboqHPB3~k$ji^j@$ zRmhtv*%J^y3{iLqw@dOxe`i1v=J6nWj;slA_> zSiGj^E%p9fxKl{EQ!cZ%xO1>SB?c6w=c1xR zMIWZVev9KEV99nFM(5tgP&<0Ubeh`yH2!x5Cd(>Cb*yrTR+I8BbhSa$?y5~}gw2fp ziaQv}Yy+0dXNcS~D=xly+g(Q>Deh0>AeJo5V7e39dQtke?P9D}-lZ7$`bL58i3P-G ztw$elUR`xUc`h(7VCcp+AW$T`i#Jg9$(3A?v`?tPnofIxTqKtE=2%?2lKUH2wbsZ8 zOT9#Et7&ftL&cuFSFClpu*loCV)A_DXeqk{>g1 z*Mc&C%9nokj2+owUr61n=IL4T0y-B4u9JL%mEARaC-ZCn#)$$xwUhmV3Vhi&6Xjp) z9_F9T!P#;3yg2YiHQ>SoE;D;u2OD!2D|@?t^8&>lc2;K9D7Btm);2{}*g}NxIWtta zU#~i1A41xDm8+Ap`Juh!pu5HfKlXpgJL7^m_au$U}qAHYEzytIQVRcoh?ZGw`0KfOzJwH8~il3rc;3g7m+^dt4zN(U$5$SWV# zZ!(7dI5W9e-Bzsj0)h2ii`?xJTb!iCJ7w!p-X7jQp`UHbLQy8o4YBEE%N`Z+@hK6o zg!8-oxLTQhJ)YWJE2UL6bx^tOk~X!Y+00W7_#{GF-7kiiTibRd!YX7o9@J?`KRk2E zkre9ak_8fTb2hKR;07bz={V;icG1TqFSk_*ey;l21bzMuAG*!Vmiw1NOXJKIrCk$E z%QO^h$C~W49Swach`dXfoAue#Cul>{M+(Jt+R8GOvl~AeSUHe4-JqY5PP2D9AA04k zK8&M(doh)rtrJyK5IvEkKF>ti?SxeRN46^c@QPTJ-Z5k!WR*2^7fs=jWPVcwhf9kPI9p!trxML@6R*4cvVNtRDeIZqNn$P{h z^)CtCeF-h^nS8T!^~_MvnwD@c_ z+wCSf(CgOEqkpgf=ZKDq5YjB#C}!iz?5E3Fge8R}}kL|QUVVlS$)(`2Fk>rc-rp9FCV2oV3uRA+0 zTZ>z89kUl^iQKvvj?bN(3~yX>B3yyTVU5dgZPfcpG37MErvPKvnwlFAC{nI9jF{}a zh(o&R(b^K*Aa}Qx?7klYu6LB8ItPc?cMWq}v>CZ`eMND#LA1UX*l(lGGJJ}eZG0zi z{k)%(_Ok2?tL0M9Yi+kUBbaFs)+JC^M&k;6uOZgn`9?*i>R0(rdn1*&z+d?J*HDp) znrh#scDs#tA+~LXJI4BFoP>Uajx%A6=)o!)6Jz>c5oJcMdNoz`v8_tVEbub}*O-IV z+3dCFVNC{-1BCHDaZ-M|j4obgPQ1gORND{URtzR75Rx?+k0xa3q#NRE-LL$5*%K30 z9-VK3ymyJtAYn!|daiUfSX)M3d-d*(dYiWHOM)q>vpWa&b?6ZMA1}M-`b=%?&8$z3 z!YRR)vH!HQxrvjRr4g_RxY{}!adPuQcu>&W4th|gQ_%k<*9u~d;-X>a8o*LNVECtD zZBJ&~1QI&B?JfJ+5dDo8`K+LO>V9i%b+bBXTpx zU9x2$t<_I1TxI5dDUVlNK9vE-hrL$VAyN+naoMIK-^U*Rp#1{gT^=xrYgPlO0MTsh*2^ zNORsBI@-GLjBiCe9zR4|N!M6Z0!kdTgM*Mxa9 z{@jzV-n^#0Ofr=`_`Le#&Ddp>-K^;~vxX7dL+{{@*iHP%;A2P@T-pfC$QHKM4Y)Gq?p#G}FTi?I?u5Xrwb2LmYkp zVhsXDdOGGHO!R;gbu`s7|K&u}k5!_Z$#=)=8mJyC{Xck!gnLdW*l;pV9!qxIXADb(!d>*b0 z7K0G$Le3oen2hJ`Y@LVnO`4@sf<$w^#In9_b&u;squ(mg_ge_KB>7d6UJpW!N*u0; z3d!Z}V_<&_IMJt-t)aVpPFvL<7N2%Hyc7wwgJpQF{Z@iRJ9!L0X0RtmVWu%~IFPBA zaU(a9lKJB_gC-$rBL3VTFP-M_$YOsU2)v-(=;2jMXQvRi#qci;HMX`%0KdTj6os@hdm&UG?~98lt?1jPNPHX zhTnj_xn7aV6T?;Se7kg;#cjQYn%^@0={2QEg8OHb?a*vHSVeujWzN>bqs40RHQ*X} z(0Sjdtwj77ZK4{_>y1w-b7K-Lz`mdK9nQkfrMjk`BQ$kvp6 zUy2~x!CJi5dMhJk`m&*UpsZ+6S(XSgUR%5J zWA2JqNO9PL-gjtkAq<^K2u?xxCI5Z`qApyuhZ|skAGlC}3;4?ZcUf4)&Mrk(#Qsfe zL?~pG*Fpj;K;nBF5e5bWK;BC}o7&r3gW}1GMA3Lq)@*BwfV{gxP;sqEm1^>##%ONG zYZ7$pyPUYB673rwkg)HqsMJ&~v-H-pJYsqH7tbjEr%=xQd5{^VBhE3=Bx%Mk-r3@j#tvm4)Aw?;-5 zoG;(R{)tcTEo*z3T`uJs;&(Tr5fqY!uC&`8FfQ}+mPia=ZlZ_|eY*63mul6q?>$WZ z`GwL4bMuz1-3pwJUt2c=s-rPXr`+AIg_qGe{S+`Cl0m08QxT(jj#Qn}OP`)6^oVlE z28O!Pql?ciWUcSZ+G5E==L~Bk=W3$0Fae=}%?`$(S8QL3r&AVIG7Mw~?AJu9lL(yxvT)K(;;@nl?5_5%WT&3^L`(IWThHPvcjCf%1h~er z2Kl;`%zO>ab(u`_aW9}KbuyOo;qwzIsQ;j5=LsWFQqd`Rt%r&5io@BvQ3fq+II}~? zhRjQwyxEv!D_08}HaXWoctSUbXSJXjGrpfEG(E5&Z1ULx*_iUV=tr>5iFuUSzT!fY z;{orxCV4RJr!lvD0z2O|3c{)LstwlCEcAx#Hu$rYj&9$FAG*!Mu|>GzhASL$yGhb| zwV;DeP%o6L$*T@k%QT}(?VZtnfU|F~Fm54_Bmi^L1}@Y+v$A(}_;)FK_6~MdMo{6v zkHfzoJ3|bYsDtV0?^?|MdpKIyVSIqxfGiMfgYpBm_V!n8%#BP<%&c7CKm=yQO5bf>Hdf##$m-De zcq?(cp9g-@bhfMB58nYL>-#*tL)!M&ZHfM=g(A1zevdc79apk4ES!~T%M8%d-&wyS zefiKIuZvRVX^6l%(eF6kq$&W@oI6&X-HwGDH1ADQ>W z7Yw9`Oul}_?M>ae`m}V-cFKX4ShnryOea-8lQ`|p$5y?CvMOtTx%qmWXwYM5MvGs zMPg~R%u)zeDAr0vv%yFU0leIgZ+BB-KR;ODPk>1jYPl;S`Q5VOx}wMnJXiB)n)?JsL;F2HEtVDGQ3y!wD;p|P` z%vAc^@i|LJ)RDMIAxYZ1SK^;unk*a2myge7Q-1Scp46~Giht>iIzy3}4A=N;I3dag z+0~_-tVYWn9n15pVkx}%Drd-0Fh=Fek#e$L^%ZSdb~aiLSz4}n^H^}Ch>{Mkh@}V< z+X|AVI8$_=jr}>* z+ciqpUqAg$`F;PV!;@bsQ<_*S80K~do>+BYwM-a&9xnAx`7m%v9BK2{SKq#~GQ2U= zF9|p)P2fTYE_+uOAPw;EHu83MR>huP71n4qMU~F3P8Qfg#PE4DJm_tmgClF_*Q-F= zx}&SPGpJtiuUs$_iwY|nEOVHeDvP2@FMEeF>yhsos=|9*2)u(Hh-yK>$?t&L{I)n& zz(2*$fk)R}DIFPu!omdFZIDItP@YWbMf%)Svi575Tk%<+8J%ptN!Ete$DHHrDVD(q zJ4#aUjr)Fgv5>wc6>ju3n48JV>Ux6W%(zl;v==_{lvn zw2>Z(Tgl+o!?wC%J71$b8B5Tri1JjLHwZ~!{`IlV-Ng*OSVJMKAsee|{fpEp*1{agti@_u z&B0$hT54^ixsY8HhZ7PkZgJqb(ws9YH&uqm*ITI-8VZq4ypNgG<~^uaMP$*O;4}M9 zh!u{#p&JK1ytdUPuBn}gdZ|%5hZa)>=kY88V{Bo0c}l4cClWVZ3e5FSUTu%ou2aw; zB76)QrjY(RVN;vuE*a9HmiV#p<;ah-;0y~${DWT%$BO^ogLsH4;SssNW7Gth>;v&R zU=9cO2Q~4tL#U}2Gk^mNpY0Sr z^Jb;<{Yq~Fc!g7nH811}jv#iq$}dd%Bn||~7bVWH5uZt+oIcw@BX#HNChfToWmhlI z0BWid8Pw8h$qJ-f8M0=5|6G^rK85FF)re?|51-D+$%=UiC{9d~#jf|gP*fyKp)q}h zrcQl{$bwf*x9kjBb1LTGi;Wn^#cnYMbkrKy3i05H*Sfw~O96<9_n2P z&2WsG_}Txunz#?GCi$l7>=(c)I$Fk8i2hs(A?K!XsJc81 z6k69=$OmA8lDrb{mEPB^M?J?9%Wr7?(X(fQsoT;#osjjzP6`b%{>HbDv|;g@GGmhP zL2&{+vQ61|nKBQU0X^x|g7icP=qb)*akO3z9d(!-(39z6k?$>c$*_*1@a6H3wm%Ql zW=^@Bxc|PMI1cHlU9igP56g)IQcqy7_gLx*eK58VCl&oBtkv1)8sstZ{tXOltl6f| z#Ke+V%SSbV}I1ax;pu210|AkRU~i7A#-2k@Bhl% z*~%l4MQ3!Wb<^PiVL>3B6Ga8#mu6kDJPOaJsu9mS?VZmAQZI3)(>gkZx7}RsV+@nl zlwt9Ny^uTqx#$x@=ZtZ_44y=H8Wzcr;i6enCS|SqmKmdAT2$pXwpsO!0xl5`kF&o2 zUQHZ-T}}Lux#PXf31hwYC9s^XAG5i&?tOptn zabi!@2OBB_jVZr;%D9=L8ce`mtiW!7HZ;XEN}HhITtRS${{GB>Px>p6*Lwqay+X=G zu7A89^*J>i5mnIUq;*4t0&|*~hd8zpSWRPT%>z6+6|QS!}c!T0Rf1WncZ>MjDB;_KPGSw(#5L6^oY zG_2%F3b5Zh-FgBh;BWgqe!)ZQNqpj2-t!av9zX=4C>4;RTVQ*teoLsFJh18Y{uU?>TEEa`! z+}moIDVlGsk_kTuz`}bXKW|6y)_Rk6lgx5rme&Ue7hCzgQYM{SG$X zwqF9`qXeX>i?bi_k_j(9HlU#BOnUbvk66#&=L%n{{`XgdbxZrkQgsa{eL!VSUVuFp( zIr^}8XPL3U5bfVVgcc}>sAO%F3ZwVa6QJNK0n~I$Qj&Pj>!nAne`DYGdOsn&-YJ8K zzrUCSAj=YlGUkI%&j?W z%5+XmS0oPym34z&+$e(j!z~~r7cQ-Eu{mJyRc$^&8u z`6g3rAci2_iy;utH>Z*hc5KA$$9kLm*L~5OhxTGU(nMK;kByTfKal?IY69l$Z~Hs} z$j%UunhYNzt^6U@<2_zYWRaM89D*ba-hR{w>~kQWx41|}96GX8zs2S*m{eCmo$i96 z6W_l`pCyx$%Gx-{RA(5y0N?G=M0&$IP%;y}-$ed~3^KPNQU6!bh<}2J>6wMOgiMwAuv2F2v~vF?2mS^s=Gongc&?9lDTBV=!s`kIeW z;ZT27=VoOoPGG=OdBE%G2rp24G*HjxdPoflB6fe31c2|D)9p>br2Tb09qy-xpPLr^ zgPypK*Aoxs{|9>F`ulp~ITS;X24AuIgPwRG^#uOJ1-_j)TmIkwcH;S;(i4zr1TEp* zwad@UfM5V0$SWN#i`dv(bYD`s{*nWGbRAzPV5iKDepHQ){jo zdQ#zMg;nDNo-T1OMP1uKCC7=%a0+6iM~^jad@F_g?Z?kDsm?Og@2H7d$;ycr;ybQK z6*;E9r@VZ*pxEpd;l*h>Gtvi7^^g^r&Rl^(E1RkKymIH*p4m|9gK01g3 zG70F@M_?4Q6Hvfsk)ZJo_D@oQ!9aI_;T^}-@7ypn9Q0uyFk<`(a0j~0p#h=K>VN_1 z0JR*)ANb@BG#>P+6ENOG_~Y?@sRWc^g+@G>qND-C0eANijz>JO@%{$fuP;C!QvhS0 z1FHLwhZcU*9+A8RojU-J&^_9{R@{R`Ae%a91E5m~;86-DI+e;nofY8032?CAkamKG zp$`Cn>C3?MkjDX_1%PfZ2SX$So^zjtfBu2iAn3yb^f+Kldjr(53?6}KzsdJ|KM%ed znwAnw2;4nBM0>cK85-@NCYT`5zMcZ@NELyYgziKJBS!)SEXbb0!%Y6}V1nlMpyh*n z76FuwqdAEqKTS`hf&Kx)~Rm)q@iY>VpH1h*KgSJGsz}v|wmvtkW@j zstE@5mx6~Kuue}i52u`DYA`Zzw|ofvs7Aq)rWn#1xPaINlG&s1j-6oWCQvZ1y{7^` z)$~Fd1vcJkI6md%g8Cl8Bjyk^R2Sq)^GXlK0q(m01a<6`LU%BNVah zLCt;OkpYy^PQ*NBB0+l-bl)Brm4xs&>QVIjn<;e59hg1?p#R-~_j}U!$)Nk`z+^uF zGKlUvXj15&DKM!T35fJp2Ng6ObPp7mt`eX-)DZrtAeM zAzBWgNuhfNz@)+y2RaC#$)GFqU@~;dgLV4fbVuw9pi(<{I8z>|r=LUys_2488s!19 z{mklK8U7#y$PA&&@L-5n$3YxO)Iy^j+&BU91EUX!!S@NBe|=2vF?5uPA2BPC;zQ6+M}R52JJtI7k4xo$06~ctEFJ^T91P`MtdMrKVG~RVEZ{@vLH&42PX?UK87?- zL58EyfO^?Mh`qH3Rb>Q=XLlULe-h6g*tHIe2U);7$lJ-{O&pCTct||x+8kIsBH$C? z2!TwXqh|a~vri zL|*^&;?Z*+iN. + +/** + * File containing tests for the helper. + * + * @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; +require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/helper.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + +/** + * Helper test case. + */ +class tool_uploadcourse_helper_testcase extends advanced_testcase { + + function test_generate_shortname() { + $data = (object) array('fullname' => 'Ah Bh Ch 01 02 03', 'idnumber' => 'ID123'); + + $this->assertSame($data->fullname, tool_uploadcourse_helper::generate_shortname($data, '%f')); + $this->assertSame($data->idnumber, tool_uploadcourse_helper::generate_shortname($data, '%i')); + $this->assertSame('AH BH CH', tool_uploadcourse_helper::generate_shortname($data, '%+8f')); + $this->assertSame('id123', tool_uploadcourse_helper::generate_shortname($data, '%-i')); + $this->assertSame('[Ah Bh Ch] = ID123', tool_uploadcourse_helper::generate_shortname($data, '[%8f] = %i')); + } + + function test_get_course_formats() { + $result = tool_uploadcourse_helper::get_course_formats(); + $this->assertSame(array_keys(get_plugin_list('format')), $result); + // Should be similar as first result, as cached. + $this->assertSame($result, tool_uploadcourse_helper::get_course_formats()); + } + + function test_get_enrolment_data() { + $this->resetAfterTest(true); + $data = array( + 'enrolment_1' => 'unknown', + 'enrolment_1_foo' => '1', + 'enrolment_1_bar' => '2', + 'enrolment_2' => 'self', + 'enrolment_2_delete' => '1', + 'enrolment_2_foo' => 'a', + 'enrolment_2_bar' => '1', + 'enrolment_3' => 'manual', + 'enrolment_3_disable' => '2', + 'enrolment_3_foo' => 'b', + 'enrolment_3_bar' => '2', + 'enrolment_4' => 'database', + 'enrolment_4_foo' => 'x', + 'enrolment_4_bar' => '3', + 'enrolment_5_test3' => 'test3', + 'enrolment_5_test2' => 'test2', + 'enrolment_5_test1' => 'test1', + 'enrolment_5' => 'flatfile', + ); + $expected = array( + 'self' => array( + 'delete' => '1', + 'foo' => 'a', + 'bar' => '1', + ), + 'manual' => array( + 'disable' => '2', + 'foo' => 'b', + 'bar' => '2', + ), + 'database' => array( + 'foo' => 'x', + 'bar' => '3', + ), + 'flatfile' => array( + 'test3' => 'test3', + 'test2' => 'test2', + 'test1' => 'test1', + ) + ); + $this->assertSame(tool_uploadcourse_helper::get_enrolment_data($data), $expected); + } + + function test_get_enrolment_plugins() { + $this->resetAfterTest(true); + $actual = tool_uploadcourse_helper::get_enrolment_plugins(); + $this->assertSame(array_keys(enrol_get_plugins(false)), array_keys($actual)); + // This should be identical as cached. + $secondactual = tool_uploadcourse_helper::get_enrolment_plugins(); + $this->assertSame($actual, $secondactual); + } + + function test_get_restore_content_dir() { + $this->resetAfterTest(true); + $this->setAdminUser(); + + $c1 = $this->getDataGenerator()->create_course(); + $c2 = $this->getDataGenerator()->create_course((object) array('shortname' => 'Yay')); + + // Creating backup file. + $bc = new backup_controller(backup::TYPE_1COURSE, $c1->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, 2); + $bc->execute_plan(); + $result = $bc->get_results(); + $this->assertTrue(isset($result['backup_destination'])); + $c1backupfile = $result['backup_destination']->copy_content_to_temp(); + $bc->destroy(); + + // Creating backup file. + $bc = new backup_controller(backup::TYPE_1COURSE, $c2->id, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_GENERAL, 2); + $bc->execute_plan(); + $result = $bc->get_results(); + $this->assertTrue(isset($result['backup_destination'])); + $c2backupfile = $result['backup_destination']->copy_content_to_temp(); + $bc->destroy(); + + // Checking restore dir. + $dir = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null); + $bcinfo = backup_general_helper::get_backup_information($dir); + $this->assertEquals($bcinfo->original_course_id, $c1->id); + $this->assertEquals($bcinfo->original_course_fullname, $c1->fullname); + + // Do it again, it should be the same directory. + $dir2 = tool_uploadcourse_helper::get_restore_content_dir($c1backupfile, null); + $this->assertEquals($dir, $dir2); + + // Get the second course. + $dir = tool_uploadcourse_helper::get_restore_content_dir($c2backupfile, null); + $bcinfo = backup_general_helper::get_backup_information($dir); + $this->assertEquals($bcinfo->original_course_id, $c2->id); + $this->assertEquals($bcinfo->original_course_fullname, $c2->fullname); + + + // Checking with a shortname. + $dir = tool_uploadcourse_helper::get_restore_content_dir(null, $c1->shortname); + $bcinfo = backup_general_helper::get_backup_information($dir); + $this->assertEquals($bcinfo->original_course_id, $c1->id); + $this->assertEquals($bcinfo->original_course_fullname, $c1->fullname); + + // Do it again, it should be the same directory. + $dir2 = tool_uploadcourse_helper::get_restore_content_dir(null, $c1->shortname); + $this->assertEquals($dir, $dir2); + + // Get the second course. + $dir = tool_uploadcourse_helper::get_restore_content_dir(null, $c2->shortname); + $bcinfo = backup_general_helper::get_backup_information($dir); + $this->assertEquals($bcinfo->original_course_id, $c2->id); + $this->assertEquals($bcinfo->original_course_fullname, $c2->fullname); + + // Restore the time limit to prevent warning. + set_time_limit(0); + } + + public function test_get_role_ids() { + $this->getDataGenerator(); + // Mimic function result. + $expected = array(); + $roles = get_all_roles(); + foreach ($roles as $role) { + $expected[$role->shortname] = $role->id; + } + + $actual = tool_uploadcourse_helper::get_role_ids(); + $this->assertSame($actual, $expected); + + // Check cache. + $this->assertSame($actual, tool_uploadcourse_helper::get_role_ids()); + } + + public function test_get_role_names() { + $this->resetAfterTest(true); + + create_role('Villain', 'villain', 'The bad guys'); + $data = array( + 'role_student' => 'Padawan', + 'role_teacher' => 'Guardian', + 'role_editingteacher' => 'Knight', + 'role_manager' => 'Master', + 'role_villain' => 'Jabba the Hutt', + 'role_android' => 'R2D2', + ); + + // Get the role IDs, but need to force the cache reset as a new role is defined. + $roleids = tool_uploadcourse_helper::get_role_ids(true); + + $expected = array( + 'role_' . $roleids['student'] => 'Padawan', + 'role_' . $roleids['teacher'] => 'Guardian', + 'role_' . $roleids['editingteacher'] => 'Knight', + 'role_' . $roleids['manager'] => 'Master', + 'role_' . $roleids['villain'] => 'Jabba the Hutt', + ); + + $actual = tool_uploadcourse_helper::get_role_names($data); + $this->assertSame($actual, $expected); + } + + public function test_increment_idnumber() { + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(array('idnumber' => 'C1')); + $c2 = $this->getDataGenerator()->create_course(array('idnumber' => 'C2')); + $c3 = $this->getDataGenerator()->create_course(array('idnumber' => 'Yo')); + + $this->assertEquals('C3', tool_uploadcourse_helper::increment_idnumber('C1')); + $this->assertEquals('Yo_2', tool_uploadcourse_helper::increment_idnumber('Yo')); + $this->assertEquals('DoesNotExist', tool_uploadcourse_helper::increment_idnumber('DoesNotExist')); + } + + public function test_increment_shortname() { + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'C1')); + $c2 = $this->getDataGenerator()->create_course(array('shortname' => 'C2')); + $c3 = $this->getDataGenerator()->create_course(array('shortname' => 'Yo')); + + // FYI: increment_shortname assumes that the course exists, and so increment the shortname immediately. + $this->assertEquals('C3', tool_uploadcourse_helper::increment_shortname('C1')); + $this->assertEquals('Yo_2', tool_uploadcourse_helper::increment_shortname('Yo')); + $this->assertEquals('DoesNotExist_2', tool_uploadcourse_helper::increment_shortname('DoesNotExist')); + } + + public function test_resolve_category() { + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_category(array('name' => 'First level')); + $c2 = $this->getDataGenerator()->create_category(array('name' => 'Second level', 'parent' => $c1->id)); + $c3 = $this->getDataGenerator()->create_category(array('idnumber' => 'C3')); + + $data = array( + 'category' => $c1->id, + 'category_path' => $c1->name . ' / ' . $c2->name, + 'category_idnumber' => $c3->idnumber, + ); + + $this->assertEquals($c1->id, tool_uploadcourse_helper::resolve_category($data)); + unset($data['category']); + $this->assertEquals($c3->id, tool_uploadcourse_helper::resolve_category($data)); + unset($data['category_idnumber']); + $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data)); + + // Adding unexisting data. + $data['category_idnumber'] = 1234; + $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data)); + $data['category'] = 1234; + $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data)); + $data['category_path'] = 'Not exist'; + $this->assertEmpty(tool_uploadcourse_helper::resolve_category($data)); + } + + public function test_resolve_category_by_idnumber() { + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_category(array('idnumber' => 'C1')); + $c2 = $this->getDataGenerator()->create_category(array('idnumber' => 'C2')); + + // Doubled for cache check. + $this->assertEquals($c1->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C1')); + $this->assertEquals($c1->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C1')); + $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C2')); + $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category_by_idnumber('C2')); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_idnumber('DoesNotExist')); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_idnumber('DoesNotExist')); + } + + public function test_resolve_category_by_path() { + $this->resetAfterTest(true); + + $cat1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1')); + $cat1_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1.1', 'parent' => $cat1->id)); + $cat1_1_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1.1.1', 'parent' => $cat1_1->id)); + $cat1_1_2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1.1.2', 'parent' => $cat1_1->id)); + $cat1_2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 1.2', 'parent' => $cat1->id)); + + $cat2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2')); + $cat2_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2.1', 'parent' => $cat2->id, 'visible' => false)); + $cat2_1_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2.1.1', 'parent' => $cat2_1->id)); + $cat2_1_2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2.1.2', 'parent' => $cat2_1->id)); + $cat2_2 = $this->getDataGenerator()->create_category(array('name' => 'Cat 2.2', 'parent' => $cat2->id)); + + $cat3 = $this->getDataGenerator()->create_category(array('name' => 'Cat 3')); + $cat3_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 3.1 Doubled', 'parent' => $cat3->id)); + $cat3_1b = $this->getDataGenerator()->create_category(array('name' => 'Cat 3.1 Doubled', 'parent' => $cat3->id)); + $cat3_1_1 = $this->getDataGenerator()->create_category(array('name' => 'Cat 3.1.1', 'parent' => $cat3_1->id)); + $cat3_fakedouble = $this->getDataGenerator()->create_category(array('name' => 'Cat 3.1.1', 'parent' => $cat3->id)); + + // Existing categories. Doubled for cache testing. + $path = array('Cat 1'); + $this->assertEquals($cat1->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEquals($cat1->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + + $path = array('Cat 1', 'Cat 1.1', 'Cat 1.1.2'); + $this->assertEquals($cat1_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEquals($cat1_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + + $path = array('Cat 1', 'Cat 1.2'); + $this->assertEquals($cat1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEquals($cat1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + + $path = array('Cat 2'); + $this->assertEquals($cat2->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEquals($cat2->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + + // Hidden category. + $path = array('Cat 2', 'Cat 2.1'); + $this->assertEquals($cat2_1->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEquals($cat2_1->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + + // Hidden parent. + $path = array('Cat 2', 'Cat 2.1', 'Cat 2.1.2'); + $this->assertEquals($cat2_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEquals($cat2_1_2->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + + // Does not exist. + $path = array('No cat 3', 'Cat 1.2'); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path)); + + $path = array('Cat 2', 'Cat 2.x'); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path)); + + // Name conflict. + $path = array('Cat 3', 'Cat 3.1 Doubled'); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path)); + + $path = array('Cat 3', 'Cat 3.1 Doubled', 'Cat 3.1.1'); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category_by_path($path)); + + $path = array('Cat 3', 'Cat 3.1.1'); + $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + $this->assertEquals($cat3_fakedouble->id, tool_uploadcourse_helper::resolve_category_by_path($path)); + } +} diff --git a/admin/tool/uploadcourse/tests/processor_test.php b/admin/tool/uploadcourse/tests/processor_test.php new file mode 100644 index 00000000000..1f61bfa7fb3 --- /dev/null +++ b/admin/tool/uploadcourse/tests/processor_test.php @@ -0,0 +1,170 @@ +. + +/** + * File containing tests for the processor. + * + * @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; +require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/processor.php'); +require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/course.php'); +require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/helper.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->libdir . '/csvlib.class.php'); + +/** + * Processor test case. + */ +class tool_uploadcourse_processor_testcase extends advanced_testcase { + + public function test_basic() { + global $DB; + $this->resetAfterTest(true); + + $content = array( + "shortname,fullname,summary", + "c1,Course 1,Course 1 summary", + "c2,Course 2,Course 2 summary", + ); + $content = implode("\n", $content); + $iid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($iid, 'uploadcourse'); + $cir->load_csv_content($content, 'utf-8', 'comma'); + $cir->init(); + + $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_ALL); + $defaults = array('category' => '1'); + + $p = new tool_uploadcourse_processor($cir, $options, $defaults); + $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1'))); + $this->assertFalse($DB->record_exists('course', array('shortname' => 'c2'))); + $p->execute(); + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1'))); + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2'))); + } + + public function test_restore_template_course() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + $c1 = $this->getDataGenerator()->create_course(); + $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id)); + + $content = array( + "shortname,fullname,summary", + "c2,Course 2,Course 2 summary", + ); + $content = implode("\n", $content); + $iid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($iid, 'uploadcourse'); + $cir->load_csv_content($content, 'utf-8', 'comma'); + $cir->init(); + + $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW, 'templatecourse' => $c1->shortname); + $defaults = array('category' => '1'); + + $p = new tool_uploadcourse_processor($cir, $options, $defaults); + $this->assertFalse($DB->record_exists('course', array('shortname' => 'c2'))); + $p->execute(); + $c2 = $DB->get_record('course', array('shortname' => 'c2')); + $modinfo = get_fast_modinfo($c2); + $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_restore_file() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + $content = array( + "shortname,fullname,summary", + "c1,Course 1,Course 1 summary", + ); + $content = implode("\n", $content); + $iid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($iid, 'uploadcourse'); + $cir->load_csv_content($content, 'utf-8', 'comma'); + $cir->init(); + + $options = array( + 'mode' => tool_uploadcourse_processor::MODE_CREATE_NEW, + 'restorefile' => __DIR__ . '/fixtures/backup.mbz', + 'templatecourse' => 'DoesNotExist' // Restorefile takes priority. + ); + $defaults = array('category' => '1'); + + $p = new tool_uploadcourse_processor($cir, $options, $defaults); + $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1'))); + $p->execute(); + $c1 = $DB->get_record('course', array('shortname' => 'c1')); + $modinfo = get_fast_modinfo($c1); + $found = false; + foreach ($modinfo->get_cms() as $cmid => $cm) { + if ($cm->modname == 'glossary' && $cm->name == 'Imported Glossary') { + $found = true; + break; + } + } + $this->assertTrue($found); + + // Restore the time limit to prevent warning. + set_time_limit(0); + } + + public function test_shortname_template() { + global $DB; + $this->resetAfterTest(true); + + $content = array( + "shortname,fullname,summary,idnumber", + ",Course 1,C1 Summary,ID123", + ); + $content = implode("\n", $content); + $iid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($iid, 'uploadcourse'); + $cir->load_csv_content($content, 'utf-8', 'comma'); + $cir->init(); + + $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW, 'shortnametemplate' => '%i: %f'); + $defaults = array('category' => '1'); + + $p = new tool_uploadcourse_processor($cir, $options, $defaults); + $this->assertFalse($DB->record_exists('course', array('idnumber' => 'ID123'))); + $p->execute(); + $this->assertTrue($DB->record_exists('course', array('idnumber' => 'ID123'))); + $c = $DB->get_record('course', array('idnumber' => 'ID123')); + $this->assertEquals('ID123: Course 1', $c->shortname); + } + +} From 1d1898ac01f4985a7ad72403c656d4ec01b77e4d Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Thu, 27 Jun 2013 12:52:24 +0800 Subject: [PATCH 04/21] MDL-13114 tool_uploadcourse: Improving the error reporting --- admin/tool/uploadcourse/classes/course.php | 162 +++++++++++++----- admin/tool/uploadcourse/classes/helper.php | 47 +++-- admin/tool/uploadcourse/classes/processor.php | 105 +++++++++++- .../lang/en/tool_uploadcourse.php | 39 +++++ admin/tool/uploadcourse/tests/course_test.php | 27 ++- admin/tool/uploadcourse/tests/helper_test.php | 16 +- 6 files changed, 320 insertions(+), 76 deletions(-) diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index b282bc98884..fc6bd0960d0 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -52,7 +52,7 @@ class tool_uploadcourse_course { protected $enrolmentdata; /** @var array errors. */ - protected $errors; + protected $errors = array(); /** @var array containing options passed from the processor. */ protected $importoptions = array(); @@ -78,6 +78,9 @@ class tool_uploadcourse_course { /** @var string course shortname. */ protected $shortname; + /** @var array errors. */ + protected $statuses = array(); + /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */ protected $updatemode; @@ -223,11 +226,15 @@ class tool_uploadcourse_course { /** * Log an error * - * @param string $message error message. + * @param string $code error code. + * @param lang_string $message error message. * @return void */ - protected function error($message) { - $this->errors[] = $message; + 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; } /** @@ -315,7 +322,7 @@ class tool_uploadcourse_course { /** * Get the directory of the object to restore. * - * @return string subdirectory in $CFG->tempdir/backup/... + * @return string|false subdirectory in $CFG->tempdir/backup/... */ protected function get_restore_content_dir() { $backupfile = null; @@ -327,7 +334,15 @@ class tool_uploadcourse_course { $shortname = $this->options['templatecourse']; } - $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname); + $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']; } @@ -335,6 +350,15 @@ class tool_uploadcourse_course { 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. * @@ -355,7 +379,7 @@ class tool_uploadcourse_course { // Validate the shortname. if (!empty($this->shortname) || is_numeric($this->shortname)) { if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) { - $this->error('invalid shortname'); + $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse')); return false; } } @@ -365,10 +389,10 @@ class tool_uploadcourse_course { // Do we want to delete the course? if ($this->options['delete']) { if (!$exists) { - $this->error('cannot delete a course that does not exist'); + $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse')); return false; } else if (!$this->can_delete()) { - $this->error('no permission to delete course'); + $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse')); return false; } @@ -379,12 +403,13 @@ class tool_uploadcourse_course { // Can we create/update the course under those conditions? if ($exists) { if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { - $this->error('course exists and mode does not allow update'); + $this->error('courseexistsanduploadnotallowed', new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); return false; } } else { if (!$this->can_create()) { - $this->error('course does not exist and mode does not allow creation'); + $this->error('coursedoesnotexistandcreatenotallowed', + new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse')); return false; } } @@ -409,62 +434,72 @@ class tool_uploadcourse_course { if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { // Mandatory fields upon creation. - $errors = 0; + $errors = array(); foreach (self::$mandatoryfields as $field) { if ((!isset($coursedata[$field]) || $coursedata[$field] === '') && (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) { - $this->error('missing value for mandatory field: ' . $field); - $errors++; + $errors[] = $field; } } - if ($errors > 0) { + 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 (!$exists) { - $this->error('cannot rename a course that does not exist'); + 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('no permission to rename the course'); + $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('the shortname to rename the course with is invalid'); + $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse')); return false; } else if ($this->exists($this->options['rename'])) { - $this->error('the shortname to rename the course with is already used'); + $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('cannot rename the course as its ID number would conflict with another one'); + $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('missing shortname and could not find a shortname template'); + $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse')); return false; } else if (!$this->can_only_create()) { - $this->error('cannot generate a shortname from template when mode allow updating'); + $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('the generated shortname is invalid'); + $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('the generated shortname is used by another course'); + $this->error('generatedshortnamealreadyinuse', + new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse')); return false; } $exists = true; } + $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse')); $this->shortname = $newshortname; } } @@ -475,12 +510,14 @@ class tool_uploadcourse_course { $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname); $exists = false; if ($this->shortname != $original) { - // TODO Log new shortname. + $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse', + array('from' => $original, 'to' => $this->shortname))); if (isset($coursedata['idnumber'])) { $originalidn = $coursedata['idnumber']; $coursedata['idnumber'] = $this->increment_idnumber($coursedata['idnumber']); if ($originalidn != $coursedata['idnumber']) { - // TODO Log new idnumber. + $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse', + array('from' => $originalidn, 'to' => $coursedata['idnumber']))); } } } @@ -491,35 +528,43 @@ class tool_uploadcourse_course { case tool_uploadcourse_processor::MODE_CREATE_NEW: case tool_uploadcourse_processor::MODE_CREATE_ALL: if ($exists) { - $this->error('the course exists and mode does not allow for update'); + $this->error('courseexistsanduploadnotallowed', + new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); return false; } break; case tool_uploadcourse_processor::MODE_UPDATE_ONLY: if (!$exists) { - $this->error('the course does not exist and mode only allow for update'); + $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('the update mode does not allow for any field to be updated'); + $this->error('updatemodedoessettonothing', + new lang_string('updatemodedoessettonothing', 'tool_uploadcourse')); return false; } } break; default: // O_o Huh?! This should really never happen here! - $this->error('unknown import mode!'); + $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse')); return false; } // Resolve the category. - // TODO Log resolve error. - $catid = tool_uploadcourse_helper::resolve_category($this->rawdata); - if (!empty($catid)) { + $errors = array(); + $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors); + if (!empty($catid) && empty($errors)) { $coursedata['category'] = $catid; + } else if (!empty($errors)) { + foreach ($errors as $key => $message) { + $this->error($key, $message); + } + return false; } // Get final data. @@ -538,15 +583,22 @@ class tool_uploadcourse_course { } // Add role renaming. - // TODO add warning about wrong roles. - foreach (tool_uploadcourse_helper::get_role_names($this->rawdata) as $rolekey => $rolename) { + $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; } - // Validation. + // Some validation. if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) { - // Warning, wrong format! - $coursedata['format'] = 'weeks'; + $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse')); + return false; } // Saving data. @@ -560,10 +612,11 @@ class tool_uploadcourse_course { // We cannot if ($this->importoptions['reset'] || $this->options['reset']) { if ($this->outcome !== self::OUTCOME_UPDATE) { - $this->error('can only reset a course that has been updated'); + $this->error('canonlyresetcourseinupdatemode', + new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse')); return false; } else if (!$this->can_reset()) { - $this->error('no permission to reset the course'); + $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse')); return false; } } @@ -586,12 +639,18 @@ class tool_uploadcourse_course { } if ($this->outcome === self::OUTCOME_DELETE) { - return $this->delete(); + if ($this->delete()) { + $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse')); + } else { + $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse')); + } } else if ($this->outcome === self::OUTCOME_CREATE) { $course = create_course((object) $this->data); + $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse')); } else if ($this->outcome === self::OUTCOME_UPDATE) { $course = (object) $this->data; update_course($course); + $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!'); @@ -608,9 +667,9 @@ class tool_uploadcourse_course { } if ($rc->execute_precheck()) { $rc->execute_plan(); + $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse')); } else { - // Error! - // $rc->get_precheck_results()); + $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse')); } $rc->destroy(); } @@ -622,6 +681,7 @@ class tool_uploadcourse_course { if ($this->importoptions['reset'] || $this->options['reset']) { if ($this->outcome === self::OUTCOME_UPDATE && $this->can_reset()) { $this->reset($course); + $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse')); } } @@ -777,4 +837,18 @@ class tool_uploadcourse_course { 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 index 87f77353852..0e24a33a178 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -222,9 +222,10 @@ class tool_uploadcourse_helper { * * @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) { + public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) { global $CFG, $DB, $USER; $cachekey = null; @@ -243,12 +244,19 @@ class tool_uploadcourse_helper { // Use false instead of null because it would consider that the cache // key has not been set. $backupid = false; - if (!empty($backupfile) && is_readable($backupfile)) { - // 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 (!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); @@ -258,6 +266,9 @@ class tool_uploadcourse_helper { $bc->execute_plan(); $backupid = $bc->get_backupid(); $bc->destroy(); + } else { + $errors['coursetorestorefromdoesnotexist'] = + new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse'); } } self::$restorecontentcache[$cachekey] = $backupid; @@ -288,17 +299,19 @@ class tool_uploadcourse_helper { * 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_, the value is the new name. */ - public static function get_role_names($data) { + 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]])) { - // Error! + $invalidroles[] = $matches[1]; continue; } $rolenames['role_' . $rolesids[$matches[1]]] = $value; @@ -306,6 +319,10 @@ class tool_uploadcourse_helper { } + if (!empty($invalidroles)) { + $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles)); + } + // Roles names. return $rolenames; } @@ -363,25 +380,33 @@ class tool_uploadcourse_helper { * - 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) { + public static function resolve_category($data, &$errors = array()) { global $DB; $catid = null; if (!empty($data['category'])) { - $category = coursecat::get($data['category'], IGNORE_MISSING); + $category = coursecat::get((int) $data['category'], IGNORE_MISSING); if (!empty($category)) { $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']); + $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'])); + $errors['couldnotresolvecatgorybypath'] = + new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse'); } return $catid; diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php index 9fb6253b839..80c1e8057d6 100644 --- a/admin/tool/uploadcourse/classes/processor.php +++ b/admin/tool/uploadcourse/classes/processor.php @@ -109,12 +109,12 @@ class tool_uploadcourse_processor { /** @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 int line data. */ - protected $linedata = array(); - /** @var bool whether the process has been started or not. */ protected $processstarted = false; @@ -166,6 +166,8 @@ class tool_uploadcourse_processor { $this->cir = $cir; $this->columns = $cir->get_columns(); $this->defaults = $defaults; + $this->validate(); + $this->reset(); } /** @@ -184,15 +186,15 @@ class tool_uploadcourse_processor { $this->linenb++; $data = $this->parse_line($line); - $course = self::get_course($data); + $course = $this->get_course($data); if ($course->prepare()) { $course->proceed(); } else { - print_object($course->get_errors()); + $this->log_error($course->get_errors()); } } - // $this->remove_restore_content(); + $this->remove_restore_content(); } /** @@ -201,7 +203,7 @@ class tool_uploadcourse_processor { * @param array $data data to import the course with. * @return tool_uploadcourse_course */ - public function get_course($data) { + protected function get_course($data) { $importoptions = array( 'candelete' => $this->allowdeletes, 'canrename' => $this->allowrenames, @@ -213,13 +215,22 @@ class tool_uploadcourse_processor { 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. * * @param string $default directory to use if none found. * @return string subdirectory in $CFG->tempdir/backup/... */ - public function get_restore_content_dir() { + protected function get_restore_content_dir() { $backupfile = null; $shortname = null; @@ -233,6 +244,25 @@ class tool_uploadcourse_processor { 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) * @@ -253,13 +283,70 @@ class tool_uploadcourse_processor { 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. + * @return array of preview data. + */ + public function preview($rows = 10) { + if ($this->processstarted) { + throw new coding_exception('Process has already been started'); + } + $this->processstarted = true; + + // 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) { + $this->log_error($course->get_errors()); + } + $row = $data; + $preview[$this->linenb] = $row; + } + + $this->remove_restore_content(); + + return $preview; + } + /** * Delete the restore object. * * @return void */ protected function remove_restore_content() { - global $CFG; 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/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php index dad679517aa..058df85cb47 100644 --- a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -24,6 +24,45 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['invalidshortname'] = 'Invalid shortname'; +$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 are not allowed'; +$string['courseidnumberincremented'] = 'Course ID number incremented {$a->from} -> {$a->to}'; +$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['courseshortnameincremented'] = 'Course shortname incremented {$a->from} -> {$a->to}'; +$string['courseshortnamegenerated'] = 'Course shortname generated'; +$string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist'; +$string['courseupdated'] = 'Course updated'; +$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['invalidbackupfile'] = 'Invalid backup file'; +$string['invalidcourseformat'] = 'Invalid course format'; +$string['invalidroles'] = 'Invalid role names: {$a}'; +$string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}'; +$string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set'; +$string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated'; +$string['unknownimportmode'] = 'Unknown import mode'; + $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 (don\t use comma-quoted as Moodle does not support it): {$a}'; $string['allowdeletes'] = 'Allow deletes'; $string['allowrenames'] = 'Allow renames'; diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php index ebf5c586b01..42dd5c821d5 100644 --- a/admin/tool/uploadcourse/tests/course_test.php +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -84,6 +84,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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'))); @@ -114,6 +115,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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. @@ -137,6 +139,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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() { @@ -145,27 +148,28 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1')); - // Try to add with existing shortnames, not allowing creation, and updating nothing. + // 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 add with non-existing shortnames. + // 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->prepare(); + $this->assertTrue($co->prepare()); $co->proceed(); $this->assertEquals('New fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1'))); @@ -506,14 +510,16 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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'); + $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; @@ -522,6 +528,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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; @@ -544,12 +551,13 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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_UPDATE_ONLY; + $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'); + $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; @@ -651,9 +659,10 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { // Wrong mode. $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; - $data = array('shortname' => $c1->shortname, 'reset' => '1'); + $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)); @@ -665,6 +674,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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)); @@ -676,6 +686,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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)); diff --git a/admin/tool/uploadcourse/tests/helper_test.php b/admin/tool/uploadcourse/tests/helper_test.php index e36728ed097..344c13c815c 100644 --- a/admin/tool/uploadcourse/tests/helper_test.php +++ b/admin/tool/uploadcourse/tests/helper_test.php @@ -208,8 +208,10 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase { 'role_' . $roleids['villain'] => 'Jabba the Hutt', ); - $actual = tool_uploadcourse_helper::get_role_names($data); + $errors = array(); + $actual = tool_uploadcourse_helper::get_role_names($data, $errors); $this->assertSame($actual, $expected); + $this->assertArrayHasKey('invalidroles', $errors); } public function test_increment_idnumber() { @@ -257,12 +259,18 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase { $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data)); // Adding unexisting data. + $errors = array(); $data['category_idnumber'] = 1234; - $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data)); + $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data, $errors)); + $this->assertArrayHasKey('couldnotresolvecatgorybyidnumber', $errors); + $errors = array(); $data['category'] = 1234; - $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data)); + $this->assertEquals($c2->id, tool_uploadcourse_helper::resolve_category($data, $errors)); + $this->assertArrayHasKey('couldnotresolvecatgorybyid', $errors); + $errors = array(); $data['category_path'] = 'Not exist'; - $this->assertEmpty(tool_uploadcourse_helper::resolve_category($data)); + $this->assertEmpty(tool_uploadcourse_helper::resolve_category($data, $errors)); + $this->assertArrayHasKey('couldnotresolvecatgorybypath', $errors); } public function test_resolve_category_by_idnumber() { From 8383903fcc49717d31c1d0309bdad54ddaf4114d Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 1 Jul 2013 09:51:34 +0800 Subject: [PATCH 05/21] MDL-13114 tool_uploadcourse: Ensure category is properly resolved --- admin/tool/uploadcourse/classes/course.php | 27 +++++++++++----------- admin/tool/uploadcourse/classes/helper.php | 15 +++++++----- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index fc6bd0960d0..c2720bde365 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -430,6 +430,18 @@ class tool_uploadcourse_course { $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) { @@ -514,7 +526,7 @@ class tool_uploadcourse_course { array('from' => $original, 'to' => $this->shortname))); if (isset($coursedata['idnumber'])) { $originalidn = $coursedata['idnumber']; - $coursedata['idnumber'] = $this->increment_idnumber($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']))); @@ -555,18 +567,6 @@ class tool_uploadcourse_course { return false; } - // Resolve the category. - $errors = array(); - $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors); - if (!empty($catid) && empty($errors)) { - $coursedata['category'] = $catid; - } else if (!empty($errors)) { - foreach ($errors as $key => $message) { - $this->error($key, $message); - } - return false; - } - // Get final data. if ($exists) { $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS); @@ -644,6 +644,7 @@ class tool_uploadcourse_course { } else { $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse')); } + return true; } else if ($this->outcome === self::OUTCOME_CREATE) { $course = create_course((object) $this->data); $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse')); diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php index 0e24a33a178..cb15e9676e1 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -389,7 +389,7 @@ class tool_uploadcourse_helper { if (!empty($data['category'])) { $category = coursecat::get((int) $data['category'], IGNORE_MISSING); - if (!empty($category)) { + if (!empty($category) && !empty($category->id)) { $catid = $category->id; } else { $errors['couldnotresolvecatgorybyid'] = @@ -399,14 +399,17 @@ class tool_uploadcourse_helper { if (empty($catid) && !empty($data['category_idnumber'])) { $catid = self::resolve_category_by_idnumber($data['category_idnumber']); - $errors['couldnotresolvecatgorybyidnumber'] = - new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse'); + 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'])); - $errors['couldnotresolvecatgorybypath'] = - new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse'); + if (empty($catid)) { + $errors['couldnotresolvecatgorybypath'] = + new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse'); + } } return $catid; From 5b2f7718f0944698d5fba7cfa3a4c35ba6eca352 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 1 Jul 2013 10:42:53 +0800 Subject: [PATCH 06/21] MDL-13114 tool_uploadcourse: Introducing output tracker --- admin/tool/uploadcourse/classes/course.php | 3 +- admin/tool/uploadcourse/classes/processor.php | 15 +- admin/tool/uploadcourse/classes/tracker.php | 213 ++++++++++++++++++ .../lang/en/tool_uploadcourse.php | 8 +- 4 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 admin/tool/uploadcourse/classes/tracker.php diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index c2720bde365..d441123e05e 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -511,7 +511,8 @@ class tool_uploadcourse_course { } $exists = true; } - $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse')); + $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse', + $newshortname)); $this->shortname = $newshortname; } } diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php index 80c1e8057d6..211e7d8e03e 100644 --- a/admin/tool/uploadcourse/classes/processor.php +++ b/admin/tool/uploadcourse/classes/processor.php @@ -181,6 +181,9 @@ class tool_uploadcourse_processor { } $this->processstarted = true; + $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_PLAIN); + $tracker->start(); + // Loop over the CSV lines. while ($line = $this->cir->next()) { $this->linenb++; @@ -189,11 +192,14 @@ class tool_uploadcourse_processor { $course = $this->get_course($data); if ($course->prepare()) { $course->proceed(); + $tracker->output($this->linenb, true, $course->get_statuses(), $data); } else { - $this->log_error($course->get_errors()); + $tracker->output($this->linenb, false, $course->get_errors(), $data); } } + $tracker->finish(); + $this->remove_restore_content(); } @@ -296,6 +302,8 @@ class tool_uploadcourse_processor { throw new coding_exception('Process has already been started'); } $this->processstarted = true; + $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_PLAIN); + $tracker->start(); // Loop over the CSV lines. $preview = array(); @@ -305,12 +313,15 @@ class tool_uploadcourse_processor { $course = $this->get_course($data); $result = $course->prepare(); if (!$result) { - $this->log_error($course->get_errors()); + $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; diff --git a/admin/tool/uploadcourse/classes/tracker.php b/admin/tool/uploadcourse/classes/tracker.php new file mode 100644 index 00000000000..48e2eda8f3d --- /dev/null +++ b/admin/tool/uploadcourse/classes/tracker.php @@ -0,0 +1,213 @@ +. + +/** + * 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(); + +/** + * 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) { + global $OUTPUT; + + 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) { + } else if ($this->outputmode == self::OUTPUT_HTML) { + echo html_writer::end_tag('table'); + } + } + + /** + * 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)); + 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', $status, 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::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('outcome', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col')); + echo html_writer::tag('th', get_string('status'), 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::end_tag('tr'); + } + } + +} diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php index 058df85cb47..9396aa06f22 100644 --- a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -48,19 +48,23 @@ $string['coursereset'] = 'Course reset'; $string['courseresetnotallowed'] = 'Course reset now allowed'; $string['courserestored'] = 'Course restored'; $string['courseshortnameincremented'] = 'Course shortname incremented {$a->from} -> {$a->to}'; -$string['courseshortnamegenerated'] = 'Course shortname generated'; +$string['courseshortnamegenerated'] = 'Course shortname generated: {$a}'; $string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist'; $string['courseupdated'] = 'Course updated'; +$string['csvline'] = 'Line'; $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['invalidbackupfile'] = 'Invalid backup file'; $string['invalidcourseformat'] = 'Invalid course format'; $string['invalidroles'] = 'Invalid role names: {$a}'; $string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}'; $string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set'; +$string['outcome'] = 'Outcome'; $string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated'; +$string['uploadcoursesresult'] = 'Upload courses results'; $string['unknownimportmode'] = 'Unknown import mode'; $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 (don\t use comma-quoted as Moodle does not support it): {$a}'; @@ -98,7 +102,6 @@ $string['uploadcourses_help'] = 'Courses may be uploaded (and optionally enrolle * The first record contains a list of fieldnames defining the format of the rest of the file * Required fieldnames are coursename, password, firstname, lastname, email'; $string['uploadcoursespreview'] = 'Upload courses preview'; -$string['uploadcoursesresult'] = 'Upload courses results'; $string['courseupdated'] = 'Course updated'; $string['courseuptodate'] = 'Course up-to-date'; $string['coursedeleted'] = 'Course deleted'; @@ -129,7 +132,6 @@ $string['ccbulk'] = 'Select for bulk operations'; $string['ccbulkall'] = 'All courses'; $string['ccbulknew'] = 'New courses'; $string['ccbulkupdated'] = 'Updated courses'; -$string['cccsvline'] = 'CSV line'; $string['cclegacy1role'] = '(Original Student) typeN=1'; $string['cclegacy2role'] = '(Original Teacher) typeN=2'; $string['cclegacy3role'] = '(Original Non-editing teacher) typeN=3'; From 96aaf654818e3d044e806685a5f186145c36b536 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 1 Jul 2013 11:00:14 +0800 Subject: [PATCH 07/21] MDL-13114 tool_uploadcourse: Renaming OUTCOME to DO --- admin/tool/uploadcourse/classes/course.php | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index d441123e05e..20ff323f1ea 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -34,13 +34,13 @@ defined('MOODLE_INTERNAL') || die(); class tool_uploadcourse_course { /** Outcome of the process: creating the course */ - const OUTCOME_CREATE = 1; + const DO_CREATE = 1; /** Outcome of the process: updating the course */ - const OUTCOME_UPDATE = 2; + const DO_UPDATE = 2; /** Outcome of the process: deleting the course */ - const OUTCOME_DELETE = 3; + const DO_DELETE = 3; /** @var array final import data. */ protected $data = array(); @@ -63,8 +63,8 @@ class tool_uploadcourse_course { /** @var array course import options. */ protected $options = array(); - /** @var int constant value of the outcome, what to do with that course */ - protected $outcome; + /** @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; @@ -396,7 +396,7 @@ class tool_uploadcourse_course { return false; } - $this->outcome = self::OUTCOME_DELETE; + $this->do = self::DO_DELETE; return true; } @@ -572,10 +572,10 @@ class tool_uploadcourse_course { if ($exists) { $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS); $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly); - $this->outcome = self::OUTCOME_UPDATE; + $this->do = self::DO_UPDATE; } else { $coursedata = $this->get_final_create_data($coursedata); - $this->outcome = self::OUTCOME_CREATE; + $this->do = self::DO_CREATE; } // Course start date. @@ -612,7 +612,7 @@ class tool_uploadcourse_course { // We cannot if ($this->importoptions['reset'] || $this->options['reset']) { - if ($this->outcome !== self::OUTCOME_UPDATE) { + if ($this->do !== self::DO_UPDATE) { $this->error('canonlyresetcourseinupdatemode', new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse')); return false; @@ -639,17 +639,17 @@ class tool_uploadcourse_course { throw new moodle_exception('Cannot proceed, errors were detected.'); } - if ($this->outcome === self::OUTCOME_DELETE) { + 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->outcome === self::OUTCOME_CREATE) { + } else if ($this->do === self::DO_CREATE) { $course = create_course((object) $this->data); $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse')); - } else if ($this->outcome === self::OUTCOME_UPDATE) { + } else if ($this->do === self::DO_UPDATE) { $course = (object) $this->data; update_course($course); $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse')); @@ -681,7 +681,7 @@ class tool_uploadcourse_course { // Reset the course. if ($this->importoptions['reset'] || $this->options['reset']) { - if ($this->outcome === self::OUTCOME_UPDATE && $this->can_reset()) { + if ($this->do === self::DO_UPDATE && $this->can_reset()) { $this->reset($course); $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse')); } From f54c49ccf02198bf8793e4e655f933a37567057b Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 1 Jul 2013 13:47:50 +0800 Subject: [PATCH 08/21] MDL-13114 tool_uploadcourse: ID number errors handling --- admin/tool/uploadcourse/classes/course.php | 9 +++++++ admin/tool/uploadcourse/tests/course_test.php | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index 20ff323f1ea..abb3ee06853 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -374,6 +374,7 @@ class tool_uploadcourse_course { * @return bool false is any error occured. */ public function prepare() { + global $DB; $this->prepared = true; // Validate the shortname. @@ -536,6 +537,14 @@ class tool_uploadcourse_course { } } + // 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: diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php index 42dd5c821d5..c1af49e2b37 100644 --- a/admin/tool/uploadcourse/tests/course_test.php +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -779,4 +779,30 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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('idnumber' => 'Taken')); + $c2 = $this->getDataGenerator()->create_course(); + + // Create with existing ID number. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => 'c2', 'summary' => 'summary', 'fullname' => 'FN', 'category' => '1', + 'idnumber' => $c1->idnumber); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('idnumberalreadyinuse', $co->get_errors()); + + // Rename to existing ID number. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => $c2->shortname, 'rename' => 'SN', 'idnumber' => $c1->idnumber); + $importoptions = array('canrename' => true); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('cannotrenameidnumberconflict', $co->get_errors()); + } + } From 5dcd7e28677602597fcd4d2808550a78fe5edb7d Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 1 Jul 2013 13:55:22 +0800 Subject: [PATCH 09/21] MDL-13114 tool_uploadcourse: Output tracker in place --- admin/tool/uploadcourse/classes/processor.php | 36 ++++++++++++++++--- admin/tool/uploadcourse/classes/tracker.php | 9 ++++- .../lang/en/tool_uploadcourse.php | 11 +++--- 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php index 211e7d8e03e..e29f6228eff 100644 --- a/admin/tool/uploadcourse/classes/processor.php +++ b/admin/tool/uploadcourse/classes/processor.php @@ -173,32 +173,54 @@ class tool_uploadcourse_processor { /** * Execute the process. * + * @param object $tracker the output tracker to use. * @return void */ - public function execute() { + public function execute($tracker = null) { if ($this->processstarted) { throw new coding_exception('Process has already been started'); } $this->processstarted = true; - $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_PLAIN); + 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(); - $tracker->output($this->linenb, true, $course->get_statuses(), $data); + + $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++; + } + + $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(); } @@ -295,14 +317,18 @@ class tool_uploadcourse_processor { * 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) { + public function preview($rows = 10, $tracker = null) { if ($this->processstarted) { throw new coding_exception('Process has already been started'); } $this->processstarted = true; - $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_PLAIN); + + if (empty($tracker)) { + $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT); + } $tracker->start(); // Loop over the CSV lines. diff --git a/admin/tool/uploadcourse/classes/tracker.php b/admin/tool/uploadcourse/classes/tracker.php index 48e2eda8f3d..2dc6be593a3 100644 --- a/admin/tool/uploadcourse/classes/tracker.php +++ b/admin/tool/uploadcourse/classes/tracker.php @@ -121,8 +121,15 @@ class tool_uploadcourse_tracker { ); if ($this->outputmode == self::OUTPUT_PLAIN) { + foreach ($message as $msg) { + $this->buffer->output($msg); + } } else if ($this->outputmode == self::OUTPUT_HTML) { - echo html_writer::end_tag('table'); + $buffer = new progress_trace_buffer(new html_list_progress_trace()); + foreach ($message as $msg) { + $buffer->output($msg); + } + $buffer->finished(); } } diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php index 9396aa06f22..27326d59f68 100644 --- a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -47,6 +47,11 @@ $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['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist'; @@ -57,6 +62,7 @@ $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['idnumberalreadyinuse'] = 'ID number already used by a course'; $string['invalidbackupfile'] = 'Invalid backup file'; $string['invalidcourseformat'] = 'Invalid course format'; $string['invalidroles'] = 'Invalid role names: {$a}'; @@ -102,15 +108,12 @@ $string['uploadcourses_help'] = 'Courses may be uploaded (and optionally enrolle * The first record contains a list of fieldnames defining the format of the rest of the file * Required fieldnames are coursename, password, firstname, lastname, email'; $string['uploadcoursespreview'] = 'Upload courses preview'; -$string['courseupdated'] = 'Course updated'; $string['courseuptodate'] = 'Course up-to-date'; +$string['courseupdated'] = 'Course updated'; $string['coursedeleted'] = 'Course deleted'; $string['courserenamed'] = 'Course renamed'; -$string['coursescreated'] = 'Courses created'; -$string['coursesdeleted'] = 'Courses deleted'; $string['coursesrenamed'] = 'Courses renamed'; $string['coursesskipped'] = 'Courses skipped'; -$string['coursesupdated'] = 'Courses updated'; $string['coursenotadded'] = 'Course not added - already exists'; $string['coursenotaddederror'] = 'Course not added - error'; $string['coursenotdeletederror'] = 'Course not deleted - error'; From b683185babe89c055b94bcbfc67aaf787b05e522 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 1 Jul 2013 15:54:42 +0800 Subject: [PATCH 10/21] MDL-13114 tool_uploadcourse: CLI uses new classes --- admin/tool/uploadcourse/cli/uploadcourse.php | 291 +++++++----------- .../lang/en/tool_uploadcourse.php | 12 +- 2 files changed, 123 insertions(+), 180 deletions(-) diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php index fd5eea3a776..bdf0819566c 100644 --- a/admin/tool/uploadcourse/cli/uploadcourse.php +++ b/admin/tool/uploadcourse/cli/uploadcourse.php @@ -15,66 +15,51 @@ // along with Moodle. If not, see . /** - * CLI Bulk course registration script from a comma separated file + * CLI Bulk course registration script from a comma separated file. * * @package tool_uploadcourse - * @subpackage uploadcourse - * @copyright 2004 onwards Martin Dougiamas (http://dougiamas.com) * @copyright 2012 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * - * Notes: - * - it is required to use the web server account when executing PHP CLI scripts - * - you need to change the "www-data" to match the apache user account - * - use "su" if "sudo" not available - * */ define('CLI_SCRIPT', true); -require(dirname(dirname(dirname(dirname(dirname(__FILE__))))).'/config.php'); - -require_once($CFG->libdir.'/adminlib.php'); -require_once($CFG->libdir.'/csvlib.class.php'); -require_once($CFG->dirroot.'/course/lib.php'); -require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); -require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); -require_once($CFG->libdir . '/filelib.php'); -require_once($CFG->libdir.'/clilib.php'); -require_once('../locallib.php'); +require(__DIR__ . '/../../../../config.php'); +require_once(__DIR__ . '/../locallib.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('verbose' => false, - 'help' => false, - 'action' => '', - 'mode' => 'nochange', - 'file' => '', - 'delimiter' => 'comma', - 'encoding' => 'UTF-8', - 'category' => false, - 'templateshortname' => false, - 'template' => false, - 'format' => $courseconfig->format, - 'numsections' => $courseconfig->numsections, - 'reset' => false, - ), - array('v' => 'verbose', - 'h' => 'help', - 'a' => 'action', - 'm' => 'mode', - 'f' => 'file', - 'd' => 'delimiter', - 'e' => 'encoding', - 'c' => 'category', - 's' => 'templateshortname', - 't' => 'template', - 'g' => 'format', - 'n' => 'numsections', - 'r' => 'reset', - )); +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); @@ -85,176 +70,132 @@ $help = "Execute Course Upload. Options: --v, --verbose Print verbose progress information -h, --help Print out this help --a, --action Action to perform - addnew, addupdate, update, forceadd --m, --mode Mode of execution - delete, rename, nochange, file, filedefaults, missing --f, --file CSV File --d, --delimiter delimiter - colon,semicolon,tab,cfg,comma --e, --encoding File encoding - utf8 etc --c, --category Course category --s, --templateshortname Template course by shortname --t, --template Template course by backup file --g, --format Course format - weeks,scorm,social,topics --n, --numsections Number of sections --r, --reset Run the course reset by default after each course import +-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 --action=addupdate \\ - --mode=delete --file=./courses.csv --delimiter=comma +\$sudo -u www-data /usr/bin/php admin/tool/uploadcourse/cli/uploadcourse.php --action=createnew \\ + --updatemode=dataonly --file=./courses.csv --delimiter=comma "; if ($options['help']) { echo $help; - die; + die(); } echo "Moodle course uploader running ...\n"; -$actions = array('addnew' => CC_COURSE_ADDNEW, - 'addupdate' => CC_COURSE_ADD_UPDATE, - 'update' => CC_COURSE_UPDATE, - 'forceadd' => CC_COURSE_ADDINC); -if (!isset($options['action']) || - !isset($actions[$options['action']]) || - ($options['action'] != 'addnew' && !isset($options['mode']))) { - echo get_string('invalidinput', 'tool_uploadcourse')."\n"; - echo $help; - die; -} +$processoroptions = array( + 'allowdeletes' => $options['allowdeletes'], + 'allowrenames' => $options['allowrenames'], + 'allowresets' => $options['allowresets'], + 'reset' => $options['reset'], + 'shortnametemplate' => $options['shortnametemplate'] +); -if (!empty($options['verbose']) || $CFG->debug) { - define('CC_DEBUG', true); -} - -if (!isset($actions[$options['action']])) { - echo get_string('invalidaction', 'tool_uploadcourse')."\n"; - echo $help; - die; -} -$options['cctype'] = $actions[$options['action']]; - -$updatetype = array('nochange' => CC_UPDATE_NOCHANGES, - 'file' => CC_UPDATE_FILEOVERRIDE, - 'filedefaults' => CC_UPDATE_ALLOVERRIDE, - 'missing' => CC_UPDATE_MISSING); -if ($options['mode'] == 'rename') { - $options['ccallowrenames'] = 1; - unset($options['mode']); -} else if ($options['mode'] == 'delete') { - $options['ccallowdeletes'] = 1; - unset($options['mode']); -} else if (!isset($updatetype[$options['mode']])) { +// 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; + die(); } -if (isset($options['mode'])) { - $options['ccupdatetype'] = $updatetype[$options['mode']]; -} -$options['ccstandardshortnames'] = 1; -$options['startdate'] = time() + 3600 * 24; -$options['hiddensections'] = $courseconfig->hiddensections; -$options['newsitems'] = $courseconfig->newsitems; -$options['showgrades'] = $courseconfig->showgrades; -$options['showreports'] = $courseconfig->showreports; -$options['maxbytes'] = $courseconfig->maxbytes; -$options['legacyfiles'] = 0; -$options['groupmode'] = $courseconfig->groupmode; -$options['groupmodeforce'] = $courseconfig->groupmodeforce; -$options['visible'] = $courseconfig->visible; -$options['lang'] = $courseconfig->lang; +$processoroptions['mode'] = $modes[$options['mode']]; -if ($options['category']) { - $split = preg_split('|(?get_record('course_categories', array('name'=>trim($cat), 'parent' => $options['category'])); - if (empty($category)) { - echo get_string('invalidcategory', 'tool_uploadcourse')."\n"; - echo $help; - die; - } - $options['category'] = $category->id; - } - $options['cccategory'] = $options['category']; -} else { - $categories = $DB->get_records('course_categories'); - if (empty($categories)) { - echo get_string('invalidcategory', 'tool_uploadcourse')."\n"; - echo $help; - die; - } - $category = array_shift($categories); - $options['cccategory'] = $category->id; +// 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']]; -if (isset($options['templateshortname'])) { - $options['ccshortname'] = $options['templateshortname']; +// File. +if (!empty($options['file'])) { + $options['file'] = realpath($options['file']); } - -$options['file'] = realpath($options['file']); if (!file_exists($options['file'])) { echo get_string('invalidcsvfile', 'tool_uploadcourse')."\n"; echo $help; - die; + die(); } +// Encoding. $encodings = textlib::get_encodings(); if (!isset($encodings[$options['encoding']])) { echo get_string('invalidencoding', 'tool_uploadcourse')."\n"; echo $help; - die; + 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; -if ($options['template']) { - $options['template'] = realpath($options['template']); +// Course template. +if (isset($options['templatecourse'])) { + $processoroptions['templatecourse'] = $options['templatecourse']; } -if ($options['template'] && !file_exists($options['template'])) { - echo get_string('invalidtemplatefile', 'tool_uploadcourse')."\n"; + +// Backup 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; + die(); } -$tmpdir = $CFG->tempdir . '/backup'; -if (!check_dir_exists($tmpdir, true, true)) { - throw new restore_controller_exception('cannot_create_backup_temp_dir'); -} -$filename = restore_controller::get_tempdir_name(SITEID, $USER->id); -$restorefile = null; -if ($options['template']) { - $restorefile = $options['template']; -} - -$formdata = (object) $options; - - -$returnurl = new moodle_url('/admin/tool/uploadcourse/index.php'); -$bulknurl = new moodle_url('/admin/tool/uploadcourse/index.php'); -$std_fields = tool_uploadcourse_std_fields(); +$processoroptions['restorefile'] = $options['restorefile']; // Emulate normal session. cron_setup_user(); -$content = file_get_contents($formdata->file); +// Let's get started! +$content = file_get_contents($options['file']); $iid = csv_import_reader::get_new_iid('uploadcourse'); $cir = new csv_import_reader($iid, 'uploadcourse'); -$readcount = $cir->load_csv_content($content, $formdata->encoding, $formdata->delimiter); -$filecolumns = tool_uploadcourse_validate_course_upload_columns($cir, $std_fields, $returnurl); +$readcount = $cir->load_csv_content($content, $options['encoding'], $options['delimiter']); 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()); } -echo "CSV read count: ".$readcount."\n"; - -$result = tool_uploadcourse_process_course_upload($formdata, $cir, $filecolumns, $restorefile, true); - -exit($result); +$processor = new tool_uploadcourse_processor($cir, $processoroptions, $defaults); +$processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_PLAIN)); diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php index 27326d59f68..30a68570a8a 100644 --- a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -56,6 +56,7 @@ $string['courseshortnameincremented'] = 'Course shortname incremented {$a->from} $string['courseshortnamegenerated'] = 'Course shortname generated: {$a}'; $string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist'; $string['courseupdated'] = 'Course updated'; +$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['errorwhilerestoringcourse'] = 'Error while restoring the course'; $string['errorwhiledeletingcourse'] = 'Error while deleting the course'; @@ -65,6 +66,10 @@ $string['id'] = 'ID'; $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['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}'; $string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set'; @@ -73,7 +78,6 @@ $string['updatemodedoessettonothing'] = 'Update mode does not allow anything to $string['uploadcoursesresult'] = 'Upload courses results'; $string['unknownimportmode'] = 'Unknown import mode'; -$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 (don\t use comma-quoted as Moodle does not support it): {$a}'; $string['allowdeletes'] = 'Allow deletes'; $string['allowrenames'] = 'Allow renames'; $string['csvdelimiter'] = 'CSV delimiter'; @@ -161,11 +165,9 @@ $string['missing'] = 'missing'; $string['incorrectformat'] = 'Invalid format specified'; $string['incorrecttemplatefile'] = 'Template file not found'; $string['invalidenrolmethod'] = 'Invalid enrolment method'; -$string['invalidcsvfile'] = 'Invalid input CSV file'; $string['invalidaction'] = 'Invalid action selected'; -$string['invalidmode'] = 'Invalid mode selected'; -$string['invalidtemplatefile'] = 'Invalid template file'; -$string['invalidencoding'] = 'Invalid encoding'; + + $string['invalidcategory'] = 'Invalid category'; $string['coursetemplatename'] = 'Course template shortname'; $string['coursetemplatename_help'] = 'Select an existing course shortname to use as a template for the creation of all courses.'; From e0164eb3c6aef141a8ef02691b6c7c89a56bf1ad Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 1 Jul 2013 15:59:35 +0800 Subject: [PATCH 11/21] MDL-13114 tool_uploadcourse: Use autoload and clean require_once --- admin/tool/uploadcourse/classes/course.php | 2 ++ admin/tool/uploadcourse/classes/helper.php | 3 +++ admin/tool/uploadcourse/classes/processor.php | 1 + admin/tool/uploadcourse/classes/tracker.php | 1 + admin/tool/uploadcourse/cli/uploadcourse.php | 2 +- admin/tool/uploadcourse/tests/course_test.php | 7 ------- admin/tool/uploadcourse/tests/helper_test.php | 3 --- admin/tool/uploadcourse/tests/processor_test.php | 6 ------ 8 files changed, 8 insertions(+), 17 deletions(-) diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index abb3ee06853..2812a7feda0 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -23,6 +23,8 @@ */ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->dirroot . '/course/lib.php'); /** * Course class. diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php index cb15e9676e1..c5f51563fb0 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -23,6 +23,9 @@ */ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/coursecatlib.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. diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php index e29f6228eff..800daf85d94 100644 --- a/admin/tool/uploadcourse/classes/processor.php +++ b/admin/tool/uploadcourse/classes/processor.php @@ -23,6 +23,7 @@ */ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/csvlib.class.php'); /** * Processor class. diff --git a/admin/tool/uploadcourse/classes/tracker.php b/admin/tool/uploadcourse/classes/tracker.php index 2dc6be593a3..3adca675a5c 100644 --- a/admin/tool/uploadcourse/classes/tracker.php +++ b/admin/tool/uploadcourse/classes/tracker.php @@ -23,6 +23,7 @@ */ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/weblib.php'); /** * Class output tracker. diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php index bdf0819566c..573af090d51 100644 --- a/admin/tool/uploadcourse/cli/uploadcourse.php +++ b/admin/tool/uploadcourse/cli/uploadcourse.php @@ -172,7 +172,7 @@ if (isset($options['templatecourse'])) { $processoroptions['templatecourse'] = $options['templatecourse']; } -// Backup file. +// Restore file. if ($options['restorefile']) { $options['restorefile'] = realpath($options['restorefile']); } diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php index c1af49e2b37..d5dfb9b4358 100644 --- a/admin/tool/uploadcourse/tests/course_test.php +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -25,13 +25,6 @@ defined('MOODLE_INTERNAL') || die(); global $CFG; -require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/processor.php'); -require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/course.php'); -require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/helper.php'); -require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); -require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); -require_once($CFG->libdir . '/csvlib.class.php'); -require_once($CFG->libdir . '/coursecatlib.php'); /** * Course test case. diff --git a/admin/tool/uploadcourse/tests/helper_test.php b/admin/tool/uploadcourse/tests/helper_test.php index 344c13c815c..a17e5439263 100644 --- a/admin/tool/uploadcourse/tests/helper_test.php +++ b/admin/tool/uploadcourse/tests/helper_test.php @@ -25,9 +25,6 @@ defined('MOODLE_INTERNAL') || die(); global $CFG; -require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/helper.php'); -require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); -require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); /** * Helper test case. diff --git a/admin/tool/uploadcourse/tests/processor_test.php b/admin/tool/uploadcourse/tests/processor_test.php index 1f61bfa7fb3..5385ddeb43d 100644 --- a/admin/tool/uploadcourse/tests/processor_test.php +++ b/admin/tool/uploadcourse/tests/processor_test.php @@ -25,12 +25,6 @@ defined('MOODLE_INTERNAL') || die(); global $CFG; -require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/processor.php'); -require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/course.php'); -require_once($CFG->dirroot . '/admin/tool/uploadcourse/classes/helper.php'); -require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); -require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); -require_once($CFG->libdir . '/csvlib.class.php'); /** * Processor test case. From 1e3e4efdb2da47d546c3b355ed7df88c82e523d6 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 1 Jul 2013 18:03:19 +0800 Subject: [PATCH 12/21] MDL-13114 tool_uploadcourse: Web UI uses the new classes --- admin/tool/uploadcourse/classes/course.php | 27 +- admin/tool/uploadcourse/classes/processor.php | 1 + .../tool/uploadcourse/classes/step1_form.php | 70 ++++ .../tool/uploadcourse/classes/step2_form.php | 263 +++++++++++++ admin/tool/uploadcourse/classes/tracker.php | 8 +- admin/tool/uploadcourse/course_form.php | 350 ------------------ admin/tool/uploadcourse/index.php | 165 +++------ .../lang/en/tool_uploadcourse.php | 52 ++- 8 files changed, 438 insertions(+), 498 deletions(-) create mode 100644 admin/tool/uploadcourse/classes/step1_form.php create mode 100644 admin/tool/uploadcourse/classes/step2_form.php delete mode 100644 admin/tool/uploadcourse/course_form.php diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index 2812a7feda0..27817458bf7 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -56,6 +56,9 @@ class tool_uploadcourse_course { /** @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(); @@ -71,6 +74,9 @@ class tool_uploadcourse_course { /** @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(); @@ -221,8 +227,8 @@ class tool_uploadcourse_course { */ protected function delete() { global $DB; - $id = $DB->get_field_select('course', 'id', 'shortname = :shortname', array('shortname' => $this->shortname), MUST_EXIST); - return delete_course($id, false); + $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname', array('shortname' => $this->shortname), MUST_EXIST); + return delete_course($this->id, false); } /** @@ -321,6 +327,18 @@ class tool_uploadcourse_course { 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. * @@ -648,7 +666,10 @@ class tool_uploadcourse_course { 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()) { @@ -659,10 +680,12 @@ class tool_uploadcourse_course { 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! diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php index 800daf85d94..3fd57fe420b 100644 --- a/admin/tool/uploadcourse/classes/processor.php +++ b/admin/tool/uploadcourse/classes/processor.php @@ -213,6 +213,7 @@ class tool_uploadcourse_processor { $deleted++; } + $data = array_merge($data, $course->get_data(), array('id' => $course->get_id())); $tracker->output($this->linenb, true, $status, $data); } else { $errors++; diff --git a/admin/tool/uploadcourse/classes/step1_form.php b/admin/tool/uploadcourse/classes/step1_form.php new file mode 100644 index 00000000000..d6b9fb25ae3 --- /dev/null +++ b/admin/tool/uploadcourse/classes/step1_form.php @@ -0,0 +1,70 @@ +. + +/** + * 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 moodleform { + + /** + * The standard form definiton. + * @return void + */ + public function definition () { + $mform = $this->_form; + + $mform->addElement('header', 'settingsheader', get_string('upload')); + + $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_action_buttons(false, get_string('uploadcourses', 'tool_uploadcourse')); + } +} \ No newline at end of file diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php new file mode 100644 index 00000000000..be9e9d4c5ed --- /dev/null +++ b/admin/tool/uploadcourse/classes/step2_form.php @@ -0,0 +1,263 @@ +. + +/** + * 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(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * 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 moodleform { + + /** + * The standard form definiton. + * @return void. + */ + public function definition () { + global $CFG, $COURSE, $DB; + + $mform = $this->_form; + $data = $this->_customdata['data']; + $courseconfig = get_config('moodlecourse'); + + // Upload settings and file. + $mform->addElement('header', 'generalhdr', get_string('general')); + + $choices = array( + tool_uploadcourse_processor::MODE_CREATE_NEW => get_string('ccoptype_addnew', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('ccoptype_addinc', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('ccoptype_addupdate', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('ccoptype_update', '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('ccupdatefromfile', 'tool_uploadcourse'), + tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS => get_string('ccupdateall', 'tool_uploadcourse'), + tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('ccupdatemissing', '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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[updatemode]', '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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[allowdeletes]', '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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[allowrenames]', '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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[allowresets]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); + + $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse')); + $mform->setDefault('options[reset]', 0); + $mform->disabledIf('options[reset]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[reset]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); + + // Default values. + $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse')); + $mform->setExpanded('defaultheader', 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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE); + $mform->disabledIf('options[shortnametemplate]', 'mode', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY); + + $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[defaults]', $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)) { + if (empty($course->legacyfiles)) { + $choices = array('0' => get_string('no'), '2' => get_string('yes')); + } + $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); + + // Restore. + $mform->addElement('header', 'restorehdr', get_string('restoreafterimport', 'tool_uploadcourse')); + $mform->setExpanded('restorehdr', true); + + $courseshortnames = $DB->get_records('course', null, $sort='shortname', 'id,shortname,idnumber'); + $formccourseshortnames = array('' => get_string('none')); + foreach ($courseshortnames as $course) { + $formccourseshortnames[$course->shortname] = $course->shortname; + } + $mform->addElement('select', 'options[templatecourse]', get_string('coursetemplatename', 'tool_uploadcourse'), $formccourseshortnames); + $mform->addHelpButton('options[templatecourse]', 'coursetemplatename', 'tool_uploadcourse'); + $mform->setDefault('options[templatecourse]', 'none'); + + $contextid = $this->_customdata['contextid']; + $mform->addElement('hidden', 'contextid', $contextid); + $mform->setType('contextid', PARAM_INT); + $mform->addElement('filepicker', 'options[restorefile]', get_string('templatefile', 'tool_uploadcourse')); + + // Hidden fields. + $mform->addElement('hidden', 'iid'); + $mform->setType('iid', 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 + */ + function add_action_buttons($cancel = true, $submitlabel = null){ + $mform =& $this->_form; + $buttonarray = array(); + $buttonarray[] = &$mform->createElement('submit', 'previewbutton', 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) { + global $DB; + + $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 index 3adca675a5c..4c307ea221f 100644 --- a/admin/tool/uploadcourse/classes/tracker.php +++ b/admin/tool/uploadcourse/classes/tracker.php @@ -175,14 +175,14 @@ class tool_uploadcourse_tracker { } else { $outcome = $OUTPUT->pix_icon('i/invalid', ''); } - echo html_writer::start_tag('tr', array('class' => 'r' . $this->rownb)); + 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', $status, 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'); } } @@ -208,12 +208,12 @@ class tool_uploadcourse_tracker { '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('outcome', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col')); - echo html_writer::tag('th', get_string('status'), 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/course_form.php b/admin/tool/uploadcourse/course_form.php deleted file mode 100644 index 3f17cc79c01..00000000000 --- a/admin/tool/uploadcourse/course_form.php +++ /dev/null @@ -1,350 +0,0 @@ -. - -/** - * Bulk course upload forms - * - * @package tool_uploadcourse - * @subpackage uploadcourse - * @copyright 2007 Dan Poltawski - * @copyright 2011 Piers Harding - * @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. - * - * @copyright 2007 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class admin_uploadcourse_form1 extends moodleform { - /** - * The standard form definiton - * @return object $form - */ - public function definition () { - $mform = $this->_form; - - $mform->addElement('header', 'settingsheader', get_string('upload')); - - $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_action_buttons(false, get_string('uploadcourses', 'tool_uploadcourse')); - } -} - - -/** - * Specify course upload details - * - * @copyright 2007 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class admin_uploadcourse_form2 extends moodleform { - /** - * The standard form definiton - * @return object $form - */ - public function definition () { - global $CFG, $COURSE, $DB; - - $mform = $this->_form; - $columns = $this->_customdata['columns']; - $data = $this->_customdata['data']; - $courseconfig = get_config('moodlecourse'); - - // I am the template course, why should it be the administrator? we have roles now, other ppl may use this script ;-). - $templatecourse = $COURSE; - - // Upload settings and file. - $mform->addElement('header', 'settingsheader', get_string('settings')); - - $choices = array(CC_COURSE_ADDNEW => get_string('ccoptype_addnew', 'tool_uploadcourse'), - CC_COURSE_ADDINC => get_string('ccoptype_addinc', 'tool_uploadcourse'), - CC_COURSE_ADD_UPDATE => get_string('ccoptype_addupdate', 'tool_uploadcourse'), - CC_COURSE_UPDATE => get_string('ccoptype_update', 'tool_uploadcourse')); - $mform->addElement('select', 'cctype', get_string('ccoptype', 'tool_uploadcourse'), $choices); - - $choices = array(CC_UPDATE_NOCHANGES => get_string('nochanges', 'tool_uploadcourse'), - CC_UPDATE_FILEOVERRIDE => get_string('ccupdatefromfile', 'tool_uploadcourse'), - CC_UPDATE_ALLOVERRIDE => get_string('ccupdateall', 'tool_uploadcourse'), - CC_UPDATE_MISSING => get_string('ccupdatemissing', 'tool_uploadcourse')); - $mform->addElement('select', 'ccupdatetype', get_string('ccupdatetype', 'tool_uploadcourse'), $choices); - $mform->setDefault('ccupdatetype', CC_UPDATE_NOCHANGES); - $mform->disabledIf('ccupdatetype', 'cctype', 'eq', CC_COURSE_ADDNEW); - $mform->disabledIf('ccupdatetype', 'cctype', 'eq', CC_COURSE_ADDINC); - - $mform->addElement('selectyesno', 'ccallowrenames', get_string('allowrenames', 'tool_uploadcourse')); - $mform->setDefault('ccallowrenames', 0); - $mform->disabledIf('ccallowrenames', 'cctype', 'eq', CC_COURSE_ADDNEW); - $mform->disabledIf('ccallowrenames', 'cctype', 'eq', CC_COURSE_ADDINC); - - $mform->addElement('selectyesno', 'ccallowdeletes', get_string('allowdeletes', 'tool_uploadcourse')); - $mform->setDefault('ccallowdeletes', 0); - $mform->disabledIf('ccallowdeletes', 'cctype', 'eq', CC_COURSE_ADDNEW); - $mform->disabledIf('ccallowdeletes', 'cctype', 'eq', CC_COURSE_ADDINC); - - $mform->addElement('selectyesno', 'reset', get_string('reset', 'tool_uploadcourse')); - $mform->setDefault('ccallowdeletes', 0); - $mform->disabledIf('ccallowdeletes', 'cctype', 'eq', CC_COURSE_ADDNEW); - $mform->disabledIf('ccallowdeletes', 'cctype', 'eq', CC_COURSE_ADDINC); - - $mform->addElement('selectyesno', 'ccstandardshortnames', get_string('ccstandardshortnames', 'tool_uploadcourse')); - $mform->setDefault('ccstandardshortnames', 1); - - // Default values. - $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse')); - $displaylist = array(); - $parentlist = array(); - make_categories_list($displaylist, $parentlist, 'moodle/course:create'); - $mform->addElement('select', 'cccategory', get_string('category'), $displaylist); - $mform->addHelpButton('cccategory', 'category'); - - $mform->addElement('text', 'ccshortname', get_string('ccshortnametemplate', 'tool_uploadcourse'), - 'maxlength="100" size="20"'); - $mform->addHelpButton('ccshortname', 'shortnamecourse', 'tool_uploadcourse'); - $mform->disabledIf('ccshortname', 'cctype', 'eq', CC_COURSE_ADD_UPDATE); - $mform->disabledIf('ccshortname', 'cctype', 'eq', CC_COURSE_UPDATE); - - $courseformats = get_plugin_list('format'); - $formcourseformats = array(); - foreach ($courseformats as $courseformat => $formatdir) { - $formcourseformats[$courseformat] = get_string('pluginname', "format_$courseformat"); - } - $mform->addElement('select', 'format', get_string('format'), $formcourseformats); - $mform->addHelpButton('format', 'format'); - $mform->setDefault('format', $courseconfig->format); - - for ($i = 0; $i <= $courseconfig->maxsections; $i++) { - $sectionmenu[$i] = "$i"; - } - $mform->addElement('select', 'numsections', get_string('numberweeks'), $sectionmenu); - $mform->setDefault('numsections', $courseconfig->numsections); - - $mform->addElement('date_selector', 'startdate', get_string('startdate')); - $mform->addHelpButton('startdate', 'startdate'); - $mform->setDefault('startdate', time() + 3600 * 24); - - $choices = array(); - $choices['0'] = get_string('hiddensectionscollapsed'); - $choices['1'] = get_string('hiddensectionsinvisible'); - $mform->addElement('select', 'hiddensections', get_string('hiddensections'), $choices); - $mform->addHelpButton('hiddensections', 'hiddensections'); - $mform->setDefault('hiddensections', $courseconfig->hiddensections); - - $options = range(0, 10); - $mform->addElement('select', 'newsitems', get_string('newsitemsnumber'), $options); - $mform->addHelpButton('newsitems', 'newsitemsnumber'); - $mform->setDefault('newsitems', $courseconfig->newsitems); - - $mform->addElement('selectyesno', 'showgrades', get_string('showgrades')); - $mform->addHelpButton('showgrades', 'showgrades'); - $mform->setDefault('showgrades', $courseconfig->showgrades); - - $mform->addElement('selectyesno', 'showreports', get_string('showreports')); - $mform->addHelpButton('showreports', 'showreports'); - $mform->setDefault('showreports', $courseconfig->showreports); - - $choices = get_max_upload_sizes($CFG->maxbytes); - $mform->addElement('select', 'maxbytes', get_string('maximumupload'), $choices); - $mform->addHelpButton('maxbytes', 'maximumupload'); - $mform->setDefault('maxbytes', $courseconfig->maxbytes); - - if (!empty($course->legacyfiles) or !empty($CFG->legacyfilesinnewcourses)) { - if (empty($course->legacyfiles)) { - // 0 or missing means no legacy files ever used in this course - new course or nobody turned on legacy files yet. - $choices = array('0'=>get_string('no'), '2'=>get_string('yes')); - } else { - $choices = array('1'=>get_string('no'), '2'=>get_string('yes')); - } - $mform->addElement('select', 'legacyfiles', get_string('courselegacyfiles'), $choices); - $mform->addHelpButton('legacyfiles', 'courselegacyfiles'); - if (!isset($courseconfig->legacyfiles)) { - // In case this was not initialised properly due to switching of $CFG->legacyfilesinnewcourses. - $courseconfig->legacyfiles = 0; - } - $mform->setDefault('legacyfiles', $courseconfig->legacyfiles); - } - - 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', 'theme', get_string('forcetheme'), $themes); - } - $courseshortnames = $DB->get_records('course', null, $sort='shortname', 'id,shortname,idnumber'); - $formccourseshortnames = array('none' => get_string('none')); - foreach ($courseshortnames as $course) { - $formccourseshortnames[$course->shortname] = $course->shortname; - } - $mform->addElement('select', 'templatename', get_string('coursetemplatename', 'tool_uploadcourse'), $formccourseshortnames); - $mform->addHelpButton('templatename', 'coursetemplatename', 'tool_uploadcourse'); - $mform->setDefault('templatename', 'none'); - - $contextid = $this->_customdata['contextid']; - $mform->addElement('hidden', 'contextid', $contextid); - $mform->addElement('filepicker', 'restorefile', get_string('templatefile', 'tool_uploadcourse')); - - enrol_course_edit_form($mform, null, get_context_instance(CONTEXT_SYSTEM)); - - $mform->addElement('header', '', get_string('groups', 'group')); - - $choices = array(); - $choices[NOGROUPS] = get_string('groupsnone', 'group'); - $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group'); - $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group'); - $mform->addElement('select', 'groupmode', get_string('groupmode', 'group'), $choices); - $mform->addHelpButton('groupmode', 'groupmode', 'group'); - $mform->setDefault('groupmode', $courseconfig->groupmode); - - $choices = array(); - $choices['0'] = get_string('no'); - $choices['1'] = get_string('yes'); - $mform->addElement('select', 'groupmodeforce', get_string('groupmodeforce', 'group'), $choices); - $mform->addHelpButton('groupmodeforce', 'groupmodeforce', 'group'); - $mform->setDefault('groupmodeforce', $courseconfig->groupmodeforce); - - // Default groupings selector. - $options = array(); - $options[0] = get_string('none'); - $mform->addElement('select', 'defaultgroupingid', get_string('defaultgrouping', 'group'), $options); - - $mform->addElement('header', '', get_string('availability')); - - $choices = array(); - $choices['0'] = get_string('courseavailablenot'); - $choices['1'] = get_string('courseavailable'); - $mform->addElement('select', 'visible', get_string('availability'), $choices); - $mform->addHelpButton('visible', 'availability'); - $mform->setDefault('visible', $courseconfig->visible); - - $mform->addElement('header', '', get_string('language')); - - $languages=array(); - $languages[''] = get_string('forceno'); - $languages += get_string_manager()->get_list_of_translations(); - $mform->addElement('select', 'lang', get_string('forcelanguage'), $languages); - $mform->setDefault('lang', $courseconfig->lang); - - // Hidden fields. - $mform->addElement('hidden', 'iid'); - $mform->setType('iid', 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); - } - - /** - * Form tweaks that depend on current data. - */ - public function definition_after_data() { - $mform = $this->_form; - $columns = $this->_customdata['columns']; - - foreach ($columns as $column) { - if ($mform->elementExists($column)) { - $mform->removeElement($column); - } - } - - } - - /** - * Server side validation. - * @param array $data - form data - * @param object $files - form files - * @return array $errors - form errors - */ - public function validation($data, $files) { - global $DB; - - $errors = parent::validation($data, $files); - $columns = $this->_customdata['columns']; - $optype = $data['cctype']; - - // Look for other required data. - if ($optype != CC_COURSE_UPDATE) { - if (!in_array('fullname', $columns)) { - if (isset($errors['cctype'])) { - $errors['cctype'] .= ' '; - } - $errors['cctype'] .= get_string('missingfield', 'error', 'fullname'); - } - if (!in_array('summary', $columns)) { - if (isset($errors['cctype'])) { - $errors['cctype'] .= ' '; - } - $errors['cctype'] .= get_string('missingfield', 'error', 'summary'); - } - } - if (!empty($data['templatename']) && $data['templatename'] != 'none') { - if (!$template = $DB->get_record('course', array('shortname' => $data['templatename']))) { - $errors['templatename'] = get_string('missingtemplate', 'tool_uploadcourse'); - } - } - - return $errors; - } - - /** - * Used to reformat the data from the editor component - * - * @return stdClass - */ - public function get_data() { - $data = parent::get_data(); - - if ($data !== null and isset($data->description)) { - $data->descriptionformat = $data->description['format']; - $data->description = $data->description['text']; - } - - return $data; - } -} diff --git a/admin/tool/uploadcourse/index.php b/admin/tool/uploadcourse/index.php index e92749fa7a9..7dc08643051 100644 --- a/admin/tool/uploadcourse/index.php +++ b/admin/tool/uploadcourse/index.php @@ -15,170 +15,91 @@ // along with Moodle. If not, see . /** - * Bulk course registration script from a comma separated file + * Bulk course registration script from a comma separated file. * * @package tool_uploadcourse - * @copyright 2004 onwards Martin Dougiamas (http://dougiamas.com) * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require('../../../config.php'); -require_once($CFG->libdir.'/adminlib.php'); -require_once($CFG->libdir.'/csvlib.class.php'); -require_once($CFG->dirroot.'/course/lib.php'); +require(__DIR__ . '/../../../config.php'); +require_once(__DIR__ . '/locallib.php'); +require_once($CFG->libdir . '/adminlib.php'); +require_once($CFG->libdir . '/clilib.php'); +require_once($CFG->libdir . '/coursecatlib.php'); +require_once($CFG->libdir . '/csvlib.class.php'); require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); -require_once($CFG->libdir . '/filelib.php'); -require_once('locallib.php'); -require_once('course_form.php'); + +admin_externalpage_setup('tooluploadcourse'); $iid = optional_param('iid', '', PARAM_INT); $previewrows = optional_param('previewrows', 10, PARAM_INT); -require_login(); -admin_externalpage_setup('tooluploadcourse'); $returnurl = new moodle_url('/admin/tool/uploadcourse/index.php'); -$bulknurl = new moodle_url('/admin/tool/uploadcourse/index.php'); -$std_fields = tool_uploadcourse_std_fields(); - if (empty($iid)) { - $mform1 = new admin_uploadcourse_form1(); - - if ($formdata = $mform1->get_data()) { + $mform1 = new tool_uploadcourse_step1_form(); + if ($form1data = $mform1->get_data()) { $iid = csv_import_reader::get_new_iid('uploadcourse'); $cir = new csv_import_reader($iid, 'uploadcourse'); - $content = $mform1->get_file_content('coursefile'); - - $readcount = $cir->load_csv_content($content, $formdata->encoding, $formdata->delimiter_name); + $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()); } - // Test if columns ok. - $filecolumns = tool_uploadcourse_validate_course_upload_columns($cir, $std_fields, $returnurl); - // Continue to form2. - } else { echo $OUTPUT->header(); - echo $OUTPUT->heading_with_help(get_string('uploadcourses', 'tool_uploadcourse'), 'uploadcourses', 'tool_uploadcourse'); - $mform1->display(); echo $OUTPUT->footer(); - die; + die(); } } else { $cir = new csv_import_reader($iid, 'uploadcourse'); - $filecolumns = tool_uploadcourse_validate_course_upload_columns($cir, $std_fields, $returnurl); } -$frontpagecontext = context_course::instance(SITEID); -$mform2 = new admin_uploadcourse_form2(null, - array('contextid' => $frontpagecontext->id, - 'columns' => $filecolumns, - 'data' => array('iid'=>$iid, 'previewrows'=>$previewrows))); +// Data to set in the form. +$data = array('iid' => $iid, '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 ($formdata = $mform2->is_cancelled()) { +if ($form2data = $mform2->is_cancelled()) { $cir->cleanup(true); redirect($returnurl); -} else if ($formdata = $mform2->get_data()) { - // Print the header. +} 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(); - echo $OUTPUT->heading(get_string('uploadcoursesresult', 'tool_uploadcourse')); - - $tmpdir = $CFG->tempdir . '/backup'; - if (!check_dir_exists($tmpdir, true, true)) { - throw new restore_controller_exception('cannot_create_backup_temp_dir'); - } - $filename = restore_controller::get_tempdir_name(SITEID, $USER->id); - $restorefile = $tmpdir . '/' . $filename; - if (!$mform2->save_file('restorefile', $restorefile)) { - $restorefile = null; - } - $bulk = isset($formdata->ccbulk) ? $formdata->ccbulk : 0; - - tool_uploadcourse_process_course_upload($formdata, $cir, $filecolumns, $restorefile); - - echo $OUTPUT->box_end(); - - if ($bulk) { - echo $OUTPUT->continue_button($bulknurl); + 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(); + echo $OUTPUT->footer(); } 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); } - echo $OUTPUT->footer(); - die; + +} else { + $processor = new tool_uploadcourse_processor($cir, $form1data->options, array()); + echo $OUTPUT->header(); + $mform2->display(); } -// Print the header. -echo $OUTPUT->header(); - -echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse')); - -// NOTE: this is JUST csv processing preview, we must not prevent import from here if there is something in the file!! -// this was intended for validation of csv formatting and encoding, not filtering the data!!!! -// we definitely must not process the whole file! - -// Preview table data. -$data = array(); -$cir->init(); -$linenum = 1; // Column header is first line. -while ($linenum <= $previewrows and $fields = $cir->next()) { - $linenum++; - $rowcols = array(); - $rowcols['line'] = $linenum; - foreach ($fields as $key => $field) { - $rowcols[$filecolumns[$key]] = s($field); - } - $rowcols['status'] = array(); - - if (isset($rowcols['shortname'])) { - $stdshortname = clean_param($rowcols['shortname'], PARAM_MULTILANG); - if ($rowcols['shortname'] !== $stdshortname) { - $rowcols['status'][] = get_string('invalidshortnameupload'); - } - if ($courseid = $DB->get_field('course', 'id', array('shortname'=>$stdshortname))) { - $rowcols['shortname'] = html_writer::link(new moodle_url('/course/view.php', - array('id' => $courseid)), - $rowcols['shortname']); - } - } else { - $rowcols['status'][] = get_string('missingshortname'); - } - - $rowcols['status'] = implode('
', $rowcols['status']); - $data[] = $rowcols; -} -if ($fields = $cir->next()) { - $data[] = array_fill(0, count($fields) + 2, '...'); -} -$cir->close(); - -$table = new html_table(); -$table->id = "ccpreview"; -$table->attributes['class'] = 'generaltable'; -$table->tablealign = 'center'; -$table->summary = get_string('uploadcoursespreview', 'tool_uploadcourse'); -$table->head = array(); -$table->data = $data; - -$table->head[] = get_string('cccsvline', 'tool_uploadcourse'); -foreach ($filecolumns as $column) { - $table->head[] = $column; -} -$table->head[] = get_string('status'); - -echo html_writer::tag('div', html_writer::table($table), array('class'=>'flexible-wrap')); - -// Print the form. -$mform2->display(); echo $OUTPUT->footer(); -die; - diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php index 30a68570a8a..29eee6950bc 100644 --- a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -24,7 +24,9 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -$string['invalidshortname'] = 'Invalid shortname'; +$string['allowdeletes'] = 'Allow deletes'; +$string['allowrenames'] = 'Allow renames'; +$string['allowresets'] = 'Allow resets'; $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'; @@ -56,8 +58,11 @@ $string['courseshortnameincremented'] = 'Course shortname incremented {$a->from} $string['courseshortnamegenerated'] = 'Course shortname generated: {$a}'; $string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist'; $string['courseupdated'] = 'Course updated'; +$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 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'; @@ -71,26 +76,40 @@ $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['outcome'] = 'Outcome'; -$string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated'; -$string['uploadcoursesresult'] = 'Upload courses results'; +$string['mode'] = 'Upload mode'; +$string['preview'] = 'Preview'; +$string['reset'] = 'Reset course after upload'; +$string['result'] = 'Result'; +$string['restoreafterimport'] = 'Restore after import'; +$string['rowpreviewnum'] = 'Preview rows'; +$string['shortnametemplate'] = 'Shortname template'; +$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['unknownimportmode'] = 'Unknown import mode'; +$string['updatemode'] = 'Update mode'; +$string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated'; +$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['uploadcoursesresult'] = 'Upload courses results'; + + + + -$string['allowdeletes'] = 'Allow deletes'; -$string['allowrenames'] = 'Allow renames'; -$string['csvdelimiter'] = 'CSV delimiter'; -$string['defaultvalues'] = 'Default values'; $string['deleteerrors'] = 'Delete errors'; -$string['encoding'] = 'Encoding'; $string['errors'] = 'Errors'; $string['invalidinput'] = 'You must specify a valid combination of --action and --mode'; $string['nochanges'] = 'No changes'; $string['pluginname'] = 'Course upload'; $string['renameerrors'] = 'Rename errors'; $string['requiredtemplate'] = 'Required. You may use template syntax here (%l = lastname, %f = firstname, %u = coursename). See help for details and examples.'; -$string['rowpreviewnum'] = 'Preview rows'; $string['uploadpicture_badcoursefield'] = 'The course attribute specified is not valid. Please, try again.'; $string['uploadpicture_cannotmovezip'] = 'Cannot move zip file to temporary directory.'; $string['uploadpicture_cannotprocessdir'] = 'Cannot process unzipped files.'; @@ -104,13 +123,7 @@ $string['uploadpicture_courseskipped'] = 'Skipping course {$a} (already has a pi $string['uploadpicture_courseupdated'] = 'Picture updated for course {$a}.'; $string['uploadpictures'] = 'Upload course pictures'; $string['uploadpictures_help'] = 'Course pictures can be uploaded as a zip file of image files. The image files should be named chosen-course-attribute.extension, for example course1234.jpg for a course with coursename course1234.'; -$string['uploadcourses'] = 'Upload courses'; -$string['uploadcourses_help'] = 'Courses may be uploaded (and optionally enrolled in courses) 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 coursename, password, firstname, lastname, email'; $string['uploadcoursespreview'] = 'Upload courses preview'; $string['courseuptodate'] = 'Course up-to-date'; $string['courseupdated'] = 'Course updated'; @@ -133,7 +146,7 @@ $string['coursenotrenamedoff'] = 'Course not renamed - renaming off'; $string['coursenotrenamedadmin'] = 'Course not renamed - no admin'; $string['invalidvalue'] = 'Invalid value for field {$a}'; $string['shortnamecourse'] = 'Shortname'; -$string['shortnamecourse_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. See help for details and examples.'; + $string['idnumbernotunique'] = 'idnumber is not unique'; $string['ccbulk'] = 'Select for bulk operations'; $string['ccbulkall'] = 'All courses'; @@ -143,7 +156,7 @@ $string['cclegacy1role'] = '(Original Student) typeN=1'; $string['cclegacy2role'] = '(Original Teacher) typeN=2'; $string['cclegacy3role'] = '(Original Non-editing teacher) typeN=3'; $string['ccnoemailduplicates'] = 'Prevent email address duplicates'; -$string['ccoptype'] = 'Upload type'; + $string['ccoptype_addinc'] = 'Add all, append number to shortnames if needed'; $string['ccoptype_addnew'] = 'Add new only, skip existing courses'; $string['ccoptype_addupdate'] = 'Add new and update existing courses'; @@ -151,13 +164,12 @@ $string['ccoptype_update'] = 'Update existing courses only'; $string['ccpasswordcron'] = 'Generated in cron'; $string['ccpasswordnew'] = 'New course password'; $string['ccpasswordold'] = 'Existing course password'; -$string['reset'] = 'Reset course after upload'; $string['ccstandardshortnames'] = 'Standardise shortnames'; $string['ccupdateall'] = 'Override with file and defaults'; $string['ccupdatefromfile'] = 'Override with file'; $string['ccupdatemissing'] = 'Fill in missing from file and defaults'; $string['ccupdatetype'] = 'Existing course details'; -$string['ccshortnametemplate'] = 'Shortname template'; + $string['ccfullnametemplate'] = 'Fullname template'; $string['ccidnumbertemplate'] = 'Idnumber template'; $string['missingtemplate'] = 'Template not found'; From 7a4ec85e2abe63d902e9d8734906881cc4d41563 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 2 Jul 2013 11:27:57 +0800 Subject: [PATCH 13/21] MDL-13114 tool_uploadcourse: Optimised the import form --- admin/tool/uploadcourse/classes/base_form.php | 92 +++++++++++++++++ .../tool/uploadcourse/classes/step1_form.php | 13 ++- .../tool/uploadcourse/classes/step2_form.php | 99 ++++++------------- admin/tool/uploadcourse/index.php | 5 +- .../lang/en/tool_uploadcourse.php | 15 +-- 5 files changed, 142 insertions(+), 82 deletions(-) create mode 100644 admin/tool/uploadcourse/classes/base_form.php diff --git a/admin/tool/uploadcourse/classes/base_form.php b/admin/tool/uploadcourse/classes/base_form.php new file mode 100644 index 00000000000..8d4e796077f --- /dev/null +++ b/admin/tool/uploadcourse/classes/base_form.php @@ -0,0 +1,92 @@ +. + +/** + * 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('ccoptype_addnew', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('ccoptype_addinc', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('ccoptype_addupdate', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('ccoptype_update', '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('ccupdatefromfile', 'tool_uploadcourse'), + tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS => get_string('ccupdateall', 'tool_uploadcourse'), + tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('ccupdatemissing', '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/step1_form.php b/admin/tool/uploadcourse/classes/step1_form.php index d6b9fb25ae3..cd0abc0b6c8 100644 --- a/admin/tool/uploadcourse/classes/step1_form.php +++ b/admin/tool/uploadcourse/classes/step1_form.php @@ -33,7 +33,7 @@ require_once($CFG->libdir.'/formslib.php'); * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class tool_uploadcourse_step1_form extends moodleform { +class tool_uploadcourse_step1_form extends tool_uploadcourse_base_form { /** * The standard form definiton. @@ -42,7 +42,7 @@ class tool_uploadcourse_step1_form extends moodleform { public function definition () { $mform = $this->_form; - $mform->addElement('header', 'settingsheader', get_string('upload')); + $mform->addElement('header', 'generalhdr', get_string('general')); $mform->addElement('filepicker', 'coursefile', get_string('file')); $mform->addRule('coursefile', null, 'required'); @@ -65,6 +65,11 @@ class tool_uploadcourse_step1_form extends moodleform { $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'tool_uploadcourse'), $choices); $mform->setType('previewrows', PARAM_INT); - $this->add_action_buttons(false, get_string('uploadcourses', 'tool_uploadcourse')); + $this->add_import_options(); + + $mform->addElement('hidden', 'showpreview', 1); + $mform->setType('showpreview', PARAM_INT); + + $this->add_action_buttons(false, get_string('preview', 'tool_uploadcourse')); } -} \ No newline at end of file +} diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php index be9e9d4c5ed..502000730d6 100644 --- a/admin/tool/uploadcourse/classes/step2_form.php +++ b/admin/tool/uploadcourse/classes/step2_form.php @@ -24,8 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -require_once($CFG->libdir.'/formslib.php'); - /** * Specify course upload details. * @@ -33,7 +31,7 @@ require_once($CFG->libdir.'/formslib.php'); * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class tool_uploadcourse_step2_form extends moodleform { +class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { /** * The standard form definiton. @@ -46,57 +44,38 @@ class tool_uploadcourse_step2_form extends moodleform { $data = $this->_customdata['data']; $courseconfig = get_config('moodlecourse'); - // Upload settings and file. - $mform->addElement('header', 'generalhdr', get_string('general')); + // Import options. + $this->add_import_options(); - $choices = array( - tool_uploadcourse_processor::MODE_CREATE_NEW => get_string('ccoptype_addnew', 'tool_uploadcourse'), - tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('ccoptype_addinc', 'tool_uploadcourse'), - tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('ccoptype_addupdate', 'tool_uploadcourse'), - tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('ccoptype_update', '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('ccupdatefromfile', 'tool_uploadcourse'), - tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS => get_string('ccupdateall', 'tool_uploadcourse'), - tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('ccupdatemissing', '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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); - $mform->disabledIf('options[updatemode]', '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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); - $mform->disabledIf('options[allowdeletes]', '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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); - $mform->disabledIf('options[allowrenames]', '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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); - $mform->disabledIf('options[allowresets]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); - - $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse')); - $mform->setDefault('options[reset]', 0); - $mform->disabledIf('options[reset]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); - $mform->disabledIf('options[reset]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); - - // Default values. - $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse')); - $mform->setExpanded('defaultheader', true); + // 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]', 'mode', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE); - $mform->disabledIf('options[shortnametemplate]', 'mode', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY); + $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); @@ -107,7 +86,7 @@ class tool_uploadcourse_step2_form extends moodleform { $choices['1'] = get_string('show'); $mform->addElement('select', 'defaults[visible]', get_string('visible'), $choices); $mform->addHelpButton('defaults[visible]', 'visible'); - $mform->setDefault('defaults[defaults]', $courseconfig->visible); + $mform->setDefault('defaults[visible]', $courseconfig->visible); $mform->addElement('date_selector', 'defaults[startdate]', get_string('startdate')); $mform->addHelpButton('defaults[startdate]', 'startdate'); @@ -182,24 +161,6 @@ class tool_uploadcourse_step2_form extends moodleform { $mform->addHelpButton('defaults[groupmodeforce]', 'groupmodeforce', 'group'); $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce); - // Restore. - $mform->addElement('header', 'restorehdr', get_string('restoreafterimport', 'tool_uploadcourse')); - $mform->setExpanded('restorehdr', true); - - $courseshortnames = $DB->get_records('course', null, $sort='shortname', 'id,shortname,idnumber'); - $formccourseshortnames = array('' => get_string('none')); - foreach ($courseshortnames as $course) { - $formccourseshortnames[$course->shortname] = $course->shortname; - } - $mform->addElement('select', 'options[templatecourse]', get_string('coursetemplatename', 'tool_uploadcourse'), $formccourseshortnames); - $mform->addHelpButton('options[templatecourse]', 'coursetemplatename', 'tool_uploadcourse'); - $mform->setDefault('options[templatecourse]', 'none'); - - $contextid = $this->_customdata['contextid']; - $mform->addElement('hidden', 'contextid', $contextid); - $mform->setType('contextid', PARAM_INT); - $mform->addElement('filepicker', 'options[restorefile]', get_string('templatefile', 'tool_uploadcourse')); - // Hidden fields. $mform->addElement('hidden', 'iid'); $mform->setType('iid', PARAM_INT); @@ -222,7 +183,7 @@ class tool_uploadcourse_step2_form extends moodleform { function add_action_buttons($cancel = true, $submitlabel = null){ $mform =& $this->_form; $buttonarray = array(); - $buttonarray[] = &$mform->createElement('submit', 'previewbutton', get_string('preview', 'tool_uploadcourse')); + $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); diff --git a/admin/tool/uploadcourse/index.php b/admin/tool/uploadcourse/index.php index 7dc08643051..152579bb1f9 100644 --- a/admin/tool/uploadcourse/index.php +++ b/admin/tool/uploadcourse/index.php @@ -28,8 +28,6 @@ require_once($CFG->libdir . '/adminlib.php'); require_once($CFG->libdir . '/clilib.php'); require_once($CFG->libdir . '/coursecatlib.php'); require_once($CFG->libdir . '/csvlib.class.php'); -require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); -require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); admin_externalpage_setup('tooluploadcourse'); @@ -89,7 +87,6 @@ if ($form2data = $mform2->is_cancelled()) { 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(); } else { echo $OUTPUT->heading(get_string('uploadcoursesresult', 'tool_uploadcourse')); $processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML)); @@ -99,6 +96,8 @@ if ($form2data = $mform2->is_cancelled()) { } 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(); } diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php index 29eee6950bc..f9d634e9c68 100644 --- a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -42,8 +42,9 @@ $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 are 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'; @@ -56,18 +57,21 @@ $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['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 values'; +$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'; @@ -85,8 +89,10 @@ $string['reset'] = 'Reset course after upload'; $string['result'] = 'Result'; $string['restoreafterimport'] = 'Restore after import'; $string['rowpreviewnum'] = 'Preview rows'; -$string['shortnametemplate'] = 'Shortname template'; +$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['updatemode'] = 'Update mode'; $string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated'; @@ -181,8 +187,5 @@ $string['invalidaction'] = 'Invalid action selected'; $string['invalidcategory'] = 'Invalid category'; -$string['coursetemplatename'] = 'Course template shortname'; -$string['coursetemplatename_help'] = 'Select an existing course shortname to use as a template for the creation of all courses.'; -$string['templatefile'] = 'Template backup file'; $string['invalidbackupfile'] = 'Invalid backup file'; From 3f57c87e1df5a4522bb48d4209fd41644ab15f75 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 2 Jul 2013 13:58:44 +0800 Subject: [PATCH 14/21] MDL-13114 tool_uploadcourse: Clean up --- admin/tool/uploadcourse/README.txt | 196 --- admin/tool/uploadcourse/classes/base_form.php | 15 +- admin/tool/uploadcourse/classes/course.php | 20 +- admin/tool/uploadcourse/classes/helper.php | 5 +- admin/tool/uploadcourse/classes/processor.php | 1 - .../tool/uploadcourse/classes/step2_form.php | 11 +- admin/tool/uploadcourse/classes/tracker.php | 3 +- admin/tool/uploadcourse/cli/uploadcourse.php | 5 +- admin/tool/uploadcourse/index.php | 14 +- .../lang/en/tool_uploadcourse.php | 99 +- admin/tool/uploadcourse/locallib.php | 1328 ----------------- admin/tool/uploadcourse/settings.php | 11 +- admin/tool/uploadcourse/tests/course_test.php | 9 +- admin/tool/uploadcourse/tests/helper_test.php | 33 +- .../uploadcourse/tests/processor_test.php | 5 + admin/tool/uploadcourse/version.php | 12 +- 16 files changed, 99 insertions(+), 1668 deletions(-) delete mode 100644 admin/tool/uploadcourse/README.txt delete mode 100644 admin/tool/uploadcourse/locallib.php diff --git a/admin/tool/uploadcourse/README.txt b/admin/tool/uploadcourse/README.txt deleted file mode 100644 index 5ee935491a0..00000000000 --- a/admin/tool/uploadcourse/README.txt +++ /dev/null @@ -1,196 +0,0 @@ -moodle-tool_uploadcourse -======================== - -Is a Moodle admin/tools plugin for uploading course outlines -in much the same way that admin/tools/uploaduser works for users. -These plugins became available from Moodle 2.2x and onwards, as -this is when the admin/tools framework first appeared. - -https://gitorious.org/moodle-tool_uploadcourse - -There is also a bulk course category upload function available at https://gitorious.org/moodle-tool_uploadcoursecategory - -If you need to manage course enrolments via bulk upload then you should look at -the core user upload facility - http://docs.moodle.org/22/en/Upload_users - -This takes CSV files as input and enables override or augmentation -with default parameter values. - -All the usual add,updated,rename, and delete functions. - - -Thanks to Moshe Golden for help with getting the command line interface going. - - -!!!! NOTE !!!! -=============== - -This plugin used to come with the full directory structure -admin/tool/uploadcourse - this is nolonger the case so the -installation proceedure has changed! - -General installation proceedures are here: -http://docs.moodle.org/20/en/Installing_contributed_modules_or_plugins - -The basic process is: -Download https://gitorious.org/moodle-tool_uploadcourse/moodle-tool_uploadcourse/archive-tarball/master -unpack the file (probably called master) with tar -xzvf master -This will give you a directory called moodle-tool_uploadcourse-moodle-tool_uploadcourse -Move this directory and rename it into it's final position: -mv moodle-tool_uploadcourse-moodle-tool_uploadcourse /admin/tool/uploadcourse - -Alternatively you can use git: -cd /admin/tool -git clone git@gitorious.org:moodle-tool_uploadcourse/moodle-tool_uploadcourse.git uploadcourse - -Be careful about leaving the .git directory in your live environment. - - -CSV File format -=============== - -Possible column names are: -fullname, shortname, category, idnumber, summary, -format, showgrades, newsitems, teacher, editingteacher, student, modinfo, -manager, coursecreator, guest, user, startdate, numsections, maxbytes, visible, groupmode, restrictmodules, -enablecompletion, completionstartonenrol, completionnotify, hiddensections, groupmodeforce, lang, theme, -cost, showreports, notifystudents, expirynotify, expirythreshold, requested, -deleted, // 1 means delete course -oldshortname, // for renaming -backupfile, // for restoring a course template after creation -templatename, // course to use as a template - the shortname -reset, // reset the course contents after upload - this resets everything - - so you loose groups, roles, logs, grades etc. Be Careful!!! - -An example file is: - -fullname,shortname,category,idnumber,summary,backupfile -Computer Science 101,CS101,Cat1,CS101,The first thing you will ever know,/path/to/backup-moodle2-course-cs101-20120213-0748-nu.mbz - -As a general rule, the input values for fields are what you find on the data entry form if you inspect the HTML element. - -Format -====== -The options for the format value are 'scorm', 'social', weeks', and 'topics'. - -Role Names -=========== - 'teacher', 'editingteacher', 'student', 'manager', -'coursecreator', 'guest', 'user' are - where config permitting - you can -substitute your own name for these roles (string value). - -Category -======== -For category you must supply the category name as it is in Moodle and this -field is case sensitive. If Sub Categories are involved then the full -category hierarchy needs to be specified as a '/' delimited string eg: -'Miscellaneous / Sub Cat / Sub Sub Cat'. The delimiter can be escaped with -a back slash eg: 'some\/category'. - -Course Templating -================= -add column backupfile which has the fully qualified path name to a file on -the server that has a a Moodle course backup in it. - -Add a column templatename which is the shortname of an existing course that -will be copied over the top of the new course. - -Course Enrolment Methods -========================= - -Enrolment methods need special CSV columns as there can be many per course, and the fields for each -method are flexible. The following is an example with two enrolment methods - manual, and self - firstly you need -the column identifying the enrolment method enrolmethod_, and then add the corresponding field values subscripted with _. -eg: -fullname,shortname,category,idnumber,summary,enrolmethod_1,status_1,enrolmethod_2,name_2,password_2,customtext1_2 -Parent,Parent,,Parent,Parent,manual,1,self,self1,letmein,this is a custom message 1 -Students,Students,,Students,Students,manual,0,self,self2,letmein,this is a custom message 2 -Teachers,Teachers,,Teachers,Teachers,manual,0,self,self3,letmein,this is a custom message 3 - -add the special columns for: - * delete - delete_ with value 1 - * disable - disable_ with value 1 - -startdate enrol_startdate enrol_enddate -======================================= -For startdate enrolstartdate, and enrolenddate the values should be supplied in the form like 31.01.2012 or -31/01/2012 that can be consumed by strtotime() (http://php.net/manual/en/function.strtotime.php) - check -your PHP locale settings for the fine tuning eg: m/d/y vs d/m/y. - -Enrolment method field 'enrolperiod' must be in seconds. If this is supplied then enrolenddate will be calculated -as enrolstartdate + enrolperiod. - -enrolperiod should be supplied in multiples of enrolment period measurements - 1 hour = 3600, 1 day = 86400 -and so on. OR - you can pass a text string that php strtotime() can recognise eg: '2 weeks' or '10 days' - -Enrolment Method Role -===================== -Default Role for an enrolment method is supplied by adding the 'role_' column. The expected value is the -descriptive label for the given role eg: 'Student', or "Teacher'. - -Enrolment example: -fullname,shortname,category,idnumber,summary,enrolmethod_1,enrolperiod_1,role_1 -a name,short1,Miscellaneous,id1,a summary,manual,864000,Manager - -Update Course: -================= -Make sure you have shortname in the csv. After uploading the file, select: -Upload type: one of the update existing related options -Existing course details: Overide with file -Allow Renames: Yes - -Update example: -fullname,shortname -new full name,short1 - - -Run it in batch mode -===================== -Execute Course Upload in batch mode - this must be run as the www-data user (or the equivalent user that the web server runs under). - -Options: --v, --verbose Print verbose progress information --h, --help Print out this help --a, --action Action to perform - addnew, addupdate, update, forceadd --m, --mode Mode of execution - delete, rename, nochange, file, filedefaults, missing --f, --file CSV File --d, --delimiter delimiter - colon,semicolon,tab,cfg,comma --e, --encoding File encoding - utf8 etc --c, --category Course category --s, --templateshortname Template course by shortname --t, --template Template course by backup file --g, --format Course format - weeks,scorm,social,topics --n, --numsections Number of sections - - -Example: -sudo -u www-data /usr/bin/php admin/tool/uploadcourse/cli/uploadcourse.php --action=addupdate \ - --mode=delete --file=./courses.csv --delimiter=comma - - - -Installation -================= -git clone this repository into /admin/tools/uploadcourse directory. - -Point your browser at Moodle, and login as admin. This should kick off -the upgrade so that Moodle can now recognise the new plugin. - -This was inspired in part by a need for a complimentary function for uploading -courses (as for users) for the the NZ MLE tools for Identity and -Access Managment (synchronising users with the School SMS): -https://gitorious.org/pla-udi -and -https://gitorious.org/pla-udi/mle_ide_tools - -Copyright (C) Piers Harding 2011 and beyond, All rights reserved - -moodle-tool_uploadcourse free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2 of the License, or (at your option) any later version. - -This library 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 -Lesser General Public License for more details. diff --git a/admin/tool/uploadcourse/classes/base_form.php b/admin/tool/uploadcourse/classes/base_form.php index 8d4e796077f..fc0ddf9a3db 100644 --- a/admin/tool/uploadcourse/classes/base_form.php +++ b/admin/tool/uploadcourse/classes/base_form.php @@ -55,18 +55,19 @@ class tool_uploadcourse_base_form extends moodleform { $mform->setExpanded('importoptionshdr', true); $choices = array( - tool_uploadcourse_processor::MODE_CREATE_NEW => get_string('ccoptype_addnew', 'tool_uploadcourse'), - tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('ccoptype_addinc', 'tool_uploadcourse'), - tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('ccoptype_addupdate', 'tool_uploadcourse'), - tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('ccoptype_update', 'tool_uploadcourse') + 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('ccupdatefromfile', 'tool_uploadcourse'), - tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS => get_string('ccupdateall', 'tool_uploadcourse'), - tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('ccupdatemissing', '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); diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index 27817458bf7..48ccc313e3b 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -206,8 +206,11 @@ class tool_uploadcourse_course { * @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; + 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; } /** @@ -227,7 +230,8 @@ class tool_uploadcourse_course { */ protected function delete() { global $DB; - $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname', array('shortname' => $this->shortname), MUST_EXIST); + $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname', + array('shortname' => $this->shortname), MUST_EXIST); return delete_course($this->id, false); } @@ -248,6 +252,7 @@ class tool_uploadcourse_course { /** * 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) { @@ -424,7 +429,8 @@ class tool_uploadcourse_course { // 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')); + $this->error('courseexistsanduploadnotallowed', + new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); return false; } } else { @@ -520,7 +526,8 @@ class tool_uploadcourse_course { new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse')); return false; } else { - $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata, $this->importoptions['shortnametemplate']); + $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; @@ -639,7 +646,7 @@ class tool_uploadcourse_course { // TODO log warnings. $this->restoredata = $this->get_restore_content_dir(); - // We cannot + // 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', @@ -837,6 +844,7 @@ class tool_uploadcourse_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) { diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php index c5f51563fb0..ad1e36d917a 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -29,10 +29,13 @@ 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 { - /** @var array category idnumber cache. */ static protected $categoryidnumbercache = array(); diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php index 3fd57fe420b..94ab8a7ec7e 100644 --- a/admin/tool/uploadcourse/classes/processor.php +++ b/admin/tool/uploadcourse/classes/processor.php @@ -257,7 +257,6 @@ class tool_uploadcourse_processor { /** * Get the directory of the object to restore. * - * @param string $default directory to use if none found. * @return string subdirectory in $CFG->tempdir/backup/... */ protected function get_restore_content_dir() { diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php index 502000730d6..711dd6322e2 100644 --- a/admin/tool/uploadcourse/classes/step2_form.php +++ b/admin/tool/uploadcourse/classes/step2_form.php @@ -51,7 +51,8 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { $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->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); @@ -113,7 +114,7 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { $mform->addElement('select', 'defaults[theme]', get_string('forcetheme'), $themes); } - $languages[] = array(); + $languages = array(); $languages[''] = get_string('forceno'); $languages += get_string_manager()->get_list_of_translations(); $mform->addElement('select', 'defaults[lang]', get_string('forcelanguage'), $languages); @@ -162,8 +163,8 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce); // Hidden fields. - $mform->addElement('hidden', 'iid'); - $mform->setType('iid', PARAM_INT); + $mform->addElement('hidden', 'importid'); + $mform->setType('importid', PARAM_INT); $mform->addElement('hidden', 'previewrows'); $mform->setType('previewrows', PARAM_INT); @@ -180,7 +181,7 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { * @param string $submitlabel label for submit button, defaults to get_string('savechanges') * @return void */ - function add_action_buttons($cancel = true, $submitlabel = null){ + public function add_action_buttons($cancel = true, $submitlabel = null) { $mform =& $this->_form; $buttonarray = array(); $buttonarray[] = &$mform->createElement('submit', 'showpreview', get_string('preview', 'tool_uploadcourse')); diff --git a/admin/tool/uploadcourse/classes/tracker.php b/admin/tool/uploadcourse/classes/tracker.php index 4c307ea221f..23374f60b88 100644 --- a/admin/tool/uploadcourse/classes/tracker.php +++ b/admin/tool/uploadcourse/classes/tracker.php @@ -207,7 +207,8 @@ class tool_uploadcourse_tracker { 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('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')); diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php index 573af090d51..0e2fefc0441 100644 --- a/admin/tool/uploadcourse/cli/uploadcourse.php +++ b/admin/tool/uploadcourse/cli/uploadcourse.php @@ -25,7 +25,6 @@ define('CLI_SCRIPT', true); require(__DIR__ . '/../../../../config.php'); -require_once(__DIR__ . '/../locallib.php'); require_once($CFG->libdir . '/clilib.php'); require_once($CFG->libdir . '/coursecatlib.php'); require_once($CFG->libdir . '/csvlib.class.php'); @@ -188,8 +187,8 @@ cron_setup_user(); // Let's get started! $content = file_get_contents($options['file']); -$iid = csv_import_reader::get_new_iid('uploadcourse'); -$cir = new csv_import_reader($iid, 'uploadcourse'); +$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) { diff --git a/admin/tool/uploadcourse/index.php b/admin/tool/uploadcourse/index.php index 152579bb1f9..667551efd44 100644 --- a/admin/tool/uploadcourse/index.php +++ b/admin/tool/uploadcourse/index.php @@ -23,24 +23,22 @@ */ require(__DIR__ . '/../../../config.php'); -require_once(__DIR__ . '/locallib.php'); require_once($CFG->libdir . '/adminlib.php'); -require_once($CFG->libdir . '/clilib.php'); require_once($CFG->libdir . '/coursecatlib.php'); require_once($CFG->libdir . '/csvlib.class.php'); admin_externalpage_setup('tooluploadcourse'); -$iid = optional_param('iid', '', PARAM_INT); +$importid = optional_param('importid', '', PARAM_INT); $previewrows = optional_param('previewrows', 10, PARAM_INT); $returnurl = new moodle_url('/admin/tool/uploadcourse/index.php'); -if (empty($iid)) { +if (empty($importid)) { $mform1 = new tool_uploadcourse_step1_form(); if ($form1data = $mform1->get_data()) { - $iid = csv_import_reader::get_new_iid('uploadcourse'); - $cir = new csv_import_reader($iid, 'uploadcourse'); + $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); @@ -57,11 +55,11 @@ if (empty($iid)) { die(); } } else { - $cir = new csv_import_reader($iid, 'uploadcourse'); + $cir = new csv_import_reader($importid, 'uploadcourse'); } // Data to set in the form. -$data = array('iid' => $iid, 'previewrows' => $previewrows); +$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) { diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php index f9d634e9c68..691d2c816ff 100644 --- a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -15,11 +15,9 @@ // along with Moodle. If not, see . /** - * Strings for component 'tool_uploadcourse', language 'en', branch 'MOODLE_22_STABLE' + * Strings for component 'tool_uploadcourse'. * * @package tool_uploadcourse - * @subpackage uploadcourse - * @copyright 2011 Petr Skoda {@link http://skodak.org} * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ @@ -61,6 +59,9 @@ $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'; @@ -84,6 +85,8 @@ $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'; @@ -94,8 +97,12 @@ $string['shortnametemplate_help'] = 'The short name of the course is displayed i $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: @@ -103,89 +110,5 @@ $string['uploadcourses_help'] = 'Courses may be uploaded via text file. The form * 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['uploadcoursesresult'] = 'Upload courses results'; - - - - - -$string['deleteerrors'] = 'Delete errors'; -$string['errors'] = 'Errors'; -$string['invalidinput'] = 'You must specify a valid combination of --action and --mode'; -$string['nochanges'] = 'No changes'; -$string['pluginname'] = 'Course upload'; -$string['renameerrors'] = 'Rename errors'; -$string['requiredtemplate'] = 'Required. You may use template syntax here (%l = lastname, %f = firstname, %u = coursename). See help for details and examples.'; -$string['uploadpicture_badcoursefield'] = 'The course attribute specified is not valid. Please, try again.'; -$string['uploadpicture_cannotmovezip'] = 'Cannot move zip file to temporary directory.'; -$string['uploadpicture_cannotprocessdir'] = 'Cannot process unzipped files.'; -$string['uploadpicture_cannotsave'] = 'Cannot save picture for course {$a}. Check original picture file.'; -$string['uploadpicture_cannotunzip'] = 'Cannot unzip pictures file.'; -$string['uploadpicture_invalidfilename'] = 'Picture file {$a} has invalid characters in its name. Skipping.'; -$string['uploadpicture_overwrite'] = 'Overwrite existing course pictures?'; -$string['uploadpicture_coursefield'] = 'Course attribute to use to match pictures:'; -$string['uploadpicture_coursenotfound'] = 'Course with a \'{$a->coursefield}\' value of \'{$a->coursevalue}\' does not exist. Skipping.'; -$string['uploadpicture_courseskipped'] = 'Skipping course {$a} (already has a picture).'; -$string['uploadpicture_courseupdated'] = 'Picture updated for course {$a}.'; -$string['uploadpictures'] = 'Upload course pictures'; -$string['uploadpictures_help'] = 'Course pictures can be uploaded as a zip file of image files. The image files should be named chosen-course-attribute.extension, for example course1234.jpg for a course with coursename course1234.'; - $string['uploadcoursespreview'] = 'Upload courses preview'; -$string['courseuptodate'] = 'Course up-to-date'; -$string['courseupdated'] = 'Course updated'; -$string['coursedeleted'] = 'Course deleted'; -$string['courserenamed'] = 'Course renamed'; -$string['coursesrenamed'] = 'Courses renamed'; -$string['coursesskipped'] = 'Courses skipped'; -$string['coursenotadded'] = 'Course not added - already exists'; -$string['coursenotaddederror'] = 'Course not added - error'; -$string['coursenotdeletederror'] = 'Course not deleted - error'; -$string['coursenotdeletedmissing'] = 'Course not deleted - missing'; -$string['coursenotdeletedoff'] = 'Course not deleted - delete off'; -$string['coursenotdeletedadmin'] = 'Course not deleted - no admin access'; -$string['coursenotupdatederror'] = 'Course not updated - error'; -$string['coursenotupdatednotexists'] = 'Course not updated - does not exist'; -$string['coursenotupdatedadmin'] = 'Course not updated - no admin'; -$string['coursenotrenamedexists'] = 'Course not renamed - target exists'; -$string['coursenotrenamedmissing'] = 'Course not renamed - source missing'; -$string['coursenotrenamedoff'] = 'Course not renamed - renaming off'; -$string['coursenotrenamedadmin'] = 'Course not renamed - no admin'; -$string['invalidvalue'] = 'Invalid value for field {$a}'; -$string['shortnamecourse'] = 'Shortname'; - -$string['idnumbernotunique'] = 'idnumber is not unique'; -$string['ccbulk'] = 'Select for bulk operations'; -$string['ccbulkall'] = 'All courses'; -$string['ccbulknew'] = 'New courses'; -$string['ccbulkupdated'] = 'Updated courses'; -$string['cclegacy1role'] = '(Original Student) typeN=1'; -$string['cclegacy2role'] = '(Original Teacher) typeN=2'; -$string['cclegacy3role'] = '(Original Non-editing teacher) typeN=3'; -$string['ccnoemailduplicates'] = 'Prevent email address duplicates'; - -$string['ccoptype_addinc'] = 'Add all, append number to shortnames if needed'; -$string['ccoptype_addnew'] = 'Add new only, skip existing courses'; -$string['ccoptype_addupdate'] = 'Add new and update existing courses'; -$string['ccoptype_update'] = 'Update existing courses only'; -$string['ccpasswordcron'] = 'Generated in cron'; -$string['ccpasswordnew'] = 'New course password'; -$string['ccpasswordold'] = 'Existing course password'; -$string['ccstandardshortnames'] = 'Standardise shortnames'; -$string['ccupdateall'] = 'Override with file and defaults'; -$string['ccupdatefromfile'] = 'Override with file'; -$string['ccupdatemissing'] = 'Fill in missing from file and defaults'; -$string['ccupdatetype'] = 'Existing course details'; - -$string['ccfullnametemplate'] = 'Fullname template'; -$string['ccidnumbertemplate'] = 'Idnumber template'; -$string['missingtemplate'] = 'Template not found'; -$string['missing'] = 'missing'; -$string['incorrectformat'] = 'Invalid format specified'; -$string['incorrecttemplatefile'] = 'Template file not found'; -$string['invalidenrolmethod'] = 'Invalid enrolment method'; -$string['invalidaction'] = 'Invalid action selected'; - - -$string['invalidcategory'] = 'Invalid category'; -$string['invalidbackupfile'] = 'Invalid backup file'; - +$string['uploadcoursesresult'] = 'Upload courses results'; diff --git a/admin/tool/uploadcourse/locallib.php b/admin/tool/uploadcourse/locallib.php deleted file mode 100644 index 79ba08f9791..00000000000 --- a/admin/tool/uploadcourse/locallib.php +++ /dev/null @@ -1,1328 +0,0 @@ -. - -/** - * Bulk course registration functions - * - * @package tool_uploadcourse - * @subpackage uploadcourse - * @copyright 2004 onwards Martin Dougiamas (http://dougiamas.com) - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -defined('MOODLE_INTERNAL') || die(); - -define('CC_COURSE_ADDNEW', 0); -define('CC_COURSE_ADDINC', 1); -define('CC_COURSE_ADD_UPDATE', 2); -define('CC_COURSE_UPDATE', 3); - -define('CC_UPDATE_NOCHANGES', 0); -define('CC_UPDATE_FILEOVERRIDE', 1); -define('CC_UPDATE_ALLOVERRIDE', 2); -define('CC_UPDATE_MISSING', 3); - -define('CC_BULK_NEW', 1); -define('CC_BULK_UPDATED', 2); -define('CC_BULK_ALL', 3); - -define('CC_PWRESET_NONE', 0); -define('CC_PWRESET_WEAK', 1); -define('CC_PWRESET_ALL', 2); -define('CC_BULK_NONE', 0); - - -/** - * Return the list of stad fields the course upload processes - */ -function tool_uploadcourse_std_fields() { - // Array of all valid fields for validation. - return $std_fields = array('fullname', 'shortname', 'category', 'idnumber', 'summary', - 'format', 'showgrades', 'newsitems', 'teacher', 'editingteacher', 'student', 'modinfo', - 'manager', 'coursecreator', 'guest', 'user', 'startdate', 'numsections', - 'maxbytes', 'visible', 'groupmode', 'restrictmodules', - 'enablecompletion', 'completionstartonenrol', 'completionnotify', - 'hiddensections', 'groupmodeforce', 'lang', 'theme', - 'cost', 'showreports', 'notifystudents', 'expirynotify', 'expirythreshold', 'requested', - 'deleted', // 1 means delete course. - 'oldshortname', // For renaming. - 'backupfile', // For restoring a course template after creation. - 'templatename', // Course to use as a template - the shortname. - 'reset', - // There are also the enrolment fields but these are free form as they vary on enrolment type - // eg: enrolmethod_1,status_1,enrolmethod_2,name_2,password_2,customtext1_2 - // manual, 1, self, self1, letmein, this is a custom message 1. - ); -} - -/** - * process the upload - * - * @param object $formdata - object of the form data - * @param object $cir - object of the CSV importer - * @param array $filecolumns - file column definitions - * @param string $restorefile - file to restore from - * @param boolean $plain - plain text output - */ -function tool_uploadcourse_process_course_upload($formdata, $cir, $filecolumns, $restorefile=null, $plain=false) { - global $CFG, $USER, $OUTPUT, $SESSION, $DB; - - $std_fields = tool_uploadcourse_std_fields(); - - @set_time_limit(60*60); // 1 hour should be enough. - raise_memory_limit(MEMORY_HUGE); - - require_capability('moodle/course:create', get_context_instance(CONTEXT_SYSTEM)); - require_capability('moodle/course:update', get_context_instance(CONTEXT_SYSTEM)); - require_capability('moodle/course:delete', get_context_instance(CONTEXT_SYSTEM)); - - $strcourserenamed = get_string('courserenamed', 'tool_uploadcourse'); - $strcoursenotrenamedexists = get_string('coursenotrenamedexists', 'tool_uploadcourse'); - $strcoursenotrenamedmissing = get_string('coursenotrenamedmissing', 'tool_uploadcourse'); - $strcoursenotrenamedoff = get_string('coursenotrenamedoff', 'tool_uploadcourse'); - - $strcourseupdated = get_string('courseupdated', 'tool_uploadcourse'); - $strcoursenotupdated = get_string('coursenotupdatederror', 'tool_uploadcourse'); - $strcoursenotupdatednotexists = get_string('coursenotupdatednotexists', 'tool_uploadcourse'); - - $strcourseuptodate = get_string('courseuptodate', 'tool_uploadcourse'); - - $strcourseadded = get_string('newcourse'); - $strcoursenotadded = get_string('coursenotadded', 'tool_uploadcourse'); - $strcoursenotaddederror = get_string('coursenotaddederror', 'tool_uploadcourse'); - - $strcoursedeleted = get_string('coursedeleted', 'tool_uploadcourse'); - $strcoursenotdeletederror = get_string('coursenotdeletederror', 'tool_uploadcourse'); - $strcoursenotdeletedmissing = get_string('coursenotdeletedmissing', 'tool_uploadcourse'); - $strcoursenotdeletedoff = get_string('coursenotdeletedoff', 'tool_uploadcourse'); - $errorstr = get_string('error'); - - $returnurl = new moodle_url('/admin/tool/uploadcourse/index.php'); - $bulknurl = new moodle_url('/admin/tool/uploadcourse/index.php'); - - $today = time(); - $today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0); - - $optype = $formdata->cctype; - - $updatetype = isset($formdata->ccupdatetype) ? $formdata->ccupdatetype : 0; - $allowrenames = (!empty($formdata->ccallowrenames) and $optype != CC_COURSE_ADDNEW and $optype != CC_COURSE_ADDINC); - $allowdeletes = (!empty($formdata->ccallowdeletes) and $optype != CC_COURSE_ADDNEW and $optype != CC_COURSE_ADDINC); - $bulk = isset($formdata->ccbulk) ? $formdata->ccbulk : 0; - $standardshortnames = $formdata->ccstandardshortnames; - - // Check for the template. - $templatepathname = null; - if (!empty($formdata->templatename) && $formdata->templatename != 'none') { - $template = $DB->get_record('course', array('shortname' => $formdata->templatename)); - - // Backup the course template. - $bc = new backup_controller(backup::TYPE_1COURSE, $template->id, backup::FORMAT_MOODLE, - backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); - $backupid = $bc->get_backupid(); - $backupbasepath = $bc->get_plan()->get_basepath(); - $bc->execute_plan(); - $bc->destroy(); - $packer = get_file_packer('application/zip'); - // Check if tmp dir exists. - $tmpdir = $CFG->tempdir . '/backup'; - if (!check_dir_exists($tmpdir, true, true)) { - throw new restore_controller_exception('cannot_create_backup_temp_dir'); - } - $filename = restore_controller::get_tempdir_name(SITEID, $USER->id); - $templatepathname = $tmpdir . '/' . $filename; - // Get the list of files in directory. - $filestemp = get_directory_list($backupbasepath, '', false, true, true); - $files = array(); - foreach ($filestemp as $file) { - // Add zip paths and fs paths to all them. - $files[$file] = $backupbasepath . '/' . $file; - } - $zippacker = get_file_packer('application/zip'); - $zippacker->archive_to_pathname($files, $templatepathname); - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($backupbasepath); - } - } - - // Check the uploaded backup file. - if (!empty($formdata->restorefile)) { - // Check if tmp dir exists. - if ($restorefile) { - $filepath = restore_controller::get_tempdir_name(SITEID, $USER->id); - $packer = get_file_packer('application/zip'); - $restorepathname = "$CFG->tempdir/backup/$filepath/"; - $result = $packer->extract_to_pathname($restorefile, $restorepathname); - // If not a backup zip file. - if (!$result) { - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($restorepathname); - fulldelete($restorefile); - } - throw new moodle_exception('invalidbackupfile', 'tool_uploadcourse'); - } - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($restorepathname); - } - } else { - $restorefile = null; - } - } - - // Verification moved to two places: after upload and into form2. - $coursesnew = 0; - $coursesupdated = 0; - $coursesuptodate = 0; // Not printed yet anywhere. - $courseserrors = 0; - $deletes = 0; - $deleteerrors = 0; - $renames = 0; - $renameerrors = 0; - $coursesskipped = 0; - $enrolmentplugins = enrol_get_plugins(false); - $courseformats = array_keys(get_plugin_list('format')); - - // Clear bulk selection. - if ($bulk) { - $SESSION->bulk_courses = array(); - } - - // Init csv import helper. - $cir->init(); - $linenum = 1; // Column header is first line. - - // Init upload progress tracker. - $upt = new tool_uploadcourse_progress_tracker($plain); - $upt->start(); // Start table. - - while ($line = $cir->next()) { - $upt->flush(); - $linenum++; - - $upt->track('line', $linenum); - - $course = new stdClass(); - - // Add fields to course object. - foreach ($line as $keynum => $value) { - if (!isset($filecolumns[$keynum])) { - // This should not happen. - continue; - } - $key = $filecolumns[$keynum]; - $course->$key = $value; - - if (in_array($key, $upt->columns)) { - // Default value in progress tracking table, can be changed later. - $upt->track($key, s($value), 'normal'); - } - } - // Validate category. - $error = false; - if (!empty($course->category)) { - $split = preg_split('|(?category); - $categories = array(); - foreach ($split as $cat) { - $cat = preg_replace('/\\\/', '', $cat); - $categories[]= $cat; - } - $course->category = 0; - foreach ($categories as $cat) { - // Does the category exist - does the category hierachy make sense. - $category = $DB->get_record('course_categories', array('name'=>trim($cat), 'parent' => $course->category)); - if (empty($category)) { - $upt->track('status', get_string('invalidvalue', 'tool_uploadcourse', 'category').' ('.$cat.' '.get_string('missing', 'tool_uploadcourse').')', 'error'); - $upt->track('category', $errorstr, 'error'); - $error = true; - break; - } - $course->category = $category->id; - } - } - // Check for category errors. - if ($error) { - $courseserrors++; - continue; - } - - if (!isset($course->shortname)) { - // Prevent warnings bellow. - $course->shortname = ''; - } - if (!empty($course->startdate) && $course->startdate != 0) { - $course->startdate = strtotime($course->startdate); - } - if (!empty($course->enrolstartdate) && $course->enrolstartdate != 0) { - $course->enrolstartdate = strtotime($course->enrolstartdate); - } - - // Check for enrolment methods. - $line_fields = (array) $course; - $enrolmethods = array(); - $enrolments = array(); - $error = false; - foreach ($line_fields as $k => $v) { - if (preg_match('/^(\w+)\_(\d+)$/', $k, $matches)) { - if (!isset($enrolments[$matches[2]])) { - $enrolments[$matches[2]] = array(); - } - if ($matches[1] == 'enrolmethod') { - if (!isset($enrolmentplugins[$v])) { - $upt->track('status', get_string('invalidenrolmethod', 'tool_uploadcourse', 'category'), 'error'); - $upt->track($k, $errorstr, 'error'); - $error = true; - } - $enrolmethods[$v] = $matches[2]; - } - $enrolments[$matches[2]][$matches[1]] = $v; - } - } - if ($error) { - continue; - } - foreach ($enrolmethods as $k => $v) { - $enrolmethods[$k] = $enrolments[$v]; - } - - // Roles. - $roles = get_all_roles(); - foreach ($roles as $role) { - if (isset($course->{$role->shortname})) { - if (in_array($role->shortname, array('teacher', 'editingteacher', 'student', - 'manager', 'coursecreator', 'guest', 'user'))) { - $course->{'role_'.$role->id} = $course->{$role->shortname}; - } - } - } - - // What type of operation is this ? - if ($optype == CC_COURSE_ADDNEW or $optype == CC_COURSE_ADDINC) { - // Course creation is a special case - the shortname may be constructed from templates using firstname and lastname - // better never try this in mixed update types. - $error = false; - if (!isset($course->fullname) or $course->fullname === '') { - $upt->track('status', get_string('missingfield', 'error', 'fullname'), 'error'); - $upt->track('fullname', $errorstr, 'error'); - $error = true; - } - if (!isset($course->summary) or $course->summary === '') { - $upt->track('status', get_string('missingfield', 'error', 'summary'), 'error'); - $upt->track('summary', $errorstr, 'error'); - $error = true; - } - if ($error) { - $courseserrors++; - continue; - } - // We require shortname too - we might use template for it though. - if (empty($course->shortname) and !empty($formdata->ccshortname)) { - $course->shortname = tool_uploadcourse_process_template($formdata->ccshortname, $course); - $upt->track('shortname', s($course->shortname)); - } - } - - // Normalize shortname. - $originalshortname = $course->shortname; - if ($standardshortnames) { - $course->shortname = clean_param($course->shortname, PARAM_MULTILANG); - } - - // Make sure we really have shortname. - if (empty($course->shortname)) { - $upt->track('status', get_string('missingfield', 'error', 'shortname'), 'error'); - $upt->track('shortname', $errorstr, 'error'); - $courseserrors++; - continue; - } - - if ($existingcourse = $DB->get_record('course', array('shortname' => $course->shortname))) { - $upt->track('id', $existingcourse->id, 'normal', false); - } - - // Find out in shortname incrementing required. - if ($existingcourse and $optype == CC_COURSE_ADDINC) { - $course->shortname = tool_uploadcourse_increment_shortname($course->shortname); - if (!empty($course->idnumber)) { - $oldidnumber = $course->idnumber; - $course->idnumber = tool_uploadcourse_increment_idnumber($course->idnumber); - if ($course->idnumber !== $oldidnumber) { - $upt->track('idnumber', s($oldidnumber).'-->'.s($course->idnumber), 'info'); - } - } - $existingcourse = false; - } - - // Check duplicate idnumber. - if (!$existingcourse and !empty($course->idnumber)) { - if ($DB->record_exists('course', array('idnumber' => $course->idnumber))) { - $upt->track('status', get_string('idnumbernotunique', 'tool_uploadcourse'), 'error'); - $upt->track('idnumber', $errorstr, 'error'); - $error = true; - } - } - - // Notify about nay shortname changes. - if ($originalshortname !== $course->shortname) { - $upt->track('shortname', '', 'normal', false); // Clear previous. - $upt->track('shortname', s($originalshortname).'-->'.s($course->shortname), 'info'); - } else { - $upt->track('shortname', s($course->shortname), 'normal', false); - } - - // Add default values for remaining fields. - $formdefaults = array(); - foreach ($std_fields as $field) { - if (isset($course->$field)) { - continue; - } - // All validation moved to form2. - if (isset($formdata->$field)) { - $course->$field = $formdata->$field; - $formdefaults[$field] = true; - if (in_array($field, $upt->columns)) { - $upt->track($field, s($course->$field), 'normal'); - } - } else { - // Process templates. - if (isset($formdata->{"cc".$field}) && !empty($formdata->{"cc".$field}) && empty($course->$field)) { - $course->$field = tool_uploadcourse_process_template($formdata->{"cc".$field}, $course); - } - } - } - // Do we run the reset ? - $resetcourse = false; - if ($course->reset) { - $resetcourse = true; - unset($course->reset); - } - - // Proof visible flag. - $course->visible = (int) $course->visible; - - if (empty($course->category)) { - $course->category = $formdata->cccategory; - } - - // Delete course. - if (!empty($course->deleted)) { - if (!$allowdeletes) { - $coursesskipped++; - $upt->track('status', $strcoursenotdeletedoff, 'warning'); - continue; - } - if ($existingcourse) { - if (delete_course($existingcourse->id, false)) { - $upt->track('status', $strcoursedeleted); - $deletes++; - } else { - $upt->track('status', $strcoursenotdeletederror, 'error'); - $deleteerrors++; - } - } else { - $upt->track('status', $strcoursenotdeletedmissing, 'error'); - $deleteerrors++; - } - continue; - } - // We do not need the deleted flag anymore. - unset($course->deleted); - - // Renaming requested? - if (!empty($course->oldshortname) ) { - if (!$allowrenames) { - $coursesskipped++; - $upt->track('status', $strcoursenotrenamedoff, 'warning'); - continue; - } - - if ($existingcourse) { - $upt->track('status', $strcoursenotrenamedexists, 'error'); - $renameerrors++; - continue; - } - - if ($standardshortnames) { - $oldshortname = clean_param($course->oldshortname, PARAM_MULTILANG); - } else { - $oldshortname = $course->oldshortname; - } - - // No guessing when looking for old shortname, it must be exact match. - if ($oldcourse = $DB->get_record('course', array('shortname'=>$oldshortname))) { - $upt->track('id', $oldcourse->id, 'normal', false); - $DB->set_field('course', 'shortname', $course->shortname, array('id'=>$oldcourse->id)); - $upt->track('shortname', '', 'normal', false); // Clear previous. - $upt->track('shortname', s($oldshortname).'-->'.s($course->shortname), 'info'); - $upt->track('status', $strcourserenamed); - $renames++; - } else { - $upt->track('status', $strcoursenotrenamedmissing, 'error'); - $renameerrors++; - continue; - } - $existingcourse = $oldcourse; - $existingcourse->shortname = $course->shortname; - } - - // Can we process with update or insert? - $skip = false; - switch ($optype) { - case CC_COURSE_ADDNEW: - if ($existingcourse) { - $coursesskipped++; - $upt->track('status', $strcoursenotadded, 'warning'); - $skip = true; - } - break; - - case CC_COURSE_ADDINC: - if ($existingcourse) { - // This should not happen! - $upt->track('status', $strcoursenotaddederror, 'error'); - $courseserrors++; - $skip = true; - } - break; - - case CC_COURSE_ADD_UPDATE: - break; - - case CC_COURSE_UPDATE: - if (!$existingcourse) { - $coursesskipped++; - $upt->track('status', $strcoursenotupdatednotexists, 'warning'); - $skip = true; - } - break; - - default: - // Unknown type. - $skip = true; - } - - // Check for the backup file as template. - $backupfile = null; - if (!empty($course->backupfile)) { - if (!is_readable($course->backupfile) || !preg_match('/(\.mbz|\.zip)$/i', $course->backupfile)) { - $upt->track('status', get_string('incorrecttemplatefile', 'tool_uploadcourse'), 'error'); - $courseserrors++; - $skip = true; - } else { - $backupfile = $course->backupfile; - } - } - - if ($skip) { - continue; - } - - // check the format - if (!empty($course->format) && !in_array($course->format, $courseformats)) { - $upt->track('status', get_string('incorrectformat', 'tool_uploadcourse'), 'error'); - $courseserrors++; - continue; - } - - $templatename = null; - if ($existingcourse) { - $course->id = $existingcourse->id; - - $upt->track('shortname', html_writer::link(new moodle_url('/course/view.php', - array('id '=> $existingcourse->id)), - s($existingcourse->shortname)), - 'normal', false); - - $existingcourse->timemodified = time(); - // Do NOT mess with timecreated or firstaccess here! - $doupdate = false; - - if ($updatetype != CC_UPDATE_NOCHANGES) { - foreach ($std_fields as $column) { - if ($column === 'shortname') { - // These can not be changed here. - continue; - } - if (!property_exists($course, $column) or !property_exists($existingcourse, $column)) { - // This should never happen. - continue; - } - // In the case $updatetype == CC_UPDATE_ALLOVERRIDE we override everything. - if ($updatetype == CC_UPDATE_MISSING) { - if (!is_null($existingcourse->$column) and $existingcourse->$column !== '') { - continue; - } - - } else if ($updatetype == CC_UPDATE_FILEOVERRIDE) { - if (!empty($formdefaults[$column])) { - // Do not override with form defaults. - continue; - } - } - if ($existingcourse->$column !== $course->$column) { - if (in_array($column, $upt->columns)) { - $upt->track($column, s($existingcourse->$column).'-->'.s($course->$column), 'info', false); - } - $existingcourse->$column = $course->$column; - $doupdate = true; - } - } - } - - if ($doupdate) { - // We want only courses that were really updated. - update_course($existingcourse); - $upt->track('status', $strcourseupdated); - $coursesupdated++; - - events_trigger('course_updated', $existingcourse); - - if ($bulk == CC_BULK_UPDATED or $bulk == CC_BULK_ALL) { - if (!in_array($course->id, $SESSION->bulk_courses)) { - $SESSION->bulk_courses[] = $course->id; - } - } - - } else { - // No course information changed. - $upt->track('status', $strcourseuptodate); - $coursesuptodate++; - - if ($bulk == CC_BULK_ALL) { - if (!in_array($course->id, $SESSION->bulk_courses)) { - $SESSION->bulk_courses[] = $course->id; - } - } - } - - } else { - // Save the new course to the database. - $course->timemodified = time(); - $course->timecreated = time(); - - // Create course - insert_record ignores any extra properties. - if (isset($course->templatename) && $course->templatename != 'none') { - $templatename = $course->templatename; - } else { - $templatename = null; - } - try { - $course = create_course($course); - } catch (moodle_exception $e) { - $upt->track('status', $e->getMessage(), 'error'); - $courseserrors++; - $skip = true; - continue; - } - $upt->track('shortname', html_writer::link(new moodle_url('/course/view.php', - array('id' => $course->id)), - s($course->shortname)), - 'normal', false); - - $upt->track('status', $strcourseadded); - $upt->track('id', $course->id, 'normal', false); - $coursesnew++; - - // Make sure course context exists. - get_context_instance(CONTEXT_COURSE, $course->id); - - events_trigger('course_created', $course); - - if ($bulk == CC_BULK_NEW or $bulk == CC_BULK_ALL) { - if (!in_array($course->id, $SESSION->bulk_courses)) { - $SESSION->bulk_courses[] = $course->id; - } - } - } - - // After creation/update, do we need to copy from template nominated in the CSV file? - if (!empty($templatename)) { - $coursetemplate = $DB->get_record('course', array('shortname' => $templatename)); - if (empty($coursetemplate)) { - $upt->track('status', get_string('incorrecttemplatefile', 'tool_uploadcourse'), 'error'); - $courseserrors++; - continue; - } - - // Backup the course template. - $bc = new backup_controller(backup::TYPE_1COURSE, $coursetemplate->id, backup::FORMAT_MOODLE, - backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); - $backupid = $bc->get_backupid(); - $backupbasepath = $bc->get_plan()->get_basepath(); - $bc->execute_plan(); - $bc->destroy(); - $packer = get_file_packer('application/zip'); - // Check if tmp dir exists. - $tmpdir = $CFG->tempdir . '/backup'; - if (!check_dir_exists($tmpdir, true, true)) { - throw new restore_controller_exception('cannot_create_backup_temp_dir'); - } - $filename = restore_controller::get_tempdir_name(SITEID, $USER->id); - $temppathname = $tmpdir . '/' . $filename; - // Get the list of files in directory. - $filestemp = get_directory_list($backupbasepath, '', false, true, true); - $files = array(); - foreach ($filestemp as $file) { - // Add zip paths and fs paths to all them. - $files[$file] = $backupbasepath . '/' . $file; - } - $zippacker = get_file_packer('application/zip'); - $zippacker->archive_to_pathname($files, $temppathname); - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($backupbasepath); - } - - // Check if tmp dir exists. - $tmpdir = $CFG->tempdir . '/backup'; - $filename = restore_controller::get_tempdir_name($course->id, $USER->id); - $pathname = $tmpdir . '/' . $filename; - $packer = get_file_packer('application/zip'); - $packer->extract_to_pathname($temppathname, $pathname); - - // Restore the backup immediately. - $rc = new restore_controller($filename, $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()) { - $precheckresults = $rc->get_precheck_results(); - if (is_array($precheckresults) && !empty($precheckresults['errors'])) { - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($pathname); - } - echo $output->precheck_notices($precheckresults); - if (!$plain) { - echo $output->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); - echo $output->footer(); - } - die(); - } - } - $rc->execute_plan(); - $rc->destroy(); - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($pathname); - } - } - - // After creation/update, do we need to copy from template? - if (!empty($templatepathname)) { - // Check if tmp dir exists. - $tmpdir = $CFG->tempdir . '/backup'; - $filename = restore_controller::get_tempdir_name($course->id, $USER->id); - $pathname = $tmpdir . '/' . $filename; - $packer = get_file_packer('application/zip'); - $packer->extract_to_pathname($templatepathname, $pathname); - - // Restore the backup immediately. - $rc = new restore_controller($filename, $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()) { - $precheckresults = $rc->get_precheck_results(); - if (is_array($precheckresults) && !empty($precheckresults['errors'])) { - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($pathname); - } - echo $output->precheck_notices($precheckresults); - if (!$plain) { - echo $output->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); - echo $output->footer(); - } - die(); - } - } - $rc->execute_plan(); - $rc->destroy(); - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($pathname); - } - } - - // After creation/update, do we need to copy from template backup file? - if (!empty($restorefile)) { - // Check if tmp dir exists. - $tmpdir = $CFG->tempdir . '/backup'; - $filename = restore_controller::get_tempdir_name($course->id, $USER->id); - $pathname = $tmpdir . '/' . $filename; - $packer = get_file_packer('application/zip'); - $packer->extract_to_pathname($restorefile, $pathname); - - // Restore the backup immediately. - $rc = new restore_controller($filename, $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()) { - $precheckresults = $rc->get_precheck_results(); - if (is_array($precheckresults) && !empty($precheckresults['errors'])) { - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($pathname); - } - echo $output->precheck_notices($precheckresults); - if (!$plain) { - echo $output->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); - echo $output->footer(); - } - die(); - } - } - $rc->execute_plan(); - $rc->destroy(); - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($pathname); - } - } - - // After creation/update, do we need to import a Moodle backup? - if (!empty($backupfile)) { - // Check if tmp dir exists. - $tmpdir = $CFG->tempdir . '/backup'; - if (!check_dir_exists($tmpdir, true, true)) { - throw new restore_controller_exception('cannot_create_backup_temp_dir'); - } - $filename = restore_controller::get_tempdir_name($course->id, $USER->id); - $pathname = $tmpdir . '/' . $filename; - $packer = get_file_packer('application/zip'); - $packer->extract_to_pathname($backupfile, $pathname); - - // Restore the backup immediately. - $rc = new restore_controller($filename, $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()) { - $precheckresults = $rc->get_precheck_results(); - if (is_array($precheckresults) && !empty($precheckresults['errors'])) { - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($pathname); - } - echo $output->precheck_notices($precheckresults); - if (!$plain) { - echo $output->continue_button(new moodle_url('/course/view.php', array('id' => $course->id))); - echo $output->footer(); - } - die(); - } - } - $rc->execute_plan(); - $rc->destroy(); - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($pathname); - } - } - - // Handle enrolment methods. - $enrol_updated = false; - $instances = enrol_get_instances($course->id, false); - foreach ($enrolments as $method) { - if (isset($method['delete']) && $method['delete']) { - // Remove the enrolment method. - foreach ($instances as $instance) { - if ($instance->enrol == $method['enrolmethod']) { - $plugin = $enrolmentplugins[$instance->enrol]; - $plugin->delete_instance($instance); - $enrol_updated = true; - break; - } - } - } else if (isset($method['disable']) && $method['disable']) { - // Disable the enrolment. - foreach ($instances as $instance) { - if ($instance->enrol == $method['enrolmethod']) { - $plugin = $enrolmentplugins[$instance->enrol]; - $plugin->update_status($instance, ENROL_INSTANCE_DISABLED); - $enrol_updated = true; - break; - } - } - } else { - // We should have this enrolment method. - $instance = null; - foreach ($instances as $i) { - if ($i->enrol == $method['enrolmethod']) { - $instance = $i; - break; - } - } - $plugin = null; - if (empty($instance)) { - $plugin = $enrolmentplugins[$method['enrolmethod']]; - $instance = new stdClass(); - $instance->id = $plugin->add_default_instance($course); - $instance->roleid = $plugin->get_config('roleid'); - } 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. - if (isset($method['role'])) { - $context = context_course::instance($course->id); - $roles = get_default_enrol_roles($context, $plugin->get_config('roleid')); - if (!empty($roles)) { - $roles = array_flip($roles); - } - if (isset($roles[$method['role']])) { - $instance->roleid = $roles[$method['role']]; - } - } - $instance->status = ENROL_INSTANCE_ENABLED; - $instance->timemodified = time(); - $DB->update_record('enrol', $instance); - $enrol_updated = true; - } - } - - // Do the course reset. - if ($resetcourse) { - $resetdata = new stdClass(); - $resetdata->reset_start_date = time(); - $resetdata->id = $course->id; - $resetdata->reset_events = true; - $resetdata->reset_logs = true; - $resetdata->reset_notes = true; - $resetdata->reset_comments = true; - $resetdata->reset_completion = true; - $resetdata->delete_blog_associations = true; - $roles = get_assignable_roles(context_course::instance($course->id)); - $roles[0] = get_string('noroles', 'role'); - $roles = array_reverse($roles, true); - $resetdata->reset_roles_local = true; - $resetdata->reset_gradebook_items = true; - $resetdata->reset_gradebook_grades = true; - $resetdata->reset_gradebook_items = true; - $resetdata->reset_groups_remove = true; - $resetdata->reset_groups_members = true; - $resetdata->reset_groupings_remove = true; - $resetdata->reset_groupings_members = true; - $resetdata->reset_groups_remove = true; - $resetdata->reset_groups_remove = true; - $resetdata->reset_start_date_old = $course->startdate; - $status = reset_course_userdata($resetdata); - } - - if ($enrol_updated) { - $coursesupdated++; - } - // Invalidate all enrol caches. - $context = context_course::instance($course->id); - $context->mark_dirty(); - } - - // Clean up backup files. - if (!empty($template)) { - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($backupbasepath); - } - } - if (!empty($restorefile)) { - if (empty($CFG->keeptempdirectoriesonbackup)) { - fulldelete($restorefile); - } - } - - $upt->close(); // Close table. - - $cir->close(); - $cir->cleanup(true); - $systemcontext = context_system::instance(); - mark_context_dirty($systemcontext->path); - - if (!$plain) { - echo $OUTPUT->box_start('boxwidthnarrow boxaligncenter generalbox', 'uploadresults'); - echo '

'; - if ($optype != CC_COURSE_UPDATE) { - echo get_string('coursescreated', 'tool_uploadcourse').': '.$coursesnew.'
'; - } - if ($optype == CC_COURSE_UPDATE or $optype == CC_COURSE_ADD_UPDATE) { - echo get_string('coursesupdated', 'tool_uploadcourse').': '.$coursesupdated.'
'; - } - if ($allowdeletes) { - echo get_string('coursesdeleted', 'tool_uploadcourse').': '.$deletes.'
'; - echo get_string('deleteerrors', 'tool_uploadcourse').': '.$deleteerrors.'
'; - } - if ($allowrenames) { - echo get_string('coursesrenamed', 'tool_uploadcourse').': '.$renames.'
'; - echo get_string('renameerrors', 'tool_uploadcourse').': '.$renameerrors.'
'; - } - if ($coursesskipped) { - echo get_string('coursesskipped', 'tool_uploadcourse').': '.$coursesskipped.'
'; - } - echo get_string('errors', 'tool_uploadcourse').': '.$courseserrors.'

'; - } else { - if ($optype != CC_COURSE_UPDATE) { - echo get_string('coursescreated', 'tool_uploadcourse').': '.$coursesnew."\n"; - } - if ($optype == CC_COURSE_UPDATE or $optype == CC_COURSE_ADD_UPDATE) { - echo get_string('coursesupdated', 'tool_uploadcourse').': '.$coursesupdated."\n"; - } - if ($allowdeletes) { - echo get_string('coursesdeleted', 'tool_uploadcourse').': '.$deletes."\n"; - echo get_string('deleteerrors', 'tool_uploadcourse').': '.$deleteerrors."\n"; - } - if ($allowrenames) { - echo get_string('coursesrenamed', 'tool_uploadcourse').': '.$renames."\n"; - echo get_string('renameerrors', 'tool_uploadcourse').': '.$renameerrors."\n"; - } - if ($coursesskipped) { - echo get_string('coursesskipped', 'tool_uploadcourse').': '.$coursesskipped."\n"; - } - echo get_string('errors', 'tool_uploadcourse').': '.$courseserrors."\n"; - echo "The End.\n"; - } -} - - -/** - * Tracking of processed courses. - * - * This class prints course information into a html table. - * - * @copyright 2007 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class tool_uploadcourse_progress_tracker { - /** @var int $_row - row marker */ - private $_row; - /** @var array $columns - output columns */ - public $columns = array('status', 'line', 'id', 'fullname', 'shortname', 'category', 'idnumber', 'summary', 'deleted'); - /** @var boolean $_plain - output is text mode */ - private $_plain; - - /** - * Constructor - * - * @param boolean $type - is this plain text output - */ - public function __construct($type = false) { - $this->_plain = $type; - } - - - /** - * Print table header. - * @return void - */ - public function start() { - $ci = 0; - if ($this->_plain) { - echo "\n\t".get_string('status')."\t".get_string('cccsvline', 'tool_uploadcourse')."\tID\t". - get_string('fullname')."\t".get_string('shortname')."\t".get_string('category')."\t". - get_string('idnumber')."\t".get_string('summary')."\t".get_string('delete')."\n"; - } else { - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - } - $this->_row = null; - } - - /** - * Flush previous line and start a new one. - * @return void - */ - public function flush() { - if (empty($this->_row) or empty($this->_row['line']['normal'])) { - // Nothing to print - each line has to have at least number. - $this->_row = array(); - foreach ($this->columns as $col) { - $this->_row[$col] = array('normal'=>'', 'info'=>'', 'warning'=>'', 'error'=>''); - } - return; - } - $ci = 0; - $ri = 1; - echo $this->_plain ? "" : ''; - foreach ($this->_row as $key => $field) { - foreach ($field as $type => $content) { - if ($field[$type] !== '') { - $field[$type] = $this->_plain ? $field[$type] : ''.$field[$type].''; - } else { - unset($field[$type]); - } - } - echo $this->_plain ? "\t" : ''; - } - echo $this->_plain ? "\n" : ''; - foreach ($this->columns as $col) { - $this->_row[$col] = array('normal'=>'', 'info'=>'', 'warning'=>'', 'error'=>''); - } - } - - /** - * Add tracking info - * @param string $col name of column - * @param string $msg message - * @param string $level 'normal', 'warning' or 'error' - * @param bool $merge true means add as new line, false means override all previous text of the same type - * @return void - */ - public function track($col, $msg, $level = 'normal', $merge = true) { - if (empty($this->_row)) { - $this->flush(); // Init arrays. - } - if (!in_array($col, $this->columns)) { - debugging('Incorrect column:'.$col); - return; - } - if ($merge) { - if ($this->_row[$col][$level] != '') { - $this->_row[$col][$level] .= $this->_plain ? '' : '
'; - } - $this->_row[$col][$level] .= $msg; - } else { - $this->_row[$col][$level] = $msg; - } - } - - /** - * Print the table end - * @return void - */ - public function close() { - $this->flush(); - echo $this->_plain ? "\n" : '
'.get_string('status').''.get_string('cccsvline', 'tool_uploadcourse').'ID'.get_string('fullname').''.get_string('shortname').''.get_string('category').''.get_string('idnumber').''.get_string('summary').''.get_string('delete').'
'; - if (!empty($field)) { - echo implode(($this->_plain ? "|" : '
'), $field); - } else { - echo $this->_plain ? '' : ' '; - } - echo $this->_plain ? '' : '
'; - } -} - -/** - * Validation callback function - verified the column line of csv file. - * Converts standard column names to lowercase. - * @param csv_import_reader $cir - * @param array $stdfields standard course fields - * @param moodle_url $returnurl return url in case of any error - * @return array list of fields - */ -function tool_uploadcourse_validate_course_upload_columns(csv_import_reader $cir, $stdfields, moodle_url $returnurl) { - $columns = $cir->get_columns(); - - if (empty($columns)) { - $cir->close(); - $cir->cleanup(); - print_error('cannotreadtmpfile', 'error', $returnurl); - } - if (count($columns) < 2) { - $cir->close(); - $cir->cleanup(); - print_error('csvfewcolumns', 'error', $returnurl); - } - - // Test columns. - $processed = array(); - foreach ($columns as $key => $unused) { - $field = $columns[$key]; - $lcfield = textlib::strtolower($field); - if (in_array($field, $stdfields) or in_array($lcfield, $stdfields)) { - // Standard fields are only lowercase. - $newfield = $lcfield; - - } else if (preg_match('/^\w+\_\d+$/', $lcfield)) { - // Special fields for enrolments. - $newfield = $lcfield; - - } else { - $cir->close(); - $cir->cleanup(); - print_error('invalidfieldname', 'error', $returnurl, $field); - } - if (in_array($newfield, $processed)) { - $cir->close(); - $cir->cleanup(); - print_error('duplicatefieldname', 'error', $returnurl, $newfield); - } - $processed[$key] = $newfield; - } - - return $processed; -} - -/** - * Increments shortname - increments trailing number or adds it if not present. - * Varifies that the new shortname does not exist yet - * @param string $shortname - * @return incremented shortname which does not exist yet - */ -function tool_uploadcourse_increment_shortname($shortname) { - global $DB, $CFG; - - if (!preg_match_all('/(.*?)([0-9]+)$/', $shortname, $matches)) { - $shortname = $shortname.'2'; - } else { - $shortname = $matches[1][0].($matches[2][0]+1); - } - - if ($DB->record_exists('course', array('shortname'=>$shortname))) { - return tool_uploadcourse_increment_shortname($shortname); - } else { - return $shortname; - } -} - -/** - * Increments idnumber - increments trailing number or adds it if not present. - * Varifies that the new idnumber does not exist yet - * @param string $idnumber - * @return incremented idnumber which does not exist yet - */ -function tool_uploadcourse_increment_idnumber($idnumber) { - global $DB, $CFG; - - if (!preg_match_all('/(.*?)([0-9]+)$/', $idnumber, $matches)) { - $idnumber = $idnumber.'2'; - } else { - $idnumber = $matches[1][0].($matches[2][0]+1); - } - - if ($DB->record_exists('course', array('idnumber'=>$idnumber))) { - return tool_uploadcourse_increment_idnumber($idnumber); - } else { - return $idnumber; - } -} - -/** - * Check if default field contains templates and apply them. - * @param string $template - potential tempalte string - * @param object $course - we need coursename, firstname and lastname - * @return object $template - course template - * @return string $result - field value - */ -function tool_uploadcourse_process_template($template, $course) { - if (is_array($template)) { - // Hack for for support of text editors with format. - $t = $template['text']; - } else { - $t = $template; - } - if (strpos($t, '%') === false) { - return $template; - } - - $shortname = isset($course->shortname) ? $course->shortname : ''; - $fullname = isset($course->fullname) ? $course->fullname : ''; - $idnumber = isset($course->idnumber) ? $course->idnumber : ''; - - $callback = partial('tool_uploadcourse_process_template_callback', $shortname, $fullname, $idnumber); - - $result = preg_replace_callback('/(?. /** - * Link to CSV course upload + * Link to CSV course upload. * * @package tool_uploadcourse - * @subpackage uploadcourse - * @copyright 2010 Petr Skoda {@link http://skodak.org} * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die; +defined('MOODLE_INTERNAL') || die(); -if (has_capability('moodle/restore:restorecourse', $systemcontext)) { +if ($hassiteconfig) { $ADMIN->add('courses', new admin_externalpage('tooluploadcourse', - get_string('uploadcourses', 'tool_uploadcourse'), - "$CFG->wwwroot/$CFG->admin/tool/uploadcourse/index.php", 'moodle/site:uploadusers')); + get_string('uploadcourses', 'tool_uploadcourse'), "/admin/tool/uploadcourse/index.php")); } diff --git a/admin/tool/uploadcourse/tests/course_test.php b/admin/tool/uploadcourse/tests/course_test.php index d5dfb9b4358..793e18467ed 100644 --- a/admin/tool/uploadcourse/tests/course_test.php +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -28,6 +28,10 @@ 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 { @@ -239,7 +243,6 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { '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()); @@ -409,7 +412,6 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { 'enablecompletion' => '1', ); - $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1'))); $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata); $this->assertTrue($co->prepare()); @@ -576,7 +578,8 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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'); + $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(); diff --git a/admin/tool/uploadcourse/tests/helper_test.php b/admin/tool/uploadcourse/tests/helper_test.php index a17e5439263..a61fa674d9e 100644 --- a/admin/tool/uploadcourse/tests/helper_test.php +++ b/admin/tool/uploadcourse/tests/helper_test.php @@ -28,10 +28,14 @@ global $CFG; /** * Helper test case. + * + * @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_testcase extends advanced_testcase { - function test_generate_shortname() { + public function test_generate_shortname() { $data = (object) array('fullname' => 'Ah Bh Ch 01 02 03', 'idnumber' => 'ID123'); $this->assertSame($data->fullname, tool_uploadcourse_helper::generate_shortname($data, '%f')); @@ -41,14 +45,14 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase { $this->assertSame('[Ah Bh Ch] = ID123', tool_uploadcourse_helper::generate_shortname($data, '[%8f] = %i')); } - function test_get_course_formats() { + public function test_get_course_formats() { $result = tool_uploadcourse_helper::get_course_formats(); $this->assertSame(array_keys(get_plugin_list('format')), $result); // Should be similar as first result, as cached. $this->assertSame($result, tool_uploadcourse_helper::get_course_formats()); } - function test_get_enrolment_data() { + public function test_get_enrolment_data() { $this->resetAfterTest(true); $data = array( 'enrolment_1' => 'unknown', @@ -94,16 +98,17 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase { $this->assertSame(tool_uploadcourse_helper::get_enrolment_data($data), $expected); } - function test_get_enrolment_plugins() { + public function test_get_enrolment_plugins() { $this->resetAfterTest(true); $actual = tool_uploadcourse_helper::get_enrolment_plugins(); $this->assertSame(array_keys(enrol_get_plugins(false)), array_keys($actual)); // This should be identical as cached. $secondactual = tool_uploadcourse_helper::get_enrolment_plugins(); - $this->assertSame($actual, $secondactual); + $this->assertEquals($actual, $secondactual); } - function test_get_restore_content_dir() { + public function test_get_restore_content_dir() { + global $CFG; $this->resetAfterTest(true); $this->setAdminUser(); @@ -144,7 +149,6 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase { $this->assertEquals($bcinfo->original_course_id, $c2->id); $this->assertEquals($bcinfo->original_course_fullname, $c2->fullname); - // Checking with a shortname. $dir = tool_uploadcourse_helper::get_restore_content_dir(null, $c1->shortname); $bcinfo = backup_general_helper::get_backup_information($dir); @@ -161,6 +165,21 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase { $this->assertEquals($bcinfo->original_course_id, $c2->id); $this->assertEquals($bcinfo->original_course_fullname, $c2->fullname); + // Cleaning content directories. + $oldcfg = isset($CFG->keeptempdirectoriesonbackup) ? $CFG->keeptempdirectoriesonbackup : false; + $dir = "$CFG->tempdir/backup/$dir"; + $this->assertTrue(file_exists($dir)); + + $CFG->keeptempdirectoriesonbackup = false; + tool_uploadcourse_helper::clean_restore_content(); + $this->assertTrue(file_exists($dir)); + + $CFG->keeptempdirectoriesonbackup = true; + tool_uploadcourse_helper::clean_restore_content(); + $this->assertFalse(file_exists($dir)); + + $CFG->keeptempdirectoriesonbackup = $oldcfg; + // Restore the time limit to prevent warning. set_time_limit(0); } diff --git a/admin/tool/uploadcourse/tests/processor_test.php b/admin/tool/uploadcourse/tests/processor_test.php index 5385ddeb43d..8aee781a219 100644 --- a/admin/tool/uploadcourse/tests/processor_test.php +++ b/admin/tool/uploadcourse/tests/processor_test.php @@ -25,9 +25,14 @@ defined('MOODLE_INTERNAL') || die(); global $CFG; +require_once($CFG->libdir . '/csvlib.class.php'); /** * Processor test case. + * + * @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_testcase extends advanced_testcase { diff --git a/admin/tool/uploadcourse/version.php b/admin/tool/uploadcourse/version.php index 1139fb236ad..112b41cb2e0 100644 --- a/admin/tool/uploadcourse/version.php +++ b/admin/tool/uploadcourse/version.php @@ -15,17 +15,15 @@ // along with Moodle. If not, see . /** - * Plugin version info + * Plugin version info. * * @package tool_uploadcourse - * @subpackage uploadcourse - * @copyright 2011 Petr Skoda {@link http://skodak.org} * @copyright 2011 Piers Harding * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -defined('MOODLE_INTERNAL') || die; -$plugin->version = 2012112301; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2011091600; // Requires this Moodle version -$plugin->component = 'tool_uploadcourse'; // Full name of the plugin (used for diagnostics). +defined('MOODLE_INTERNAL') || die(); +$plugin->version = 2013070200; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2013062100; // Requires this Moodle version. +$plugin->component = 'tool_uploadcourse'; // Full name of the plugin (used for diagnostics). From 8c313d1a7f15549b6511a3d460792e29850816ae Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 8 Jul 2013 18:15:17 +0800 Subject: [PATCH 15/21] MDL-13114 tool_uploadcourse: Helper uses MUC for caching --- admin/tool/uploadcourse/classes/helper.php | 112 ++++++++++-------- admin/tool/uploadcourse/db/caches.php | 31 +++++ .../lang/en/tool_uploadcourse.php | 1 + 3 files changed, 97 insertions(+), 47 deletions(-) create mode 100644 admin/tool/uploadcourse/db/caches.php diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php index ad1e36d917a..917e265ee3c 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -24,6 +24,7 @@ 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'); @@ -36,24 +37,6 @@ require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); */ class tool_uploadcourse_helper { - /** @var array category idnumber cache. */ - static protected $categoryidnumbercache = array(); - - /** @var array category path cache. */ - static protected $categorypathcache = array(); - - /** @var array course formats cache. */ - static protected $courseformatscache; - - /** @var array enrolment plugins cache. */ - static protected $enrolpluginscache; - - /** @var string restore content path cache. */ - static protected $restorecontentcache; - - /** @var array roles cache. */ - static protected $rolescache; - /** * Remove the restore content from disk and cache. * @@ -62,11 +45,14 @@ class tool_uploadcourse_helper { public static function clean_restore_content() { global $CFG; if (!empty($CFG->keeptempdirectoriesonbackup)) { - foreach (self::$restorecontentcache as $backupid) { + $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'); } - self::$restorecontentcache = array(); } /** @@ -147,10 +133,12 @@ class tool_uploadcourse_helper { * @return array */ public static function get_course_formats() { - if (empty(self::$courseformatscache)) { - self::$courseformatscache = array_keys(get_plugin_list('format')); + $cache = cache::make('tool_uploadcourse', 'helper'); + if (($formats = $cache->get('format')) === false) { + $formats = array_keys(get_plugin_list('format')); + $cache->set('format', $formats); } - return self::$courseformatscache; + return $formats; } /** @@ -214,10 +202,12 @@ class tool_uploadcourse_helper { * @return array */ public static function get_enrolment_plugins() { - if (empty(self::$enrolpluginscache)) { - self::$enrolpluginscache = enrol_get_plugins(false); + $cache = cache::make('tool_uploadcourse', 'helper'); + if (($enrol = $cache->get('enrol')) === false) { + $enrol = enrol_get_plugins(false); + $cache->set('enrol', $enrol); } - return self::$enrolpluginscache; + return $enrol; } /** @@ -237,16 +227,17 @@ class tool_uploadcourse_helper { $cachekey = null; if (!empty($backupfile)) { $backupfile = realpath($backupfile); - $cachekey = '[path]:' . $backupfile; + $cachekey = 'backup_path:' . $backupfile; } else if (!empty($shortname) || is_numeric($shortname)) { - $cachekey = '[sn]:' . $shortname; + $cachekey = 'backup_sn:' . $shortname; } if (empty($cachekey)) { return false; } - if (!isset(self::$restorecontentcache[$cachekey])) { + $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; @@ -277,10 +268,15 @@ class tool_uploadcourse_helper { new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse'); } } - self::$restorecontentcache[$cachekey] = $backupid; + $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 self::$restorecontentcache[$cachekey]; + return $backupid; } /** @@ -288,17 +284,19 @@ class tool_uploadcourse_helper { * * The result is cached for faster execution. * - * @param bool $invalidate invalidate the cache. * @return array */ - public static function get_role_ids($invalidate = false) { - if (empty(self::$rolescache) || $invalidate) { - $roles = get_all_roles(); - foreach ($roles as $role) { - self::$rolescache[$role->shortname] = $role->id; + 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 self::$rolescache; + return $roles; } /** @@ -429,12 +427,26 @@ class tool_uploadcourse_helper { */ public static function resolve_category_by_idnumber($idnumber) { global $DB; - if (!isset(self::$categoryidnumbercache[$idnumber])) { + $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); - self::$categoryidnumbercache[$idnumber] = $id; + + // 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); } - return self::$categoryidnumbercache[$idnumber]; + + // Little hack to be able to differenciate between the cache not set and a category not found. + if ($id == -1) { + $id = false; + } + + return $id; } /** @@ -445,8 +457,9 @@ class tool_uploadcourse_helper { */ public static function resolve_category_by_path(array $path) { global $DB; - $cachekey = serialize($path); - if (!isset(self::$categorypathcache[$cachekey])) { + $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)) { @@ -454,7 +467,7 @@ class tool_uploadcourse_helper { 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 = false; + $id = -1; break; } $record = reset($records); @@ -462,13 +475,18 @@ class tool_uploadcourse_helper { $parent = $record->id; } else { // Not found. - $id = false; + $id = -1; break; } } - self::$categorypathcache[$cachekey] = $id; + $cache->set($cachekey, $id); } - return self::$categorypathcache[$cachekey]; + + // 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/db/caches.php b/admin/tool/uploadcourse/db/caches.php new file mode 100644 index 00000000000..7c0c7c0989f --- /dev/null +++ b/admin/tool/uploadcourse/db/caches.php @@ -0,0 +1,31 @@ +. + +/** + * Cache definitions. + * + * @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(); + +$definitions = array( + 'helper' => array( + 'mode' => cache_store::MODE_REQUEST, + ) +); diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php index 691d2c816ff..83138d36a49 100644 --- a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -25,6 +25,7 @@ $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'; From f802b04f0370bae00eb12421dcac6b5cb6fa4c28 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 9 Jul 2013 11:38:11 +0800 Subject: [PATCH 16/21] MDL-13114 tool_uploadcourse: Better Unit Test coverage --- admin/tool/uploadcourse/classes/helper.php | 14 +- admin/tool/uploadcourse/tests/course_test.php | 121 +++++++++++++++++- admin/tool/uploadcourse/tests/helper_test.php | 16 ++- .../uploadcourse/tests/processor_test.php | 56 ++++++++ 4 files changed, 198 insertions(+), 9 deletions(-) diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php index 917e265ee3c..72444e1b144 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -60,10 +60,10 @@ class tool_uploadcourse_helper { * * @param array|object $data course data. * @param string $templateshortname template of shortname. - * @return null|string shortname based on the template. + * @return null|string shortname based on the template, or null when an error occured. */ public static function generate_shortname($data, $templateshortname) { - if (is_null($templateshortname)) { + if (empty($templateshortname) && !is_numeric($templateshortname)) { return null; } if (strpos($templateshortname, '%') === false) { @@ -71,30 +71,32 @@ class tool_uploadcourse_helper { } $course = (object) $data; - $shortname = isset($course->shortname) ? $course->shortname : ''; $fullname = isset($course->fullname) ? $course->fullname : ''; $idnumber = isset($course->idnumber) ? $course->idnumber : ''; - $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $shortname, $fullname, $idnumber); + $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber); $result = preg_replace_callback('/(?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' => '', '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); @@ -497,6 +519,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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; @@ -566,6 +589,24 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $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' => 'invalid'); + $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() { @@ -779,7 +820,7 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { public function test_idnumber_problems() { $this->resetAfterTest(true); - $c1 = $this->getDataGenerator()->create_course(array('idnumber' => 'Taken')); + $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'sntaken', 'idnumber' => 'taken')); $c2 = $this->getDataGenerator()->create_course(); // Create with existing ID number. @@ -799,6 +840,84 @@ class tool_uploadcourse_course_testcase extends advanced_testcase { $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); $this->assertFalse($co->prepare()); $this->assertArrayHasKey('cannotrenameidnumberconflict', $co->get_errors()); + + // Incrementing shortname increments idnumber. + $mode = tool_uploadcourse_processor::MODE_CREATE_ALL; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('shortname' => $c1->shortname, 'idnumber' => $c1->idnumber, 'summary' => 'S', 'fullname' => 'F', + 'category' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), array()); + $this->assertTrue($co->prepare()); + $this->assertArrayHasKey('courseshortnameincremented', $co->get_statuses()); + $this->assertArrayHasKey('courseidnumberincremented', $co->get_statuses()); + $data = $co->get_data(); + $this->assertEquals('sntaken_2', $data['shortname']); + $this->assertEquals('taken_2', $data['idnumber']); + + // Incrementing shortname increments idnumber unless available. + $mode = tool_uploadcourse_processor::MODE_CREATE_ALL; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('shortname' => $c1->shortname, 'idnumber' => 'nottaken', 'summary' => 'S', 'fullname' => 'F', + 'category' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), array()); + $this->assertTrue($co->prepare()); + $this->assertArrayHasKey('courseshortnameincremented', $co->get_statuses()); + $this->assertArrayNotHasKey('courseidnumberincremented', $co->get_statuses()); + $data = $co->get_data(); + $this->assertEquals('sntaken_2', $data['shortname']); + $this->assertEquals('nottaken', $data['idnumber']); + } + + public function test_generate_shortname() { + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'taken')); + + // Generate a shortname. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('summary' => 'summary', 'fullname' => 'FN', 'category' => '1', 'idnumber' => 'IDN'); + $importoptions = array('shortnametemplate' => '%i'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertTrue($co->prepare()); + $this->assertArrayHasKey('courseshortnamegenerated', $co->get_statuses()); + + // Generate a shortname without a template. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('summary' => 'summary', 'fullname' => 'FN', 'category' => '1'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), array()); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('missingshortnamenotemplate', $co->get_errors()); + + // Generate a shortname in update mode. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('summary' => 'summary', 'fullname' => 'FN', 'category' => '1'); + $importoptions = array('shortnametemplate' => '%f'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + // Commented because we never get here as the course without shortname does not exist. + // $this->assertArrayHasKey('cannotgenerateshortnameupdatemode', $co->get_errors()); + + // Generate a shortname to a course that already exists. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('summary' => 'summary', 'fullname' => 'taken', 'category' => '1'); + $importoptions = array('shortnametemplate' => '%f'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('generatedshortnamealreadyinuse', $co->get_errors()); + + // Generate a shortname to a course that already exists will be incremented. + $mode = tool_uploadcourse_processor::MODE_CREATE_ALL; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('summary' => 'summary', 'fullname' => 'taken', 'category' => '1'); + $importoptions = array('shortnametemplate' => '%f'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertTrue($co->prepare()); + $this->assertArrayHasKey('courseshortnamegenerated', $co->get_statuses()); + $this->assertArrayHasKey('courseshortnameincremented', $co->get_statuses()); } } diff --git a/admin/tool/uploadcourse/tests/helper_test.php b/admin/tool/uploadcourse/tests/helper_test.php index a61fa674d9e..f81c59f4a55 100644 --- a/admin/tool/uploadcourse/tests/helper_test.php +++ b/admin/tool/uploadcourse/tests/helper_test.php @@ -36,13 +36,19 @@ global $CFG; class tool_uploadcourse_helper_testcase extends advanced_testcase { public function test_generate_shortname() { - $data = (object) array('fullname' => 'Ah Bh Ch 01 02 03', 'idnumber' => 'ID123'); + $data = (object) array('fullname' => 'Ah bh Ch 01 02 03', 'idnumber' => 'ID123'); $this->assertSame($data->fullname, tool_uploadcourse_helper::generate_shortname($data, '%f')); $this->assertSame($data->idnumber, tool_uploadcourse_helper::generate_shortname($data, '%i')); + $this->assertSame('Ah Bh Ch', tool_uploadcourse_helper::generate_shortname($data, '%~8f')); $this->assertSame('AH BH CH', tool_uploadcourse_helper::generate_shortname($data, '%+8f')); $this->assertSame('id123', tool_uploadcourse_helper::generate_shortname($data, '%-i')); - $this->assertSame('[Ah Bh Ch] = ID123', tool_uploadcourse_helper::generate_shortname($data, '[%8f] = %i')); + $this->assertSame('[Ah bh Ch] = ID123', tool_uploadcourse_helper::generate_shortname($data, '[%8f] = %i')); + $this->assertSame('0', tool_uploadcourse_helper::generate_shortname($data, '0')); + $this->assertSame('%unknown', tool_uploadcourse_helper::generate_shortname($data, '%unknown')); + + $this->assertNull(tool_uploadcourse_helper::generate_shortname($data, '')); + $this->assertNull(tool_uploadcourse_helper::generate_shortname(array(), '%f')); } public function test_get_course_formats() { @@ -165,6 +171,12 @@ class tool_uploadcourse_helper_testcase extends advanced_testcase { $this->assertEquals($bcinfo->original_course_id, $c2->id); $this->assertEquals($bcinfo->original_course_fullname, $c2->fullname); + // Get a course that does not exist. + $errors = array(); + $dir = tool_uploadcourse_helper::get_restore_content_dir(null, 'DoesNotExist', $errors); + $this->assertFalse($dir); + $this->assertArrayHasKey('coursetorestorefromdoesnotexist', $errors); + // Cleaning content directories. $oldcfg = isset($CFG->keeptempdirectoriesonbackup) ? $CFG->keeptempdirectoriesonbackup : false; $dir = "$CFG->tempdir/backup/$dir"; diff --git a/admin/tool/uploadcourse/tests/processor_test.php b/admin/tool/uploadcourse/tests/processor_test.php index 8aee781a219..648fcc6d936 100644 --- a/admin/tool/uploadcourse/tests/processor_test.php +++ b/admin/tool/uploadcourse/tests/processor_test.php @@ -166,4 +166,60 @@ class tool_uploadcourse_processor_testcase extends advanced_testcase { $this->assertEquals('ID123: Course 1', $c->shortname); } + public function test_empty_csv() { + $this->resetAfterTest(true); + + $content = array(); + $content = implode("\n", $content); + $iid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($iid, 'uploadcourse'); + $cir->load_csv_content($content, 'utf-8', 'comma'); + $cir->init(); + + $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW); + $this->setExpectedException('moodle_exception'); + $p = new tool_uploadcourse_processor($cir, $options, array()); + } + + public function test_not_enough_columns() { + $this->resetAfterTest(true); + + $content = array( + "shortname", + "c1", + ); + $content = implode("\n", $content); + $iid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($iid, 'uploadcourse'); + $cir->load_csv_content($content, 'utf-8', 'comma'); + $cir->init(); + + $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_NEW); + $this->setExpectedException('moodle_exception'); + $p = new tool_uploadcourse_processor($cir, $options, array()); + } + + public function test_preview() { + global $DB; + $this->resetAfterTest(true); + + $content = array( + "shortname,fullname,summary", + "c1,Course 1,Course 1 summary", + "c2,Course 2,Course 2 summary", + ); + $content = implode("\n", $content); + $iid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($iid, 'uploadcourse'); + $cir->load_csv_content($content, 'utf-8', 'comma'); + $cir->init(); + + $options = array('mode' => tool_uploadcourse_processor::MODE_CREATE_ALL); + $defaults = array('category' => '1'); + + $p = new tool_uploadcourse_processor($cir, $options, $defaults); + // Nothing special to expect here, just make sure no exceptions are thrown. + $p->preview(); + } + } From 76fca7168cfb02f09eb9590889a23a3e046c4566 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 9 Jul 2013 13:34:14 +0800 Subject: [PATCH 17/21] MDL-13114 tool_uploadcourse: Simple Behat tests for create and update --- .../uploadcourse/tests/behat/create.feature | 46 +++++++++++++++++++ .../uploadcourse/tests/behat/update.feature | 32 +++++++++++++ .../uploadcourse/tests/fixtures/courses.csv | 4 ++ 3 files changed, 82 insertions(+) create mode 100644 admin/tool/uploadcourse/tests/behat/create.feature create mode 100644 admin/tool/uploadcourse/tests/behat/update.feature create mode 100644 admin/tool/uploadcourse/tests/fixtures/courses.csv diff --git a/admin/tool/uploadcourse/tests/behat/create.feature b/admin/tool/uploadcourse/tests/behat/create.feature new file mode 100644 index 00000000000..ee95ca1f525 --- /dev/null +++ b/admin/tool/uploadcourse/tests/behat/create.feature @@ -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 index 00000000000..3a06d163c2e --- /dev/null +++ b/admin/tool/uploadcourse/tests/behat/update.feature @@ -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/fixtures/courses.csv b/admin/tool/uploadcourse/tests/fixtures/courses.csv new file mode 100644 index 00000000000..2daebe0bd44 --- /dev/null +++ b/admin/tool/uploadcourse/tests/fixtures/courses.csv @@ -0,0 +1,4 @@ +shortname,fullname,summary,category,idnumber +C1,Course 1,Summary 1,1,ID1 +C2,Course 2,Summary 2,1,ID2 +C3,Course 3,Summary 3,1,ID3 \ No newline at end of file From 22b3be36338f01dcbea2f4e710857c92022cbc01 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Tue, 9 Jul 2013 16:34:31 +0800 Subject: [PATCH 18/21] MDL-13114 tool_uploadcourse: Clean up after scheck scan --- admin/tool/uploadcourse/classes/helper.php | 1 - admin/tool/uploadcourse/classes/step2_form.php | 7 +------ admin/tool/uploadcourse/classes/tracker.php | 2 -- admin/tool/uploadcourse/cli/uploadcourse.php | 4 ++-- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php index 72444e1b144..8a1fb2c7bdf 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -390,7 +390,6 @@ class tool_uploadcourse_helper { * @return int category ID. */ public static function resolve_category($data, &$errors = array()) { - global $DB; $catid = null; if (!empty($data['category'])) { diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php index 711dd6322e2..7464e4ea627 100644 --- a/admin/tool/uploadcourse/classes/step2_form.php +++ b/admin/tool/uploadcourse/classes/step2_form.php @@ -38,7 +38,7 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { * @return void. */ public function definition () { - global $CFG, $COURSE, $DB; + global $CFG; $mform = $this->_form; $data = $this->_customdata['data']; @@ -134,9 +134,6 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { $mform->setDefault('defaults[showreports]', $courseconfig->showreports); if (!empty($CFG->legacyfilesinnewcourses)) { - if (empty($course->legacyfiles)) { - $choices = array('0' => get_string('no'), '2' => get_string('yes')); - } $mform->addElement('select', 'defaults[legacyfiles]', get_string('courselegacyfiles'), $choices); $mform->addHelpButton('defaults[legacyfiles]', 'courselegacyfiles'); if (!isset($courseconfig->legacyfiles)) { @@ -198,8 +195,6 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { * @return array $errors - form errors */ public function validation($data, $files) { - global $DB; - $errors = parent::validation($data, $files); $columns = $this->_customdata['columns']; $optype = $data['options']['mode']; diff --git a/admin/tool/uploadcourse/classes/tracker.php b/admin/tool/uploadcourse/classes/tracker.php index 23374f60b88..0daa84527c5 100644 --- a/admin/tool/uploadcourse/classes/tracker.php +++ b/admin/tool/uploadcourse/classes/tracker.php @@ -107,8 +107,6 @@ class tool_uploadcourse_tracker { * @return void */ public function results($total, $created, $updated, $deleted, $errors) { - global $OUTPUT; - if ($this->outputmode == self::NO_OUTPUT) { return; } diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php index 0e2fefc0441..a232bee2051 100644 --- a/admin/tool/uploadcourse/cli/uploadcourse.php +++ b/admin/tool/uploadcourse/cli/uploadcourse.php @@ -192,9 +192,9 @@ $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', $returnurl, $cir->get_error()); + print_error('csvfileerror', 'tool_uploadcourse', '', $cir->get_error()); } else if ($readcount == 0) { - print_error('csvemptyfile', 'error', $returnurl, $cir->get_error()); + 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)); From a1af15de039708235fde059ae0fefe47f2030e0b Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Thu, 11 Jul 2013 10:13:18 +0800 Subject: [PATCH 19/21] MDL-13114 tool_uploadcourse: Removing summary from mandatory fields --- admin/tool/uploadcourse/classes/course.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php index 48ccc313e3b..629e498f82c 100644 --- a/admin/tool/uploadcourse/classes/course.php +++ b/admin/tool/uploadcourse/classes/course.php @@ -98,7 +98,7 @@ class tool_uploadcourse_course { 'groupmode', 'groupmodeforce', 'groupmodeforce', 'enablecompletion'); /** @var array fields required on course creation. */ - static protected $mandatoryfields = array('fullname', 'summary', 'category'); + static protected $mandatoryfields = array('fullname', 'category'); /** @var array fields which are considered as options. */ static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null, From 41525e551073fed3f89803e180790dd96d2187d8 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 15 Jul 2013 10:02:48 +0800 Subject: [PATCH 20/21] MDL-13114 tool_uploadcourse: Removing use of deprecated get_plugin_list() --- admin/tool/uploadcourse/classes/helper.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php index 8a1fb2c7bdf..b328a423437 100644 --- a/admin/tool/uploadcourse/classes/helper.php +++ b/admin/tool/uploadcourse/classes/helper.php @@ -130,17 +130,10 @@ class tool_uploadcourse_helper { /** * Return the available course formats. * - * The result is cached for faster execution. - * * @return array */ public static function get_course_formats() { - $cache = cache::make('tool_uploadcourse', 'helper'); - if (($formats = $cache->get('format')) === false) { - $formats = array_keys(get_plugin_list('format')); - $cache->set('format', $formats); - } - return $formats; + return array_keys(core_component::get_plugin_list('format')); } /** From c122e7845d07fac90d27cdd604dc9e26ca45ba6f Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Mon, 15 Jul 2013 11:45:34 +0800 Subject: [PATCH 21/21] MDL-13114 tool_uploadcourse: Adding to standard plugins --- lib/pluginlib.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 1706683964b..8ba08fdf3bd 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -838,7 +838,7 @@ class plugin_manager { 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon', 'langimport', 'multilangupgrade', 'phpunit', 'profiling', 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', - 'unittest', 'uploaduser', 'unsuproles', 'xmldb' + 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb' ), 'webservice' => array(