diff --git a/admin/tool/uploadcourse/classes/course.php b/admin/tool/uploadcourse/classes/course.php new file mode 100644 index 00000000000..b282bc98884 --- /dev/null +++ b/admin/tool/uploadcourse/classes/course.php @@ -0,0 +1,780 @@ +. + +/** + * File containing the course class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Course class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_course { + + /** Outcome of the process: creating the course */ + const OUTCOME_CREATE = 1; + + /** Outcome of the process: updating the course */ + const OUTCOME_UPDATE = 2; + + /** Outcome of the process: deleting the course */ + const OUTCOME_DELETE = 3; + + /** @var array final import data. */ + protected $data = array(); + + /** @var array default values. */ + protected $defaults = array(); + + /** @var array enrolment data. */ + protected $enrolmentdata; + + /** @var array errors. */ + protected $errors; + + /** @var array containing options passed from the processor. */ + protected $importoptions = array(); + + /** @var int import mode. Matches tool_uploadcourse_processor::MODE_* */ + protected $mode; + + /** @var array course import options. */ + protected $options = array(); + + /** @var int constant value of the outcome, what to do with that course */ + protected $outcome; + + /** @var bool set to true once we have prepared the course */ + protected $prepared = false; + + /** @var array course import data. */ + protected $rawdata = array(); + + /** @var array restore directory. */ + protected $restoredata; + + /** @var string course shortname. */ + protected $shortname; + + /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */ + protected $updatemode; + + /** @var array fields allowed as course data. */ + static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate', + 'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes', + 'groupmode', 'groupmodeforce', 'groupmodeforce', 'enablecompletion'); + + /** @var array fields required on course creation. */ + static protected $mandatoryfields = array('fullname', 'summary', 'category'); + + /** @var array fields which are considered as options. */ + static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null, + 'templatecourse' => null, 'reset' => false); + + /** @var array options determining what can or cannot be done at an import level. */ + static protected $importoptionsdefaults = array('canrename' => false, 'candelete' => false, 'canreset' => false, + 'reset' => false, 'restoredir' => null, 'shortnametemplate' => null); + + /** + * Constructor + * + * @param int $mode import mode, constant matching tool_uploadcourse_processor::MODE_* + * @param int $updatemode update mode, constant matching tool_uploadcourse_processor::UPDATE_* + * @param array $rawdata raw course data. + * @param array $defaults default course data. + * @param array $importoptions import options. + */ + public function __construct($mode, $updatemode, $rawdata, $defaults = array(), $importoptions = array()) { + + if ($mode !== tool_uploadcourse_processor::MODE_CREATE_NEW && + $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL && + $mode !== tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE && + $mode !== tool_uploadcourse_processor::MODE_UPDATE_ONLY) { + throw new coding_exception('Incorrect mode.'); + } else if ($updatemode !== tool_uploadcourse_processor::UPDATE_NOTHING && + $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY && + $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS && + $updatemode !== tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS) { + throw new coding_exception('Incorrect update mode.'); + } + + $this->mode = $mode; + $this->updatemode = $updatemode; + + if (isset($rawdata['shortname'])) { + $this->shortname = $rawdata['shortname']; + } + $this->rawdata = $rawdata; + $this->defaults = $defaults; + + // Extract course options. + foreach (self::$optionfields as $option => $default) { + $this->options[$option] = isset($rawdata[$option]) ? $rawdata[$option] : $default; + } + + // Import options. + foreach (self::$importoptionsdefaults as $option => $default) { + $this->importoptions[$option] = isset($importoptions[$option]) ? $importoptions[$option] : $default; + } + } + + /** + * Does the mode allow for course creation? + * + * @return bool + */ + public function can_create() { + return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL, + tool_uploadcourse_processor::MODE_CREATE_NEW, + tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE) + ); + } + + /** + * Does the mode allow for course deletion? + * + * @return bool + */ + public function can_delete() { + return $this->importoptions['candelete']; + } + + /** + * Does the mode only allow for course creation? + * + * @return bool + */ + public function can_only_create() { + return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL, + tool_uploadcourse_processor::MODE_CREATE_NEW)); + } + + /** + * Does the mode allow for course rename? + * + * @return bool + */ + public function can_rename() { + return $this->importoptions['canrename']; + } + + /** + * Does the mode allow for course reset? + * + * @return bool + */ + public function can_reset() { + return $this->importoptions['canreset']; + } + + /** + * Does the mode allow for course update? + * + * @return bool + */ + public function can_update() { + return in_array($this->mode, array(tool_uploadcourse_processor::MODE_UPDATE_ONLY, + tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)) && $this->updatemode != tool_uploadcourse_processor::UPDATE_NOTHING; + } + + /** + * Can we use default values? + * + * @return bool + */ + public function can_use_defaults() { + return in_array($this->updatemode, array(tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS, + tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS)); + } + + /** + * Delete the current course. + * + * @return bool + */ + protected function delete() { + global $DB; + $id = $DB->get_field_select('course', 'id', 'shortname = :shortname', array('shortname' => $this->shortname), MUST_EXIST); + return delete_course($id, false); + } + + /** + * Log an error + * + * @param string $message error message. + * @return void + */ + protected function error($message) { + $this->errors[] = $message; + } + + /** + * Return whether the course exists or not. + * + * @return bool + */ + protected function exists($shortname = null) { + global $DB; + if (is_null($shortname)) { + $shortname = $this->shortname; + } + if (!empty($shortname) || is_numeric($shortname)) { + return $DB->record_exists('course', array('shortname' => $shortname)); + } + return false; + } + + /** + * Return the data that will be used upon saving. + * + * @return null|array + */ + public function get_data() { + return $this->data; + } + + /** + * Return the errors found during preparation. + * + * @return array + */ + public function get_errors() { + return $this->errors; + } + + /** + * Assemble the course data based on defaults. + * + * This returns the final data to be passed to create_course(). + * + * @param array $data current data. + * @return array + */ + protected function get_final_create_data($data) { + foreach (self::$validfields as $field) { + if (!isset($data[$field]) && isset($this->defaults[$field])) { + $data[$field] = $this->defaults[$field]; + } + } + $data['shortname'] = $this->shortname; + return $data; + } + + /** + * Assemble the course data based on defaults. + * + * This returns the final data to be passed to update_course(). + * + * @param array $data current data. + * @param bool $usedefaults are defaults allowed? + * @param bool $missingonly ignore fields which are already set. + * @return array + */ + protected function get_final_update_data($data, $usedefaults = false, $missingonly = false) { + global $DB; + $newdata = array(); + $existingdata = $DB->get_record('course', array('shortname' => $this->shortname)); + foreach (self::$validfields as $field) { + if ($missingonly) { + if (!is_null($existingdata->$field) and $existingdata->$field !== '') { + continue; + } + } + if (isset($data[$field])) { + $newdata[$field] = $data[$field]; + } else if ($usedefaults && isset($this->defaults[$field])) { + $newdata[$field] = $this->defaults[$field]; + } + } + $newdata['id'] = $existingdata->id; + return $newdata; + } + + /** + * Get the directory of the object to restore. + * + * @return string subdirectory in $CFG->tempdir/backup/... + */ + protected function get_restore_content_dir() { + $backupfile = null; + $shortname = null; + + if (!empty($this->options['backupfile'])) { + $backupfile = $this->options['backupfile']; + } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) { + $shortname = $this->options['templatecourse']; + } + + $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname); + if (empty($dir) && !empty($this->importoptions['restoredir'])) { + $dir = $this->importoptions['restoredir']; + } + + return $dir; + } + + /** + * Return whether there were errors with this course. + * + * @return boolean + */ + public function has_errors() { + return !empty($this->errors); + } + + /** + * Validates and prepares the data. + * + * @return bool false is any error occured. + */ + public function prepare() { + $this->prepared = true; + + // Validate the shortname. + if (!empty($this->shortname) || is_numeric($this->shortname)) { + if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) { + $this->error('invalid shortname'); + return false; + } + } + + $exists = $this->exists(); + + // Do we want to delete the course? + if ($this->options['delete']) { + if (!$exists) { + $this->error('cannot delete a course that does not exist'); + return false; + } else if (!$this->can_delete()) { + $this->error('no permission to delete course'); + return false; + } + + $this->outcome = self::OUTCOME_DELETE; + return true; + } + + // Can we create/update the course under those conditions? + if ($exists) { + if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { + $this->error('course exists and mode does not allow update'); + return false; + } + } else { + if (!$this->can_create()) { + $this->error('course does not exist and mode does not allow creation'); + return false; + } + } + + // Basic data. + $coursedata = array(); + foreach ($this->rawdata as $field => $value) { + if (!in_array($field, self::$validfields)) { + continue; + } else if ($field == 'shortname') { + // Let's leave it apart from now, use $this->shortname only. + continue; + } + $coursedata[$field] = $value; + } + + $mode = $this->mode; + $updatemode = $this->updatemode; + $usedefaults = $this->can_use_defaults(); + + // If the course does not exist, or will be forced created. + if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { + + // Mandatory fields upon creation. + $errors = 0; + foreach (self::$mandatoryfields as $field) { + if ((!isset($coursedata[$field]) || $coursedata[$field] === '') && + (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) { + $this->error('missing value for mandatory field: ' . $field); + $errors++; + } + } + if ($errors > 0) { + return false; + } + } + + // Should the course be renamed? + if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) { + if (!$exists) { + $this->error('cannot rename a course that does not exist'); + return false; + } else if (!$this->can_rename()) { + $this->error('no permission to rename the course'); + return false; + } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) { + $this->error('the shortname to rename the course with is invalid'); + return false; + } else if ($this->exists($this->options['rename'])) { + $this->error('the shortname to rename the course with is already used'); + return false; + } else if (isset($coursedata['idnumber']) && + $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn', + array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) { + $this->error('cannot rename the course as its ID number would conflict with another one'); + return false; + } + $coursedata['shortname'] = $this->options['rename']; + } + + // Should we generate a shortname? + if (empty($this->shortname) && !is_numeric($this->shortname)) { + if (empty($this->importoptions['shortnametemplate'])) { + $this->error('missing shortname and could not find a shortname template'); + return false; + } else if (!$this->can_only_create()) { + $this->error('cannot generate a shortname from template when mode allow updating'); + return false; + } else { + $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata, $this->importoptions['shortnametemplate']); + if (is_null($newshortname)) { + $this->error('the generated shortname is invalid'); + return false; + } else if ($this->exists($newshortname)) { + if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) { + $this->error('the generated shortname is used by another course'); + return false; + } + $exists = true; + } + $this->shortname = $newshortname; + } + } + + // If exists, but we only want to create courses, increment the shortname. + if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) { + $original = $this->shortname; + $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname); + $exists = false; + if ($this->shortname != $original) { + // TODO Log new shortname. + if (isset($coursedata['idnumber'])) { + $originalidn = $coursedata['idnumber']; + $coursedata['idnumber'] = $this->increment_idnumber($coursedata['idnumber']); + if ($originalidn != $coursedata['idnumber']) { + // TODO Log new idnumber. + } + } + } + } + + // Ultimate check mode vs. existence. + switch ($mode) { + case tool_uploadcourse_processor::MODE_CREATE_NEW: + case tool_uploadcourse_processor::MODE_CREATE_ALL: + if ($exists) { + $this->error('the course exists and mode does not allow for update'); + return false; + } + break; + case tool_uploadcourse_processor::MODE_UPDATE_ONLY: + if (!$exists) { + $this->error('the course does not exist and mode only allow for update'); + return false; + } + // No break! + case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE: + if ($exists) { + if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) { + $this->error('the update mode does not allow for any field to be updated'); + return false; + } + } + break; + default: + // O_o Huh?! This should really never happen here! + $this->error('unknown import mode!'); + return false; + } + + // Resolve the category. + // TODO Log resolve error. + $catid = tool_uploadcourse_helper::resolve_category($this->rawdata); + if (!empty($catid)) { + $coursedata['category'] = $catid; + } + + // Get final data. + if ($exists) { + $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS); + $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly); + $this->outcome = self::OUTCOME_UPDATE; + } else { + $coursedata = $this->get_final_create_data($coursedata); + $this->outcome = self::OUTCOME_CREATE; + } + + // Course start date. + if (!empty($coursedata['startdate'])) { + $coursedata['startdate'] = strtotime($coursedata['startdate']); + } + + // Add role renaming. + // TODO add warning about wrong roles. + foreach (tool_uploadcourse_helper::get_role_names($this->rawdata) as $rolekey => $rolename) { + $coursedata[$rolekey] = $rolename; + } + + // Validation. + if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) { + // Warning, wrong format! + $coursedata['format'] = 'weeks'; + } + + // Saving data. + $this->data = $coursedata; + $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata); + + // Restore data. + // TODO log warnings. + $this->restoredata = $this->get_restore_content_dir(); + + // We cannot + if ($this->importoptions['reset'] || $this->options['reset']) { + if ($this->outcome !== self::OUTCOME_UPDATE) { + $this->error('can only reset a course that has been updated'); + return false; + } else if (!$this->can_reset()) { + $this->error('no permission to reset the course'); + return false; + } + } + + return true; + } + + /** + * Proceed with the import of the course. + * + * @return void + */ + public function proceed() { + global $CFG, $USER; + + if (!$this->prepared) { + throw new coding_exception('The course has not been prepared.'); + } else if ($this->has_errors()) { + throw new moodle_exception('Cannot proceed, errors were detected.'); + } + + if ($this->outcome === self::OUTCOME_DELETE) { + return $this->delete(); + } else if ($this->outcome === self::OUTCOME_CREATE) { + $course = create_course((object) $this->data); + } else if ($this->outcome === self::OUTCOME_UPDATE) { + $course = (object) $this->data; + update_course($course); + } else { + // Strangely the outcome has not been defined, or is unknown! + throw new coding_exception('Unknown outcome!'); + } + + // Restore a course. + if (!empty($this->restoredata)) { + $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO, + backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); + + // Check if the format conversion must happen first. + if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) { + $rc->convert(); + } + if ($rc->execute_precheck()) { + $rc->execute_plan(); + } else { + // Error! + // $rc->get_precheck_results()); + } + $rc->destroy(); + } + + // Proceed with enrolment data. + $this->process_enrolment_data($course); + + // Reset the course. + if ($this->importoptions['reset'] || $this->options['reset']) { + if ($this->outcome === self::OUTCOME_UPDATE && $this->can_reset()) { + $this->reset($course); + } + } + + // Mark context as dirty. + $context = context_course::instance($course->id); + $context->mark_dirty(); + } + + /** + * Add the enrolment data for the course. + * + * @param object $course course record. + * @return void + */ + protected function process_enrolment_data($course) { + global $DB; + + $enrolmentdata = $this->enrolmentdata; + if (empty($enrolmentdata)) { + return; + } + + $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins(); + $instances = enrol_get_instances($course->id, false); + foreach ($enrolmentdata as $enrolmethod => $method) { + + $instance = null; + foreach ($instances as $i) { + if ($i->enrol == $enrolmethod) { + $instance = $i; + break; + } + } + + $todelete = isset($method['delete']) && $method['delete']; + $todisable = isset($method['disable']) && $method['disable']; + unset($method['delete']); + unset($method['disable']); + + if (!empty($instance) && $todelete) { + // Remove the enrolment method. + foreach ($instances as $instance) { + if ($instance->enrol == $enrolmethod) { + $plugin = $enrolmentplugins[$instance->enrol]; + $plugin->delete_instance($instance); + break; + } + } + } else if (!empty($instance) && $todisable) { + // Disable the enrolment. + foreach ($instances as $instance) { + if ($instance->enrol == $enrolmethod) { + $plugin = $enrolmentplugins[$instance->enrol]; + $plugin->update_status($instance, ENROL_INSTANCE_DISABLED); + $enrol_updated = true; + break; + } + } + } else { + $plugin = null; + if (empty($instance)) { + $plugin = $enrolmentplugins[$enrolmethod]; + $instance = new stdClass(); + $instance->id = $plugin->add_default_instance($course); + $instance->roleid = $plugin->get_config('roleid'); + $instance->status = ENROL_INSTANCE_ENABLED; + } else { + $plugin = $enrolmentplugins[$instance->enrol]; + $plugin->update_status($instance, ENROL_INSTANCE_ENABLED); + } + + // Now update values. + foreach ($method as $k => $v) { + $instance->{$k} = $v; + } + + // Sort out the start, end and date. + $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0); + $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0); + + // Is the enrolment period set? + if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) { + if (preg_match('/^\d+$/', $method['enrolperiod'])) { + $method['enrolperiod'] = (int) $method['enrolperiod']; + } else { + // Try and convert period to seconds. + $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']); + } + $instance->enrolperiod = $method['enrolperiod']; + } + if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) { + $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod']; + } + if ($instance->enrolenddate > 0) { + $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate; + } + if ($instance->enrolenddate < $instance->enrolstartdate) { + $instance->enrolenddate = $instance->enrolstartdate; + } + + // Sort out the given role. This does not filter the roles allowed in the course. + if (isset($method['role'])) { + $roleids = tool_uploadcourse_helper::get_role_ids(); + if (isset($roleids[$method['role']])) { + $instance->roleid = $roleids[$method['role']]; + } + } + + $instance->timemodified = time(); + $DB->update_record('enrol', $instance); + } + } + } + + /** + * Reset the current course. + * + * This does not reset any of the content of the activities. + * + * @return array status array of array component, item, error. + */ + protected function reset($course) { + global $DB; + + $resetdata = new stdClass(); + $resetdata->id = $course->id; + $resetdata->reset_start_date = time(); + $resetdata->reset_logs = true; + $resetdata->reset_events = true; + $resetdata->reset_notes = true; + $resetdata->delete_blog_associations = true; + $resetdata->reset_completion = true; + $resetdata->reset_roles_overrides = true; + $resetdata->reset_roles_local = true; + $resetdata->reset_groups_members = true; + $resetdata->reset_groups_remove = true; + $resetdata->reset_groupings_members = true; + $resetdata->reset_groupings_remove = true; + $resetdata->reset_gradebook_items = true; + $resetdata->reset_gradebook_grades = true; + $resetdata->reset_comments = true; + + if (empty($course->startdate)) { + $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id)); + } + $resetdata->reset_start_date_old = $course->startdate; + + // Add roles. + $roles = tool_uploadcourse_helper::get_role_ids(); + $resetdata->unenrol_users = array_values($roles); + $resetdata->unenrol_users[] = 0; // Enrolled without role. + + return reset_course_userdata($resetdata); + } + +} diff --git a/admin/tool/uploadcourse/classes/helper.php b/admin/tool/uploadcourse/classes/helper.php new file mode 100644 index 00000000000..87f77353852 --- /dev/null +++ b/admin/tool/uploadcourse/classes/helper.php @@ -0,0 +1,440 @@ +. + +/** + * File containing the helper class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Class containing a set of helpers. + */ +class tool_uploadcourse_helper { + + + /** @var array category idnumber cache. */ + static protected $categoryidnumbercache = array(); + + /** @var array category path cache. */ + static protected $categorypathcache = array(); + + /** @var array course formats cache. */ + static protected $courseformatscache; + + /** @var array enrolment plugins cache. */ + static protected $enrolpluginscache; + + /** @var string restore content path cache. */ + static protected $restorecontentcache; + + /** @var array roles cache. */ + static protected $rolescache; + + /** + * Remove the restore content from disk and cache. + * + * @return void + */ + public static function clean_restore_content() { + global $CFG; + if (!empty($CFG->keeptempdirectoriesonbackup)) { + foreach (self::$restorecontentcache as $backupid) { + fulldelete("$CFG->tempdir/backup/$backupid/"); + } + } + self::$restorecontentcache = array(); + } + + /** + * Generate a shortname based on a template. + * + * @param array|object $data course data. + * @param string $templateshortname template of shortname. + * @return null|string shortname based on the template. + */ + public static function generate_shortname($data, $templateshortname) { + if (is_null($templateshortname)) { + return null; + } + if (strpos($templateshortname, '%') === false) { + return $templateshortname; + } + + $course = (object) $data; + $shortname = isset($course->shortname) ? $course->shortname : ''; + $fullname = isset($course->fullname) ? $course->fullname : ''; + $idnumber = isset($course->idnumber) ? $course->idnumber : ''; + + $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $shortname, $fullname, $idnumber); + $result = preg_replace_callback('/(? array( + * 'option1' => value, + * 'option2' => value + * ), + * 'method2' => array( + * 'option1' => value, + * 'option2' => value + * ) + * ) + * + * @param array $data data to extract the enrolment data from. + * @return array + */ + public static function get_enrolment_data($data) { + $enrolmethods = array(); + $enroloptions = array(); + foreach ($data as $field => $value) { + + // Enrolmnent data. + $matches = array(); + if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) { + $key = $matches[1]; + if (!isset($enroloptions[$key])) { + $enroloptions[$key] = array(); + } + if (empty($matches[3])) { + $enrolmethods[$key] = $value; + } else { + $enroloptions[$key][$matches[3]] = $value; + } + } + } + + // Combining enrolment methods and their options in a single array. + $enrolmentdata = array(); + if (!empty($enrolmethods)) { + $enrolmentplugins = self::get_enrolment_plugins(); + foreach ($enrolmethods as $key => $method) { + if (!array_key_exists($method, $enrolmentplugins)) { + // Error! + continue; + } + $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key]; + } + } + return $enrolmentdata; + } + + /** + * Return the enrolment plugins. + * + * The result is cached for faster execution. + * + * @return array + */ + public static function get_enrolment_plugins() { + if (empty(self::$enrolpluginscache)) { + self::$enrolpluginscache = enrol_get_plugins(false); + } + return self::$enrolpluginscache; + } + + /** + * Get the restore content tempdir. + * + * The tempdir is the sub directory in which the backup has been extracted. + * This caches the result for better performance. + * + * @param string $backupfile path to a backup file. + * @param string $shortname shortname of a course. + * @return string|false false when the backup couldn't retrieved. + */ + public static function get_restore_content_dir($backupfile = null, $shortname = null) { + global $CFG, $DB, $USER; + + $cachekey = null; + if (!empty($backupfile)) { + $backupfile = realpath($backupfile); + $cachekey = '[path]:' . $backupfile; + } else if (!empty($shortname) || is_numeric($shortname)) { + $cachekey = '[sn]:' . $shortname; + } + + if (empty($cachekey)) { + return false; + } + + if (!isset(self::$restorecontentcache[$cachekey])) { + // Use false instead of null because it would consider that the cache + // key has not been set. + $backupid = false; + if (!empty($backupfile) && is_readable($backupfile)) { + // Extracting the backup file. + $packer = get_file_packer('application/vnd.moodle.backup'); + $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id); + $path = "$CFG->tempdir/backup/$backupid/"; + $result = $packer->extract_to_pathname($backupfile, $path); + } else if (!empty($shortname) || is_numeric($shortname)) { + // Creating restore from an existing course. + $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING); + if (!empty($courseid)) { + $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE, + backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); + $bc->execute_plan(); + $backupid = $bc->get_backupid(); + $bc->destroy(); + } + } + self::$restorecontentcache[$cachekey] = $backupid; + } + + return self::$restorecontentcache[$cachekey]; + } + + /** + * Return the role IDs. + * + * The result is cached for faster execution. + * + * @param bool $invalidate invalidate the cache. + * @return array + */ + public static function get_role_ids($invalidate = false) { + if (empty(self::$rolescache) || $invalidate) { + $roles = get_all_roles(); + foreach ($roles as $role) { + self::$rolescache[$role->shortname] = $role->id; + } + } + return self::$rolescache; + } + + /** + * Get the role renaming data from the passed data. + * + * @param array $data data to extract the names from. + * @return array where the key is the role_, the value is the new name. + */ + public static function get_role_names($data) { + $rolenames = array(); + $rolesids = self::get_role_ids(); + foreach ($data as $field => $value) { + + $matches = array(); + if (preg_match('/^role_(.+)?$/', $field, $matches)) { + if (!isset($rolesids[$matches[1]])) { + // Error! + continue; + } + $rolenames['role_' . $rolesids[$matches[1]]] = $value; + } + + } + + // Roles names. + return $rolenames; + } + + /** + * Helper to increment an ID number. + * + * This first checks if the ID number is in use. + * + * @param string $idnumber ID number to increment. + * @return string new ID number. + */ + public static function increment_idnumber($idnumber) { + global $DB; + while ($DB->record_exists('course', array('idnumber' => $idnumber))) { + $matches = array(); + if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) { + $newidnumber = $idnumber . '_2'; + } else { + $newidnumber = $matches[1] . ((int) $matches[2] + 1); + } + $idnumber = $newidnumber; + } + return $idnumber; + } + + /** + * Helper to increment a shortname. + * + * This considers that the shortname passed has to be incremented. + * + * @param string $shortname shortname to increment. + * @return string new shortname. + */ + public static function increment_shortname($shortname) { + global $DB; + do { + $matches = array(); + if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) { + $newshortname = $shortname . '_2'; + } else { + $newshortname = $matches[1] . ($matches[2]+1); + } + $shortname = $newshortname; + } while ($DB->record_exists('course', array('shortname' => $shortname))); + return $shortname; + } + + /** + * Resolve a category based on the data passed. + * + * Key accepted are: + * - category, which is supposed to be a category ID. + * - category_idnumber + * - category_path, array of categories from parent to child. + * + * @param array $data to resolve the category from. + * @return int category ID. + */ + public static function resolve_category($data) { + global $DB; + $catid = null; + + if (!empty($data['category'])) { + $category = coursecat::get($data['category'], IGNORE_MISSING); + if (!empty($category)) { + $catid = $category->id; + } + } + + if (empty($catid) && !empty($data['category_idnumber'])) { + $catid = self::resolve_category_by_idnumber($data['category_idnumber']); + } + + if (empty($catid) && !empty($data['category_path'])) { + $catid = self::resolve_category_by_path(explode(' / ', $data['category_path'])); + } + + return $catid; + } + + /** + * Resolve a category by ID number. + * + * @param string $idnumber category ID number. + * @return int category ID. + */ + public static function resolve_category_by_idnumber($idnumber) { + global $DB; + if (!isset(self::$categoryidnumbercache[$idnumber])) { + $params = array('idnumber' => $idnumber); + $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING); + self::$categoryidnumbercache[$idnumber] = $id; + } + return self::$categoryidnumbercache[$idnumber]; + } + + /** + * Resolve a category by path. + * + * @param array $path category names indexed from parent to children. + * @return int category ID. + */ + public static function resolve_category_by_path(array $path) { + global $DB; + $cachekey = serialize($path); + if (!isset(self::$categorypathcache[$cachekey])) { + $parent = 0; + $sql = 'name = :name AND parent = :parent'; + while ($name = array_shift($path)) { + $params = array('name' => $name, 'parent' => $parent); + if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) { + if (count($records) > 1) { + // Too many records with the same name! + $id = false; + break; + } + $record = reset($records); + $id = $record->id; + $parent = $record->id; + } else { + // Not found. + $id = false; + break; + } + } + self::$categorypathcache[$cachekey] = $id; + } + return self::$categorypathcache[$cachekey]; + } + +} diff --git a/admin/tool/uploadcourse/classes/processor.php b/admin/tool/uploadcourse/classes/processor.php new file mode 100644 index 00000000000..9fb6253b839 --- /dev/null +++ b/admin/tool/uploadcourse/classes/processor.php @@ -0,0 +1,265 @@ +. + +/** + * File containing processor class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Processor class. + * + * @package tool_uploadcourse + * @copyright 2013 Frédéric Massart + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_uploadcourse_processor { + + /** + * Create courses that do not exist yet. + */ + const MODE_CREATE_NEW = 1; + + /** + * Create all courses, appending a suffix to the shortname if the course exists. + */ + const MODE_CREATE_ALL = 2; + + /** + * Create courses, and update the ones that already exist. + */ + const MODE_CREATE_OR_UPDATE = 3; + + /** + * Only update existing courses. + */ + const MODE_UPDATE_ONLY = 4; + + /** + * During update, do not update anything... O_o Huh?! + */ + const UPDATE_NOTHING = 0; + + /** + * During update, only use data passed from the CSV. + */ + const UPDATE_ALL_WITH_DATA_ONLY = 1; + + /** + * During update, use either data from the CSV, or defaults. + */ + const UPDATE_ALL_WITH_DATA_OR_DEFAUTLS = 2; + + /** + * During update, update missing values from either data from the CSV, or defaults. + */ + const UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS = 3; + + /** @var int processor mode. */ + protected $mode; + + /** @var int upload mode. */ + protected $updatemode; + + /** @var bool are renames allowed. */ + protected $allowrenames = false; + + /** @var bool are deletes allowed. */ + protected $allowdeletes = false; + + /** @var bool are resets allowed. */ + protected $allowresets = false; + + /** @var string path to a restore file. */ + protected $restorefile; + + /** @var string shortname of the course to be restored. */ + protected $templatecourse; + + /** @var string reset courses after processing them. */ + protected $reset; + + /** @var string template to generate a course shortname. */ + protected $shortnametemplate; + + /** @var csv_import_reader */ + protected $cir; + + /** @var array default values. */ + protected $defaults = array(); + + /** @var array CSV columns. */ + protected $columns = array(); + + /** @var int line number. */ + protected $linenb = 0; + + /** @var int line data. */ + protected $linedata = array(); + + /** @var bool whether the process has been started or not. */ + protected $processstarted = false; + + /** + * Constructor + * + * @param csv_import_reader $cir import reader object + * @param array $options options of the process + * @param array $defaults default data value + */ + public function __construct(csv_import_reader $cir, array $options, array $defaults = array()) { + + if (!isset($options['mode']) || !in_array($options['mode'], array(self::MODE_CREATE_NEW, self::MODE_CREATE_ALL, + self::MODE_CREATE_OR_UPDATE, self::MODE_UPDATE_ONLY))) { + throw new coding_exception('Unknown process mode'); + } + + // Force int to make sure === comparison work as expected. + $this->mode = (int) $options['mode']; + + $this->updatemode = self::UPDATE_NOTHING; + if (isset($options['updatemode'])) { + // Force int to make sure === comparison work as expected. + $this->updatemode = (int) $options['updatemode']; + } + if (isset($options['allowrenames'])) { + $this->allowrenames = $options['allowrenames']; + } + if (isset($options['allowdeletes'])) { + $this->allowdeletes = $options['allowdeletes']; + } + if (isset($options['allowresets'])) { + $this->allowresets = $options['allowresets']; + } + + if (isset($options['restorefile'])) { + $this->restorefile = $options['restorefile']; + } + if (isset($options['templatecourse'])) { + $this->templatecourse = $options['templatecourse']; + } + if (isset($options['reset'])) { + $this->reset = $options['reset']; + } + if (isset($options['shortnametemplate'])) { + $this->shortnametemplate = $options['shortnametemplate']; + } + + $this->cir = $cir; + $this->columns = $cir->get_columns(); + $this->defaults = $defaults; + } + + /** + * Execute the process. + * + * @return void + */ + public function execute() { + if ($this->processstarted) { + throw new coding_exception('Process has already been started'); + } + $this->processstarted = true; + + // Loop over the CSV lines. + while ($line = $this->cir->next()) { + $this->linenb++; + + $data = $this->parse_line($line); + $course = self::get_course($data); + if ($course->prepare()) { + $course->proceed(); + } else { + print_object($course->get_errors()); + } + } + + // $this->remove_restore_content(); + } + + /** + * Return a course import object. + * + * @param array $data data to import the course with. + * @return tool_uploadcourse_course + */ + public function get_course($data) { + $importoptions = array( + 'candelete' => $this->allowdeletes, + 'canrename' => $this->allowrenames, + 'canreset' => $this->allowresets, + 'reset' => $this->reset, + 'restoredir' => $this->get_restore_content_dir(), + 'shortnametemplate' => $this->shortnametemplate + ); + return new tool_uploadcourse_course($this->mode, $this->updatemode, $data, $this->defaults, $importoptions); + } + + /** + * Get the directory of the object to restore. + * + * @param string $default directory to use if none found. + * @return string subdirectory in $CFG->tempdir/backup/... + */ + public function get_restore_content_dir() { + $backupfile = null; + $shortname = null; + + if (!empty($this->restorefile)) { + $backupfile = $this->restorefile; + } else if (!empty($this->templatecourse) || is_numeric($this->templatecourse)) { + $shortname = $this->templatecourse; + } + + $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname); + return $dir; + } + + /** + * Parse a line to return an array(column => value) + * + * @param array $line returned by csv_import_reader + * @return array + */ + protected function parse_line($line) { + $data = array(); + foreach ($line as $keynum => $value) { + if (!isset($this->columns[$keynum])) { + // This should not happen. + continue; + } + + $key = $this->columns[$keynum]; + $data[$key] = $value; + } + return $data; + } + + /** + * Delete the restore object. + * + * @return void + */ + protected function remove_restore_content() { + global $CFG; + tool_uploadcourse_helper::clean_restore_content(); + } +}