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). +