diff --git a/admin/tool/uploadcourse/classes/base_form.php b/admin/tool/uploadcourse/classes/base_form.php new file mode 100644 index 00000000000..fc0ddf9a3db --- /dev/null +++ b/admin/tool/uploadcourse/classes/base_form.php @@ -0,0 +1,93 @@ +. + +/** + * File containing the base import form. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir.'/formslib.php'); + +/** + * Base import form. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_base_form extends moodleform { + + /** + * Empty definition. + * + * @return void + */ + public function definition() { + } + + /** + * Adds the import settings part. + * + * @return void + */ + public function add_import_options() { + $mform = $this->_form; + + // Upload settings and file. + $mform->addElement('header', 'importoptionshdr', get_string('importoptions', 'tool_uploadcourse')); + $mform->setExpanded('importoptionshdr', true); + + $choices = array( + tool_uploadcourse_processor::MODE_CREATE_NEW => get_string('createnew', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('createall', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('createorupdate', 'tool_uploadcourse'), + tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('updateonly', 'tool_uploadcourse') + ); + $mform->addElement('select', 'options[mode]', get_string('mode', 'tool_uploadcourse'), $choices); + + $choices = array( + tool_uploadcourse_processor::UPDATE_NOTHING => get_string('nochanges', 'tool_uploadcourse'), + tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY => get_string('updatewithdataonly', 'tool_uploadcourse'), + tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS => + get_string('updatewithdataordefaults', 'tool_uploadcourse'), + tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('updatemissing', 'tool_uploadcourse') + ); + $mform->addElement('select', 'options[updatemode]', get_string('updatemode', 'tool_uploadcourse'), $choices); + $mform->setDefault('options[updatemode]', tool_uploadcourse_processor::UPDATE_NOTHING); + $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); + + $mform->addElement('selectyesno', 'options[allowdeletes]', get_string('allowdeletes', 'tool_uploadcourse')); + $mform->setDefault('options[allowdeletes]', 0); + $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); + + $mform->addElement('selectyesno', 'options[allowrenames]', get_string('allowrenames', 'tool_uploadcourse')); + $mform->setDefault('options[allowrenames]', 0); + $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); + + $mform->addElement('selectyesno', 'options[allowresets]', get_string('allowresets', 'tool_uploadcourse')); + $mform->setDefault('options[allowresets]', 0); + $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); + } + +} diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php new file mode 100644 index 00000000000..629e498f82c --- /dev/null +++ b/admin/tool/uploadcourse/classes/course.php @@ -0,0 +1,898 @@ +. + +/** + * File containing the course class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); +require_once($CFG->dirroot . '/course/lib.php'); + +/** + * Course class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_course { + + /** Outcome of the process: creating the course */ + const DO_CREATE = 1; + + /** Outcome of the process: updating the course */ + const DO_UPDATE = 2; + + /** Outcome of the process: deleting the course */ + const DO_DELETE = 3; + + /** @var array final import data. */ + protected $data = array(); + + /** @var array default values. */ + protected $defaults = array(); + + /** @var array enrolment data. */ + protected $enrolmentdata; + + /** @var array errors. */ + protected $errors = array(); + + /** @var int the ID of the course that had been processed. */ + protected $id; + + /** @var array containing options passed from the processor. */ + protected $importoptions = array(); + + /** @var int import mode. Matches tool_uploadcourse_processor::MODE_* */ + protected $mode; + + /** @var array course import options. */ + protected $options = array(); + + /** @var int constant value of self::DO_*, what to do with that course */ + protected $do; + + /** @var bool set to true once we have prepared the course */ + protected $prepared = false; + + /** @var bool set to true once we have started the process of the course */ + protected $processstarted = false; + + /** @var array course import data. */ + protected $rawdata = array(); + + /** @var array restore directory. */ + protected $restoredata; + + /** @var string course shortname. */ + protected $shortname; + + /** @var array errors. */ + protected $statuses = array(); + + /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */ + protected $updatemode; + + /** @var array fields allowed as course data. */ + static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate', + 'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes', + 'groupmode', 'groupmodeforce', 'groupmodeforce', 'enablecompletion'); + + /** @var array fields required on course creation. */ + static protected $mandatoryfields = array('fullname', 'category'); + + /** @var array fields which are considered as options. */ + static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null, + 'templatecourse' => null, 'reset' => false); + + /** @var array options determining what can or cannot be done at an import level. */ + static protected $importoptionsdefaults = array('canrename' => false, 'candelete' => false, 'canreset' => false, + 'reset' => false, 'restoredir' => null, 'shortnametemplate' => null); + + /** + * Constructor + * + * @param int $mode import mode, constant matching tool_uploadcourse_processor::MODE_* + * @param int $updatemode update mode, constant matching tool_uploadcourse_processor::UPDATE_* + * @param array $rawdata raw course data. + * @param array $defaults default course data. + * @param array $importoptions import options. + */ + public function __construct($mode, $updatemode, $rawdata, $defaults = array(), $importoptions = array()) { + + if ($mode !== tool_uploadcourse_processor::MODE_CREATE_NEW && + $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL && + $mode !== tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE && + $mode !== tool_uploadcourse_processor::MODE_UPDATE_ONLY) { + throw new coding_exception('Incorrect mode.'); + } else if ($updatemode !== tool_uploadcourse_processor::UPDATE_NOTHING && + $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY && + $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS && + $updatemode !== tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS) { + throw new coding_exception('Incorrect update mode.'); + } + + $this->mode = $mode; + $this->updatemode = $updatemode; + + if (isset($rawdata['shortname'])) { + $this->shortname = $rawdata['shortname']; + } + $this->rawdata = $rawdata; + $this->defaults = $defaults; + + // Extract course options. + foreach (self::$optionfields as $option => $default) { + $this->options[$option] = isset($rawdata[$option]) ? $rawdata[$option] : $default; + } + + // Import options. + foreach (self::$importoptionsdefaults as $option => $default) { + $this->importoptions[$option] = isset($importoptions[$option]) ? $importoptions[$option] : $default; + } + } + + /** + * Does the mode allow for course creation? + * + * @return bool + */ + public function can_create() { + return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL, + tool_uploadcourse_processor::MODE_CREATE_NEW, + tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE) + ); + } + + /** + * Does the mode allow for course deletion? + * + * @return bool + */ + public function can_delete() { + return $this->importoptions['candelete']; + } + + /** + * Does the mode only allow for course creation? + * + * @return bool + */ + public function can_only_create() { + return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL, + tool_uploadcourse_processor::MODE_CREATE_NEW)); + } + + /** + * Does the mode allow for course rename? + * + * @return bool + */ + public function can_rename() { + return $this->importoptions['canrename']; + } + + /** + * Does the mode allow for course reset? + * + * @return bool + */ + public function can_reset() { + return $this->importoptions['canreset']; + } + + /** + * Does the mode allow for course update? + * + * @return bool + */ + public function can_update() { + return in_array($this->mode, + array( + tool_uploadcourse_processor::MODE_UPDATE_ONLY, + tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE) + ) && $this->updatemode != tool_uploadcourse_processor::UPDATE_NOTHING; + } + + /** + * Can we use default values? + * + * @return bool + */ + public function can_use_defaults() { + return in_array($this->updatemode, array(tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS, + tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS)); + } + + /** + * Delete the current course. + * + * @return bool + */ + protected function delete() { + global $DB; + $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname', + array('shortname' => $this->shortname), MUST_EXIST); + return delete_course($this->id, false); + } + + /** + * Log an error + * + * @param string $code error code. + * @param lang_string $message error message. + * @return void + */ + protected function error($code, lang_string $message) { + if (array_key_exists($code, $this->errors)) { + throw new coding_exception('Error code already defined'); + } + $this->errors[$code] = $message; + } + + /** + * Return whether the course exists or not. + * + * @param string $shortname the shortname to use to check if the course exists. Falls back on $this->shortname if empty. + * @return bool + */ + protected function exists($shortname = null) { + global $DB; + if (is_null($shortname)) { + $shortname = $this->shortname; + } + if (!empty($shortname) || is_numeric($shortname)) { + return $DB->record_exists('course', array('shortname' => $shortname)); + } + return false; + } + + /** + * Return the data that will be used upon saving. + * + * @return null|array + */ + public function get_data() { + return $this->data; + } + + /** + * Return the errors found during preparation. + * + * @return array + */ + public function get_errors() { + return $this->errors; + } + + /** + * Assemble the course data based on defaults. + * + * This returns the final data to be passed to create_course(). + * + * @param array $data current data. + * @return array + */ + protected function get_final_create_data($data) { + foreach (self::$validfields as $field) { + if (!isset($data[$field]) && isset($this->defaults[$field])) { + $data[$field] = $this->defaults[$field]; + } + } + $data['shortname'] = $this->shortname; + return $data; + } + + /** + * Assemble the course data based on defaults. + * + * This returns the final data to be passed to update_course(). + * + * @param array $data current data. + * @param bool $usedefaults are defaults allowed? + * @param bool $missingonly ignore fields which are already set. + * @return array + */ + protected function get_final_update_data($data, $usedefaults = false, $missingonly = false) { + global $DB; + $newdata = array(); + $existingdata = $DB->get_record('course', array('shortname' => $this->shortname)); + foreach (self::$validfields as $field) { + if ($missingonly) { + if (!is_null($existingdata->$field) and $existingdata->$field !== '') { + continue; + } + } + if (isset($data[$field])) { + $newdata[$field] = $data[$field]; + } else if ($usedefaults && isset($this->defaults[$field])) { + $newdata[$field] = $this->defaults[$field]; + } + } + $newdata['id'] = $existingdata->id; + return $newdata; + } + + /** + * Return the ID of the processed course. + * + * @return int|null + */ + public function get_id() { + if (!$this->processstarted) { + throw new coding_exception('The course has not been processed yet!'); + } + return $this->id; + } + + /** + * Get the directory of the object to restore. + * + * @return string|false subdirectory in $CFG->tempdir/backup/... + */ + protected function get_restore_content_dir() { + $backupfile = null; + $shortname = null; + + if (!empty($this->options['backupfile'])) { + $backupfile = $this->options['backupfile']; + } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) { + $shortname = $this->options['templatecourse']; + } + + $errors = array(); + $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname, $errors); + if (!empty($errors)) { + foreach ($errors as $key => $message) { + $this->error($key, $message); + } + return false; + } + + if (empty($dir) && !empty($this->importoptions['restoredir'])) { + $dir = $this->importoptions['restoredir']; + } + + return $dir; + } + + /** + * Return the errors found during preparation. + * + * @return array + */ + public function get_statuses() { + return $this->statuses; + } + + /** + * Return whether there were errors with this course. + * + * @return boolean + */ + public function has_errors() { + return !empty($this->errors); + } + + /** + * Validates and prepares the data. + * + * @return bool false is any error occured. + */ + public function prepare() { + global $DB; + $this->prepared = true; + + // Validate the shortname. + if (!empty($this->shortname) || is_numeric($this->shortname)) { + if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) { + $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse')); + return false; + } + } + + $exists = $this->exists(); + + // Do we want to delete the course? + if ($this->options['delete']) { + if (!$exists) { + $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse')); + return false; + } else if (!$this->can_delete()) { + $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse')); + return false; + } + + $this->do = self::DO_DELETE; + return true; + } + + // Can we create/update the course under those conditions? + if ($exists) { + if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { + $this->error('courseexistsanduploadnotallowed', + new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); + return false; + } + } else { + if (!$this->can_create()) { + $this->error('coursedoesnotexistandcreatenotallowed', + new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse')); + return false; + } + } + + // Basic data. + $coursedata = array(); + foreach ($this->rawdata as $field => $value) { + if (!in_array($field, self::$validfields)) { + continue; + } else if ($field == 'shortname') { + // Let's leave it apart from now, use $this->shortname only. + continue; + } + $coursedata[$field] = $value; + } + + $mode = $this->mode; + $updatemode = $this->updatemode; + $usedefaults = $this->can_use_defaults(); + + // Resolve the category, and fail if not found. + $errors = array(); + $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors); + if (empty($errors)) { + $coursedata['category'] = $catid; + } else { + foreach ($errors as $key => $message) { + $this->error($key, $message); + } + return false; + } + + // If the course does not exist, or will be forced created. + if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { + + // Mandatory fields upon creation. + $errors = array(); + foreach (self::$mandatoryfields as $field) { + if ((!isset($coursedata[$field]) || $coursedata[$field] === '') && + (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) { + $errors[] = $field; + } + } + if (!empty($errors)) { + $this->error('missingmandatoryfields', new lang_string('missingmandatoryfields', 'tool_uploadcourse', + implode(', ', $errors))); + return false; + } + } + + // Should the course be renamed? + if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) { + if (!$this->can_update()) { + $this->error('canonlyrenameinupdatemode', new lang_string('canonlyrenameinupdatemode', 'tool_uploadcourse')); + return false; + } else if (!$exists) { + $this->error('cannotrenamecoursenotexist', new lang_string('cannotrenamecoursenotexist', 'tool_uploadcourse')); + return false; + } else if (!$this->can_rename()) { + $this->error('courserenamingnotallowed', new lang_string('courserenamingnotallowed', 'tool_uploadcourse')); + return false; + } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) { + $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse')); + return false; + } else if ($this->exists($this->options['rename'])) { + $this->error('cannotrenameshortnamealreadyinuse', + new lang_string('cannotrenameshortnamealreadyinuse', 'tool_uploadcourse')); + return false; + } else if (isset($coursedata['idnumber']) && + $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn', + array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) { + $this->error('cannotrenameidnumberconflict', new lang_string('cannotrenameidnumberconflict', 'tool_uploadcourse')); + return false; + } + $coursedata['shortname'] = $this->options['rename']; + $this->status('courserenamed', new lang_string('courserenamed', 'tool_uploadcourse', + array('from' => $this->shortname, 'to' => $coursedata['shortname']))); + } + + // Should we generate a shortname? + if (empty($this->shortname) && !is_numeric($this->shortname)) { + if (empty($this->importoptions['shortnametemplate'])) { + $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse')); + return false; + } else if (!$this->can_only_create()) { + $this->error('cannotgenerateshortnameupdatemode', + new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse')); + return false; + } else { + $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata, + $this->importoptions['shortnametemplate']); + if (is_null($newshortname)) { + $this->error('generatedshortnameinvalid', new lang_string('generatedshortnameinvalid', 'tool_uploadcourse')); + return false; + } else if ($this->exists($newshortname)) { + if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { + $this->error('generatedshortnamealreadyinuse', + new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse')); + return false; + } + $exists = true; + } + $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse', + $newshortname)); + $this->shortname = $newshortname; + } + } + + // If exists, but we only want to create courses, increment the shortname. + if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { + $original = $this->shortname; + $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname); + $exists = false; + if ($this->shortname != $original) { + $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse', + array('from' => $original, 'to' => $this->shortname))); + if (isset($coursedata['idnumber'])) { + $originalidn = $coursedata['idnumber']; + $coursedata['idnumber'] = tool_uploadcourse_helper::increment_idnumber($coursedata['idnumber']); + if ($originalidn != $coursedata['idnumber']) { + $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse', + array('from' => $originalidn, 'to' => $coursedata['idnumber']))); + } + } + } + } + + // If the course does not exist, ensure that the ID number is not taken. + if (!$exists && isset($coursedata['idnumber'])) { + if ($DB->count_records_select('course', 'idnumber = :idn', array('idn' => $coursedata['idnumber'])) > 0) { + $this->error('idnumberalreadyinuse', new lang_string('idnumberalreadyinuse', 'tool_uploadcourse')); + return false; + } + } + + // Ultimate check mode vs. existence. + switch ($mode) { + case tool_uploadcourse_processor::MODE_CREATE_NEW: + case tool_uploadcourse_processor::MODE_CREATE_ALL: + if ($exists) { + $this->error('courseexistsanduploadnotallowed', + new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse')); + return false; + } + break; + case tool_uploadcourse_processor::MODE_UPDATE_ONLY: + if (!$exists) { + $this->error('coursedoesnotexistandcreatenotallowed', + new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse')); + return false; + } + // No break! + case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE: + if ($exists) { + if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) { + $this->error('updatemodedoessettonothing', + new lang_string('updatemodedoessettonothing', 'tool_uploadcourse')); + return false; + } + } + break; + default: + // O_o Huh?! This should really never happen here! + $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse')); + return false; + } + + // Get final data. + if ($exists) { + $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS); + $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly); + $this->do = self::DO_UPDATE; + } else { + $coursedata = $this->get_final_create_data($coursedata); + $this->do = self::DO_CREATE; + } + + // Course start date. + if (!empty($coursedata['startdate'])) { + $coursedata['startdate'] = strtotime($coursedata['startdate']); + } + + // Add role renaming. + $errors = array(); + $rolenames = tool_uploadcourse_helper::get_role_names($this->rawdata, $errors); + if (!empty($errors)) { + foreach ($errors as $key => $message) { + $this->error($key, $message); + } + return false; + } + foreach ($rolenames as $rolekey => $rolename) { + $coursedata[$rolekey] = $rolename; + } + + // Some validation. + if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) { + $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse')); + return false; + } + + // Saving data. + $this->data = $coursedata; + $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata); + + // Restore data. + // TODO log warnings. + $this->restoredata = $this->get_restore_content_dir(); + + // We can only reset courses when allowed and we are updating the course. + if ($this->importoptions['reset'] || $this->options['reset']) { + if ($this->do !== self::DO_UPDATE) { + $this->error('canonlyresetcourseinupdatemode', + new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse')); + return false; + } else if (!$this->can_reset()) { + $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse')); + return false; + } + } + + return true; + } + + /** + * Proceed with the import of the course. + * + * @return void + */ + public function proceed() { + global $CFG, $USER; + + if (!$this->prepared) { + throw new coding_exception('The course has not been prepared.'); + } else if ($this->has_errors()) { + throw new moodle_exception('Cannot proceed, errors were detected.'); + } else if ($this->processstarted) { + throw new coding_exception('The process has already been started.'); + } + $this->processstarted = true; + + if ($this->do === self::DO_DELETE) { + if ($this->delete()) { + $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse')); + } else { + $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse')); + } + return true; + } else if ($this->do === self::DO_CREATE) { + $course = create_course((object) $this->data); + $this->id = $course->id; + $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse')); + } else if ($this->do === self::DO_UPDATE) { + $course = (object) $this->data; + update_course($course); + $this->id = $course->id; + $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse')); + } else { + // Strangely the outcome has not been defined, or is unknown! + throw new coding_exception('Unknown outcome!'); + } + + // Restore a course. + if (!empty($this->restoredata)) { + $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO, + backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); + + // Check if the format conversion must happen first. + if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) { + $rc->convert(); + } + if ($rc->execute_precheck()) { + $rc->execute_plan(); + $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse')); + } else { + $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse')); + } + $rc->destroy(); + } + + // Proceed with enrolment data. + $this->process_enrolment_data($course); + + // Reset the course. + if ($this->importoptions['reset'] || $this->options['reset']) { + if ($this->do === self::DO_UPDATE && $this->can_reset()) { + $this->reset($course); + $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse')); + } + } + + // Mark context as dirty. + $context = context_course::instance($course->id); + $context->mark_dirty(); + } + + /** + * Add the enrolment data for the course. + * + * @param object $course course record. + * @return void + */ + protected function process_enrolment_data($course) { + global $DB; + + $enrolmentdata = $this->enrolmentdata; + if (empty($enrolmentdata)) { + return; + } + + $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins(); + $instances = enrol_get_instances($course->id, false); + foreach ($enrolmentdata as $enrolmethod => $method) { + + $instance = null; + foreach ($instances as $i) { + if ($i->enrol == $enrolmethod) { + $instance = $i; + break; + } + } + + $todelete = isset($method['delete']) && $method['delete']; + $todisable = isset($method['disable']) && $method['disable']; + unset($method['delete']); + unset($method['disable']); + + if (!empty($instance) && $todelete) { + // Remove the enrolment method. + foreach ($instances as $instance) { + if ($instance->enrol == $enrolmethod) { + $plugin = $enrolmentplugins[$instance->enrol]; + $plugin->delete_instance($instance); + break; + } + } + } else if (!empty($instance) && $todisable) { + // Disable the enrolment. + foreach ($instances as $instance) { + if ($instance->enrol == $enrolmethod) { + $plugin = $enrolmentplugins[$instance->enrol]; + $plugin->update_status($instance, ENROL_INSTANCE_DISABLED); + $enrol_updated = true; + break; + } + } + } else { + $plugin = null; + if (empty($instance)) { + $plugin = $enrolmentplugins[$enrolmethod]; + $instance = new stdClass(); + $instance->id = $plugin->add_default_instance($course); + $instance->roleid = $plugin->get_config('roleid'); + $instance->status = ENROL_INSTANCE_ENABLED; + } else { + $plugin = $enrolmentplugins[$instance->enrol]; + $plugin->update_status($instance, ENROL_INSTANCE_ENABLED); + } + + // Now update values. + foreach ($method as $k => $v) { + $instance->{$k} = $v; + } + + // Sort out the start, end and date. + $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0); + $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0); + + // Is the enrolment period set? + if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) { + if (preg_match('/^\d+$/', $method['enrolperiod'])) { + $method['enrolperiod'] = (int) $method['enrolperiod']; + } else { + // Try and convert period to seconds. + $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']); + } + $instance->enrolperiod = $method['enrolperiod']; + } + if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) { + $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod']; + } + if ($instance->enrolenddate > 0) { + $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate; + } + if ($instance->enrolenddate < $instance->enrolstartdate) { + $instance->enrolenddate = $instance->enrolstartdate; + } + + // Sort out the given role. This does not filter the roles allowed in the course. + if (isset($method['role'])) { + $roleids = tool_uploadcourse_helper::get_role_ids(); + if (isset($roleids[$method['role']])) { + $instance->roleid = $roleids[$method['role']]; + } + } + + $instance->timemodified = time(); + $DB->update_record('enrol', $instance); + } + } + } + + /** + * Reset the current course. + * + * This does not reset any of the content of the activities. + * + * @param stdClass $course the course object of the course to reset. + * @return array status array of array component, item, error. + */ + protected function reset($course) { + global $DB; + + $resetdata = new stdClass(); + $resetdata->id = $course->id; + $resetdata->reset_start_date = time(); + $resetdata->reset_logs = true; + $resetdata->reset_events = true; + $resetdata->reset_notes = true; + $resetdata->delete_blog_associations = true; + $resetdata->reset_completion = true; + $resetdata->reset_roles_overrides = true; + $resetdata->reset_roles_local = true; + $resetdata->reset_groups_members = true; + $resetdata->reset_groups_remove = true; + $resetdata->reset_groupings_members = true; + $resetdata->reset_groupings_remove = true; + $resetdata->reset_gradebook_items = true; + $resetdata->reset_gradebook_grades = true; + $resetdata->reset_comments = true; + + if (empty($course->startdate)) { + $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id)); + } + $resetdata->reset_start_date_old = $course->startdate; + + // Add roles. + $roles = tool_uploadcourse_helper::get_role_ids(); + $resetdata->unenrol_users = array_values($roles); + $resetdata->unenrol_users[] = 0; // Enrolled without role. + + return reset_course_userdata($resetdata); + } + + /** + * Log a status + * + * @param string $code status code. + * @param lang_string $message status message. + * @return void + */ + protected function status($code, lang_string $message) { + if (array_key_exists($code, $this->statuses)) { + throw new coding_exception('Status code already defined'); + } + $this->statuses[$code] = $message; + } + +} diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php new file mode 100644 index 00000000000..b328a423437 --- /dev/null +++ b/admin/tool/uploadcourse/classes/helper.php @@ -0,0 +1,486 @@ +. + +/** + * File containing the helper class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/coursecatlib.php'); +require_once($CFG->dirroot . '/cache/lib.php'); +require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); +require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); + +/** + * Class containing a set of helpers. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_helper { + + /** + * Remove the restore content from disk and cache. + * + * @return void + */ + public static function clean_restore_content() { + global $CFG; + if (!empty($CFG->keeptempdirectoriesonbackup)) { + $cache = cache::make('tool_uploadcourse', 'helper'); + $backupids = (array) $cache->get('backupids'); + foreach ($backupids as $cachekey => $backupid) { + $cache->delete($cachekey); + fulldelete("$CFG->tempdir/backup/$backupid/"); + } + $cache->delete('backupids'); + } + } + + /** + * Generate a shortname based on a template. + * + * @param array|object $data course data. + * @param string $templateshortname template of shortname. + * @return null|string shortname based on the template, or null when an error occured. + */ + public static function generate_shortname($data, $templateshortname) { + if (empty($templateshortname) && !is_numeric($templateshortname)) { + return null; + } + if (strpos($templateshortname, '%') === false) { + return $templateshortname; + } + + $course = (object) $data; + $fullname = isset($course->fullname) ? $course->fullname : ''; + $idnumber = isset($course->idnumber) ? $course->idnumber : ''; + + $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber); + $result = preg_replace_callback('/(? array( + * 'option1' => value, + * 'option2' => value + * ), + * 'method2' => array( + * 'option1' => value, + * 'option2' => value + * ) + * ) + * + * @param array $data data to extract the enrolment data from. + * @return array + */ + public static function get_enrolment_data($data) { + $enrolmethods = array(); + $enroloptions = array(); + foreach ($data as $field => $value) { + + // Enrolmnent data. + $matches = array(); + if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) { + $key = $matches[1]; + if (!isset($enroloptions[$key])) { + $enroloptions[$key] = array(); + } + if (empty($matches[3])) { + $enrolmethods[$key] = $value; + } else { + $enroloptions[$key][$matches[3]] = $value; + } + } + } + + // Combining enrolment methods and their options in a single array. + $enrolmentdata = array(); + if (!empty($enrolmethods)) { + $enrolmentplugins = self::get_enrolment_plugins(); + foreach ($enrolmethods as $key => $method) { + if (!array_key_exists($method, $enrolmentplugins)) { + // Error! + continue; + } + $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key]; + } + } + return $enrolmentdata; + } + + /** + * Return the enrolment plugins. + * + * The result is cached for faster execution. + * + * @return array + */ + public static function get_enrolment_plugins() { + $cache = cache::make('tool_uploadcourse', 'helper'); + if (($enrol = $cache->get('enrol')) === false) { + $enrol = enrol_get_plugins(false); + $cache->set('enrol', $enrol); + } + return $enrol; + } + + /** + * Get the restore content tempdir. + * + * The tempdir is the sub directory in which the backup has been extracted. + * This caches the result for better performance. + * + * @param string $backupfile path to a backup file. + * @param string $shortname shortname of a course. + * @param array $errors will be populated with errors found. + * @return string|false false when the backup couldn't retrieved. + */ + public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) { + global $CFG, $DB, $USER; + + $cachekey = null; + if (!empty($backupfile)) { + $backupfile = realpath($backupfile); + $cachekey = 'backup_path:' . $backupfile; + } else if (!empty($shortname) || is_numeric($shortname)) { + $cachekey = 'backup_sn:' . $shortname; + } + + if (empty($cachekey)) { + return false; + } + + $cache = cache::make('tool_uploadcourse', 'helper'); + if (($backupid = $cache->get($cachekey)) === false) { + // Use false instead of null because it would consider that the cache + // key has not been set. + $backupid = false; + if (!empty($backupfile)) { + if (!is_readable($backupfile)) { + $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse'); + } else { + // Extracting the backup file. + $packer = get_file_packer('application/vnd.moodle.backup'); + $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id); + $path = "$CFG->tempdir/backup/$backupid/"; + $result = $packer->extract_to_pathname($backupfile, $path); + if (!$result) { + $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse'); + } + } + } else if (!empty($shortname) || is_numeric($shortname)) { + // Creating restore from an existing course. + $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING); + if (!empty($courseid)) { + $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); + $bc->execute_plan(); + $backupid = $bc->get_backupid(); + $bc->destroy(); + } else { + $errors['coursetorestorefromdoesnotexist'] = + new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse'); + } + } + $cache->set($cachekey, $backupid); + + // Store all the directories to be able to remove them in self::clean_restore_content(). + $backupids = (array) $cache->get('backupids'); + $backupids[$cachekey] = $backupid; + $cache->set('backupids', $backupids); + } + + return $backupid; + } + + /** + * Return the role IDs. + * + * The result is cached for faster execution. + * + * @return array + */ + public static function get_role_ids() { + $cache = cache::make('tool_uploadcourse', 'helper'); + if (($roles = $cache->get('roles')) === false) { + $roles = array(); + $rolesraw = get_all_roles(); + foreach ($rolesraw as $role) { + $roles[$role->shortname] = $role->id; + } + $cache->set('roles', $roles); + } + return $roles; + } + + /** + * Get the role renaming data from the passed data. + * + * @param array $data data to extract the names from. + * @param array $errors will be populated with errors found. + * @return array where the key is the role_, the value is the new name. + */ + public static function get_role_names($data, &$errors = array()) { + $rolenames = array(); + $rolesids = self::get_role_ids(); + $invalidroles = array(); + foreach ($data as $field => $value) { + + $matches = array(); + if (preg_match('/^role_(.+)?$/', $field, $matches)) { + if (!isset($rolesids[$matches[1]])) { + $invalidroles[] = $matches[1]; + continue; + } + $rolenames['role_' . $rolesids[$matches[1]]] = $value; + } + + } + + if (!empty($invalidroles)) { + $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles)); + } + + // Roles names. + return $rolenames; + } + + /** + * Helper to increment an ID number. + * + * This first checks if the ID number is in use. + * + * @param string $idnumber ID number to increment. + * @return string new ID number. + */ + public static function increment_idnumber($idnumber) { + global $DB; + while ($DB->record_exists('course', array('idnumber' => $idnumber))) { + $matches = array(); + if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) { + $newidnumber = $idnumber . '_2'; + } else { + $newidnumber = $matches[1] . ((int) $matches[2] + 1); + } + $idnumber = $newidnumber; + } + return $idnumber; + } + + /** + * Helper to increment a shortname. + * + * This considers that the shortname passed has to be incremented. + * + * @param string $shortname shortname to increment. + * @return string new shortname. + */ + public static function increment_shortname($shortname) { + global $DB; + do { + $matches = array(); + if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) { + $newshortname = $shortname . '_2'; + } else { + $newshortname = $matches[1] . ($matches[2]+1); + } + $shortname = $newshortname; + } while ($DB->record_exists('course', array('shortname' => $shortname))); + return $shortname; + } + + /** + * Resolve a category based on the data passed. + * + * Key accepted are: + * - category, which is supposed to be a category ID. + * - category_idnumber + * - category_path, array of categories from parent to child. + * + * @param array $data to resolve the category from. + * @param array $errors will be populated with errors found. + * @return int category ID. + */ + public static function resolve_category($data, &$errors = array()) { + $catid = null; + + if (!empty($data['category'])) { + $category = coursecat::get((int) $data['category'], IGNORE_MISSING); + if (!empty($category) && !empty($category->id)) { + $catid = $category->id; + } else { + $errors['couldnotresolvecatgorybyid'] = + new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse'); + } + } + + if (empty($catid) && !empty($data['category_idnumber'])) { + $catid = self::resolve_category_by_idnumber($data['category_idnumber']); + if (empty($catid)) { + $errors['couldnotresolvecatgorybyidnumber'] = + new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse'); + } + } + if (empty($catid) && !empty($data['category_path'])) { + $catid = self::resolve_category_by_path(explode(' / ', $data['category_path'])); + if (empty($catid)) { + $errors['couldnotresolvecatgorybypath'] = + new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse'); + } + } + + return $catid; + } + + /** + * Resolve a category by ID number. + * + * @param string $idnumber category ID number. + * @return int category ID. + */ + public static function resolve_category_by_idnumber($idnumber) { + global $DB; + $cache = cache::make('tool_uploadcourse', 'helper'); + $cachekey = 'cat_idn_' . $idnumber; + if (($id = $cache->get($cachekey)) === false) { + $params = array('idnumber' => $idnumber); + $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING); + + // Little hack to be able to differenciate between the cache not set and a category not found. + if ($id === false) { + $id = -1; + } + + $cache->set($cachekey, $id); + } + + // Little hack to be able to differenciate between the cache not set and a category not found. + if ($id == -1) { + $id = false; + } + + return $id; + } + + /** + * Resolve a category by path. + * + * @param array $path category names indexed from parent to children. + * @return int category ID. + */ + public static function resolve_category_by_path(array $path) { + global $DB; + $cache = cache::make('tool_uploadcourse', 'helper'); + $cachekey = 'cat_path_' . serialize($path); + if (($id = $cache->get($cachekey)) === false) { + $parent = 0; + $sql = 'name = :name AND parent = :parent'; + while ($name = array_shift($path)) { + $params = array('name' => $name, 'parent' => $parent); + if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) { + if (count($records) > 1) { + // Too many records with the same name! + $id = -1; + break; + } + $record = reset($records); + $id = $record->id; + $parent = $record->id; + } else { + // Not found. + $id = -1; + break; + } + } + $cache->set($cachekey, $id); + } + + // We save -1 when the category has not been found to be able to know if the cache was set. + if ($id == -1) { + $id = false; + } + return $id; + } + +} diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php new file mode 100644 index 00000000000..94ab8a7ec7e --- /dev/null +++ b/admin/tool/uploadcourse/classes/processor.php @@ -0,0 +1,390 @@ +. + +/** + * File containing processor class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/csvlib.class.php'); + +/** + * Processor class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_processor { + + /** + * Create courses that do not exist yet. + */ + const MODE_CREATE_NEW = 1; + + /** + * Create all courses, appending a suffix to the shortname if the course exists. + */ + const MODE_CREATE_ALL = 2; + + /** + * Create courses, and update the ones that already exist. + */ + const MODE_CREATE_OR_UPDATE = 3; + + /** + * Only update existing courses. + */ + const MODE_UPDATE_ONLY = 4; + + /** + * During update, do not update anything... O_o Huh?! + */ + const UPDATE_NOTHING = 0; + + /** + * During update, only use data passed from the CSV. + */ + const UPDATE_ALL_WITH_DATA_ONLY = 1; + + /** + * During update, use either data from the CSV, or defaults. + */ + const UPDATE_ALL_WITH_DATA_OR_DEFAUTLS = 2; + + /** + * During update, update missing values from either data from the CSV, or defaults. + */ + const UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS = 3; + + /** @var int processor mode. */ + protected $mode; + + /** @var int upload mode. */ + protected $updatemode; + + /** @var bool are renames allowed. */ + protected $allowrenames = false; + + /** @var bool are deletes allowed. */ + protected $allowdeletes = false; + + /** @var bool are resets allowed. */ + protected $allowresets = false; + + /** @var string path to a restore file. */ + protected $restorefile; + + /** @var string shortname of the course to be restored. */ + protected $templatecourse; + + /** @var string reset courses after processing them. */ + protected $reset; + + /** @var string template to generate a course shortname. */ + protected $shortnametemplate; + + /** @var csv_import_reader */ + protected $cir; + + /** @var array default values. */ + protected $defaults = array(); + + /** @var array CSV columns. */ + protected $columns = array(); + + /** @var array of errors where the key is the line number. */ + protected $errors = array(); + + /** @var int line number. */ + protected $linenb = 0; + + /** @var bool whether the process has been started or not. */ + protected $processstarted = false; + + /** + * Constructor + * + * @param csv_import_reader $cir import reader object + * @param array $options options of the process + * @param array $defaults default data value + */ + public function __construct(csv_import_reader $cir, array $options, array $defaults = array()) { + + if (!isset($options['mode']) || !in_array($options['mode'], array(self::MODE_CREATE_NEW, self::MODE_CREATE_ALL, + self::MODE_CREATE_OR_UPDATE, self::MODE_UPDATE_ONLY))) { + throw new coding_exception('Unknown process mode'); + } + + // Force int to make sure === comparison work as expected. + $this->mode = (int) $options['mode']; + + $this->updatemode = self::UPDATE_NOTHING; + if (isset($options['updatemode'])) { + // Force int to make sure === comparison work as expected. + $this->updatemode = (int) $options['updatemode']; + } + if (isset($options['allowrenames'])) { + $this->allowrenames = $options['allowrenames']; + } + if (isset($options['allowdeletes'])) { + $this->allowdeletes = $options['allowdeletes']; + } + if (isset($options['allowresets'])) { + $this->allowresets = $options['allowresets']; + } + + if (isset($options['restorefile'])) { + $this->restorefile = $options['restorefile']; + } + if (isset($options['templatecourse'])) { + $this->templatecourse = $options['templatecourse']; + } + if (isset($options['reset'])) { + $this->reset = $options['reset']; + } + if (isset($options['shortnametemplate'])) { + $this->shortnametemplate = $options['shortnametemplate']; + } + + $this->cir = $cir; + $this->columns = $cir->get_columns(); + $this->defaults = $defaults; + $this->validate(); + $this->reset(); + } + + /** + * Execute the process. + * + * @param object $tracker the output tracker to use. + * @return void + */ + public function execute($tracker = null) { + if ($this->processstarted) { + throw new coding_exception('Process has already been started'); + } + $this->processstarted = true; + + if (empty($tracker)) { + $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT); + } + $tracker->start(); + + $total = 0; + $created = 0; + $updated = 0; + $deleted = 0; + $errors = 0; + + // Loop over the CSV lines. + while ($line = $this->cir->next()) { + $this->linenb++; + $total++; + + $data = $this->parse_line($line); + $course = $this->get_course($data); + if ($course->prepare()) { + $course->proceed(); + + $status = $course->get_statuses(); + if (array_key_exists('coursecreated', $status)) { + $created++; + } else if (array_key_exists('courseupdated', $status)) { + $updated++; + } else if (array_key_exists('coursedeleted', $status)) { + $deleted++; + } + + $data = array_merge($data, $course->get_data(), array('id' => $course->get_id())); + $tracker->output($this->linenb, true, $status, $data); + } else { + $errors++; + $tracker->output($this->linenb, false, $course->get_errors(), $data); + } + } + + $tracker->finish(); + $tracker->results($total, $created, $updated, $deleted, $errors); + + $this->remove_restore_content(); + } + + /** + * Return a course import object. + * + * @param array $data data to import the course with. + * @return tool_uploadcourse_course + */ + protected function get_course($data) { + $importoptions = array( + 'candelete' => $this->allowdeletes, + 'canrename' => $this->allowrenames, + 'canreset' => $this->allowresets, + 'reset' => $this->reset, + 'restoredir' => $this->get_restore_content_dir(), + 'shortnametemplate' => $this->shortnametemplate + ); + return new tool_uploadcourse_course($this->mode, $this->updatemode, $data, $this->defaults, $importoptions); + } + + /** + * Return the errors. + * + * @return array + */ + public function get_errors() { + return $this->errors; + } + + /** + * Get the directory of the object to restore. + * + * @return string subdirectory in $CFG->tempdir/backup/... + */ + protected function get_restore_content_dir() { + $backupfile = null; + $shortname = null; + + if (!empty($this->restorefile)) { + $backupfile = $this->restorefile; + } else if (!empty($this->templatecourse) || is_numeric($this->templatecourse)) { + $shortname = $this->templatecourse; + } + + $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname); + return $dir; + } + + /** + * Log errors on the current line. + * + * @param array $errors array of errors + * @return void + */ + protected function log_error($errors) { + if (empty($errors)) { + return; + } + + foreach ($errors as $code => $langstring) { + if (!isset($this->errors[$this->linenb])) { + $this->errors[$this->linenb] = array(); + } + $this->errors[$this->linenb][$code] = $langstring; + } + } + + /** + * Parse a line to return an array(column => value) + * + * @param array $line returned by csv_import_reader + * @return array + */ + protected function parse_line($line) { + $data = array(); + foreach ($line as $keynum => $value) { + if (!isset($this->columns[$keynum])) { + // This should not happen. + continue; + } + + $key = $this->columns[$keynum]; + $data[$key] = $value; + } + return $data; + } + + /** + * Return a preview of the import. + * + * This only returns passed data, along with the errors. + * + * @param integer $rows number of rows to preview. + * @param object $tracker the output tracker to use. + * @return array of preview data. + */ + public function preview($rows = 10, $tracker = null) { + if ($this->processstarted) { + throw new coding_exception('Process has already been started'); + } + $this->processstarted = true; + + if (empty($tracker)) { + $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT); + } + $tracker->start(); + + // Loop over the CSV lines. + $preview = array(); + while (($line = $this->cir->next()) && $rows > $this->linenb) { + $this->linenb++; + $data = $this->parse_line($line); + $course = $this->get_course($data); + $result = $course->prepare(); + if (!$result) { + $tracker->output($this->linenb, $result, $course->get_errors(), $data); + } else { + $tracker->output($this->linenb, $result, $course->get_statuses(), $data); + } + $row = $data; + $preview[$this->linenb] = $row; + } + + $tracker->finish(); + $this->remove_restore_content(); + + return $preview; + } + + /** + * Delete the restore object. + * + * @return void + */ + protected function remove_restore_content() { + tool_uploadcourse_helper::clean_restore_content(); + } + + /** + * Reset the current process. + * + * @return void. + */ + public function reset() { + $this->processstarted = false; + $this->linenb = 0; + $this->cir->init(); + $this->errors = array(); + } + + /** + * Validation. + * + * @return void + */ + protected function validate() { + if (empty($this->columns)) { + throw new moodle_exception('cannotreadtmpfile', 'error'); + } else if (count($this->columns) < 2) { + throw new moodle_exception('csvfewcolumns', 'error'); + } + } +} diff --git a/admin/tool/uploadcourse/classes/step1_form.php b/admin/tool/uploadcourse/classes/step1_form.php new file mode 100644 index 00000000000..cd0abc0b6c8 --- /dev/null +++ b/admin/tool/uploadcourse/classes/step1_form.php @@ -0,0 +1,75 @@ +. + +/** + * File containing the step 1 of the upload form. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Upload a file CVS file with course information. + * + * @package tool_uploadcourse + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_step1_form extends tool_uploadcourse_base_form { + + /** + * The standard form definiton. + * @return void + */ + public function definition () { + $mform = $this->_form; + + $mform->addElement('header', 'generalhdr', get_string('general')); + + $mform->addElement('filepicker', 'coursefile', get_string('file')); + $mform->addRule('coursefile', null, 'required'); + + $choices = csv_import_reader::get_delimiter_list(); + $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'tool_uploadcourse'), $choices); + if (array_key_exists('cfg', $choices)) { + $mform->setDefault('delimiter_name', 'cfg'); + } else if (get_string('listsep', 'langconfig') == ';') { + $mform->setDefault('delimiter_name', 'semicolon'); + } else { + $mform->setDefault('delimiter_name', 'comma'); + } + + $choices = textlib::get_encodings(); + $mform->addElement('select', 'encoding', get_string('encoding', 'tool_uploadcourse'), $choices); + $mform->setDefault('encoding', 'UTF-8'); + + $choices = array('10' => 10, '20' => 20, '100' => 100, '1000' => 1000, '100000' => 100000); + $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'tool_uploadcourse'), $choices); + $mform->setType('previewrows', PARAM_INT); + + $this->add_import_options(); + + $mform->addElement('hidden', 'showpreview', 1); + $mform->setType('showpreview', PARAM_INT); + + $this->add_action_buttons(false, get_string('preview', 'tool_uploadcourse')); + } +} diff --git a/admin/tool/uploadcourse/classes/step2_form.php b/admin/tool/uploadcourse/classes/step2_form.php new file mode 100644 index 00000000000..7464e4ea627 --- /dev/null +++ b/admin/tool/uploadcourse/classes/step2_form.php @@ -0,0 +1,220 @@ +. + +/** + * Bulk course upload step 2. + * + * @package tool_uploadcourse + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Specify course upload details. + * + * @package tool_uploadcourse + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form { + + /** + * The standard form definiton. + * @return void. + */ + public function definition () { + global $CFG; + + $mform = $this->_form; + $data = $this->_customdata['data']; + $courseconfig = get_config('moodlecourse'); + + // Import options. + $this->add_import_options(); + + // Course options. + $mform->addElement('header', 'courseoptionshdr', get_string('courseprocess', 'tool_uploadcourse')); + $mform->setExpanded('courseoptionshdr', true); + + $mform->addElement('text', 'options[shortnametemplate]', get_string('shortnametemplate', 'tool_uploadcourse'), + 'maxlength="100" size="20"'); + $mform->setType('options[shortnametemplate]', PARAM_RAW); + $mform->addHelpButton('options[shortnametemplate]', 'shortnametemplate', 'tool_uploadcourse'); + $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE); + $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY); + + $contextid = $this->_customdata['contextid']; + $mform->addElement('hidden', 'contextid', $contextid); + $mform->setType('contextid', PARAM_INT); + $mform->addElement('filepicker', 'options[restorefile]', get_string('templatefile', 'tool_uploadcourse')); + $mform->addHelpButton('options[restorefile]', 'templatefile', 'tool_uploadcourse'); + + $mform->addElement('text', 'options[templatecourse]', get_string('coursetemplatename', 'tool_uploadcourse')); + $mform->setType('options[templatecourse]', PARAM_TEXT); + $mform->addHelpButton('options[templatecourse]', 'coursetemplatename', 'tool_uploadcourse'); + + $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse')); + $mform->setDefault('options[reset]', 0); + $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW); + $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL); + $mform->disabledIf('options[reset]', 'options[allowresets]', 'eq', 0); + + // Default values. + $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse')); + $mform->setExpanded('defaultheader', true); + + $displaylist = coursecat::make_categories_list('moodle/course:create'); + $mform->addElement('select', 'defaults[category]', get_string('coursecategory'), $displaylist); + $mform->addHelpButton('defaults[category]', 'coursecategory'); + + $choices = array(); + $choices['0'] = get_string('hide'); + $choices['1'] = get_string('show'); + $mform->addElement('select', 'defaults[visible]', get_string('visible'), $choices); + $mform->addHelpButton('defaults[visible]', 'visible'); + $mform->setDefault('defaults[visible]', $courseconfig->visible); + + $mform->addElement('date_selector', 'defaults[startdate]', get_string('startdate')); + $mform->addHelpButton('defaults[startdate]', 'startdate'); + $mform->setDefault('defaults[startdate]', time() + 3600 * 24); + + $courseformats = get_sorted_course_formats(true); + $formcourseformats = array(); + foreach ($courseformats as $courseformat) { + $formcourseformats[$courseformat] = get_string('pluginname', "format_$courseformat"); + } + $mform->addElement('select', 'defaults[format]', get_string('format'), $formcourseformats); + $mform->addHelpButton('defaults[format]', 'format'); + $mform->setDefault('defaults[format]', $courseconfig->format); + + if (!empty($CFG->allowcoursethemes)) { + $themeobjects = get_list_of_themes(); + $themes=array(); + $themes[''] = get_string('forceno'); + foreach ($themeobjects as $key => $theme) { + if (empty($theme->hidefromselector)) { + $themes[$key] = get_string('pluginname', 'theme_'.$theme->name); + } + } + $mform->addElement('select', 'defaults[theme]', get_string('forcetheme'), $themes); + } + + $languages = array(); + $languages[''] = get_string('forceno'); + $languages += get_string_manager()->get_list_of_translations(); + $mform->addElement('select', 'defaults[lang]', get_string('forcelanguage'), $languages); + $mform->setDefault('defaults[lang]', $courseconfig->lang); + + $options = range(0, 10); + $mform->addElement('select', 'defaults[newsitems]', get_string('newsitemsnumber'), $options); + $mform->addHelpButton('defaults[newsitems]', 'newsitemsnumber'); + $mform->setDefault('defaults[newsitems]', $courseconfig->newsitems); + + $mform->addElement('selectyesno', 'defaults[showgrades]', get_string('showgrades')); + $mform->addHelpButton('defaults[showgrades]', 'showgrades'); + $mform->setDefault('defaults[showgrades]', $courseconfig->showgrades); + + $mform->addElement('selectyesno', 'defaults[showreports]', get_string('showreports')); + $mform->addHelpButton('defaults[showreports]', 'showreports'); + $mform->setDefault('defaults[showreports]', $courseconfig->showreports); + + if (!empty($CFG->legacyfilesinnewcourses)) { + $mform->addElement('select', 'defaults[legacyfiles]', get_string('courselegacyfiles'), $choices); + $mform->addHelpButton('defaults[legacyfiles]', 'courselegacyfiles'); + if (!isset($courseconfig->legacyfiles)) { + $courseconfig->legacyfiles = 0; + } + $mform->setDefault('defaults[legacyfiles]', $courseconfig->legacyfiles); + } + + $choices = get_max_upload_sizes($CFG->maxbytes); + $mform->addElement('select', 'defaults[maxbytes]', get_string('maximumupload'), $choices); + $mform->addHelpButton('defaults[maxbytes]', 'maximumupload'); + $mform->setDefault('defaults[maxbytes]', $courseconfig->maxbytes); + + $choices = array(); + $choices[NOGROUPS] = get_string('groupsnone', 'group'); + $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group'); + $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group'); + $mform->addElement('select', 'defaults[groupmode]', get_string('groupmode', 'group'), $choices); + $mform->addHelpButton('defaults[groupmode]', 'groupmode', 'group'); + $mform->setDefault('defaults[groupmode]', $courseconfig->groupmode); + + $mform->addElement('selectyesno', 'defaults[groupmodeforce]', get_string('groupmodeforce', 'group')); + $mform->addHelpButton('defaults[groupmodeforce]', 'groupmodeforce', 'group'); + $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce); + + // Hidden fields. + $mform->addElement('hidden', 'importid'); + $mform->setType('importid', PARAM_INT); + + $mform->addElement('hidden', 'previewrows'); + $mform->setType('previewrows', PARAM_INT); + + $this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse')); + + $this->set_data($data); + } + + /** + * Add actopm buttons. + * + * @param bool $cancel whether to show cancel button, default true + * @param string $submitlabel label for submit button, defaults to get_string('savechanges') + * @return void + */ + public function add_action_buttons($cancel = true, $submitlabel = null) { + $mform =& $this->_form; + $buttonarray = array(); + $buttonarray[] = &$mform->createElement('submit', 'showpreview', get_string('preview', 'tool_uploadcourse')); + $buttonarray[] = &$mform->createElement('submit', 'submitbutton', $submitlabel); + $buttonarray[] = &$mform->createElement('cancel'); + $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); + $mform->closeHeaderBefore('buttonar'); + } + + /** + * Server side validation. + * @param array $data - form data + * @param object $files - form files + * @return array $errors - form errors + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + $columns = $this->_customdata['columns']; + $optype = $data['options']['mode']; + + // Look for other required data. + if ($optype != tool_uploadcourse_processor::MODE_UPDATE_ONLY) { + if (!in_array('fullname', $columns)) { + if (isset($errors['mode'])) { + $errors['mode'] .= ' '; + } + $errors['mode'] .= get_string('missingfield', 'error', 'fullname'); + } + if (!in_array('summary', $columns)) { + if (isset($errors['mode'])) { + $errors['mode'] .= ' '; + } + $errors['mode'] .= get_string('missingfield', 'error', 'summary'); + } + } + + return $errors; + } +} diff --git a/admin/tool/uploadcourse/classes/tracker.php b/admin/tool/uploadcourse/classes/tracker.php new file mode 100644 index 00000000000..0daa84527c5 --- /dev/null +++ b/admin/tool/uploadcourse/classes/tracker.php @@ -0,0 +1,220 @@ +. + +/** + * Output tracker. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/weblib.php'); + +/** + * Class output tracker. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_tracker { + + /** + * Constant to output nothing. + */ + const NO_OUTPUT = 0; + + /** + * Constant to output HTML. + */ + const OUTPUT_HTML = 1; + + /** + * Constant to output plain text. + */ + const OUTPUT_PLAIN = 2; + + /** + * @var array columns to display. + */ + protected $columns = array('line', 'result', 'id', 'shortname', 'fullname', 'idnumber', 'status'); + + /** + * @var int row number. + */ + protected $rownb = 0; + + /** + * @var int chosen output mode. + */ + protected $outputmode; + + /** + * @var object output buffer. + */ + protected $buffer; + + /** + * Constructor. + * + * @param int $outputmode desired output mode. + */ + public function __construct($outputmode = self::NO_OUTPUT) { + $this->outputmode = $outputmode; + if ($this->outputmode == self::OUTPUT_PLAIN) { + $this->buffer = new progress_trace_buffer(new text_progress_trace()); + } + } + + /** + * Finish the output. + * + * @return void + */ + public function finish() { + if ($this->outputmode == self::NO_OUTPUT) { + return; + } + + if ($this->outputmode == self::OUTPUT_HTML) { + echo html_writer::end_tag('table'); + } + } + + /** + * Output the results. + * + * @param int $total total courses. + * @param int $created count of courses created. + * @param int $updated count of courses updated. + * @param int $deleted count of courses deleted. + * @param int $errors count of errors. + * @return void + */ + public function results($total, $created, $updated, $deleted, $errors) { + if ($this->outputmode == self::NO_OUTPUT) { + return; + } + + $message = array( + get_string('coursestotal', 'tool_uploadcourse', $total), + get_string('coursescreated', 'tool_uploadcourse', $created), + get_string('coursesupdated', 'tool_uploadcourse', $updated), + get_string('coursesdeleted', 'tool_uploadcourse', $deleted), + get_string('courseserrors', 'tool_uploadcourse', $errors) + ); + + if ($this->outputmode == self::OUTPUT_PLAIN) { + foreach ($message as $msg) { + $this->buffer->output($msg); + } + } else if ($this->outputmode == self::OUTPUT_HTML) { + $buffer = new progress_trace_buffer(new html_list_progress_trace()); + foreach ($message as $msg) { + $buffer->output($msg); + } + $buffer->finished(); + } + } + + /** + * Output one more line. + * + * @param int $line line number. + * @param bool $outcome success or not? + * @param array $status array of statuses. + * @param array $data extra data to display. + * @return void + */ + public function output($line, $outcome, $status, $data) { + global $OUTPUT; + if ($this->outputmode == self::NO_OUTPUT) { + return; + } + + if ($this->outputmode == self::OUTPUT_PLAIN) { + $message = array( + $line, + $outcome ? 'OK' : 'NOK', + isset($data['id']) ? $data['id'] : '', + isset($data['shortname']) ? $data['shortname'] : '', + isset($data['fullname']) ? $data['fullname'] : '', + isset($data['idnumber']) ? $data['idnumber'] : '' + ); + $this->buffer->output(implode("\t", $message)); + if (!empty($status)) { + foreach ($status as $st) { + $this->buffer->output($st, 1); + } + } + } else if ($this->outputmode == self::OUTPUT_HTML) { + $ci = 0; + $this->rownb++; + if (is_array($status)) { + $status = implode(html_writer::empty_tag('br'), $status); + } + if ($outcome) { + $outcome = $OUTPUT->pix_icon('i/valid', ''); + } else { + $outcome = $OUTPUT->pix_icon('i/invalid', ''); + } + echo html_writer::start_tag('tr', array('class' => 'r' . $this->rownb % 2)); + echo html_writer::tag('td', $line, array('class' => 'c' . $ci++)); + echo html_writer::tag('td', $outcome, array('class' => 'c' . $ci++)); + echo html_writer::tag('td', isset($data['id']) ? $data['id'] : '', array('class' => 'c' . $ci++)); + echo html_writer::tag('td', isset($data['shortname']) ? $data['shortname'] : '', array('class' => 'c' . $ci++)); + echo html_writer::tag('td', isset($data['fullname']) ? $data['fullname'] : '', array('class' => 'c' . $ci++)); + echo html_writer::tag('td', isset($data['idnumber']) ? $data['idnumber'] : '', array('class' => 'c' . $ci++)); + echo html_writer::tag('td', $status, array('class' => 'c' . $ci++)); + echo html_writer::end_tag('tr'); + } + } + + /** + * Start the output. + * + * @return void + */ + public function start() { + if ($this->outputmode == self::NO_OUTPUT) { + return; + } + + if ($this->outputmode == self::OUTPUT_PLAIN) { + $columns = array_flip($this->columns); + unset($columns['status']); + $columns = array_flip($columns); + $this->buffer->output(implode("\t", $columns)); + } else if ($this->outputmode == self::OUTPUT_HTML) { + $ci = 0; + echo html_writer::start_tag('table', array('class' => 'generaltable boxaligncenter flexible-wrap', + 'summary' => get_string('uploadcoursesresult', 'tool_uploadcourse'))); + echo html_writer::start_tag('tr', array('class' => 'heading r' . $this->rownb)); + echo html_writer::tag('th', get_string('csvline', 'tool_uploadcourse'), + array('class' => 'c' . $ci++, 'scope' => 'col')); + echo html_writer::tag('th', get_string('result', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col')); + echo html_writer::tag('th', get_string('id', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col')); + echo html_writer::tag('th', get_string('shortname'), array('class' => 'c' . $ci++, 'scope' => 'col')); + echo html_writer::tag('th', get_string('fullname'), array('class' => 'c' . $ci++, 'scope' => 'col')); + echo html_writer::tag('th', get_string('idnumber'), array('class' => 'c' . $ci++, 'scope' => 'col')); + echo html_writer::tag('th', get_string('status'), array('class' => 'c' . $ci++, 'scope' => 'col')); + echo html_writer::end_tag('tr'); + } + } + +} diff --git a/admin/tool/uploadcourse/cli/uploadcourse.php b/admin/tool/uploadcourse/cli/uploadcourse.php new file mode 100644 index 00000000000..a232bee2051 --- /dev/null +++ b/admin/tool/uploadcourse/cli/uploadcourse.php @@ -0,0 +1,200 @@ +. + +/** + * CLI Bulk course registration script from a comma separated file. + * + * @package tool_uploadcourse + * @copyright 2012 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require(__DIR__ . '/../../../../config.php'); +require_once($CFG->libdir . '/clilib.php'); +require_once($CFG->libdir . '/coursecatlib.php'); +require_once($CFG->libdir . '/csvlib.class.php'); + +$courseconfig = get_config('moodlecourse'); + +// Now get cli options. +list($options, $unrecognized) = cli_get_params(array( + 'help' => false, + 'mode' => '', + 'updatemode' => 'nothing', + 'file' => '', + 'delimiter' => 'comma', + 'encoding' => 'UTF-8', + 'shortnametemplate' => '', + 'templatecourse' => false, + 'restorefile' => false, + 'allowdeletes' => false, + 'allowrenames' => false, + 'allowresets' => false, + 'reset' => false, + 'category' => coursecat::get_default()->id, +), +array( + 'h' => 'help', + 'm' => 'mode', + 'u' => 'updatemode', + 'f' => 'file', + 'd' => 'delimiter', + 'e' => 'encoding', + 't' => 'templatecourse', + 'r' => 'restorefile', + 'g' => 'format', +)); + +if ($unrecognized) { + $unrecognized = implode("\n ", $unrecognized); + cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); +} + +$help = +"Execute Course Upload. + +Options: +-h, --help Print out this help +-m, --mode Import mode: createnew, createall, createorupdate, update +-u, --updatemode Update mode: nothing, dataonly, dataordefaults¸ missingonly +-f, --file CSV file +-d, --delimiter CSV delimiter: colon, semicolon, tab, cfg, comma +-e, --encoding CSV file encoding: utf8, ... etc +-t, --templatecourse Shortname of the course to restore after import +-r, --restorefile Backup file to restore after import +--reset Run the course reset after each course import +--allowdeletes Allow courses to be deleted +--allowrenames Allow courses to be renamed +--allowresets Allow courses to be reset +--shortnametemplate Template to generate the shortname from +--category ID of default category (--updatemode dataordefaults will use this value) + + +Example: +\$sudo -u www-data /usr/bin/php admin/tool/uploadcourse/cli/uploadcourse.php --action=createnew \\ + --updatemode=dataonly --file=./courses.csv --delimiter=comma +"; + +if ($options['help']) { + echo $help; + die(); +} +echo "Moodle course uploader running ...\n"; + +$processoroptions = array( + 'allowdeletes' => $options['allowdeletes'], + 'allowrenames' => $options['allowrenames'], + 'allowresets' => $options['allowresets'], + 'reset' => $options['reset'], + 'shortnametemplate' => $options['shortnametemplate'] +); + +// Confirm that the mode is valid. +$modes = array( + 'createnew' => tool_uploadcourse_processor::MODE_CREATE_NEW, + 'createall' => tool_uploadcourse_processor::MODE_CREATE_ALL, + 'createorupdate' => tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE, + 'update' => tool_uploadcourse_processor::MODE_UPDATE_ONLY +); +if (!isset($options['mode']) || !isset($modes[$options['mode']])) { + echo get_string('invalidmode', 'tool_uploadcourse')."\n"; + echo $help; + die(); +} +$processoroptions['mode'] = $modes[$options['mode']]; + +// Check that the update mode is valid. +$updatemodes = array( + 'nothing' => tool_uploadcourse_processor::UPDATE_NOTHING, + 'dataonly' => tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY, + 'dataordefaults' => tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS, + 'missingonly' => tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS +); +if (($processoroptions['mode'] === tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE || + $processoroptions['mode'] === tool_uploadcourse_processor::MODE_UPDATE_ONLY) + && (!isset($options['updatemode']) || !isset($updatemodes[$options['updatemode']]))) { + echo get_string('invalideupdatemode', 'tool_uploadcourse')."\n"; + echo $help; + die(); +} +$processoroptions['updatemode'] = $updatemodes[$options['updatemode']]; + +// File. +if (!empty($options['file'])) { + $options['file'] = realpath($options['file']); +} +if (!file_exists($options['file'])) { + echo get_string('invalidcsvfile', 'tool_uploadcourse')."\n"; + echo $help; + die(); +} + +// Encoding. +$encodings = textlib::get_encodings(); +if (!isset($encodings[$options['encoding']])) { + echo get_string('invalidencoding', 'tool_uploadcourse')."\n"; + echo $help; + die(); +} + +// Default values. +$defaults = array(); +$defaults['category'] = $options['category']; +$defaults['startdate'] = time() + 3600 * 24; +$defaults['newsitems'] = $courseconfig->newsitems; +$defaults['showgrades'] = $courseconfig->showgrades; +$defaults['showreports'] = $courseconfig->showreports; +$defaults['maxbytes'] = $courseconfig->maxbytes; +$defaults['legacyfiles'] = $CFG->legacyfilesinnewcourses; +$defaults['groupmode'] = $courseconfig->groupmode; +$defaults['groupmodeforce'] = $courseconfig->groupmodeforce; +$defaults['visible'] = $courseconfig->visible; +$defaults['lang'] = $courseconfig->lang; + +// Course template. +if (isset($options['templatecourse'])) { + $processoroptions['templatecourse'] = $options['templatecourse']; +} + +// Restore file. +if ($options['restorefile']) { + $options['restorefile'] = realpath($options['restorefile']); +} +if ($options['restorefile'] && !file_exists($options['restorefile'])) { + echo get_string('invalidrestorefile', 'tool_uploadcourse')."\n"; + echo $help; + die(); +} +$processoroptions['restorefile'] = $options['restorefile']; + +// Emulate normal session. +cron_setup_user(); + +// Let's get started! +$content = file_get_contents($options['file']); +$importid = csv_import_reader::get_new_iid('uploadcourse'); +$cir = new csv_import_reader($importid, 'uploadcourse'); +$readcount = $cir->load_csv_content($content, $options['encoding'], $options['delimiter']); +unset($content); +if ($readcount === false) { + print_error('csvfileerror', 'tool_uploadcourse', '', $cir->get_error()); +} else if ($readcount == 0) { + print_error('csvemptyfile', 'error', '', $cir->get_error()); +} +$processor = new tool_uploadcourse_processor($cir, $processoroptions, $defaults); +$processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_PLAIN)); 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/index.php b/admin/tool/uploadcourse/index.php new file mode 100644 index 00000000000..667551efd44 --- /dev/null +++ b/admin/tool/uploadcourse/index.php @@ -0,0 +1,102 @@ +. + +/** + * Bulk course registration script from a comma separated file. + * + * @package tool_uploadcourse + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); +require_once($CFG->libdir . '/coursecatlib.php'); +require_once($CFG->libdir . '/csvlib.class.php'); + +admin_externalpage_setup('tooluploadcourse'); + +$importid = optional_param('importid', '', PARAM_INT); +$previewrows = optional_param('previewrows', 10, PARAM_INT); + +$returnurl = new moodle_url('/admin/tool/uploadcourse/index.php'); + +if (empty($importid)) { + $mform1 = new tool_uploadcourse_step1_form(); + if ($form1data = $mform1->get_data()) { + $importid = csv_import_reader::get_new_iid('uploadcourse'); + $cir = new csv_import_reader($importid, 'uploadcourse'); + $content = $mform1->get_file_content('coursefile'); + $readcount = $cir->load_csv_content($content, $form1data->encoding, $form1data->delimiter_name); + unset($content); + if ($readcount === false) { + print_error('csvfileerror', 'tool_uploadcourse', $returnurl, $cir->get_error()); + } else if ($readcount == 0) { + print_error('csvemptyfile', 'error', $returnurl, $cir->get_error()); + } + } else { + echo $OUTPUT->header(); + echo $OUTPUT->heading_with_help(get_string('uploadcourses', 'tool_uploadcourse'), 'uploadcourses', 'tool_uploadcourse'); + $mform1->display(); + echo $OUTPUT->footer(); + die(); + } +} else { + $cir = new csv_import_reader($importid, 'uploadcourse'); +} + +// Data to set in the form. +$data = array('importid' => $importid, 'previewrows' => $previewrows); +if (!empty($form1data)) { + // Get options from the first form to pass it onto the second. + foreach ($form1data->options as $key => $value) { + $data["options[$key]"] = $value; + } +} +$context = context_system::instance(); +$mform2 = new tool_uploadcourse_step2_form(null, array('contextid' => $context->id, 'columns' => $cir->get_columns(), + 'data' => $data)); + +// If a file has been uploaded, then process it. +if ($form2data = $mform2->is_cancelled()) { + $cir->cleanup(true); + redirect($returnurl); +} else if ($form2data = $mform2->get_data()) { + + $options = (array) $form2data->options; + $defaults = (array) $form2data->defaults; + $processor = new tool_uploadcourse_processor($cir, $options, $defaults); + + echo $OUTPUT->header(); + if (isset($form2data->showpreview)) { + echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse')); + $processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML)); + $mform2->display(); + } else { + echo $OUTPUT->heading(get_string('uploadcoursesresult', 'tool_uploadcourse')); + $processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML)); + echo $OUTPUT->continue_button($returnurl); + } + +} else { + $processor = new tool_uploadcourse_processor($cir, $form1data->options, array()); + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse')); + $processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML)); + $mform2->display(); +} + +echo $OUTPUT->footer(); diff --git a/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php new file mode 100644 index 00000000000..83138d36a49 --- /dev/null +++ b/admin/tool/uploadcourse/lang/en/tool_uploadcourse.php @@ -0,0 +1,115 @@ +. + +/** + * Strings for component 'tool_uploadcourse'. + * + * @package tool_uploadcourse + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['allowdeletes'] = 'Allow deletes'; +$string['allowrenames'] = 'Allow renames'; +$string['allowresets'] = 'Allow resets'; +$string['cachedef_helper'] = 'Helper caching'; +$string['cannotdeletecoursenotexist'] = 'Cannot delete a course that does not exist'; +$string['cannotgenerateshortnameupdatemode'] = 'Cannot generate a shortname when updates are allowed'; +$string['cannotreadbackupfile'] = 'Cannot read the backup file'; +$string['cannotrenamecoursenotexist'] = 'Cannot rename a course that does not exist'; +$string['cannotrenameidnumberconflict'] = 'Cannot rename the course, the ID number conflicts with an existing course'; +$string['cannotrenameshortnamealreadyinuse'] = 'Cannot rename the course, the shortname is already used'; +$string['canonlyrenameinupdatemode'] = 'Can only rename a course when update is allowed'; +$string['canonlyresetcourseinupdatemode'] = 'Can only reset a course in update mode'; +$string['couldnotresolvecatgorybyid'] = 'Could not resolve category by ID'; +$string['couldnotresolvecatgorybyidnumber'] = 'Could not resolve category by ID number'; +$string['couldnotresolvecatgorybypath'] = 'Could not resolve category by path'; +$string['coursecreated'] = 'Course created'; +$string['coursedeleted'] = 'Course deleted'; +$string['coursedeletionnotallowed'] = 'Course deletion is not allowed'; +$string['coursedoesnotexistandcreatenotallowed'] = 'The course does not exist and creating course is not allowed'; +$string['courseexistsanduploadnotallowed'] = 'The course exists and update is not allowed'; +$string['courseidnumberincremented'] = 'Course ID number incremented {$a->from} -> {$a->to}'; +$string['courseprocess'] = 'Course process'; +$string['courserenamed'] = 'Course renamed'; +$string['courserenamingnotallowed'] = 'Course renaming is not allowed'; +$string['coursereset'] = 'Course reset'; +$string['courseresetnotallowed'] = 'Course reset now allowed'; +$string['courserestored'] = 'Course restored'; +$string['coursestotal'] = 'Courses total: {$a}'; +$string['coursescreated'] = 'Courses created: {$a}'; +$string['coursesupdated'] = 'Courses updated: {$a}'; +$string['coursesdeleted'] = 'Courses deleted: {$a}'; +$string['courseserrors'] = 'Courses errors: {$a}'; +$string['courseshortnameincremented'] = 'Course shortname incremented {$a->from} -> {$a->to}'; +$string['courseshortnamegenerated'] = 'Course shortname generated: {$a}'; +$string['coursetemplatename'] = 'Restore from this course after upload'; +$string['coursetemplatename_help'] = 'Enter an existing course shortname to use as a template for the creation of all courses.'; +$string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist'; +$string['courseupdated'] = 'Course updated'; +$string['createall'] = 'Create all, increment shortname if needed'; +$string['createnew'] = 'Create new courses only, skip existing ones'; +$string['createorupdate'] = 'Create new courses, or update existing ones'; +$string['csvdelimiter'] = 'CSV delimiter'; +$string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}'; +$string['csvline'] = 'Line'; +$string['defaultvalues'] = 'Default course values'; +$string['encoding'] = 'Encoding'; +$string['errorwhilerestoringcourse'] = 'Error while restoring the course'; +$string['errorwhiledeletingcourse'] = 'Error while deleting the course'; +$string['generatedshortnameinvalid'] = 'The generated shortname is invalid'; +$string['generatedshortnamealreadyinuse'] = 'The generated shortname is already in use'; +$string['id'] = 'ID'; +$string['importoptions'] = 'Import options'; +$string['idnumberalreadyinuse'] = 'ID number already used by a course'; +$string['invalidbackupfile'] = 'Invalid backup file'; +$string['invalidcourseformat'] = 'Invalid course format'; +$string['invalidcsvfile'] = 'Invalid input CSV file'; +$string['invalidencoding'] = 'Invalid encoding'; +$string['invalidmode'] = 'Invalid mode selected'; +$string['invalideupdatemode'] = 'Invalid update mode selected'; +$string['invalidroles'] = 'Invalid role names: {$a}'; +$string['invalidshortname'] = 'Invalid shortname'; +$string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}'; +$string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set'; +$string['mode'] = 'Upload mode'; +$string['nochanges'] = 'No changes'; +$string['pluginname'] = 'Course upload'; +$string['preview'] = 'Preview'; +$string['reset'] = 'Reset course after upload'; +$string['result'] = 'Result'; +$string['restoreafterimport'] = 'Restore after import'; +$string['rowpreviewnum'] = 'Preview rows'; +$string['shortnametemplate'] = 'Template to generate a shortname'; +$string['shortnametemplate_help'] = 'The short name of the course is displayed in the navigation. You may use template syntax here (%f = fullname, %i = idnumber), or enter an initial value that is incremented.'; +$string['templatefile'] = 'Restore from this file after upload'; +$string['templatefile_help'] = 'Select a file to use as a template for the creation of all courses.'; +$string['unknownimportmode'] = 'Unknown import mode'; +$string['updatemissing'] = 'Fill in missing from CSV data and defaults'; +$string['updatemode'] = 'Update mode'; +$string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated'; +$string['updateonly'] = 'Only update existing courses'; +$string['updatewithdataordefaults'] = 'Update with CSV data and defaults'; +$string['updatewithdataonly'] = 'Update with CSV data only'; +$string['uploadcourses'] = 'Upload courses'; +$string['uploadcourses_help'] = 'Courses may be uploaded via text file. The format of the file should be as follows: + +* Each line of the file contains one record +* Each record is a series of data separated by commas (or other delimiters) +* The first record contains a list of fieldnames defining the format of the rest of the file +* Required fieldnames are shortname, fullname, summary and category'; +$string['uploadcoursespreview'] = 'Upload courses preview'; +$string['uploadcoursesresult'] = 'Upload courses results'; diff --git a/admin/tool/uploadcourse/settings.php b/admin/tool/uploadcourse/settings.php new file mode 100644 index 00000000000..9b485d90a0d --- /dev/null +++ b/admin/tool/uploadcourse/settings.php @@ -0,0 +1,30 @@ +. + +/** + * Link to CSV course upload. + * + * @package tool_uploadcourse + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +if ($hassiteconfig) { + $ADMIN->add('courses', new admin_externalpage('tooluploadcourse', + get_string('uploadcourses', 'tool_uploadcourse'), "/admin/tool/uploadcourse/index.php")); +} diff --git a/admin/tool/uploadcourse/tests/behat/create.feature b/admin/tool/uploadcourse/tests/behat/create.feature new file mode 100644 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/course_test.php b/admin/tool/uploadcourse/tests/course_test.php new file mode 100644 index 00000000000..f04a2eae104 --- /dev/null +++ b/admin/tool/uploadcourse/tests/course_test.php @@ -0,0 +1,923 @@ +. + +/** + * File containing tests for the course class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Course test case. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late + */ +class tool_uploadcourse_course_testcase extends advanced_testcase { + + public function test_proceed_without_prepare() { + $this->resetAfterTest(true); + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array(); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->setExpectedException('coding_exception'); + $co->proceed(); + } + + public function test_proceed_when_prepare_failed() { + $this->resetAfterTest(true); + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array(); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->setExpectedException('moodle_exception'); + $co->proceed(); + } + + public function test_proceed_when_already_started() { + $this->resetAfterTest(true); + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('shortname' => 'test', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->setExpectedException('coding_exception'); + $co->proceed(); + } + + public function test_invalid_shortname() { + $this->resetAfterTest(true); + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('shortname' => '', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('invalidshortname', $co->get_errors()); + } + + public function test_create() { + global $DB; + $this->resetAfterTest(true); + + // Existing course. + $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1', 'summary' => 'Yay!')); + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1'))); + + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + + // Try to add a new course. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $data = array('shortname' => 'newcourse', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $this->assertFalse($DB->record_exists('course', array('shortname' => 'newcourse'))); + $co->proceed(); + $this->assertTrue($DB->record_exists('course', array('shortname' => 'newcourse'))); + + // Try to add a new course, that already exists. + $coursecount = $DB->count_records('course', array()); + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $data = array('shortname' => 'c1', 'fullname' => 'C1FN', 'summary' => 'C1', 'category' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('courseexistsanduploadnotallowed', $co->get_errors()); + $this->assertEquals($coursecount, $DB->count_records('course', array())); + $this->assertNotEquals('C1', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1'))); + + // Try to add new with shortname incrementation. + $mode = tool_uploadcourse_processor::MODE_CREATE_ALL; + $data = array('shortname' => 'c1', 'fullname' => 'C1FN', 'summary' => 'C1', 'category' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2'))); + } + + public function test_delete() { + global $DB; + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(); + $c2 = $this->getDataGenerator()->create_course(); + + $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname))); + $this->assertFalse($DB->record_exists('course', array('shortname' => 'DoesNotExist'))); + + $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + + // Try delete when option not available. + $importoptions = array('candelete' => false); + $data = array('shortname' => $c1->shortname, 'delete' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('coursedeletionnotallowed', $co->get_errors()); + $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname))); + + // Try delete when not requested. + $importoptions = array('candelete' => true); + $data = array('shortname' => $c1->shortname, 'delete' => 0); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname))); + + // Try delete when requested. + $importoptions = array('candelete' => true); + $data = array('shortname' => $c1->shortname, 'delete' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertFalse($DB->record_exists('course', array('shortname' => $c1->shortname))); + $this->assertTrue($DB->record_exists('course', array('shortname' => $c2->shortname))); + + // Try deleting non-existing record, this should not fail. + $data = array('shortname' => 'DoesNotExist', 'delete' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('cannotdeletecoursenotexist', $co->get_errors()); + } + + public function test_update() { + global $DB; + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1')); + + // Try to update with existing shortnames, not allowing creation, and updating nothing. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array('shortname' => 'c1', 'fullname' => 'New fullname'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('updatemodedoessettonothing', $co->get_errors()); + + // Try to update with non-existing shortnames. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => 'DoesNotExist', 'fullname' => 'New fullname'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('coursedoesnotexistandcreatenotallowed', $co->get_errors()); + + // Try a proper update. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => 'c1', 'fullname' => 'New fullname'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertEquals('New fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1'))); + + // Try a proper update with defaults. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS; + $data = array('shortname' => 'c1', 'fullname' => 'Another fullname'); + $defaults = array('fullname' => 'Not this one', 'summary' => 'Awesome summary'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertEquals('Another fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1'))); + $this->assertEquals('Awesome summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1'))); + + // Try a proper update missing only. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS; + $DB->set_field('course', 'summary', '', array('shortname' => 'c1')); + $this->assertEquals('', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1'))); + $data = array('shortname' => 'c1', 'summary' => 'Fill in summary'); + $defaults = array('summary' => 'Do not use this summary'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertEquals('Fill in summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1'))); + + // Try a proper update missing only using defaults. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS; + $DB->set_field('course', 'summary', '', array('shortname' => 'c1')); + $this->assertEquals('', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1'))); + $data = array('shortname' => 'c1'); + $defaults = array('summary' => 'Use this summary'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertEquals('Use this summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1'))); + } + + public function test_data_saved() { + global $DB; + $this->resetAfterTest(true); + + // Create. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array( + 'shortname' => 'c1', + 'fullname' => 'Fullname', + 'category' => '1', + 'visible' => '0', + 'startdate' => '8 June 1990', + 'idnumber' => '123abc', + 'summary' => 'Summary', + 'format' => 'weeks', + 'theme' => 'afterburner', + 'lang' => 'en', + 'newsitems' => '7', + 'showgrades' => '0', + 'showreports' => '1', + 'legacyfiles' => '1', + 'maxbytes' => '1234', + 'groupmode' => '2', + 'groupmodeforce' => '1', + 'enablecompletion' => '1', + + 'role_teacher' => 'Knight', + 'role_manager' => 'Jedi', + + 'enrolment_1' => 'guest', + 'enrolment_2' => 'self', + 'enrolment_2_roleid' => '1', + 'enrolment_3' => 'manual', + 'enrolment_3_disable' => '1', + ); + + $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1'))); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1'))); + $course = $DB->get_record('course', array('shortname' => 'c1')); + $ctx = context_course::instance($course->id); + + $this->assertEquals($data['fullname'], $course->fullname); + $this->assertEquals($data['category'], $course->category); + $this->assertEquals($data['visible'], $course->visible); + $this->assertEquals(mktime(0, 0, 0, 6, 8, 1990), $course->startdate); + $this->assertEquals($data['idnumber'], $course->idnumber); + $this->assertEquals($data['summary'], $course->summary); + $this->assertEquals($data['format'], $course->format); + $this->assertEquals($data['theme'], $course->theme); + $this->assertEquals($data['lang'], $course->lang); + $this->assertEquals($data['newsitems'], $course->newsitems); + $this->assertEquals($data['showgrades'], $course->showgrades); + $this->assertEquals($data['showreports'], $course->showreports); + $this->assertEquals($data['legacyfiles'], $course->legacyfiles); + $this->assertEquals($data['maxbytes'], $course->maxbytes); + $this->assertEquals($data['groupmode'], $course->groupmode); + $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce); + $this->assertEquals($data['enablecompletion'], $course->enablecompletion); + + // Roles. + $roleids = array(); + $roles = get_all_roles(); + foreach ($roles as $role) { + $roleids[$role->shortname] = $role->id; + } + $this->assertEquals('Knight', $DB->get_field_select('role_names', 'name', + 'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['teacher']))); + $this->assertEquals('Jedi', $DB->get_field_select('role_names', 'name', + 'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['manager']))); + + // Enrolment methods. + $enroldata = array(); + $instances = enrol_get_instances($course->id, false); + $this->assertCount(3, $instances); + foreach ($instances as $instance) { + $enroldata[$instance->enrol] = $instance; + } + + $this->assertNotEmpty($enroldata['guest']); + $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['guest']->status); + $this->assertNotEmpty($enroldata['self']); + $this->assertEquals($data['enrolment_2_roleid'], $enroldata['self']->roleid); + $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['self']->status); + $this->assertNotEmpty($enroldata['manual']); + $this->assertEquals(ENROL_INSTANCE_DISABLED, $enroldata['manual']->status); + + // Update existing course. + $cat = $this->getDataGenerator()->create_category(); + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array( + 'shortname' => 'c1', + 'fullname' => 'Fullname 2', + 'category' => $cat->id, + 'visible' => '1', + 'startdate' => '11 June 1984', + 'idnumber' => 'changeidn', + 'summary' => 'Summary 2', + 'format' => 'topics', + 'theme' => 'clean', + 'lang' => '', + 'newsitems' => '2', + 'showgrades' => '1', + 'showreports' => '0', + 'legacyfiles' => '0', + 'maxbytes' => '4321', + 'groupmode' => '1', + 'groupmodeforce' => '0', + 'enablecompletion' => '0', + + 'role_teacher' => 'Teacher', + 'role_manager' => 'Manager', + + 'enrolment_1' => 'guest', + 'enrolment_1_disable' => '1', + 'enrolment_2' => 'self', + 'enrolment_2_roleid' => '2', + 'enrolment_3' => 'manual', + 'enrolment_3_delete' => '1', + ); + + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1'))); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $co->proceed(); + $course = $DB->get_record('course', array('shortname' => 'c1')); + $ctx = context_course::instance($course->id); + + $this->assertEquals($data['fullname'], $course->fullname); + $this->assertEquals($data['category'], $course->category); + $this->assertEquals($data['visible'], $course->visible); + $this->assertEquals(mktime(0, 0, 0, 6, 11, 1984), $course->startdate); + $this->assertEquals($data['idnumber'], $course->idnumber); + $this->assertEquals($data['summary'], $course->summary); + $this->assertEquals($data['format'], $course->format); + $this->assertEquals($data['theme'], $course->theme); + $this->assertEquals($data['lang'], $course->lang); + $this->assertEquals($data['newsitems'], $course->newsitems); + $this->assertEquals($data['showgrades'], $course->showgrades); + $this->assertEquals($data['showreports'], $course->showreports); + $this->assertEquals($data['legacyfiles'], $course->legacyfiles); + $this->assertEquals($data['maxbytes'], $course->maxbytes); + $this->assertEquals($data['groupmode'], $course->groupmode); + $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce); + $this->assertEquals($data['enablecompletion'], $course->enablecompletion); + + // Roles. + $roleids = array(); + $roles = get_all_roles(); + foreach ($roles as $role) { + $roleids[$role->shortname] = $role->id; + } + $this->assertEquals('Teacher', $DB->get_field_select('role_names', 'name', + 'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['teacher']))); + $this->assertEquals('Manager', $DB->get_field_select('role_names', 'name', + 'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['manager']))); + + // Enrolment methods. + $enroldata = array(); + $instances = enrol_get_instances($course->id, false); + $this->assertCount(2, $instances); + foreach ($instances as $instance) { + $enroldata[$instance->enrol] = $instance; + } + + $this->assertNotEmpty($enroldata['guest']); + $this->assertEquals(ENROL_INSTANCE_DISABLED, $enroldata['guest']->status); + $this->assertNotEmpty($enroldata['self']); + $this->assertEquals($data['enrolment_2_roleid'], $enroldata['self']->roleid); + $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['self']->status); + } + + public function test_default_data_saved() { + global $DB; + $this->resetAfterTest(true); + + // Create. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING; + $data = array( + 'shortname' => 'c1', + ); + $defaultdata = array( + 'fullname' => 'Fullname', + 'category' => '1', + 'visible' => '0', + 'startdate' => '8 June 1990', + 'idnumber' => '123abc', + 'summary' => 'Summary', + 'format' => 'weeks', + 'theme' => 'afterburner', + 'lang' => 'en', + 'newsitems' => '7', + 'showgrades' => '0', + 'showreports' => '1', + 'legacyfiles' => '1', + 'maxbytes' => '1234', + 'groupmode' => '2', + 'groupmodeforce' => '1', + 'enablecompletion' => '1', + ); + + $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1'))); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1'))); + $course = $DB->get_record('course', array('shortname' => 'c1')); + $ctx = context_course::instance($course->id); + + $this->assertEquals($defaultdata['fullname'], $course->fullname); + $this->assertEquals($defaultdata['category'], $course->category); + $this->assertEquals($defaultdata['visible'], $course->visible); + $this->assertEquals(mktime(0, 0, 0, 6, 8, 1990), $course->startdate); + $this->assertEquals($defaultdata['idnumber'], $course->idnumber); + $this->assertEquals($defaultdata['summary'], $course->summary); + $this->assertEquals($defaultdata['format'], $course->format); + $this->assertEquals($defaultdata['theme'], $course->theme); + $this->assertEquals($defaultdata['lang'], $course->lang); + $this->assertEquals($defaultdata['newsitems'], $course->newsitems); + $this->assertEquals($defaultdata['showgrades'], $course->showgrades); + $this->assertEquals($defaultdata['showreports'], $course->showreports); + $this->assertEquals($defaultdata['legacyfiles'], $course->legacyfiles); + $this->assertEquals($defaultdata['maxbytes'], $course->maxbytes); + $this->assertEquals($defaultdata['groupmode'], $course->groupmode); + $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce); + $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion); + + // Update. + $cat = $this->getDataGenerator()->create_category(); + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS; + $data = array( + 'shortname' => 'c1', + ); + $defaultdata = array( + 'fullname' => 'Fullname 2', + 'category' => $cat->id, + 'visible' => '1', + 'startdate' => '11 June 1984', + 'idnumber' => 'changedid', + 'summary' => 'Summary 2', + 'format' => 'topics', + 'theme' => 'clean', + 'lang' => '', + 'newsitems' => '2', + 'showgrades' => '1', + 'showreports' => '0', + 'legacyfiles' => '0', + 'maxbytes' => '1111', + 'groupmode' => '1', + 'groupmodeforce' => '0', + 'enablecompletion' => '0', + ); + + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1'))); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1'))); + $course = $DB->get_record('course', array('shortname' => 'c1')); + $ctx = context_course::instance($course->id); + + $this->assertEquals($defaultdata['fullname'], $course->fullname); + $this->assertEquals($defaultdata['category'], $course->category); + $this->assertEquals($defaultdata['visible'], $course->visible); + $this->assertEquals(mktime(0, 0, 0, 6, 11, 1984), $course->startdate); + $this->assertEquals($defaultdata['idnumber'], $course->idnumber); + $this->assertEquals($defaultdata['summary'], $course->summary); + $this->assertEquals($defaultdata['format'], $course->format); + $this->assertEquals($defaultdata['theme'], $course->theme); + $this->assertEquals($defaultdata['lang'], $course->lang); + $this->assertEquals($defaultdata['newsitems'], $course->newsitems); + $this->assertEquals($defaultdata['showgrades'], $course->showgrades); + $this->assertEquals($defaultdata['showreports'], $course->showreports); + $this->assertEquals($defaultdata['legacyfiles'], $course->legacyfiles); + $this->assertEquals($defaultdata['maxbytes'], $course->maxbytes); + $this->assertEquals($defaultdata['groupmode'], $course->groupmode); + $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce); + $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion); + } + + public function test_rename() { + global $DB; + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1')); + $c2 = $this->getDataGenerator()->create_course(array('shortname' => 'c2')); + + // Cannot rename when creating. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $importoptions = array('canrename' => true); + $data = array('shortname' => 'c1', 'rename' => 'newshortname'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('courseexistsanduploadnotallowed', $co->get_errors()); + + // Cannot rename when creating. + $mode = tool_uploadcourse_processor::MODE_CREATE_ALL; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $importoptions = array('canrename' => true); + $data = array('shortname' => 'c1', 'rename' => 'newshortname', 'category' => 1, 'summary' => 'S', 'fullname' => 'F'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('canonlyrenameinupdatemode', $co->get_errors()); + + // Error when not allowed to rename the course. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $importoptions = array('canrename' => false); + $data = array('shortname' => 'c1', 'rename' => 'newshortname'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('courserenamingnotallowed', $co->get_errors()); + + // Can rename when updating. + $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $importoptions = array('canrename' => true); + $data = array('shortname' => 'c1', 'rename' => 'newshortname'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertEquals('newshortname', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id))); + + // Can rename when updating. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $importoptions = array('canrename' => true); + $data = array('shortname' => 'newshortname', 'rename' => 'newshortname2'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertEquals('newshortname2', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id))); + + // Error when course does not exist. + $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $importoptions = array('canrename' => true); + $data = array('shortname' => 'DoesNotExist', 'rename' => 'c1', 'category' => 1, 'summary' => 'S', 'fullname' => 'F'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('cannotrenamecoursenotexist', $co->get_errors()); + + // Renaming still updates the other values. + $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS; + $importoptions = array('canrename' => true); + $data = array('shortname' => 'newshortname2', 'rename' => 'c1', 'fullname' => 'Another fullname!'); + $defaultdata = array('summary' => 'New summary!'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata, $importoptions); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertEquals('c1', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id))); + $this->assertEquals('New summary!', $DB->get_field_select('course', 'summary', 'id = :id', array('id' => $c1->id))); + $this->assertEquals('Another fullname!', $DB->get_field_select('course', 'fullname', 'id = :id', array('id' => $c1->id))); + + // Renaming with invalid shortname. + $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $importoptions = array('canrename' => true); + $data = array('shortname' => 'c1', 'rename' => '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() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + $c1 = $this->getDataGenerator()->create_course(); + $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id)); + + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => 'A1', 'templatecourse' => $c1->shortname, 'summary' => 'A', 'category' => 1, + 'fullname' => 'A1'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $co->proceed(); + $course = $DB->get_record('course', array('shortname' => 'A1')); + $modinfo = get_fast_modinfo($course); + $found = false; + foreach ($modinfo->get_cms() as $cmid => $cm) { + if ($cm->modname == 'forum' && $cm->name == $c1f1->name) { + $found = true; + break; + } + } + $this->assertTrue($found); + + // Restore the time limit to prevent warning. + set_time_limit(0); + } + + public function test_restore_file() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + + $c1 = $this->getDataGenerator()->create_course(); + $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id)); + + // Restore from a file, checking that the file takes priority over the templatecourse. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => 'A1', 'backupfile' => __DIR__ . '/fixtures/backup.mbz', + 'summary' => 'A', 'category' => 1, 'fullname' => 'A1', 'templatecourse' => $c1->shortname); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $co->proceed(); + $course = $DB->get_record('course', array('shortname' => 'A1')); + $modinfo = get_fast_modinfo($course); + $found = false; + foreach ($modinfo->get_cms() as $cmid => $cm) { + if ($cm->modname == 'glossary' && $cm->name == 'Imported Glossary') { + $found = true; + } else if ($cm->modname == 'forum' && $cm->name == $c1f1->name) { + // We should not find this! + $this->assertTrue(false); + } + } + $this->assertTrue($found); + + // Restore the time limit to prevent warning. + set_time_limit(0); + } + + /** + * Testing the reset on groups, group members and enrolments. + */ + public function test_reset() { + global $DB; + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(); + $c1ctx = context_course::instance($c1->id); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); + + $u1 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($u1->id, $c1->id, $studentrole->id); + $this->assertCount(1, get_enrolled_users($c1ctx)); + + $g1 = $this->getDataGenerator()->create_group(array('courseid' => $c1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $g1->id, 'userid' => $u1->id)); + $this->assertEquals(1, $DB->count_records('groups', array('courseid' => $c1->id))); + $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id))); + + // Wrong mode. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => 'DoesNotExist', 'reset' => '1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => 1); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('canonlyresetcourseinupdatemode', $co->get_errors()); + $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id))); + $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id))); + $this->assertCount(1, get_enrolled_users($c1ctx)); + + // Reset not allowed. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => $c1->shortname, 'reset' => '1'); + $importoptions = array('canreset' => false); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('courseresetnotallowed', $co->get_errors()); + $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id))); + $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id))); + $this->assertCount(1, get_enrolled_users($c1ctx)); + + // Reset allowed but not requested. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => $c1->shortname, 'reset' => '0'); + $importoptions = array('canreset' => true); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id))); + $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id))); + $this->assertCount(1, get_enrolled_users($c1ctx)); + + // Reset passed as a default parameter, should not be taken in account. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => $c1->shortname); + $importoptions = array('canreset' => true); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array('reset' => 1), $importoptions); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id))); + $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id))); + $this->assertCount(1, get_enrolled_users($c1ctx)); + + // Reset executed from data. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => $c1->shortname, 'reset' => 1); + $importoptions = array('canreset' => true); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertFalse($DB->record_exists('groups', array('id' => $g1->id))); + $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id))); + $this->assertCount(0, get_enrolled_users($c1ctx)); + + // Reset executed from import option. + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => $c1->shortname, 'reset' => 0); + $importoptions = array('reset' => 1, 'canreset' => true); + $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions); + + $g1 = $this->getDataGenerator()->create_group(array('courseid' => $c1->id)); + $this->getDataGenerator()->create_group_member(array('groupid' => $g1->id, 'userid' => $u1->id)); + $this->assertEquals(1, $DB->count_records('groups', array('courseid' => $c1->id))); + $this->assertTrue($co->prepare()); + $co->proceed(); + $this->assertFalse($DB->record_exists('groups', array('id' => $g1->id))); + } + + public function test_create_bad_category() { + $this->resetAfterTest(true); + + // Ensure fails when category cannot be resolved upon creation. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => 'c1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => 'Wrong cat'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors()); + + // Ensure fails when category cannot be resolved upon update. + $c1 = $this->getDataGenerator()->create_course(); + $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => $c1->shortname, 'category' => 'Wrong cat'); + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertFalse($co->prepare()); + $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors()); + } + + public function test_enrolment_data() { + $this->resetAfterTest(true); + + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY; + $data = array('shortname' => 'c1', 'summary' => 'S', 'fullname' => 'FN', 'category' => '1'); + $data['enrolment_1'] = 'manual'; + $data['enrolment_1_role'] = 'teacher'; + $data['enrolment_1_startdate'] = '2nd July 2013'; + $data['enrolment_1_enddate'] = '2nd August 2013'; + $data['enrolment_1_enrolperiod'] = '10 days'; + $co = new tool_uploadcourse_course($mode, $updatemode, $data); + $this->assertTrue($co->prepare()); + $co->proceed(); + + // Enrolment methods. + $enroldata = array(); + $instances = enrol_get_instances($co->get_id(), false); + foreach ($instances as $instance) { + $enroldata[$instance->enrol] = $instance; + } + + $this->assertNotEmpty($enroldata['manual']); + $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['manual']->status); + $this->assertEquals(strtotime($data['enrolment_1_startdate']), $enroldata['manual']->enrolstartdate); + $this->assertEquals(strtotime('1970-01-01 GMT + ' . $data['enrolment_1_enrolperiod']), $enroldata['manual']->enrolperiod); + $this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate); + } + + public function test_idnumber_problems() { + $this->resetAfterTest(true); + + $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'sntaken', 'idnumber' => 'taken')); + $c2 = $this->getDataGenerator()->create_course(); + + // Create with existing ID number. + $mode = tool_uploadcourse_processor::MODE_CREATE_NEW; + $updatemode = 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()); + + // 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/fixtures/backup.mbz b/admin/tool/uploadcourse/tests/fixtures/backup.mbz new file mode 100644 index 00000000000..25da1743542 Binary files /dev/null and b/admin/tool/uploadcourse/tests/fixtures/backup.mbz differ 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 diff --git a/admin/tool/uploadcourse/tests/helper_test.php b/admin/tool/uploadcourse/tests/helper_test.php new file mode 100644 index 00000000000..f81c59f4a55 --- /dev/null +++ b/admin/tool/uploadcourse/tests/helper_test.php @@ -0,0 +1,389 @@ +. + +/** + * 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; + +/** + * 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 { + + 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')); + $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('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() { + $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()); + } + + public 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); + } + + 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->assertEquals($actual, $secondactual); + } + + public function test_get_restore_content_dir() { + global $CFG; + $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); + + // 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"; + $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); + } + + 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', + ); + + $errors = array(); + $actual = tool_uploadcourse_helper::get_role_names($data, $errors); + $this->assertSame($actual, $expected); + $this->assertArrayHasKey('invalidroles', $errors); + } + + 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. + $errors = array(); + $data['category_idnumber'] = 1234; + $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, $errors)); + $this->assertArrayHasKey('couldnotresolvecatgorybyid', $errors); + $errors = array(); + $data['category_path'] = 'Not exist'; + $this->assertEmpty(tool_uploadcourse_helper::resolve_category($data, $errors)); + $this->assertArrayHasKey('couldnotresolvecatgorybypath', $errors); + } + + 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..648fcc6d936 --- /dev/null +++ b/admin/tool/uploadcourse/tests/processor_test.php @@ -0,0 +1,225 @@ +. + +/** + * 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->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 { + + 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); + } + + 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(); + } + +} diff --git a/admin/tool/uploadcourse/version.php b/admin/tool/uploadcourse/version.php new file mode 100644 index 00000000000..112b41cb2e0 --- /dev/null +++ b/admin/tool/uploadcourse/version.php @@ -0,0 +1,29 @@ +. + +/** + * Plugin version info. + * + * @package tool_uploadcourse + * @copyright 2011 Piers Harding + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +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). diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 8635bd309d3..2f2b72d4e93 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -840,7 +840,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(