--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This hack is intended for clustered sites that do not want
+ * to use shared cachedir for component cache.
+ *
+ * This file needs to be called after any change in PHP files in dataroot,
+ * that is before upgrade and install.
+ *
+ * @package core
+ * @copyright 2013 Petr Skoda (skodak) {@link http://skodak.org}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+define('CLI_SCRIPT', true);
+define('ABORT_AFTER_CONFIG', true); // We need just the values from config.php.
+define('CACHE_DISABLE_ALL', true); // This prevents reading of existing caches.
+define('IGNORE_COMPONENT_CACHE', true);
+
+require(__DIR__.'/../../config.php');
+require_once($CFG->libdir.'/clilib.php');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(
+ array(
+ 'file' => false,
+ 'rebuild' => false,
+ 'print' => false,
+ 'help' => false
+ ),
+ array(
+ 'h' => 'help'
+ )
+);
+
+if ($unrecognized) {
+ $unrecognized = implode("\n ", $unrecognized);
+ cli_error(get_string('cliunknowoption', 'admin', $unrecognized), 2);
+}
+
+if (!$options['rebuild'] and !$options['file'] and !$options['print']) {
+ $help =
+"Create alternative component cache file
+
+Options:
+-h, --help Print out this help
+--rebuild Rebuild \$CFG->alternative_component_cache file
+--file=filepath Save component cache to file
+--print Print component cache file content
+
+Example:
+\$ php admin/cli/rebuild_alternative_component_cache.php --rebuild
+";
+
+ echo $help;
+ exit(0);
+}
+
+error_reporting(E_ALL | E_STRICT);
+ini_set('display_errors', 1);
+
+$content = core_component::get_cache_content();
+
+if ($options['print']) {
+ echo $content;
+ exit(0);
+}
+
+if ($options['rebuild']) {
+ if (empty($CFG->alternative_component_cache)) {
+ fwrite(STDERR, 'config.php does not contain $CFG->alternative_component_cache setting');
+ fwrite(STDERR, "\n");
+ exit(2);
+ }
+ $target = $CFG->alternative_component_cache;
+} else {
+ $target = $options['file'];
+}
+
+if (!$target) {
+ fwrite(STDERR, "Invalid target file $target");
+ fwrite(STDERR, "\n");
+ exit(1);
+}
+
+$bytes = file_put_contents($target, $content);
+
+if (!$bytes) {
+ fwrite(STDERR, "Error writing to $target");
+ fwrite(STDERR, "\n");
+ exit(1);
+}
+
+// Success.
+echo "File $target was updated\n";
+exit(0);
define('PHPUNIT_TEST', false);
+define('IGNORE_COMPONENT_CACHE', true);
+
// Check that PHP is of a sufficient version
if (version_compare(phpversion(), "5.3.3") < 0) {
$phpversion = phpversion();
define('NO_OUTPUT_BUFFERING', true);
-if (empty($_GET['cache']) and empty($_POST['cache'])) {
+if (empty($_GET['cache']) and empty($_POST['cache']) and empty($_GET['sesskey']) and empty($_POST['sesskey'])) {
// Prevent caching at all cost when visiting this page directly,
// we redirect to self once we known no upgrades are necessary.
// Note: $_GET and $_POST are used here intentionally because our param cleaning is not loaded yet.
+ // Note2: the sesskey is present in all block editing hacks, we can not redirect there, so enable caching.
define('CACHE_DISABLE_ALL', true);
// Force OPcache reset if used, we do not want any stale caches
// Now we can be sure everything was upgraded and caches work fine,
// redirect if necessary to make sure caching is enabled.
-if (!$cache) {
+if (!$cache and !optional_param('sesskey', '', PARAM_RAW)) {
redirect(new moodle_url($PAGE->url, array('cache' => 1)));
}
+++ /dev/null
-<?php
-
-if (!isset($CFG)) {
-
- require('../config.php');
- require_once($CFG->libdir.'/adminlib.php');
-
- admin_externalpage_setup('oacleanup');
-
- echo $OUTPUT->header();
- online_assignment_cleanup(true);
- echo $OUTPUT->footer();
-
-}
-
-
-
-function online_assignment_cleanup($output=false) {
- global $CFG, $DB, $OUTPUT;
-
- if ($output) {
- echo $OUTPUT->heading('Online Assignment Cleanup');
- echo '<center>';
- }
-
-
- /// We don't want to run this code if we are doing an upgrade from an assignment
- /// version earlier than 2005041400
- /// because the assignment type field will not exist
- $amv = $DB->get_field('modules', 'version', array('name'=>'assignment'));
- if ((int)$amv < 2005041400) {
- if ($output) {
- echo '</center>';
- }
- return;
- }
-
-
- /// get the module id for assignments from db
- $arecord = $DB->get_record('modules', array('name', 'assignment'));
- $aid = $arecord->id;
-
-
- /// get a list of all courses on this site
- list($ctxselect, $ctxjoin) = context_instance_preload_sql('c.id', CONTEXT_COURSE, 'ctx');
- $sql = "SELECT c.* $ctxselect FROM {course} c $ctxjoin";
- $courses = $DB->get_records_sql($sql);
-
- /// cycle through each course
- foreach ($courses as $course) {
- context_helper::preload_from_record($course);
- $context = context_course::instance($course->id);
-
- if (empty($course->fullname)) {
- $fullname = get_string('course').': '.$course->id;
- } else {
- $fullname = format_string($course->fullname, true, array('context' => $context));
- }
- if ($output) echo $OUTPUT->heading($fullname);
-
- /// retrieve a list of sections beyond what is currently being shown
- $courseformatoptions = course_get_format($course)->get_format_options();
- if (!isset($courseformatoptions['numsections'])) {
- // Course format does not use numsections
- if ($output) {
- echo 'No extra sections<br />';
- }
- continue;
- }
- $sql = "SELECT *
- FROM {course_sections}
- WHERE course=? AND section>?
- ORDER BY section ASC";
- $params = array($course->id, $courseformatoptions['numsections']);
- if (!($xsections = $DB->get_records_sql($sql, $params))) {
- if ($output) echo 'No extra sections<br />';
- continue;
- }
-
- /// cycle through each of the xtra sections
- foreach ($xsections as $xsection) {
-
- if ($output) echo 'Checking Section: '.$xsection->section.'<br />';
-
- /// grab any module instances from the sequence field
- if (!empty($xsection->sequence)) {
- $instances = explode(',', $xsection->sequence);
-
- /// cycle through the instances
- foreach ($instances as $instance) {
- /// is this an instance of an online assignment
- $sql = "SELECT a.id
- FROM {course_modules} cm, {assignment} a
- WHERE cm.id = ? AND cm.module = ? AND
- cm.instance = a.id AND a.assignmenttype = 'online'";
- $params = array($instance, $aid);
-
- /// if record exists then we need to move instance to it's correct section
- if ($DB->record_exists_sql($sql, $params)) {
-
- /// check the new section id
- /// the journal update erroneously stored it in course_sections->section
- $newsection = $xsection->section;
- /// double check the new section
- if ($newsection > $courseformatoptions['numsections']) {
- /// get the record for section 0 for this course
- if (!($zerosection = $DB->get_record('course_sections', array('course'=>$course->id, 'section'=>'0')))) {
- continue;
- }
- $newsection = $zerosection->id;
- }
-
- /// grab the section record
- if (!($section = $DB->get_record('course_sections', array('id'=>$newsection)))) {
- if ($output) {
- echo 'Serious error: Cannot retrieve section: '.$newsection.' for course: '. $fullname .'<br />';
- }
- continue;
- }
-
- /// explode the sequence
- if (($sequence = explode(',', $section->sequence)) === false) {
- $sequence = array();
- }
-
- /// add instance to correct section
- array_push($sequence, $instance);
-
- /// implode the sequence
- $section->sequence = implode(',', $sequence);
-
- $DB->set_field('course_sections', 'sequence', $section->sequence, array('id'=>$section->id));
-
- /// now we need to remove the instance from the old sequence
-
- /// grab the old section record
- if (!($section = $DB->get_record('course_sections', array('id'=>$xsection->id)))) {
- if ($output) echo 'Serious error: Cannot retrieve old section: '.$xsection->id.' for course: '.$fullname.'<br />';
- continue;
- }
-
- /// explode the sequence
- if (($sequence = explode(',', $section->sequence)) === false) {
- $sequence = array();
- }
-
- /// remove the old value from the array
- $key = array_search($instance, $sequence);
- unset($sequence[$key]);
-
- /// implode the sequence
- $section->sequence = implode(',', $sequence);
-
- $DB->set_field('course_sections', 'sequence', $section->sequence, array('id'=>$section->id));
-
-
- if ($output) echo 'Online Assignment (instance '.$instance.') moved from section '.$section->id.': to section '.$newsection.'<br />';
-
- }
- }
- }
-
- /// if the summary and sequence are empty then remove this section
- if (empty($xsection->summary) and empty($xsection->sequence)) {
- $DB->delete_records('course_sections', array('id'=>$xsection->id));
- if ($output) echo 'Deleting empty section '.$xsection->section.'<br />';
- }
- }
- }
-
- echo '</center>';
-}
-
-
$a = new stdClass;
$a->roleid = $roleid;
$a->context = $contextname;
- print_error('cannotassignrolehere', '', get_context_url($context), $a);
+ print_error('cannotassignrolehere', '', $context->get_url(), $a);
}
// Work out an appropriate page title.
/** The context this table relates to. */
protected $context;
- /** The capabilities to display. Initialised as fetch_context_capabilities($context). */
+ /** The capabilities to display. Initialised as $context->get_capabilities(). */
protected $capabilities = array();
/** Added as an id="" attribute to the table on output. */
$module->load_settings($ADMIN, 'modsettings', $hassiteconfig);
}
- // hidden script for converting journals to online assignments (or something like that) linked from elsewhere
- $ADMIN->add('modsettings', new admin_externalpage('oacleanup', 'Online Assignment Cleanup', $CFG->wwwroot.'/'.$CFG->admin.'/oacleanup.php', 'moodle/site:config', true));
-
// course formats
$ADMIN->add('modules', new admin_category('formatsettings', new lang_string('courseformats')));
$temp = new admin_settingpage('manageformats', new lang_string('manageformats', 'core_admin'));
// Is not really necessary but adding it as is a CLI_SCRIPT.
define('CLI_SCRIPT', true);
+define('CACHE_DISABLE_ALL', true);
// Basic functions.
require_once(__DIR__ . '/../../../../lib/clilib.php');
define('CLI_SCRIPT', true);
define('ABORT_AFTER_CONFIG', true);
define('NO_OUTPUT_BUFFERING', true);
+define('IGNORE_COMPONENT_CACHE', true);
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', '1');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+$string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically.';
$string['allavailablesteps'] = 'All the available steps definitions';
$string['giveninfo'] = 'Given. Processes to set up the environment';
+$string['infoheading'] = 'Info';
$string['installinfo'] = 'Read {$a} for installation and tests execution info';
$string['moreinfoin'] = 'More info in {$a}';
$string['newstepsinfo'] = 'Read {$a} for info about how to add new steps definitions';
get_string('newstepsinfo', 'tool_behat', $writestepslink)
);
- // List of steps
+ // List of steps.
$html .= $this->output->box_start();
- $html .= html_writer::tag('h1', 'Info');
+ $html .= html_writer::tag('h1', get_string('infoheading', 'tool_behat'));
+ $html .= html_writer::tag('div', get_string('aim', 'tool_behat'));
$html .= html_writer::empty_tag('div');
$html .= html_writer::empty_tag('ul');
$html .= html_writer::empty_tag('li');
opcache_reset();
}
+define('IGNORE_COMPONENT_CACHE', true);
+
require_once(__DIR__.'/../../../../lib/clilib.php');
require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
require_once(__DIR__.'/../../../../lib/testing/lib.php');
die; // no access from web!
}
+define('IGNORE_COMPONENT_CACHE', true);
+
require_once(__DIR__.'/../../../../lib/clilib.php');
require_once(__DIR__.'/../../../../lib/phpunit/bootstraplib.php');
require_once(__DIR__.'/../../../../lib/testing/lib.php');
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing the base import form.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Base import form.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_base_form extends moodleform {
+
+ /**
+ * Empty definition.
+ *
+ * @return void
+ */
+ public function definition() {
+ }
+
+ /**
+ * Adds the import settings part.
+ *
+ * @return void
+ */
+ public function add_import_options() {
+ $mform = $this->_form;
+
+ // Upload settings and file.
+ $mform->addElement('header', 'importoptionshdr', get_string('importoptions', 'tool_uploadcourse'));
+ $mform->setExpanded('importoptionshdr', true);
+
+ $choices = array(
+ tool_uploadcourse_processor::MODE_CREATE_NEW => get_string('createnew', 'tool_uploadcourse'),
+ tool_uploadcourse_processor::MODE_CREATE_ALL => get_string('createall', 'tool_uploadcourse'),
+ tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE => get_string('createorupdate', 'tool_uploadcourse'),
+ tool_uploadcourse_processor::MODE_UPDATE_ONLY => get_string('updateonly', 'tool_uploadcourse')
+ );
+ $mform->addElement('select', 'options[mode]', get_string('mode', 'tool_uploadcourse'), $choices);
+
+ $choices = array(
+ tool_uploadcourse_processor::UPDATE_NOTHING => get_string('nochanges', 'tool_uploadcourse'),
+ tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY => get_string('updatewithdataonly', 'tool_uploadcourse'),
+ tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS =>
+ get_string('updatewithdataordefaults', 'tool_uploadcourse'),
+ tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS => get_string('updatemissing', 'tool_uploadcourse')
+ );
+ $mform->addElement('select', 'options[updatemode]', get_string('updatemode', 'tool_uploadcourse'), $choices);
+ $mform->setDefault('options[updatemode]', tool_uploadcourse_processor::UPDATE_NOTHING);
+ $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+ $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+ $mform->addElement('selectyesno', 'options[allowdeletes]', get_string('allowdeletes', 'tool_uploadcourse'));
+ $mform->setDefault('options[allowdeletes]', 0);
+ $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+ $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+ $mform->addElement('selectyesno', 'options[allowrenames]', get_string('allowrenames', 'tool_uploadcourse'));
+ $mform->setDefault('options[allowrenames]', 0);
+ $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+ $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+
+ $mform->addElement('selectyesno', 'options[allowresets]', get_string('allowresets', 'tool_uploadcourse'));
+ $mform->setDefault('options[allowresets]', 0);
+ $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+ $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing the course class.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+require_once($CFG->dirroot . '/course/lib.php');
+
+/**
+ * Course class.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_course {
+
+ /** Outcome of the process: creating the course */
+ const DO_CREATE = 1;
+
+ /** Outcome of the process: updating the course */
+ const DO_UPDATE = 2;
+
+ /** Outcome of the process: deleting the course */
+ const DO_DELETE = 3;
+
+ /** @var array final import data. */
+ protected $data = array();
+
+ /** @var array default values. */
+ protected $defaults = array();
+
+ /** @var array enrolment data. */
+ protected $enrolmentdata;
+
+ /** @var array errors. */
+ protected $errors = array();
+
+ /** @var int the ID of the course that had been processed. */
+ protected $id;
+
+ /** @var array containing options passed from the processor. */
+ protected $importoptions = array();
+
+ /** @var int import mode. Matches tool_uploadcourse_processor::MODE_* */
+ protected $mode;
+
+ /** @var array course import options. */
+ protected $options = array();
+
+ /** @var int constant value of self::DO_*, what to do with that course */
+ protected $do;
+
+ /** @var bool set to true once we have prepared the course */
+ protected $prepared = false;
+
+ /** @var bool set to true once we have started the process of the course */
+ protected $processstarted = false;
+
+ /** @var array course import data. */
+ protected $rawdata = array();
+
+ /** @var array restore directory. */
+ protected $restoredata;
+
+ /** @var string course shortname. */
+ protected $shortname;
+
+ /** @var array errors. */
+ protected $statuses = array();
+
+ /** @var int update mode. Matches tool_uploadcourse_processor::UPDATE_* */
+ protected $updatemode;
+
+ /** @var array fields allowed as course data. */
+ static protected $validfields = array('fullname', 'shortname', 'idnumber', 'category', 'visible', 'startdate',
+ 'summary', 'format', 'theme', 'lang', 'newsitems', 'showgrades', 'showreports', 'legacyfiles', 'maxbytes',
+ 'groupmode', 'groupmodeforce', 'groupmodeforce', 'enablecompletion');
+
+ /** @var array fields required on course creation. */
+ static protected $mandatoryfields = array('fullname', 'category');
+
+ /** @var array fields which are considered as options. */
+ static protected $optionfields = array('delete' => false, 'rename' => null, 'backupfile' => null,
+ 'templatecourse' => null, 'reset' => false);
+
+ /** @var array options determining what can or cannot be done at an import level. */
+ static protected $importoptionsdefaults = array('canrename' => false, 'candelete' => false, 'canreset' => false,
+ 'reset' => false, 'restoredir' => null, 'shortnametemplate' => null);
+
+ /**
+ * Constructor
+ *
+ * @param int $mode import mode, constant matching tool_uploadcourse_processor::MODE_*
+ * @param int $updatemode update mode, constant matching tool_uploadcourse_processor::UPDATE_*
+ * @param array $rawdata raw course data.
+ * @param array $defaults default course data.
+ * @param array $importoptions import options.
+ */
+ public function __construct($mode, $updatemode, $rawdata, $defaults = array(), $importoptions = array()) {
+
+ if ($mode !== tool_uploadcourse_processor::MODE_CREATE_NEW &&
+ $mode !== tool_uploadcourse_processor::MODE_CREATE_ALL &&
+ $mode !== tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE &&
+ $mode !== tool_uploadcourse_processor::MODE_UPDATE_ONLY) {
+ throw new coding_exception('Incorrect mode.');
+ } else if ($updatemode !== tool_uploadcourse_processor::UPDATE_NOTHING &&
+ $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY &&
+ $updatemode !== tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS &&
+ $updatemode !== tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS) {
+ throw new coding_exception('Incorrect update mode.');
+ }
+
+ $this->mode = $mode;
+ $this->updatemode = $updatemode;
+
+ if (isset($rawdata['shortname'])) {
+ $this->shortname = $rawdata['shortname'];
+ }
+ $this->rawdata = $rawdata;
+ $this->defaults = $defaults;
+
+ // Extract course options.
+ foreach (self::$optionfields as $option => $default) {
+ $this->options[$option] = isset($rawdata[$option]) ? $rawdata[$option] : $default;
+ }
+
+ // Import options.
+ foreach (self::$importoptionsdefaults as $option => $default) {
+ $this->importoptions[$option] = isset($importoptions[$option]) ? $importoptions[$option] : $default;
+ }
+ }
+
+ /**
+ * Does the mode allow for course creation?
+ *
+ * @return bool
+ */
+ public function can_create() {
+ return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL,
+ tool_uploadcourse_processor::MODE_CREATE_NEW,
+ tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)
+ );
+ }
+
+ /**
+ * Does the mode allow for course deletion?
+ *
+ * @return bool
+ */
+ public function can_delete() {
+ return $this->importoptions['candelete'];
+ }
+
+ /**
+ * Does the mode only allow for course creation?
+ *
+ * @return bool
+ */
+ public function can_only_create() {
+ return in_array($this->mode, array(tool_uploadcourse_processor::MODE_CREATE_ALL,
+ tool_uploadcourse_processor::MODE_CREATE_NEW));
+ }
+
+ /**
+ * Does the mode allow for course rename?
+ *
+ * @return bool
+ */
+ public function can_rename() {
+ return $this->importoptions['canrename'];
+ }
+
+ /**
+ * Does the mode allow for course reset?
+ *
+ * @return bool
+ */
+ public function can_reset() {
+ return $this->importoptions['canreset'];
+ }
+
+ /**
+ * Does the mode allow for course update?
+ *
+ * @return bool
+ */
+ public function can_update() {
+ return in_array($this->mode,
+ array(
+ tool_uploadcourse_processor::MODE_UPDATE_ONLY,
+ tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE)
+ ) && $this->updatemode != tool_uploadcourse_processor::UPDATE_NOTHING;
+ }
+
+ /**
+ * Can we use default values?
+ *
+ * @return bool
+ */
+ public function can_use_defaults() {
+ return in_array($this->updatemode, array(tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS,
+ tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS));
+ }
+
+ /**
+ * Delete the current course.
+ *
+ * @return bool
+ */
+ protected function delete() {
+ global $DB;
+ $this->id = $DB->get_field_select('course', 'id', 'shortname = :shortname',
+ array('shortname' => $this->shortname), MUST_EXIST);
+ return delete_course($this->id, false);
+ }
+
+ /**
+ * Log an error
+ *
+ * @param string $code error code.
+ * @param lang_string $message error message.
+ * @return void
+ */
+ protected function error($code, lang_string $message) {
+ if (array_key_exists($code, $this->errors)) {
+ throw new coding_exception('Error code already defined');
+ }
+ $this->errors[$code] = $message;
+ }
+
+ /**
+ * Return whether the course exists or not.
+ *
+ * @param string $shortname the shortname to use to check if the course exists. Falls back on $this->shortname if empty.
+ * @return bool
+ */
+ protected function exists($shortname = null) {
+ global $DB;
+ if (is_null($shortname)) {
+ $shortname = $this->shortname;
+ }
+ if (!empty($shortname) || is_numeric($shortname)) {
+ return $DB->record_exists('course', array('shortname' => $shortname));
+ }
+ return false;
+ }
+
+ /**
+ * Return the data that will be used upon saving.
+ *
+ * @return null|array
+ */
+ public function get_data() {
+ return $this->data;
+ }
+
+ /**
+ * Return the errors found during preparation.
+ *
+ * @return array
+ */
+ public function get_errors() {
+ return $this->errors;
+ }
+
+ /**
+ * Assemble the course data based on defaults.
+ *
+ * This returns the final data to be passed to create_course().
+ *
+ * @param array $data current data.
+ * @return array
+ */
+ protected function get_final_create_data($data) {
+ foreach (self::$validfields as $field) {
+ if (!isset($data[$field]) && isset($this->defaults[$field])) {
+ $data[$field] = $this->defaults[$field];
+ }
+ }
+ $data['shortname'] = $this->shortname;
+ return $data;
+ }
+
+ /**
+ * Assemble the course data based on defaults.
+ *
+ * This returns the final data to be passed to update_course().
+ *
+ * @param array $data current data.
+ * @param bool $usedefaults are defaults allowed?
+ * @param bool $missingonly ignore fields which are already set.
+ * @return array
+ */
+ protected function get_final_update_data($data, $usedefaults = false, $missingonly = false) {
+ global $DB;
+ $newdata = array();
+ $existingdata = $DB->get_record('course', array('shortname' => $this->shortname));
+ foreach (self::$validfields as $field) {
+ if ($missingonly) {
+ if (!is_null($existingdata->$field) and $existingdata->$field !== '') {
+ continue;
+ }
+ }
+ if (isset($data[$field])) {
+ $newdata[$field] = $data[$field];
+ } else if ($usedefaults && isset($this->defaults[$field])) {
+ $newdata[$field] = $this->defaults[$field];
+ }
+ }
+ $newdata['id'] = $existingdata->id;
+ return $newdata;
+ }
+
+ /**
+ * Return the ID of the processed course.
+ *
+ * @return int|null
+ */
+ public function get_id() {
+ if (!$this->processstarted) {
+ throw new coding_exception('The course has not been processed yet!');
+ }
+ return $this->id;
+ }
+
+ /**
+ * Get the directory of the object to restore.
+ *
+ * @return string|false subdirectory in $CFG->tempdir/backup/...
+ */
+ protected function get_restore_content_dir() {
+ $backupfile = null;
+ $shortname = null;
+
+ if (!empty($this->options['backupfile'])) {
+ $backupfile = $this->options['backupfile'];
+ } else if (!empty($this->options['templatecourse']) || is_numeric($this->options['templatecourse'])) {
+ $shortname = $this->options['templatecourse'];
+ }
+
+ $errors = array();
+ $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname, $errors);
+ if (!empty($errors)) {
+ foreach ($errors as $key => $message) {
+ $this->error($key, $message);
+ }
+ return false;
+ }
+
+ if (empty($dir) && !empty($this->importoptions['restoredir'])) {
+ $dir = $this->importoptions['restoredir'];
+ }
+
+ return $dir;
+ }
+
+ /**
+ * Return the errors found during preparation.
+ *
+ * @return array
+ */
+ public function get_statuses() {
+ return $this->statuses;
+ }
+
+ /**
+ * Return whether there were errors with this course.
+ *
+ * @return boolean
+ */
+ public function has_errors() {
+ return !empty($this->errors);
+ }
+
+ /**
+ * Validates and prepares the data.
+ *
+ * @return bool false is any error occured.
+ */
+ public function prepare() {
+ global $DB;
+ $this->prepared = true;
+
+ // Validate the shortname.
+ if (!empty($this->shortname) || is_numeric($this->shortname)) {
+ if ($this->shortname !== clean_param($this->shortname, PARAM_TEXT)) {
+ $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
+ return false;
+ }
+ }
+
+ $exists = $this->exists();
+
+ // Do we want to delete the course?
+ if ($this->options['delete']) {
+ if (!$exists) {
+ $this->error('cannotdeletecoursenotexist', new lang_string('cannotdeletecoursenotexist', 'tool_uploadcourse'));
+ return false;
+ } else if (!$this->can_delete()) {
+ $this->error('coursedeletionnotallowed', new lang_string('coursedeletionnotallowed', 'tool_uploadcourse'));
+ return false;
+ }
+
+ $this->do = self::DO_DELETE;
+ return true;
+ }
+
+ // Can we create/update the course under those conditions?
+ if ($exists) {
+ if ($this->mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
+ $this->error('courseexistsanduploadnotallowed',
+ new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
+ return false;
+ }
+ } else {
+ if (!$this->can_create()) {
+ $this->error('coursedoesnotexistandcreatenotallowed',
+ new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
+ return false;
+ }
+ }
+
+ // Basic data.
+ $coursedata = array();
+ foreach ($this->rawdata as $field => $value) {
+ if (!in_array($field, self::$validfields)) {
+ continue;
+ } else if ($field == 'shortname') {
+ // Let's leave it apart from now, use $this->shortname only.
+ continue;
+ }
+ $coursedata[$field] = $value;
+ }
+
+ $mode = $this->mode;
+ $updatemode = $this->updatemode;
+ $usedefaults = $this->can_use_defaults();
+
+ // Resolve the category, and fail if not found.
+ $errors = array();
+ $catid = tool_uploadcourse_helper::resolve_category($this->rawdata, $errors);
+ if (empty($errors)) {
+ $coursedata['category'] = $catid;
+ } else {
+ foreach ($errors as $key => $message) {
+ $this->error($key, $message);
+ }
+ return false;
+ }
+
+ // If the course does not exist, or will be forced created.
+ if (!$exists || $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
+
+ // Mandatory fields upon creation.
+ $errors = array();
+ foreach (self::$mandatoryfields as $field) {
+ if ((!isset($coursedata[$field]) || $coursedata[$field] === '') &&
+ (!isset($this->defaults[$field]) || $this->defaults[$field] === '')) {
+ $errors[] = $field;
+ }
+ }
+ if (!empty($errors)) {
+ $this->error('missingmandatoryfields', new lang_string('missingmandatoryfields', 'tool_uploadcourse',
+ implode(', ', $errors)));
+ return false;
+ }
+ }
+
+ // Should the course be renamed?
+ if (!empty($this->options['rename']) || is_numeric($this->options['rename'])) {
+ if (!$this->can_update()) {
+ $this->error('canonlyrenameinupdatemode', new lang_string('canonlyrenameinupdatemode', 'tool_uploadcourse'));
+ return false;
+ } else if (!$exists) {
+ $this->error('cannotrenamecoursenotexist', new lang_string('cannotrenamecoursenotexist', 'tool_uploadcourse'));
+ return false;
+ } else if (!$this->can_rename()) {
+ $this->error('courserenamingnotallowed', new lang_string('courserenamingnotallowed', 'tool_uploadcourse'));
+ return false;
+ } else if ($this->options['rename'] !== clean_param($this->options['rename'], PARAM_TEXT)) {
+ $this->error('invalidshortname', new lang_string('invalidshortname', 'tool_uploadcourse'));
+ return false;
+ } else if ($this->exists($this->options['rename'])) {
+ $this->error('cannotrenameshortnamealreadyinuse',
+ new lang_string('cannotrenameshortnamealreadyinuse', 'tool_uploadcourse'));
+ return false;
+ } else if (isset($coursedata['idnumber']) &&
+ $DB->count_records_select('course', 'idnumber = :idn AND shortname != :sn',
+ array('idn' => $coursedata['idnumber'], 'sn' => $this->shortname)) > 0) {
+ $this->error('cannotrenameidnumberconflict', new lang_string('cannotrenameidnumberconflict', 'tool_uploadcourse'));
+ return false;
+ }
+ $coursedata['shortname'] = $this->options['rename'];
+ $this->status('courserenamed', new lang_string('courserenamed', 'tool_uploadcourse',
+ array('from' => $this->shortname, 'to' => $coursedata['shortname'])));
+ }
+
+ // Should we generate a shortname?
+ if (empty($this->shortname) && !is_numeric($this->shortname)) {
+ if (empty($this->importoptions['shortnametemplate'])) {
+ $this->error('missingshortnamenotemplate', new lang_string('missingshortnamenotemplate', 'tool_uploadcourse'));
+ return false;
+ } else if (!$this->can_only_create()) {
+ $this->error('cannotgenerateshortnameupdatemode',
+ new lang_string('cannotgenerateshortnameupdatemode', 'tool_uploadcourse'));
+ return false;
+ } else {
+ $newshortname = tool_uploadcourse_helper::generate_shortname($coursedata,
+ $this->importoptions['shortnametemplate']);
+ if (is_null($newshortname)) {
+ $this->error('generatedshortnameinvalid', new lang_string('generatedshortnameinvalid', 'tool_uploadcourse'));
+ return false;
+ } else if ($this->exists($newshortname)) {
+ if ($mode === tool_uploadcourse_processor::MODE_CREATE_NEW) {
+ $this->error('generatedshortnamealreadyinuse',
+ new lang_string('generatedshortnamealreadyinuse', 'tool_uploadcourse'));
+ return false;
+ }
+ $exists = true;
+ }
+ $this->status('courseshortnamegenerated', new lang_string('courseshortnamegenerated', 'tool_uploadcourse',
+ $newshortname));
+ $this->shortname = $newshortname;
+ }
+ }
+
+ // If exists, but we only want to create courses, increment the shortname.
+ if ($exists && $mode === tool_uploadcourse_processor::MODE_CREATE_ALL) {
+ $original = $this->shortname;
+ $this->shortname = tool_uploadcourse_helper::increment_shortname($this->shortname);
+ $exists = false;
+ if ($this->shortname != $original) {
+ $this->status('courseshortnameincremented', new lang_string('courseshortnameincremented', 'tool_uploadcourse',
+ array('from' => $original, 'to' => $this->shortname)));
+ if (isset($coursedata['idnumber'])) {
+ $originalidn = $coursedata['idnumber'];
+ $coursedata['idnumber'] = tool_uploadcourse_helper::increment_idnumber($coursedata['idnumber']);
+ if ($originalidn != $coursedata['idnumber']) {
+ $this->status('courseidnumberincremented', new lang_string('courseidnumberincremented', 'tool_uploadcourse',
+ array('from' => $originalidn, 'to' => $coursedata['idnumber'])));
+ }
+ }
+ }
+ }
+
+ // If the course does not exist, ensure that the ID number is not taken.
+ if (!$exists && isset($coursedata['idnumber'])) {
+ if ($DB->count_records_select('course', 'idnumber = :idn', array('idn' => $coursedata['idnumber'])) > 0) {
+ $this->error('idnumberalreadyinuse', new lang_string('idnumberalreadyinuse', 'tool_uploadcourse'));
+ return false;
+ }
+ }
+
+ // Ultimate check mode vs. existence.
+ switch ($mode) {
+ case tool_uploadcourse_processor::MODE_CREATE_NEW:
+ case tool_uploadcourse_processor::MODE_CREATE_ALL:
+ if ($exists) {
+ $this->error('courseexistsanduploadnotallowed',
+ new lang_string('courseexistsanduploadnotallowed', 'tool_uploadcourse'));
+ return false;
+ }
+ break;
+ case tool_uploadcourse_processor::MODE_UPDATE_ONLY:
+ if (!$exists) {
+ $this->error('coursedoesnotexistandcreatenotallowed',
+ new lang_string('coursedoesnotexistandcreatenotallowed', 'tool_uploadcourse'));
+ return false;
+ }
+ // No break!
+ case tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE:
+ if ($exists) {
+ if ($updatemode === tool_uploadcourse_processor::UPDATE_NOTHING) {
+ $this->error('updatemodedoessettonothing',
+ new lang_string('updatemodedoessettonothing', 'tool_uploadcourse'));
+ return false;
+ }
+ }
+ break;
+ default:
+ // O_o Huh?! This should really never happen here!
+ $this->error('unknownimportmode', new lang_string('unknownimportmode', 'tool_uploadcourse'));
+ return false;
+ }
+
+ // Get final data.
+ if ($exists) {
+ $missingonly = ($updatemode === tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS);
+ $coursedata = $this->get_final_update_data($coursedata, $usedefaults, $missingonly);
+ $this->do = self::DO_UPDATE;
+ } else {
+ $coursedata = $this->get_final_create_data($coursedata);
+ $this->do = self::DO_CREATE;
+ }
+
+ // Course start date.
+ if (!empty($coursedata['startdate'])) {
+ $coursedata['startdate'] = strtotime($coursedata['startdate']);
+ }
+
+ // Add role renaming.
+ $errors = array();
+ $rolenames = tool_uploadcourse_helper::get_role_names($this->rawdata, $errors);
+ if (!empty($errors)) {
+ foreach ($errors as $key => $message) {
+ $this->error($key, $message);
+ }
+ return false;
+ }
+ foreach ($rolenames as $rolekey => $rolename) {
+ $coursedata[$rolekey] = $rolename;
+ }
+
+ // Some validation.
+ if (!empty($coursedata['format']) && !in_array($coursedata['format'], tool_uploadcourse_helper::get_course_formats())) {
+ $this->error('invalidcourseformat', new lang_string('invalidcourseformat', 'tool_uploadcourse'));
+ return false;
+ }
+
+ // Saving data.
+ $this->data = $coursedata;
+ $this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
+
+ // Restore data.
+ // TODO log warnings.
+ $this->restoredata = $this->get_restore_content_dir();
+
+ // We can only reset courses when allowed and we are updating the course.
+ if ($this->importoptions['reset'] || $this->options['reset']) {
+ if ($this->do !== self::DO_UPDATE) {
+ $this->error('canonlyresetcourseinupdatemode',
+ new lang_string('canonlyresetcourseinupdatemode', 'tool_uploadcourse'));
+ return false;
+ } else if (!$this->can_reset()) {
+ $this->error('courseresetnotallowed', new lang_string('courseresetnotallowed', 'tool_uploadcourse'));
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Proceed with the import of the course.
+ *
+ * @return void
+ */
+ public function proceed() {
+ global $CFG, $USER;
+
+ if (!$this->prepared) {
+ throw new coding_exception('The course has not been prepared.');
+ } else if ($this->has_errors()) {
+ throw new moodle_exception('Cannot proceed, errors were detected.');
+ } else if ($this->processstarted) {
+ throw new coding_exception('The process has already been started.');
+ }
+ $this->processstarted = true;
+
+ if ($this->do === self::DO_DELETE) {
+ if ($this->delete()) {
+ $this->status('coursedeleted', new lang_string('coursedeleted', 'tool_uploadcourse'));
+ } else {
+ $this->error('errorwhiledeletingcourse', new lang_string('errorwhiledeletingcourse', 'tool_uploadcourse'));
+ }
+ return true;
+ } else if ($this->do === self::DO_CREATE) {
+ $course = create_course((object) $this->data);
+ $this->id = $course->id;
+ $this->status('coursecreated', new lang_string('coursecreated', 'tool_uploadcourse'));
+ } else if ($this->do === self::DO_UPDATE) {
+ $course = (object) $this->data;
+ update_course($course);
+ $this->id = $course->id;
+ $this->status('courseupdated', new lang_string('courseupdated', 'tool_uploadcourse'));
+ } else {
+ // Strangely the outcome has not been defined, or is unknown!
+ throw new coding_exception('Unknown outcome!');
+ }
+
+ // Restore a course.
+ if (!empty($this->restoredata)) {
+ $rc = new restore_controller($this->restoredata, $course->id, backup::INTERACTIVE_NO,
+ backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
+
+ // Check if the format conversion must happen first.
+ if ($rc->get_status() == backup::STATUS_REQUIRE_CONV) {
+ $rc->convert();
+ }
+ if ($rc->execute_precheck()) {
+ $rc->execute_plan();
+ $this->status('courserestored', new lang_string('courserestored', 'tool_uploadcourse'));
+ } else {
+ $this->error('errorwhilerestoringcourse', new lang_string('errorwhilerestoringthecourse', 'tool_uploadcourse'));
+ }
+ $rc->destroy();
+ }
+
+ // Proceed with enrolment data.
+ $this->process_enrolment_data($course);
+
+ // Reset the course.
+ if ($this->importoptions['reset'] || $this->options['reset']) {
+ if ($this->do === self::DO_UPDATE && $this->can_reset()) {
+ $this->reset($course);
+ $this->status('coursereset', new lang_string('coursereset', 'tool_uploadcourse'));
+ }
+ }
+
+ // Mark context as dirty.
+ $context = context_course::instance($course->id);
+ $context->mark_dirty();
+ }
+
+ /**
+ * Add the enrolment data for the course.
+ *
+ * @param object $course course record.
+ * @return void
+ */
+ protected function process_enrolment_data($course) {
+ global $DB;
+
+ $enrolmentdata = $this->enrolmentdata;
+ if (empty($enrolmentdata)) {
+ return;
+ }
+
+ $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
+ $instances = enrol_get_instances($course->id, false);
+ foreach ($enrolmentdata as $enrolmethod => $method) {
+
+ $instance = null;
+ foreach ($instances as $i) {
+ if ($i->enrol == $enrolmethod) {
+ $instance = $i;
+ break;
+ }
+ }
+
+ $todelete = isset($method['delete']) && $method['delete'];
+ $todisable = isset($method['disable']) && $method['disable'];
+ unset($method['delete']);
+ unset($method['disable']);
+
+ if (!empty($instance) && $todelete) {
+ // Remove the enrolment method.
+ foreach ($instances as $instance) {
+ if ($instance->enrol == $enrolmethod) {
+ $plugin = $enrolmentplugins[$instance->enrol];
+ $plugin->delete_instance($instance);
+ break;
+ }
+ }
+ } else if (!empty($instance) && $todisable) {
+ // Disable the enrolment.
+ foreach ($instances as $instance) {
+ if ($instance->enrol == $enrolmethod) {
+ $plugin = $enrolmentplugins[$instance->enrol];
+ $plugin->update_status($instance, ENROL_INSTANCE_DISABLED);
+ $enrol_updated = true;
+ break;
+ }
+ }
+ } else {
+ $plugin = null;
+ if (empty($instance)) {
+ $plugin = $enrolmentplugins[$enrolmethod];
+ $instance = new stdClass();
+ $instance->id = $plugin->add_default_instance($course);
+ $instance->roleid = $plugin->get_config('roleid');
+ $instance->status = ENROL_INSTANCE_ENABLED;
+ } else {
+ $plugin = $enrolmentplugins[$instance->enrol];
+ $plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
+ }
+
+ // Now update values.
+ foreach ($method as $k => $v) {
+ $instance->{$k} = $v;
+ }
+
+ // Sort out the start, end and date.
+ $instance->enrolstartdate = (isset($method['startdate']) ? strtotime($method['startdate']) : 0);
+ $instance->enrolenddate = (isset($method['enddate']) ? strtotime($method['enddate']) : 0);
+
+ // Is the enrolment period set?
+ if (isset($method['enrolperiod']) && ! empty($method['enrolperiod'])) {
+ if (preg_match('/^\d+$/', $method['enrolperiod'])) {
+ $method['enrolperiod'] = (int) $method['enrolperiod'];
+ } else {
+ // Try and convert period to seconds.
+ $method['enrolperiod'] = strtotime('1970-01-01 GMT + ' . $method['enrolperiod']);
+ }
+ $instance->enrolperiod = $method['enrolperiod'];
+ }
+ if ($instance->enrolstartdate > 0 && isset($method['enrolperiod'])) {
+ $instance->enrolenddate = $instance->enrolstartdate + $method['enrolperiod'];
+ }
+ if ($instance->enrolenddate > 0) {
+ $instance->enrolperiod = $instance->enrolenddate - $instance->enrolstartdate;
+ }
+ if ($instance->enrolenddate < $instance->enrolstartdate) {
+ $instance->enrolenddate = $instance->enrolstartdate;
+ }
+
+ // Sort out the given role. This does not filter the roles allowed in the course.
+ if (isset($method['role'])) {
+ $roleids = tool_uploadcourse_helper::get_role_ids();
+ if (isset($roleids[$method['role']])) {
+ $instance->roleid = $roleids[$method['role']];
+ }
+ }
+
+ $instance->timemodified = time();
+ $DB->update_record('enrol', $instance);
+ }
+ }
+ }
+
+ /**
+ * Reset the current course.
+ *
+ * This does not reset any of the content of the activities.
+ *
+ * @param stdClass $course the course object of the course to reset.
+ * @return array status array of array component, item, error.
+ */
+ protected function reset($course) {
+ global $DB;
+
+ $resetdata = new stdClass();
+ $resetdata->id = $course->id;
+ $resetdata->reset_start_date = time();
+ $resetdata->reset_logs = true;
+ $resetdata->reset_events = true;
+ $resetdata->reset_notes = true;
+ $resetdata->delete_blog_associations = true;
+ $resetdata->reset_completion = true;
+ $resetdata->reset_roles_overrides = true;
+ $resetdata->reset_roles_local = true;
+ $resetdata->reset_groups_members = true;
+ $resetdata->reset_groups_remove = true;
+ $resetdata->reset_groupings_members = true;
+ $resetdata->reset_groupings_remove = true;
+ $resetdata->reset_gradebook_items = true;
+ $resetdata->reset_gradebook_grades = true;
+ $resetdata->reset_comments = true;
+
+ if (empty($course->startdate)) {
+ $course->startdate = $DB->get_field_select('course', 'startdate', 'id = :id', array('id' => $course->id));
+ }
+ $resetdata->reset_start_date_old = $course->startdate;
+
+ // Add roles.
+ $roles = tool_uploadcourse_helper::get_role_ids();
+ $resetdata->unenrol_users = array_values($roles);
+ $resetdata->unenrol_users[] = 0; // Enrolled without role.
+
+ return reset_course_userdata($resetdata);
+ }
+
+ /**
+ * Log a status
+ *
+ * @param string $code status code.
+ * @param lang_string $message status message.
+ * @return void
+ */
+ protected function status($code, lang_string $message) {
+ if (array_key_exists($code, $this->statuses)) {
+ throw new coding_exception('Status code already defined');
+ }
+ $this->statuses[$code] = $message;
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing the helper class.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->dirroot . '/cache/lib.php');
+require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
+require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
+
+/**
+ * Class containing a set of helpers.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_helper {
+
+ /**
+ * Remove the restore content from disk and cache.
+ *
+ * @return void
+ */
+ public static function clean_restore_content() {
+ global $CFG;
+ if (!empty($CFG->keeptempdirectoriesonbackup)) {
+ $cache = cache::make('tool_uploadcourse', 'helper');
+ $backupids = (array) $cache->get('backupids');
+ foreach ($backupids as $cachekey => $backupid) {
+ $cache->delete($cachekey);
+ fulldelete("$CFG->tempdir/backup/$backupid/");
+ }
+ $cache->delete('backupids');
+ }
+ }
+
+ /**
+ * Generate a shortname based on a template.
+ *
+ * @param array|object $data course data.
+ * @param string $templateshortname template of shortname.
+ * @return null|string shortname based on the template, or null when an error occured.
+ */
+ public static function generate_shortname($data, $templateshortname) {
+ if (empty($templateshortname) && !is_numeric($templateshortname)) {
+ return null;
+ }
+ if (strpos($templateshortname, '%') === false) {
+ return $templateshortname;
+ }
+
+ $course = (object) $data;
+ $fullname = isset($course->fullname) ? $course->fullname : '';
+ $idnumber = isset($course->idnumber) ? $course->idnumber : '';
+
+ $callback = partial(array('tool_uploadcourse_helper', 'generate_shortname_callback'), $fullname, $idnumber);
+ $result = preg_replace_callback('/(?<!%)%([+~-])?(\d)*([fi])/', $callback, $templateshortname);
+
+ if (!is_null($result)) {
+ $result = clean_param($result, PARAM_TEXT);
+ }
+
+ if (empty($result) && !is_numeric($result)) {
+ $result = null;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Callback used when generating a shortname based on a template.
+ *
+ * @param string $fullname full name.
+ * @param string $idnumber ID number.
+ * @param array $block result from preg_replace_callback.
+ * @return string
+ */
+ public static function generate_shortname_callback($fullname, $idnumber, $block) {
+ switch ($block[3]) {
+ case 'f':
+ $repl = $fullname;
+ break;
+ case 'i':
+ $repl = $idnumber;
+ break;
+ default:
+ return $block[0];
+ }
+
+ switch ($block[1]) {
+ case '+':
+ $repl = textlib::strtoupper($repl);
+ break;
+ case '-':
+ $repl = textlib::strtolower($repl);
+ break;
+ case '~':
+ $repl = textlib::strtotitle($repl);
+ break;
+ }
+
+ if (!empty($block[2])) {
+ $repl = textlib::substr($repl, 0, $block[2]);
+ }
+
+ return $repl;
+ }
+
+ /**
+ * Return the available course formats.
+ *
+ * @return array
+ */
+ public static function get_course_formats() {
+ return array_keys(core_component::get_plugin_list('format'));
+ }
+
+ /**
+ * Extract enrolment data from passed data.
+ *
+ * Constructs an array of methods, and their options:
+ * array(
+ * 'method1' => array(
+ * 'option1' => value,
+ * 'option2' => value
+ * ),
+ * 'method2' => array(
+ * 'option1' => value,
+ * 'option2' => value
+ * )
+ * )
+ *
+ * @param array $data data to extract the enrolment data from.
+ * @return array
+ */
+ public static function get_enrolment_data($data) {
+ $enrolmethods = array();
+ $enroloptions = array();
+ foreach ($data as $field => $value) {
+
+ // Enrolmnent data.
+ $matches = array();
+ if (preg_match('/^enrolment_(\d+)(_(.+))?$/', $field, $matches)) {
+ $key = $matches[1];
+ if (!isset($enroloptions[$key])) {
+ $enroloptions[$key] = array();
+ }
+ if (empty($matches[3])) {
+ $enrolmethods[$key] = $value;
+ } else {
+ $enroloptions[$key][$matches[3]] = $value;
+ }
+ }
+ }
+
+ // Combining enrolment methods and their options in a single array.
+ $enrolmentdata = array();
+ if (!empty($enrolmethods)) {
+ $enrolmentplugins = self::get_enrolment_plugins();
+ foreach ($enrolmethods as $key => $method) {
+ if (!array_key_exists($method, $enrolmentplugins)) {
+ // Error!
+ continue;
+ }
+ $enrolmentdata[$enrolmethods[$key]] = $enroloptions[$key];
+ }
+ }
+ return $enrolmentdata;
+ }
+
+ /**
+ * Return the enrolment plugins.
+ *
+ * The result is cached for faster execution.
+ *
+ * @return array
+ */
+ public static function get_enrolment_plugins() {
+ $cache = cache::make('tool_uploadcourse', 'helper');
+ if (($enrol = $cache->get('enrol')) === false) {
+ $enrol = enrol_get_plugins(false);
+ $cache->set('enrol', $enrol);
+ }
+ return $enrol;
+ }
+
+ /**
+ * Get the restore content tempdir.
+ *
+ * The tempdir is the sub directory in which the backup has been extracted.
+ * This caches the result for better performance.
+ *
+ * @param string $backupfile path to a backup file.
+ * @param string $shortname shortname of a course.
+ * @param array $errors will be populated with errors found.
+ * @return string|false false when the backup couldn't retrieved.
+ */
+ public static function get_restore_content_dir($backupfile = null, $shortname = null, &$errors = array()) {
+ global $CFG, $DB, $USER;
+
+ $cachekey = null;
+ if (!empty($backupfile)) {
+ $backupfile = realpath($backupfile);
+ $cachekey = 'backup_path:' . $backupfile;
+ } else if (!empty($shortname) || is_numeric($shortname)) {
+ $cachekey = 'backup_sn:' . $shortname;
+ }
+
+ if (empty($cachekey)) {
+ return false;
+ }
+
+ $cache = cache::make('tool_uploadcourse', 'helper');
+ if (($backupid = $cache->get($cachekey)) === false) {
+ // Use false instead of null because it would consider that the cache
+ // key has not been set.
+ $backupid = false;
+ if (!empty($backupfile)) {
+ if (!is_readable($backupfile)) {
+ $errors['cannotreadbackupfile'] = new lang_string('cannotreadbackupfile', 'tool_uploadcourse');
+ } else {
+ // Extracting the backup file.
+ $packer = get_file_packer('application/vnd.moodle.backup');
+ $backupid = restore_controller::get_tempdir_name(SITEID, $USER->id);
+ $path = "$CFG->tempdir/backup/$backupid/";
+ $result = $packer->extract_to_pathname($backupfile, $path);
+ if (!$result) {
+ $errors['invalidbackupfile'] = new lang_string('invalidbackupfile', 'tool_uploadcourse');
+ }
+ }
+ } else if (!empty($shortname) || is_numeric($shortname)) {
+ // Creating restore from an existing course.
+ $courseid = $DB->get_field('course', 'id', array('shortname' => $shortname), IGNORE_MISSING);
+ if (!empty($courseid)) {
+ $bc = new backup_controller(backup::TYPE_1COURSE, $courseid, backup::FORMAT_MOODLE,
+ backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
+ $bc->execute_plan();
+ $backupid = $bc->get_backupid();
+ $bc->destroy();
+ } else {
+ $errors['coursetorestorefromdoesnotexist'] =
+ new lang_string('coursetorestorefromdoesnotexist', 'tool_uploadcourse');
+ }
+ }
+ $cache->set($cachekey, $backupid);
+
+ // Store all the directories to be able to remove them in self::clean_restore_content().
+ $backupids = (array) $cache->get('backupids');
+ $backupids[$cachekey] = $backupid;
+ $cache->set('backupids', $backupids);
+ }
+
+ return $backupid;
+ }
+
+ /**
+ * Return the role IDs.
+ *
+ * The result is cached for faster execution.
+ *
+ * @return array
+ */
+ public static function get_role_ids() {
+ $cache = cache::make('tool_uploadcourse', 'helper');
+ if (($roles = $cache->get('roles')) === false) {
+ $roles = array();
+ $rolesraw = get_all_roles();
+ foreach ($rolesraw as $role) {
+ $roles[$role->shortname] = $role->id;
+ }
+ $cache->set('roles', $roles);
+ }
+ return $roles;
+ }
+
+ /**
+ * Get the role renaming data from the passed data.
+ *
+ * @param array $data data to extract the names from.
+ * @param array $errors will be populated with errors found.
+ * @return array where the key is the role_<id>, the value is the new name.
+ */
+ public static function get_role_names($data, &$errors = array()) {
+ $rolenames = array();
+ $rolesids = self::get_role_ids();
+ $invalidroles = array();
+ foreach ($data as $field => $value) {
+
+ $matches = array();
+ if (preg_match('/^role_(.+)?$/', $field, $matches)) {
+ if (!isset($rolesids[$matches[1]])) {
+ $invalidroles[] = $matches[1];
+ continue;
+ }
+ $rolenames['role_' . $rolesids[$matches[1]]] = $value;
+ }
+
+ }
+
+ if (!empty($invalidroles)) {
+ $errors['invalidroles'] = new lang_string('invalidroles', 'tool_uploadcourse', implode(', ', $invalidroles));
+ }
+
+ // Roles names.
+ return $rolenames;
+ }
+
+ /**
+ * Helper to increment an ID number.
+ *
+ * This first checks if the ID number is in use.
+ *
+ * @param string $idnumber ID number to increment.
+ * @return string new ID number.
+ */
+ public static function increment_idnumber($idnumber) {
+ global $DB;
+ while ($DB->record_exists('course', array('idnumber' => $idnumber))) {
+ $matches = array();
+ if (!preg_match('/(.*?)([0-9]+)$/', $idnumber, $matches)) {
+ $newidnumber = $idnumber . '_2';
+ } else {
+ $newidnumber = $matches[1] . ((int) $matches[2] + 1);
+ }
+ $idnumber = $newidnumber;
+ }
+ return $idnumber;
+ }
+
+ /**
+ * Helper to increment a shortname.
+ *
+ * This considers that the shortname passed has to be incremented.
+ *
+ * @param string $shortname shortname to increment.
+ * @return string new shortname.
+ */
+ public static function increment_shortname($shortname) {
+ global $DB;
+ do {
+ $matches = array();
+ if (!preg_match('/(.*?)([0-9]+)$/', $shortname, $matches)) {
+ $newshortname = $shortname . '_2';
+ } else {
+ $newshortname = $matches[1] . ($matches[2]+1);
+ }
+ $shortname = $newshortname;
+ } while ($DB->record_exists('course', array('shortname' => $shortname)));
+ return $shortname;
+ }
+
+ /**
+ * Resolve a category based on the data passed.
+ *
+ * Key accepted are:
+ * - category, which is supposed to be a category ID.
+ * - category_idnumber
+ * - category_path, array of categories from parent to child.
+ *
+ * @param array $data to resolve the category from.
+ * @param array $errors will be populated with errors found.
+ * @return int category ID.
+ */
+ public static function resolve_category($data, &$errors = array()) {
+ $catid = null;
+
+ if (!empty($data['category'])) {
+ $category = coursecat::get((int) $data['category'], IGNORE_MISSING);
+ if (!empty($category) && !empty($category->id)) {
+ $catid = $category->id;
+ } else {
+ $errors['couldnotresolvecatgorybyid'] =
+ new lang_string('couldnotresolvecatgorybyid', 'tool_uploadcourse');
+ }
+ }
+
+ if (empty($catid) && !empty($data['category_idnumber'])) {
+ $catid = self::resolve_category_by_idnumber($data['category_idnumber']);
+ if (empty($catid)) {
+ $errors['couldnotresolvecatgorybyidnumber'] =
+ new lang_string('couldnotresolvecatgorybyidnumber', 'tool_uploadcourse');
+ }
+ }
+ if (empty($catid) && !empty($data['category_path'])) {
+ $catid = self::resolve_category_by_path(explode(' / ', $data['category_path']));
+ if (empty($catid)) {
+ $errors['couldnotresolvecatgorybypath'] =
+ new lang_string('couldnotresolvecatgorybypath', 'tool_uploadcourse');
+ }
+ }
+
+ return $catid;
+ }
+
+ /**
+ * Resolve a category by ID number.
+ *
+ * @param string $idnumber category ID number.
+ * @return int category ID.
+ */
+ public static function resolve_category_by_idnumber($idnumber) {
+ global $DB;
+ $cache = cache::make('tool_uploadcourse', 'helper');
+ $cachekey = 'cat_idn_' . $idnumber;
+ if (($id = $cache->get($cachekey)) === false) {
+ $params = array('idnumber' => $idnumber);
+ $id = $DB->get_field_select('course_categories', 'id', 'idnumber = :idnumber', $params, IGNORE_MISSING);
+
+ // Little hack to be able to differenciate between the cache not set and a category not found.
+ if ($id === false) {
+ $id = -1;
+ }
+
+ $cache->set($cachekey, $id);
+ }
+
+ // Little hack to be able to differenciate between the cache not set and a category not found.
+ if ($id == -1) {
+ $id = false;
+ }
+
+ return $id;
+ }
+
+ /**
+ * Resolve a category by path.
+ *
+ * @param array $path category names indexed from parent to children.
+ * @return int category ID.
+ */
+ public static function resolve_category_by_path(array $path) {
+ global $DB;
+ $cache = cache::make('tool_uploadcourse', 'helper');
+ $cachekey = 'cat_path_' . serialize($path);
+ if (($id = $cache->get($cachekey)) === false) {
+ $parent = 0;
+ $sql = 'name = :name AND parent = :parent';
+ while ($name = array_shift($path)) {
+ $params = array('name' => $name, 'parent' => $parent);
+ if ($records = $DB->get_records_select('course_categories', $sql, $params, null, 'id, parent')) {
+ if (count($records) > 1) {
+ // Too many records with the same name!
+ $id = -1;
+ break;
+ }
+ $record = reset($records);
+ $id = $record->id;
+ $parent = $record->id;
+ } else {
+ // Not found.
+ $id = -1;
+ break;
+ }
+ }
+ $cache->set($cachekey, $id);
+ }
+
+ // We save -1 when the category has not been found to be able to know if the cache was set.
+ if ($id == -1) {
+ $id = false;
+ }
+ return $id;
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing processor class.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/csvlib.class.php');
+
+/**
+ * Processor class.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_processor {
+
+ /**
+ * Create courses that do not exist yet.
+ */
+ const MODE_CREATE_NEW = 1;
+
+ /**
+ * Create all courses, appending a suffix to the shortname if the course exists.
+ */
+ const MODE_CREATE_ALL = 2;
+
+ /**
+ * Create courses, and update the ones that already exist.
+ */
+ const MODE_CREATE_OR_UPDATE = 3;
+
+ /**
+ * Only update existing courses.
+ */
+ const MODE_UPDATE_ONLY = 4;
+
+ /**
+ * During update, do not update anything... O_o Huh?!
+ */
+ const UPDATE_NOTHING = 0;
+
+ /**
+ * During update, only use data passed from the CSV.
+ */
+ const UPDATE_ALL_WITH_DATA_ONLY = 1;
+
+ /**
+ * During update, use either data from the CSV, or defaults.
+ */
+ const UPDATE_ALL_WITH_DATA_OR_DEFAUTLS = 2;
+
+ /**
+ * During update, update missing values from either data from the CSV, or defaults.
+ */
+ const UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS = 3;
+
+ /** @var int processor mode. */
+ protected $mode;
+
+ /** @var int upload mode. */
+ protected $updatemode;
+
+ /** @var bool are renames allowed. */
+ protected $allowrenames = false;
+
+ /** @var bool are deletes allowed. */
+ protected $allowdeletes = false;
+
+ /** @var bool are resets allowed. */
+ protected $allowresets = false;
+
+ /** @var string path to a restore file. */
+ protected $restorefile;
+
+ /** @var string shortname of the course to be restored. */
+ protected $templatecourse;
+
+ /** @var string reset courses after processing them. */
+ protected $reset;
+
+ /** @var string template to generate a course shortname. */
+ protected $shortnametemplate;
+
+ /** @var csv_import_reader */
+ protected $cir;
+
+ /** @var array default values. */
+ protected $defaults = array();
+
+ /** @var array CSV columns. */
+ protected $columns = array();
+
+ /** @var array of errors where the key is the line number. */
+ protected $errors = array();
+
+ /** @var int line number. */
+ protected $linenb = 0;
+
+ /** @var bool whether the process has been started or not. */
+ protected $processstarted = false;
+
+ /**
+ * Constructor
+ *
+ * @param csv_import_reader $cir import reader object
+ * @param array $options options of the process
+ * @param array $defaults default data value
+ */
+ public function __construct(csv_import_reader $cir, array $options, array $defaults = array()) {
+
+ if (!isset($options['mode']) || !in_array($options['mode'], array(self::MODE_CREATE_NEW, self::MODE_CREATE_ALL,
+ self::MODE_CREATE_OR_UPDATE, self::MODE_UPDATE_ONLY))) {
+ throw new coding_exception('Unknown process mode');
+ }
+
+ // Force int to make sure === comparison work as expected.
+ $this->mode = (int) $options['mode'];
+
+ $this->updatemode = self::UPDATE_NOTHING;
+ if (isset($options['updatemode'])) {
+ // Force int to make sure === comparison work as expected.
+ $this->updatemode = (int) $options['updatemode'];
+ }
+ if (isset($options['allowrenames'])) {
+ $this->allowrenames = $options['allowrenames'];
+ }
+ if (isset($options['allowdeletes'])) {
+ $this->allowdeletes = $options['allowdeletes'];
+ }
+ if (isset($options['allowresets'])) {
+ $this->allowresets = $options['allowresets'];
+ }
+
+ if (isset($options['restorefile'])) {
+ $this->restorefile = $options['restorefile'];
+ }
+ if (isset($options['templatecourse'])) {
+ $this->templatecourse = $options['templatecourse'];
+ }
+ if (isset($options['reset'])) {
+ $this->reset = $options['reset'];
+ }
+ if (isset($options['shortnametemplate'])) {
+ $this->shortnametemplate = $options['shortnametemplate'];
+ }
+
+ $this->cir = $cir;
+ $this->columns = $cir->get_columns();
+ $this->defaults = $defaults;
+ $this->validate();
+ $this->reset();
+ }
+
+ /**
+ * Execute the process.
+ *
+ * @param object $tracker the output tracker to use.
+ * @return void
+ */
+ public function execute($tracker = null) {
+ if ($this->processstarted) {
+ throw new coding_exception('Process has already been started');
+ }
+ $this->processstarted = true;
+
+ if (empty($tracker)) {
+ $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
+ }
+ $tracker->start();
+
+ $total = 0;
+ $created = 0;
+ $updated = 0;
+ $deleted = 0;
+ $errors = 0;
+
+ // Loop over the CSV lines.
+ while ($line = $this->cir->next()) {
+ $this->linenb++;
+ $total++;
+
+ $data = $this->parse_line($line);
+ $course = $this->get_course($data);
+ if ($course->prepare()) {
+ $course->proceed();
+
+ $status = $course->get_statuses();
+ if (array_key_exists('coursecreated', $status)) {
+ $created++;
+ } else if (array_key_exists('courseupdated', $status)) {
+ $updated++;
+ } else if (array_key_exists('coursedeleted', $status)) {
+ $deleted++;
+ }
+
+ $data = array_merge($data, $course->get_data(), array('id' => $course->get_id()));
+ $tracker->output($this->linenb, true, $status, $data);
+ } else {
+ $errors++;
+ $tracker->output($this->linenb, false, $course->get_errors(), $data);
+ }
+ }
+
+ $tracker->finish();
+ $tracker->results($total, $created, $updated, $deleted, $errors);
+
+ $this->remove_restore_content();
+ }
+
+ /**
+ * Return a course import object.
+ *
+ * @param array $data data to import the course with.
+ * @return tool_uploadcourse_course
+ */
+ protected function get_course($data) {
+ $importoptions = array(
+ 'candelete' => $this->allowdeletes,
+ 'canrename' => $this->allowrenames,
+ 'canreset' => $this->allowresets,
+ 'reset' => $this->reset,
+ 'restoredir' => $this->get_restore_content_dir(),
+ 'shortnametemplate' => $this->shortnametemplate
+ );
+ return new tool_uploadcourse_course($this->mode, $this->updatemode, $data, $this->defaults, $importoptions);
+ }
+
+ /**
+ * Return the errors.
+ *
+ * @return array
+ */
+ public function get_errors() {
+ return $this->errors;
+ }
+
+ /**
+ * Get the directory of the object to restore.
+ *
+ * @return string subdirectory in $CFG->tempdir/backup/...
+ */
+ protected function get_restore_content_dir() {
+ $backupfile = null;
+ $shortname = null;
+
+ if (!empty($this->restorefile)) {
+ $backupfile = $this->restorefile;
+ } else if (!empty($this->templatecourse) || is_numeric($this->templatecourse)) {
+ $shortname = $this->templatecourse;
+ }
+
+ $dir = tool_uploadcourse_helper::get_restore_content_dir($backupfile, $shortname);
+ return $dir;
+ }
+
+ /**
+ * Log errors on the current line.
+ *
+ * @param array $errors array of errors
+ * @return void
+ */
+ protected function log_error($errors) {
+ if (empty($errors)) {
+ return;
+ }
+
+ foreach ($errors as $code => $langstring) {
+ if (!isset($this->errors[$this->linenb])) {
+ $this->errors[$this->linenb] = array();
+ }
+ $this->errors[$this->linenb][$code] = $langstring;
+ }
+ }
+
+ /**
+ * Parse a line to return an array(column => value)
+ *
+ * @param array $line returned by csv_import_reader
+ * @return array
+ */
+ protected function parse_line($line) {
+ $data = array();
+ foreach ($line as $keynum => $value) {
+ if (!isset($this->columns[$keynum])) {
+ // This should not happen.
+ continue;
+ }
+
+ $key = $this->columns[$keynum];
+ $data[$key] = $value;
+ }
+ return $data;
+ }
+
+ /**
+ * Return a preview of the import.
+ *
+ * This only returns passed data, along with the errors.
+ *
+ * @param integer $rows number of rows to preview.
+ * @param object $tracker the output tracker to use.
+ * @return array of preview data.
+ */
+ public function preview($rows = 10, $tracker = null) {
+ if ($this->processstarted) {
+ throw new coding_exception('Process has already been started');
+ }
+ $this->processstarted = true;
+
+ if (empty($tracker)) {
+ $tracker = new tool_uploadcourse_tracker(tool_uploadcourse_tracker::NO_OUTPUT);
+ }
+ $tracker->start();
+
+ // Loop over the CSV lines.
+ $preview = array();
+ while (($line = $this->cir->next()) && $rows > $this->linenb) {
+ $this->linenb++;
+ $data = $this->parse_line($line);
+ $course = $this->get_course($data);
+ $result = $course->prepare();
+ if (!$result) {
+ $tracker->output($this->linenb, $result, $course->get_errors(), $data);
+ } else {
+ $tracker->output($this->linenb, $result, $course->get_statuses(), $data);
+ }
+ $row = $data;
+ $preview[$this->linenb] = $row;
+ }
+
+ $tracker->finish();
+ $this->remove_restore_content();
+
+ return $preview;
+ }
+
+ /**
+ * Delete the restore object.
+ *
+ * @return void
+ */
+ protected function remove_restore_content() {
+ tool_uploadcourse_helper::clean_restore_content();
+ }
+
+ /**
+ * Reset the current process.
+ *
+ * @return void.
+ */
+ public function reset() {
+ $this->processstarted = false;
+ $this->linenb = 0;
+ $this->cir->init();
+ $this->errors = array();
+ }
+
+ /**
+ * Validation.
+ *
+ * @return void
+ */
+ protected function validate() {
+ if (empty($this->columns)) {
+ throw new moodle_exception('cannotreadtmpfile', 'error');
+ } else if (count($this->columns) < 2) {
+ throw new moodle_exception('csvfewcolumns', 'error');
+ }
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing the step 1 of the upload form.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->libdir.'/formslib.php');
+
+/**
+ * Upload a file CVS file with course information.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2011 Piers Harding
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_step1_form extends tool_uploadcourse_base_form {
+
+ /**
+ * The standard form definiton.
+ * @return void
+ */
+ public function definition () {
+ $mform = $this->_form;
+
+ $mform->addElement('header', 'generalhdr', get_string('general'));
+
+ $mform->addElement('filepicker', 'coursefile', get_string('file'));
+ $mform->addRule('coursefile', null, 'required');
+
+ $choices = csv_import_reader::get_delimiter_list();
+ $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'tool_uploadcourse'), $choices);
+ if (array_key_exists('cfg', $choices)) {
+ $mform->setDefault('delimiter_name', 'cfg');
+ } else if (get_string('listsep', 'langconfig') == ';') {
+ $mform->setDefault('delimiter_name', 'semicolon');
+ } else {
+ $mform->setDefault('delimiter_name', 'comma');
+ }
+
+ $choices = textlib::get_encodings();
+ $mform->addElement('select', 'encoding', get_string('encoding', 'tool_uploadcourse'), $choices);
+ $mform->setDefault('encoding', 'UTF-8');
+
+ $choices = array('10' => 10, '20' => 20, '100' => 100, '1000' => 1000, '100000' => 100000);
+ $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'tool_uploadcourse'), $choices);
+ $mform->setType('previewrows', PARAM_INT);
+
+ $this->add_import_options();
+
+ $mform->addElement('hidden', 'showpreview', 1);
+ $mform->setType('showpreview', PARAM_INT);
+
+ $this->add_action_buttons(false, get_string('preview', 'tool_uploadcourse'));
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Bulk course upload step 2.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2011 Piers Harding
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Specify course upload details.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2011 Piers Harding
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
+
+ /**
+ * The standard form definiton.
+ * @return void.
+ */
+ public function definition () {
+ global $CFG;
+
+ $mform = $this->_form;
+ $data = $this->_customdata['data'];
+ $courseconfig = get_config('moodlecourse');
+
+ // Import options.
+ $this->add_import_options();
+
+ // Course options.
+ $mform->addElement('header', 'courseoptionshdr', get_string('courseprocess', 'tool_uploadcourse'));
+ $mform->setExpanded('courseoptionshdr', true);
+
+ $mform->addElement('text', 'options[shortnametemplate]', get_string('shortnametemplate', 'tool_uploadcourse'),
+ 'maxlength="100" size="20"');
+ $mform->setType('options[shortnametemplate]', PARAM_RAW);
+ $mform->addHelpButton('options[shortnametemplate]', 'shortnametemplate', 'tool_uploadcourse');
+ $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE);
+ $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY);
+
+ $contextid = $this->_customdata['contextid'];
+ $mform->addElement('hidden', 'contextid', $contextid);
+ $mform->setType('contextid', PARAM_INT);
+ $mform->addElement('filepicker', 'options[restorefile]', get_string('templatefile', 'tool_uploadcourse'));
+ $mform->addHelpButton('options[restorefile]', 'templatefile', 'tool_uploadcourse');
+
+ $mform->addElement('text', 'options[templatecourse]', get_string('coursetemplatename', 'tool_uploadcourse'));
+ $mform->setType('options[templatecourse]', PARAM_TEXT);
+ $mform->addHelpButton('options[templatecourse]', 'coursetemplatename', 'tool_uploadcourse');
+
+ $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse'));
+ $mform->setDefault('options[reset]', 0);
+ $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+ $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+ $mform->disabledIf('options[reset]', 'options[allowresets]', 'eq', 0);
+
+ // Default values.
+ $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadcourse'));
+ $mform->setExpanded('defaultheader', true);
+
+ $displaylist = coursecat::make_categories_list('moodle/course:create');
+ $mform->addElement('select', 'defaults[category]', get_string('coursecategory'), $displaylist);
+ $mform->addHelpButton('defaults[category]', 'coursecategory');
+
+ $choices = array();
+ $choices['0'] = get_string('hide');
+ $choices['1'] = get_string('show');
+ $mform->addElement('select', 'defaults[visible]', get_string('visible'), $choices);
+ $mform->addHelpButton('defaults[visible]', 'visible');
+ $mform->setDefault('defaults[visible]', $courseconfig->visible);
+
+ $mform->addElement('date_selector', 'defaults[startdate]', get_string('startdate'));
+ $mform->addHelpButton('defaults[startdate]', 'startdate');
+ $mform->setDefault('defaults[startdate]', time() + 3600 * 24);
+
+ $courseformats = get_sorted_course_formats(true);
+ $formcourseformats = array();
+ foreach ($courseformats as $courseformat) {
+ $formcourseformats[$courseformat] = get_string('pluginname', "format_$courseformat");
+ }
+ $mform->addElement('select', 'defaults[format]', get_string('format'), $formcourseformats);
+ $mform->addHelpButton('defaults[format]', 'format');
+ $mform->setDefault('defaults[format]', $courseconfig->format);
+
+ if (!empty($CFG->allowcoursethemes)) {
+ $themeobjects = get_list_of_themes();
+ $themes=array();
+ $themes[''] = get_string('forceno');
+ foreach ($themeobjects as $key => $theme) {
+ if (empty($theme->hidefromselector)) {
+ $themes[$key] = get_string('pluginname', 'theme_'.$theme->name);
+ }
+ }
+ $mform->addElement('select', 'defaults[theme]', get_string('forcetheme'), $themes);
+ }
+
+ $languages = array();
+ $languages[''] = get_string('forceno');
+ $languages += get_string_manager()->get_list_of_translations();
+ $mform->addElement('select', 'defaults[lang]', get_string('forcelanguage'), $languages);
+ $mform->setDefault('defaults[lang]', $courseconfig->lang);
+
+ $options = range(0, 10);
+ $mform->addElement('select', 'defaults[newsitems]', get_string('newsitemsnumber'), $options);
+ $mform->addHelpButton('defaults[newsitems]', 'newsitemsnumber');
+ $mform->setDefault('defaults[newsitems]', $courseconfig->newsitems);
+
+ $mform->addElement('selectyesno', 'defaults[showgrades]', get_string('showgrades'));
+ $mform->addHelpButton('defaults[showgrades]', 'showgrades');
+ $mform->setDefault('defaults[showgrades]', $courseconfig->showgrades);
+
+ $mform->addElement('selectyesno', 'defaults[showreports]', get_string('showreports'));
+ $mform->addHelpButton('defaults[showreports]', 'showreports');
+ $mform->setDefault('defaults[showreports]', $courseconfig->showreports);
+
+ if (!empty($CFG->legacyfilesinnewcourses)) {
+ $mform->addElement('select', 'defaults[legacyfiles]', get_string('courselegacyfiles'), $choices);
+ $mform->addHelpButton('defaults[legacyfiles]', 'courselegacyfiles');
+ if (!isset($courseconfig->legacyfiles)) {
+ $courseconfig->legacyfiles = 0;
+ }
+ $mform->setDefault('defaults[legacyfiles]', $courseconfig->legacyfiles);
+ }
+
+ $choices = get_max_upload_sizes($CFG->maxbytes);
+ $mform->addElement('select', 'defaults[maxbytes]', get_string('maximumupload'), $choices);
+ $mform->addHelpButton('defaults[maxbytes]', 'maximumupload');
+ $mform->setDefault('defaults[maxbytes]', $courseconfig->maxbytes);
+
+ $choices = array();
+ $choices[NOGROUPS] = get_string('groupsnone', 'group');
+ $choices[SEPARATEGROUPS] = get_string('groupsseparate', 'group');
+ $choices[VISIBLEGROUPS] = get_string('groupsvisible', 'group');
+ $mform->addElement('select', 'defaults[groupmode]', get_string('groupmode', 'group'), $choices);
+ $mform->addHelpButton('defaults[groupmode]', 'groupmode', 'group');
+ $mform->setDefault('defaults[groupmode]', $courseconfig->groupmode);
+
+ $mform->addElement('selectyesno', 'defaults[groupmodeforce]', get_string('groupmodeforce', 'group'));
+ $mform->addHelpButton('defaults[groupmodeforce]', 'groupmodeforce', 'group');
+ $mform->setDefault('defaults[groupmodeforce]', $courseconfig->groupmodeforce);
+
+ // Hidden fields.
+ $mform->addElement('hidden', 'importid');
+ $mform->setType('importid', PARAM_INT);
+
+ $mform->addElement('hidden', 'previewrows');
+ $mform->setType('previewrows', PARAM_INT);
+
+ $this->add_action_buttons(true, get_string('uploadcourses', 'tool_uploadcourse'));
+
+ $this->set_data($data);
+ }
+
+ /**
+ * Add actopm buttons.
+ *
+ * @param bool $cancel whether to show cancel button, default true
+ * @param string $submitlabel label for submit button, defaults to get_string('savechanges')
+ * @return void
+ */
+ public function add_action_buttons($cancel = true, $submitlabel = null) {
+ $mform =& $this->_form;
+ $buttonarray = array();
+ $buttonarray[] = &$mform->createElement('submit', 'showpreview', get_string('preview', 'tool_uploadcourse'));
+ $buttonarray[] = &$mform->createElement('submit', 'submitbutton', $submitlabel);
+ $buttonarray[] = &$mform->createElement('cancel');
+ $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+ $mform->closeHeaderBefore('buttonar');
+ }
+
+ /**
+ * Server side validation.
+ * @param array $data - form data
+ * @param object $files - form files
+ * @return array $errors - form errors
+ */
+ public function validation($data, $files) {
+ $errors = parent::validation($data, $files);
+ $columns = $this->_customdata['columns'];
+ $optype = $data['options']['mode'];
+
+ // Look for other required data.
+ if ($optype != tool_uploadcourse_processor::MODE_UPDATE_ONLY) {
+ if (!in_array('fullname', $columns)) {
+ if (isset($errors['mode'])) {
+ $errors['mode'] .= ' ';
+ }
+ $errors['mode'] .= get_string('missingfield', 'error', 'fullname');
+ }
+ if (!in_array('summary', $columns)) {
+ if (isset($errors['mode'])) {
+ $errors['mode'] .= ' ';
+ }
+ $errors['mode'] .= get_string('missingfield', 'error', 'summary');
+ }
+ }
+
+ return $errors;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Output tracker.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+require_once($CFG->libdir . '/weblib.php');
+
+/**
+ * Class output tracker.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class tool_uploadcourse_tracker {
+
+ /**
+ * Constant to output nothing.
+ */
+ const NO_OUTPUT = 0;
+
+ /**
+ * Constant to output HTML.
+ */
+ const OUTPUT_HTML = 1;
+
+ /**
+ * Constant to output plain text.
+ */
+ const OUTPUT_PLAIN = 2;
+
+ /**
+ * @var array columns to display.
+ */
+ protected $columns = array('line', 'result', 'id', 'shortname', 'fullname', 'idnumber', 'status');
+
+ /**
+ * @var int row number.
+ */
+ protected $rownb = 0;
+
+ /**
+ * @var int chosen output mode.
+ */
+ protected $outputmode;
+
+ /**
+ * @var object output buffer.
+ */
+ protected $buffer;
+
+ /**
+ * Constructor.
+ *
+ * @param int $outputmode desired output mode.
+ */
+ public function __construct($outputmode = self::NO_OUTPUT) {
+ $this->outputmode = $outputmode;
+ if ($this->outputmode == self::OUTPUT_PLAIN) {
+ $this->buffer = new progress_trace_buffer(new text_progress_trace());
+ }
+ }
+
+ /**
+ * Finish the output.
+ *
+ * @return void
+ */
+ public function finish() {
+ if ($this->outputmode == self::NO_OUTPUT) {
+ return;
+ }
+
+ if ($this->outputmode == self::OUTPUT_HTML) {
+ echo html_writer::end_tag('table');
+ }
+ }
+
+ /**
+ * Output the results.
+ *
+ * @param int $total total courses.
+ * @param int $created count of courses created.
+ * @param int $updated count of courses updated.
+ * @param int $deleted count of courses deleted.
+ * @param int $errors count of errors.
+ * @return void
+ */
+ public function results($total, $created, $updated, $deleted, $errors) {
+ if ($this->outputmode == self::NO_OUTPUT) {
+ return;
+ }
+
+ $message = array(
+ get_string('coursestotal', 'tool_uploadcourse', $total),
+ get_string('coursescreated', 'tool_uploadcourse', $created),
+ get_string('coursesupdated', 'tool_uploadcourse', $updated),
+ get_string('coursesdeleted', 'tool_uploadcourse', $deleted),
+ get_string('courseserrors', 'tool_uploadcourse', $errors)
+ );
+
+ if ($this->outputmode == self::OUTPUT_PLAIN) {
+ foreach ($message as $msg) {
+ $this->buffer->output($msg);
+ }
+ } else if ($this->outputmode == self::OUTPUT_HTML) {
+ $buffer = new progress_trace_buffer(new html_list_progress_trace());
+ foreach ($message as $msg) {
+ $buffer->output($msg);
+ }
+ $buffer->finished();
+ }
+ }
+
+ /**
+ * Output one more line.
+ *
+ * @param int $line line number.
+ * @param bool $outcome success or not?
+ * @param array $status array of statuses.
+ * @param array $data extra data to display.
+ * @return void
+ */
+ public function output($line, $outcome, $status, $data) {
+ global $OUTPUT;
+ if ($this->outputmode == self::NO_OUTPUT) {
+ return;
+ }
+
+ if ($this->outputmode == self::OUTPUT_PLAIN) {
+ $message = array(
+ $line,
+ $outcome ? 'OK' : 'NOK',
+ isset($data['id']) ? $data['id'] : '',
+ isset($data['shortname']) ? $data['shortname'] : '',
+ isset($data['fullname']) ? $data['fullname'] : '',
+ isset($data['idnumber']) ? $data['idnumber'] : ''
+ );
+ $this->buffer->output(implode("\t", $message));
+ if (!empty($status)) {
+ foreach ($status as $st) {
+ $this->buffer->output($st, 1);
+ }
+ }
+ } else if ($this->outputmode == self::OUTPUT_HTML) {
+ $ci = 0;
+ $this->rownb++;
+ if (is_array($status)) {
+ $status = implode(html_writer::empty_tag('br'), $status);
+ }
+ if ($outcome) {
+ $outcome = $OUTPUT->pix_icon('i/valid', '');
+ } else {
+ $outcome = $OUTPUT->pix_icon('i/invalid', '');
+ }
+ echo html_writer::start_tag('tr', array('class' => 'r' . $this->rownb % 2));
+ echo html_writer::tag('td', $line, array('class' => 'c' . $ci++));
+ echo html_writer::tag('td', $outcome, array('class' => 'c' . $ci++));
+ echo html_writer::tag('td', isset($data['id']) ? $data['id'] : '', array('class' => 'c' . $ci++));
+ echo html_writer::tag('td', isset($data['shortname']) ? $data['shortname'] : '', array('class' => 'c' . $ci++));
+ echo html_writer::tag('td', isset($data['fullname']) ? $data['fullname'] : '', array('class' => 'c' . $ci++));
+ echo html_writer::tag('td', isset($data['idnumber']) ? $data['idnumber'] : '', array('class' => 'c' . $ci++));
+ echo html_writer::tag('td', $status, array('class' => 'c' . $ci++));
+ echo html_writer::end_tag('tr');
+ }
+ }
+
+ /**
+ * Start the output.
+ *
+ * @return void
+ */
+ public function start() {
+ if ($this->outputmode == self::NO_OUTPUT) {
+ return;
+ }
+
+ if ($this->outputmode == self::OUTPUT_PLAIN) {
+ $columns = array_flip($this->columns);
+ unset($columns['status']);
+ $columns = array_flip($columns);
+ $this->buffer->output(implode("\t", $columns));
+ } else if ($this->outputmode == self::OUTPUT_HTML) {
+ $ci = 0;
+ echo html_writer::start_tag('table', array('class' => 'generaltable boxaligncenter flexible-wrap',
+ 'summary' => get_string('uploadcoursesresult', 'tool_uploadcourse')));
+ echo html_writer::start_tag('tr', array('class' => 'heading r' . $this->rownb));
+ echo html_writer::tag('th', get_string('csvline', 'tool_uploadcourse'),
+ array('class' => 'c' . $ci++, 'scope' => 'col'));
+ echo html_writer::tag('th', get_string('result', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+ echo html_writer::tag('th', get_string('id', 'tool_uploadcourse'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+ echo html_writer::tag('th', get_string('shortname'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+ echo html_writer::tag('th', get_string('fullname'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+ echo html_writer::tag('th', get_string('idnumber'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+ echo html_writer::tag('th', get_string('status'), array('class' => 'c' . $ci++, 'scope' => 'col'));
+ echo html_writer::end_tag('tr');
+ }
+ }
+
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * CLI Bulk course registration script from a comma separated file.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2012 Piers Harding
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+define('CLI_SCRIPT', true);
+
+require(__DIR__ . '/../../../../config.php');
+require_once($CFG->libdir . '/clilib.php');
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+
+$courseconfig = get_config('moodlecourse');
+
+// Now get cli options.
+list($options, $unrecognized) = cli_get_params(array(
+ 'help' => false,
+ 'mode' => '',
+ 'updatemode' => 'nothing',
+ 'file' => '',
+ 'delimiter' => 'comma',
+ 'encoding' => 'UTF-8',
+ 'shortnametemplate' => '',
+ 'templatecourse' => false,
+ 'restorefile' => false,
+ 'allowdeletes' => false,
+ 'allowrenames' => false,
+ 'allowresets' => false,
+ 'reset' => false,
+ 'category' => coursecat::get_default()->id,
+),
+array(
+ 'h' => 'help',
+ 'm' => 'mode',
+ 'u' => 'updatemode',
+ 'f' => 'file',
+ 'd' => 'delimiter',
+ 'e' => 'encoding',
+ 't' => 'templatecourse',
+ 'r' => 'restorefile',
+ 'g' => 'format',
+));
+
+if ($unrecognized) {
+ $unrecognized = implode("\n ", $unrecognized);
+ cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
+}
+
+$help =
+"Execute Course Upload.
+
+Options:
+-h, --help Print out this help
+-m, --mode Import mode: createnew, createall, createorupdate, update
+-u, --updatemode Update mode: nothing, dataonly, dataordefaults¸ missingonly
+-f, --file CSV file
+-d, --delimiter CSV delimiter: colon, semicolon, tab, cfg, comma
+-e, --encoding CSV file encoding: utf8, ... etc
+-t, --templatecourse Shortname of the course to restore after import
+-r, --restorefile Backup file to restore after import
+--reset Run the course reset after each course import
+--allowdeletes Allow courses to be deleted
+--allowrenames Allow courses to be renamed
+--allowresets Allow courses to be reset
+--shortnametemplate Template to generate the shortname from
+--category ID of default category (--updatemode dataordefaults will use this value)
+
+
+Example:
+\$sudo -u www-data /usr/bin/php admin/tool/uploadcourse/cli/uploadcourse.php --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));
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Defines message providers (types of message sent) for the PayPal enrolment plugin.
+ * Cache definitions.
*
- * @package enrol_authorize
- * @copyright 2012 Andrew Davis
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
-$messageproviders = array(
- 'authorize_enrolment' => array(),
+$definitions = array(
+ 'helper' => array(
+ 'mode' => cache_store::MODE_REQUEST,
+ )
);
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Bulk course registration script from a comma separated file.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2011 Piers Harding
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require(__DIR__ . '/../../../config.php');
+require_once($CFG->libdir . '/adminlib.php');
+require_once($CFG->libdir . '/coursecatlib.php');
+require_once($CFG->libdir . '/csvlib.class.php');
+
+admin_externalpage_setup('tooluploadcourse');
+
+$importid = optional_param('importid', '', PARAM_INT);
+$previewrows = optional_param('previewrows', 10, PARAM_INT);
+
+$returnurl = new moodle_url('/admin/tool/uploadcourse/index.php');
+
+if (empty($importid)) {
+ $mform1 = new tool_uploadcourse_step1_form();
+ if ($form1data = $mform1->get_data()) {
+ $importid = csv_import_reader::get_new_iid('uploadcourse');
+ $cir = new csv_import_reader($importid, 'uploadcourse');
+ $content = $mform1->get_file_content('coursefile');
+ $readcount = $cir->load_csv_content($content, $form1data->encoding, $form1data->delimiter_name);
+ unset($content);
+ if ($readcount === false) {
+ print_error('csvfileerror', 'tool_uploadcourse', $returnurl, $cir->get_error());
+ } else if ($readcount == 0) {
+ print_error('csvemptyfile', 'error', $returnurl, $cir->get_error());
+ }
+ } else {
+ echo $OUTPUT->header();
+ echo $OUTPUT->heading_with_help(get_string('uploadcourses', 'tool_uploadcourse'), 'uploadcourses', 'tool_uploadcourse');
+ $mform1->display();
+ echo $OUTPUT->footer();
+ die();
+ }
+} else {
+ $cir = new csv_import_reader($importid, 'uploadcourse');
+}
+
+// Data to set in the form.
+$data = array('importid' => $importid, 'previewrows' => $previewrows);
+if (!empty($form1data)) {
+ // Get options from the first form to pass it onto the second.
+ foreach ($form1data->options as $key => $value) {
+ $data["options[$key]"] = $value;
+ }
+}
+$context = context_system::instance();
+$mform2 = new tool_uploadcourse_step2_form(null, array('contextid' => $context->id, 'columns' => $cir->get_columns(),
+ 'data' => $data));
+
+// If a file has been uploaded, then process it.
+if ($form2data = $mform2->is_cancelled()) {
+ $cir->cleanup(true);
+ redirect($returnurl);
+} else if ($form2data = $mform2->get_data()) {
+
+ $options = (array) $form2data->options;
+ $defaults = (array) $form2data->defaults;
+ $processor = new tool_uploadcourse_processor($cir, $options, $defaults);
+
+ echo $OUTPUT->header();
+ if (isset($form2data->showpreview)) {
+ echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse'));
+ $processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+ $mform2->display();
+ } else {
+ echo $OUTPUT->heading(get_string('uploadcoursesresult', 'tool_uploadcourse'));
+ $processor->execute(new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+ echo $OUTPUT->continue_button($returnurl);
+ }
+
+} else {
+ $processor = new tool_uploadcourse_processor($cir, $form1data->options, array());
+ echo $OUTPUT->header();
+ echo $OUTPUT->heading(get_string('uploadcoursespreview', 'tool_uploadcourse'));
+ $processor->preview($previewrows, new tool_uploadcourse_tracker(tool_uploadcourse_tracker::OUTPUT_HTML));
+ $mform2->display();
+}
+
+echo $OUTPUT->footer();
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for component 'tool_uploadcourse'.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2011 Piers Harding
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['allowdeletes'] = 'Allow deletes';
+$string['allowrenames'] = 'Allow renames';
+$string['allowresets'] = 'Allow resets';
+$string['cachedef_helper'] = 'Helper caching';
+$string['cannotdeletecoursenotexist'] = 'Cannot delete a course that does not exist';
+$string['cannotgenerateshortnameupdatemode'] = 'Cannot generate a shortname when updates are allowed';
+$string['cannotreadbackupfile'] = 'Cannot read the backup file';
+$string['cannotrenamecoursenotexist'] = 'Cannot rename a course that does not exist';
+$string['cannotrenameidnumberconflict'] = 'Cannot rename the course, the ID number conflicts with an existing course';
+$string['cannotrenameshortnamealreadyinuse'] = 'Cannot rename the course, the shortname is already used';
+$string['canonlyrenameinupdatemode'] = 'Can only rename a course when update is allowed';
+$string['canonlyresetcourseinupdatemode'] = 'Can only reset a course in update mode';
+$string['couldnotresolvecatgorybyid'] = 'Could not resolve category by ID';
+$string['couldnotresolvecatgorybyidnumber'] = 'Could not resolve category by ID number';
+$string['couldnotresolvecatgorybypath'] = 'Could not resolve category by path';
+$string['coursecreated'] = 'Course created';
+$string['coursedeleted'] = 'Course deleted';
+$string['coursedeletionnotallowed'] = 'Course deletion is not allowed';
+$string['coursedoesnotexistandcreatenotallowed'] = 'The course does not exist and creating course is not allowed';
+$string['courseexistsanduploadnotallowed'] = 'The course exists and update is not allowed';
+$string['courseidnumberincremented'] = 'Course ID number incremented {$a->from} -> {$a->to}';
+$string['courseprocess'] = 'Course process';
+$string['courserenamed'] = 'Course renamed';
+$string['courserenamingnotallowed'] = 'Course renaming is not allowed';
+$string['coursereset'] = 'Course reset';
+$string['courseresetnotallowed'] = 'Course reset now allowed';
+$string['courserestored'] = 'Course restored';
+$string['coursestotal'] = 'Courses total: {$a}';
+$string['coursescreated'] = 'Courses created: {$a}';
+$string['coursesupdated'] = 'Courses updated: {$a}';
+$string['coursesdeleted'] = 'Courses deleted: {$a}';
+$string['courseserrors'] = 'Courses errors: {$a}';
+$string['courseshortnameincremented'] = 'Course shortname incremented {$a->from} -> {$a->to}';
+$string['courseshortnamegenerated'] = 'Course shortname generated: {$a}';
+$string['coursetemplatename'] = 'Restore from this course after upload';
+$string['coursetemplatename_help'] = 'Enter an existing course shortname to use as a template for the creation of all courses.';
+$string['coursetorestorefromdoesnotexist'] = 'The course to restore from does not exist';
+$string['courseupdated'] = 'Course updated';
+$string['createall'] = 'Create all, increment shortname if needed';
+$string['createnew'] = 'Create new courses only, skip existing ones';
+$string['createorupdate'] = 'Create new courses, or update existing ones';
+$string['csvdelimiter'] = 'CSV delimiter';
+$string['csvfileerror'] = 'There is something wrong with the format of the CSV file. Please check the number of headings and columns match, and that the delimiter and file encoding are correct: {$a}';
+$string['csvline'] = 'Line';
+$string['defaultvalues'] = 'Default course values';
+$string['encoding'] = 'Encoding';
+$string['errorwhilerestoringcourse'] = 'Error while restoring the course';
+$string['errorwhiledeletingcourse'] = 'Error while deleting the course';
+$string['generatedshortnameinvalid'] = 'The generated shortname is invalid';
+$string['generatedshortnamealreadyinuse'] = 'The generated shortname is already in use';
+$string['id'] = 'ID';
+$string['importoptions'] = 'Import options';
+$string['idnumberalreadyinuse'] = 'ID number already used by a course';
+$string['invalidbackupfile'] = 'Invalid backup file';
+$string['invalidcourseformat'] = 'Invalid course format';
+$string['invalidcsvfile'] = 'Invalid input CSV file';
+$string['invalidencoding'] = 'Invalid encoding';
+$string['invalidmode'] = 'Invalid mode selected';
+$string['invalideupdatemode'] = 'Invalid update mode selected';
+$string['invalidroles'] = 'Invalid role names: {$a}';
+$string['invalidshortname'] = 'Invalid shortname';
+$string['missingmandatoryfields'] = 'Missing value for mandatory fields: {$a}';
+$string['missingshortnamenotemplate'] = 'Missing shortname and shortname template not set';
+$string['mode'] = 'Upload mode';
+$string['nochanges'] = 'No changes';
+$string['pluginname'] = 'Course upload';
+$string['preview'] = 'Preview';
+$string['reset'] = 'Reset course after upload';
+$string['result'] = 'Result';
+$string['restoreafterimport'] = 'Restore after import';
+$string['rowpreviewnum'] = 'Preview rows';
+$string['shortnametemplate'] = 'Template to generate a shortname';
+$string['shortnametemplate_help'] = 'The short name of the course is displayed in the navigation. You may use template syntax here (%f = fullname, %i = idnumber), or enter an initial value that is incremented.';
+$string['templatefile'] = 'Restore from this file after upload';
+$string['templatefile_help'] = 'Select a file to use as a template for the creation of all courses.';
+$string['unknownimportmode'] = 'Unknown import mode';
+$string['updatemissing'] = 'Fill in missing from CSV data and defaults';
+$string['updatemode'] = 'Update mode';
+$string['updatemodedoessettonothing'] = 'Update mode does not allow anything to be updated';
+$string['updateonly'] = 'Only update existing courses';
+$string['updatewithdataordefaults'] = 'Update with CSV data and defaults';
+$string['updatewithdataonly'] = 'Update with CSV data only';
+$string['uploadcourses'] = 'Upload courses';
+$string['uploadcourses_help'] = 'Courses may be uploaded via text file. The format of the file should be as follows:
+
+* Each line of the file contains one record
+* Each record is a series of data separated by commas (or other delimiters)
+* The first record contains a list of fieldnames defining the format of the rest of the file
+* Required fieldnames are shortname, fullname, summary and category';
+$string['uploadcoursespreview'] = 'Upload courses preview';
+$string['uploadcoursesresult'] = 'Upload courses results';
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Authorize.Net enrolment plugin upgrades.
+ * Link to CSV course upload.
*
- * @package enrol_authorize
- * @copyright 2006 Eugene Venter
- * @author Eugene Venter
+ * @package tool_uploadcourse
+ * @copyright 2011 Piers Harding
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-function xmldb_enrol_authorize_upgrade($oldversion) {
- global $CFG, $DB;
+defined('MOODLE_INTERNAL') || die();
- $dbman = $DB->get_manager();
-
-
- // Moodle v2.3.0 release upgrade line
- // Put any upgrade step following this
-
-
- // Moodle v2.4.0 release upgrade line
- // Put any upgrade step following this
-
-
- // Moodle v2.5.0 release upgrade line.
- // Put any upgrade step following this.
-
-
- return true;
+if ($hassiteconfig) {
+ $ADMIN->add('courses', new admin_externalpage('tooluploadcourse',
+ get_string('uploadcourses', 'tool_uploadcourse'), "/admin/tool/uploadcourse/index.php"));
}
--- /dev/null
+@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"
--- /dev/null
+@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"
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing tests for the course class.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+/**
+ * Course test case.
+ *
+ * @package tool_uploadcourse
+ * @copyright 2013 Frédéric Massart
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
+ */
+class tool_uploadcourse_course_testcase extends advanced_testcase {
+
+ public function test_proceed_without_prepare() {
+ $this->resetAfterTest(true);
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+ $data = array();
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->setExpectedException('coding_exception');
+ $co->proceed();
+ }
+
+ public function test_proceed_when_prepare_failed() {
+ $this->resetAfterTest(true);
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+ $data = array();
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertFalse($co->prepare());
+ $this->setExpectedException('moodle_exception');
+ $co->proceed();
+ }
+
+ public function test_proceed_when_already_started() {
+ $this->resetAfterTest(true);
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+ $data = array('shortname' => 'test', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->setExpectedException('coding_exception');
+ $co->proceed();
+ }
+
+ public function test_invalid_shortname() {
+ $this->resetAfterTest(true);
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+ $data = array('shortname' => '<invalid>', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('invalidshortname', $co->get_errors());
+ }
+
+ public function test_create() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Existing course.
+ $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1', 'summary' => 'Yay!'));
+ $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+
+ $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+
+ // Try to add a new course.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $data = array('shortname' => 'newcourse', 'fullname' => 'New course', 'summary' => 'New', 'category' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $this->assertFalse($DB->record_exists('course', array('shortname' => 'newcourse')));
+ $co->proceed();
+ $this->assertTrue($DB->record_exists('course', array('shortname' => 'newcourse')));
+
+ // Try to add a new course, that already exists.
+ $coursecount = $DB->count_records('course', array());
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $data = array('shortname' => 'c1', 'fullname' => 'C1FN', 'summary' => 'C1', 'category' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('courseexistsanduploadnotallowed', $co->get_errors());
+ $this->assertEquals($coursecount, $DB->count_records('course', array()));
+ $this->assertNotEquals('C1', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+ // Try to add new with shortname incrementation.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+ $data = array('shortname' => 'c1', 'fullname' => 'C1FN', 'summary' => 'C1', 'category' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertTrue($DB->record_exists('course', array('shortname' => 'c2')));
+ }
+
+ public function test_delete() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ $c1 = $this->getDataGenerator()->create_course();
+ $c2 = $this->getDataGenerator()->create_course();
+
+ $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+ $this->assertFalse($DB->record_exists('course', array('shortname' => 'DoesNotExist')));
+
+ $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+
+ // Try delete when option not available.
+ $importoptions = array('candelete' => false);
+ $data = array('shortname' => $c1->shortname, 'delete' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('coursedeletionnotallowed', $co->get_errors());
+ $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+
+ // Try delete when not requested.
+ $importoptions = array('candelete' => true);
+ $data = array('shortname' => $c1->shortname, 'delete' => 0);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertTrue($DB->record_exists('course', array('shortname' => $c1->shortname)));
+
+ // Try delete when requested.
+ $importoptions = array('candelete' => true);
+ $data = array('shortname' => $c1->shortname, 'delete' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertFalse($DB->record_exists('course', array('shortname' => $c1->shortname)));
+ $this->assertTrue($DB->record_exists('course', array('shortname' => $c2->shortname)));
+
+ // Try deleting non-existing record, this should not fail.
+ $data = array('shortname' => 'DoesNotExist', 'delete' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('cannotdeletecoursenotexist', $co->get_errors());
+ }
+
+ public function test_update() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1'));
+
+ // Try to update with existing shortnames, not allowing creation, and updating nothing.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+ $data = array('shortname' => 'c1', 'fullname' => 'New fullname');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('updatemodedoessettonothing', $co->get_errors());
+
+ // Try to update with non-existing shortnames.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => 'DoesNotExist', 'fullname' => 'New fullname');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('coursedoesnotexistandcreatenotallowed', $co->get_errors());
+
+ // Try a proper update.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => 'c1', 'fullname' => 'New fullname');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertEquals('New fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1')));
+
+ // Try a proper update with defaults.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+ $data = array('shortname' => 'c1', 'fullname' => 'Another fullname');
+ $defaults = array('fullname' => 'Not this one', 'summary' => 'Awesome summary');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertEquals('Another fullname', $DB->get_field_select('course', 'fullname', 'shortname = :s', array('s' => 'c1')));
+ $this->assertEquals('Awesome summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+ // Try a proper update missing only.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS;
+ $DB->set_field('course', 'summary', '', array('shortname' => 'c1'));
+ $this->assertEquals('', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+ $data = array('shortname' => 'c1', 'summary' => 'Fill in summary');
+ $defaults = array('summary' => 'Do not use this summary');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertEquals('Fill in summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+
+ // Try a proper update missing only using defaults.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS;
+ $DB->set_field('course', 'summary', '', array('shortname' => 'c1'));
+ $this->assertEquals('', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+ $data = array('shortname' => 'c1');
+ $defaults = array('summary' => 'Use this summary');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaults);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertEquals('Use this summary', $DB->get_field_select('course', 'summary', 'shortname = :s', array('s' => 'c1')));
+ }
+
+ public function test_data_saved() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Create.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+ $data = array(
+ 'shortname' => 'c1',
+ 'fullname' => 'Fullname',
+ 'category' => '1',
+ 'visible' => '0',
+ 'startdate' => '8 June 1990',
+ 'idnumber' => '123abc',
+ 'summary' => 'Summary',
+ 'format' => 'weeks',
+ 'theme' => 'afterburner',
+ 'lang' => 'en',
+ 'newsitems' => '7',
+ 'showgrades' => '0',
+ 'showreports' => '1',
+ 'legacyfiles' => '1',
+ 'maxbytes' => '1234',
+ 'groupmode' => '2',
+ 'groupmodeforce' => '1',
+ 'enablecompletion' => '1',
+
+ 'role_teacher' => 'Knight',
+ 'role_manager' => 'Jedi',
+
+ 'enrolment_1' => 'guest',
+ 'enrolment_2' => 'self',
+ 'enrolment_2_roleid' => '1',
+ 'enrolment_3' => 'manual',
+ 'enrolment_3_disable' => '1',
+ );
+
+ $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1')));
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+ $course = $DB->get_record('course', array('shortname' => 'c1'));
+ $ctx = context_course::instance($course->id);
+
+ $this->assertEquals($data['fullname'], $course->fullname);
+ $this->assertEquals($data['category'], $course->category);
+ $this->assertEquals($data['visible'], $course->visible);
+ $this->assertEquals(mktime(0, 0, 0, 6, 8, 1990), $course->startdate);
+ $this->assertEquals($data['idnumber'], $course->idnumber);
+ $this->assertEquals($data['summary'], $course->summary);
+ $this->assertEquals($data['format'], $course->format);
+ $this->assertEquals($data['theme'], $course->theme);
+ $this->assertEquals($data['lang'], $course->lang);
+ $this->assertEquals($data['newsitems'], $course->newsitems);
+ $this->assertEquals($data['showgrades'], $course->showgrades);
+ $this->assertEquals($data['showreports'], $course->showreports);
+ $this->assertEquals($data['legacyfiles'], $course->legacyfiles);
+ $this->assertEquals($data['maxbytes'], $course->maxbytes);
+ $this->assertEquals($data['groupmode'], $course->groupmode);
+ $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce);
+ $this->assertEquals($data['enablecompletion'], $course->enablecompletion);
+
+ // Roles.
+ $roleids = array();
+ $roles = get_all_roles();
+ foreach ($roles as $role) {
+ $roleids[$role->shortname] = $role->id;
+ }
+ $this->assertEquals('Knight', $DB->get_field_select('role_names', 'name',
+ 'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['teacher'])));
+ $this->assertEquals('Jedi', $DB->get_field_select('role_names', 'name',
+ 'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['manager'])));
+
+ // Enrolment methods.
+ $enroldata = array();
+ $instances = enrol_get_instances($course->id, false);
+ $this->assertCount(3, $instances);
+ foreach ($instances as $instance) {
+ $enroldata[$instance->enrol] = $instance;
+ }
+
+ $this->assertNotEmpty($enroldata['guest']);
+ $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['guest']->status);
+ $this->assertNotEmpty($enroldata['self']);
+ $this->assertEquals($data['enrolment_2_roleid'], $enroldata['self']->roleid);
+ $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['self']->status);
+ $this->assertNotEmpty($enroldata['manual']);
+ $this->assertEquals(ENROL_INSTANCE_DISABLED, $enroldata['manual']->status);
+
+ // Update existing course.
+ $cat = $this->getDataGenerator()->create_category();
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array(
+ 'shortname' => 'c1',
+ 'fullname' => 'Fullname 2',
+ 'category' => $cat->id,
+ 'visible' => '1',
+ 'startdate' => '11 June 1984',
+ 'idnumber' => 'changeidn',
+ 'summary' => 'Summary 2',
+ 'format' => 'topics',
+ 'theme' => 'clean',
+ 'lang' => '',
+ 'newsitems' => '2',
+ 'showgrades' => '1',
+ 'showreports' => '0',
+ 'legacyfiles' => '0',
+ 'maxbytes' => '4321',
+ 'groupmode' => '1',
+ 'groupmodeforce' => '0',
+ 'enablecompletion' => '0',
+
+ 'role_teacher' => 'Teacher',
+ 'role_manager' => 'Manager',
+
+ 'enrolment_1' => 'guest',
+ 'enrolment_1_disable' => '1',
+ 'enrolment_2' => 'self',
+ 'enrolment_2_roleid' => '2',
+ 'enrolment_3' => 'manual',
+ 'enrolment_3_delete' => '1',
+ );
+
+ $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $course = $DB->get_record('course', array('shortname' => 'c1'));
+ $ctx = context_course::instance($course->id);
+
+ $this->assertEquals($data['fullname'], $course->fullname);
+ $this->assertEquals($data['category'], $course->category);
+ $this->assertEquals($data['visible'], $course->visible);
+ $this->assertEquals(mktime(0, 0, 0, 6, 11, 1984), $course->startdate);
+ $this->assertEquals($data['idnumber'], $course->idnumber);
+ $this->assertEquals($data['summary'], $course->summary);
+ $this->assertEquals($data['format'], $course->format);
+ $this->assertEquals($data['theme'], $course->theme);
+ $this->assertEquals($data['lang'], $course->lang);
+ $this->assertEquals($data['newsitems'], $course->newsitems);
+ $this->assertEquals($data['showgrades'], $course->showgrades);
+ $this->assertEquals($data['showreports'], $course->showreports);
+ $this->assertEquals($data['legacyfiles'], $course->legacyfiles);
+ $this->assertEquals($data['maxbytes'], $course->maxbytes);
+ $this->assertEquals($data['groupmode'], $course->groupmode);
+ $this->assertEquals($data['groupmodeforce'], $course->groupmodeforce);
+ $this->assertEquals($data['enablecompletion'], $course->enablecompletion);
+
+ // Roles.
+ $roleids = array();
+ $roles = get_all_roles();
+ foreach ($roles as $role) {
+ $roleids[$role->shortname] = $role->id;
+ }
+ $this->assertEquals('Teacher', $DB->get_field_select('role_names', 'name',
+ 'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['teacher'])));
+ $this->assertEquals('Manager', $DB->get_field_select('role_names', 'name',
+ 'roleid = :roleid AND contextid = :ctxid', array('ctxid' => $ctx->id, 'roleid' => $roleids['manager'])));
+
+ // Enrolment methods.
+ $enroldata = array();
+ $instances = enrol_get_instances($course->id, false);
+ $this->assertCount(2, $instances);
+ foreach ($instances as $instance) {
+ $enroldata[$instance->enrol] = $instance;
+ }
+
+ $this->assertNotEmpty($enroldata['guest']);
+ $this->assertEquals(ENROL_INSTANCE_DISABLED, $enroldata['guest']->status);
+ $this->assertNotEmpty($enroldata['self']);
+ $this->assertEquals($data['enrolment_2_roleid'], $enroldata['self']->roleid);
+ $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['self']->status);
+ }
+
+ public function test_default_data_saved() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ // Create.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
+ $data = array(
+ 'shortname' => 'c1',
+ );
+ $defaultdata = array(
+ 'fullname' => 'Fullname',
+ 'category' => '1',
+ 'visible' => '0',
+ 'startdate' => '8 June 1990',
+ 'idnumber' => '123abc',
+ 'summary' => 'Summary',
+ 'format' => 'weeks',
+ 'theme' => 'afterburner',
+ 'lang' => 'en',
+ 'newsitems' => '7',
+ 'showgrades' => '0',
+ 'showreports' => '1',
+ 'legacyfiles' => '1',
+ 'maxbytes' => '1234',
+ 'groupmode' => '2',
+ 'groupmodeforce' => '1',
+ 'enablecompletion' => '1',
+ );
+
+ $this->assertFalse($DB->record_exists('course', array('shortname' => 'c1')));
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+ $course = $DB->get_record('course', array('shortname' => 'c1'));
+ $ctx = context_course::instance($course->id);
+
+ $this->assertEquals($defaultdata['fullname'], $course->fullname);
+ $this->assertEquals($defaultdata['category'], $course->category);
+ $this->assertEquals($defaultdata['visible'], $course->visible);
+ $this->assertEquals(mktime(0, 0, 0, 6, 8, 1990), $course->startdate);
+ $this->assertEquals($defaultdata['idnumber'], $course->idnumber);
+ $this->assertEquals($defaultdata['summary'], $course->summary);
+ $this->assertEquals($defaultdata['format'], $course->format);
+ $this->assertEquals($defaultdata['theme'], $course->theme);
+ $this->assertEquals($defaultdata['lang'], $course->lang);
+ $this->assertEquals($defaultdata['newsitems'], $course->newsitems);
+ $this->assertEquals($defaultdata['showgrades'], $course->showgrades);
+ $this->assertEquals($defaultdata['showreports'], $course->showreports);
+ $this->assertEquals($defaultdata['legacyfiles'], $course->legacyfiles);
+ $this->assertEquals($defaultdata['maxbytes'], $course->maxbytes);
+ $this->assertEquals($defaultdata['groupmode'], $course->groupmode);
+ $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce);
+ $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion);
+
+ // Update.
+ $cat = $this->getDataGenerator()->create_category();
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+ $data = array(
+ 'shortname' => 'c1',
+ );
+ $defaultdata = array(
+ 'fullname' => 'Fullname 2',
+ 'category' => $cat->id,
+ 'visible' => '1',
+ 'startdate' => '11 June 1984',
+ 'idnumber' => 'changedid',
+ 'summary' => 'Summary 2',
+ 'format' => 'topics',
+ 'theme' => 'clean',
+ 'lang' => '',
+ 'newsitems' => '2',
+ 'showgrades' => '1',
+ 'showreports' => '0',
+ 'legacyfiles' => '0',
+ 'maxbytes' => '1111',
+ 'groupmode' => '1',
+ 'groupmodeforce' => '0',
+ 'enablecompletion' => '0',
+ );
+
+ $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertTrue($DB->record_exists('course', array('shortname' => 'c1')));
+ $course = $DB->get_record('course', array('shortname' => 'c1'));
+ $ctx = context_course::instance($course->id);
+
+ $this->assertEquals($defaultdata['fullname'], $course->fullname);
+ $this->assertEquals($defaultdata['category'], $course->category);
+ $this->assertEquals($defaultdata['visible'], $course->visible);
+ $this->assertEquals(mktime(0, 0, 0, 6, 11, 1984), $course->startdate);
+ $this->assertEquals($defaultdata['idnumber'], $course->idnumber);
+ $this->assertEquals($defaultdata['summary'], $course->summary);
+ $this->assertEquals($defaultdata['format'], $course->format);
+ $this->assertEquals($defaultdata['theme'], $course->theme);
+ $this->assertEquals($defaultdata['lang'], $course->lang);
+ $this->assertEquals($defaultdata['newsitems'], $course->newsitems);
+ $this->assertEquals($defaultdata['showgrades'], $course->showgrades);
+ $this->assertEquals($defaultdata['showreports'], $course->showreports);
+ $this->assertEquals($defaultdata['legacyfiles'], $course->legacyfiles);
+ $this->assertEquals($defaultdata['maxbytes'], $course->maxbytes);
+ $this->assertEquals($defaultdata['groupmode'], $course->groupmode);
+ $this->assertEquals($defaultdata['groupmodeforce'], $course->groupmodeforce);
+ $this->assertEquals($defaultdata['enablecompletion'], $course->enablecompletion);
+ }
+
+ public function test_rename() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'c1'));
+ $c2 = $this->getDataGenerator()->create_course(array('shortname' => 'c2'));
+
+ // Cannot rename when creating.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $importoptions = array('canrename' => true);
+ $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('courseexistsanduploadnotallowed', $co->get_errors());
+
+ // Cannot rename when creating.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_ALL;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $importoptions = array('canrename' => true);
+ $data = array('shortname' => 'c1', 'rename' => 'newshortname', 'category' => 1, 'summary' => 'S', 'fullname' => 'F');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('canonlyrenameinupdatemode', $co->get_errors());
+
+ // Error when not allowed to rename the course.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $importoptions = array('canrename' => false);
+ $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('courserenamingnotallowed', $co->get_errors());
+
+ // Can rename when updating.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $importoptions = array('canrename' => true);
+ $data = array('shortname' => 'c1', 'rename' => 'newshortname');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertEquals('newshortname', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+
+ // Can rename when updating.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $importoptions = array('canrename' => true);
+ $data = array('shortname' => 'newshortname', 'rename' => 'newshortname2');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertEquals('newshortname2', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+
+ // Error when course does not exist.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $importoptions = array('canrename' => true);
+ $data = array('shortname' => 'DoesNotExist', 'rename' => 'c1', 'category' => 1, 'summary' => 'S', 'fullname' => 'F');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('cannotrenamecoursenotexist', $co->get_errors());
+
+ // Renaming still updates the other values.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_OR_DEFAUTLS;
+ $importoptions = array('canrename' => true);
+ $data = array('shortname' => 'newshortname2', 'rename' => 'c1', 'fullname' => 'Another fullname!');
+ $defaultdata = array('summary' => 'New summary!');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, $defaultdata, $importoptions);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertEquals('c1', $DB->get_field_select('course', 'shortname', 'id = :id', array('id' => $c1->id)));
+ $this->assertEquals('New summary!', $DB->get_field_select('course', 'summary', 'id = :id', array('id' => $c1->id)));
+ $this->assertEquals('Another fullname!', $DB->get_field_select('course', 'fullname', 'id = :id', array('id' => $c1->id)));
+
+ // Renaming with invalid shortname.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $importoptions = array('canrename' => true);
+ $data = array('shortname' => 'c1', 'rename' => '<span>invalid</span>');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('invalidshortname', $co->get_errors());
+
+ // Renaming with invalid shortname.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $importoptions = array('canrename' => true);
+ $data = array('shortname' => 'c1', 'rename' => 'c2');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
+ }
+
+ public function test_restore_course() {
+ global $DB;
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+
+ $c1 = $this->getDataGenerator()->create_course();
+ $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id));
+
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => 'A1', 'templatecourse' => $c1->shortname, 'summary' => 'A', 'category' => 1,
+ 'fullname' => 'A1');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $course = $DB->get_record('course', array('shortname' => 'A1'));
+ $modinfo = get_fast_modinfo($course);
+ $found = false;
+ foreach ($modinfo->get_cms() as $cmid => $cm) {
+ if ($cm->modname == 'forum' && $cm->name == $c1f1->name) {
+ $found = true;
+ break;
+ }
+ }
+ $this->assertTrue($found);
+
+ // Restore the time limit to prevent warning.
+ set_time_limit(0);
+ }
+
+ public function test_restore_file() {
+ global $DB;
+ $this->resetAfterTest(true);
+ $this->setAdminUser();
+
+ $c1 = $this->getDataGenerator()->create_course();
+ $c1f1 = $this->getDataGenerator()->create_module('forum', array('course' => $c1->id));
+
+ // Restore from a file, checking that the file takes priority over the templatecourse.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => 'A1', 'backupfile' => __DIR__ . '/fixtures/backup.mbz',
+ 'summary' => 'A', 'category' => 1, 'fullname' => 'A1', 'templatecourse' => $c1->shortname);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $course = $DB->get_record('course', array('shortname' => 'A1'));
+ $modinfo = get_fast_modinfo($course);
+ $found = false;
+ foreach ($modinfo->get_cms() as $cmid => $cm) {
+ if ($cm->modname == 'glossary' && $cm->name == 'Imported Glossary') {
+ $found = true;
+ } else if ($cm->modname == 'forum' && $cm->name == $c1f1->name) {
+ // We should not find this!
+ $this->assertTrue(false);
+ }
+ }
+ $this->assertTrue($found);
+
+ // Restore the time limit to prevent warning.
+ set_time_limit(0);
+ }
+
+ /**
+ * Testing the reset on groups, group members and enrolments.
+ */
+ public function test_reset() {
+ global $DB;
+ $this->resetAfterTest(true);
+
+ $c1 = $this->getDataGenerator()->create_course();
+ $c1ctx = context_course::instance($c1->id);
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+
+ $u1 = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($u1->id, $c1->id, $studentrole->id);
+ $this->assertCount(1, get_enrolled_users($c1ctx));
+
+ $g1 = $this->getDataGenerator()->create_group(array('courseid' => $c1->id));
+ $this->getDataGenerator()->create_group_member(array('groupid' => $g1->id, 'userid' => $u1->id));
+ $this->assertEquals(1, $DB->count_records('groups', array('courseid' => $c1->id)));
+ $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+
+ // Wrong mode.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => 'DoesNotExist', 'reset' => '1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => 1);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('canonlyresetcourseinupdatemode', $co->get_errors());
+ $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+ $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+ $this->assertCount(1, get_enrolled_users($c1ctx));
+
+ // Reset not allowed.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => $c1->shortname, 'reset' => '1');
+ $importoptions = array('canreset' => false);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('courseresetnotallowed', $co->get_errors());
+ $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+ $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+ $this->assertCount(1, get_enrolled_users($c1ctx));
+
+ // Reset allowed but not requested.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => $c1->shortname, 'reset' => '0');
+ $importoptions = array('canreset' => true);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+ $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+ $this->assertCount(1, get_enrolled_users($c1ctx));
+
+ // Reset passed as a default parameter, should not be taken in account.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => $c1->shortname);
+ $importoptions = array('canreset' => true);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array('reset' => 1), $importoptions);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertTrue($DB->record_exists('groups', array('id' => $g1->id)));
+ $this->assertTrue($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+ $this->assertCount(1, get_enrolled_users($c1ctx));
+
+ // Reset executed from data.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => $c1->shortname, 'reset' => 1);
+ $importoptions = array('canreset' => true);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertFalse($DB->record_exists('groups', array('id' => $g1->id)));
+ $this->assertFalse($DB->record_exists('groups_members', array('groupid' => $g1->id, 'userid' => $u1->id)));
+ $this->assertCount(0, get_enrolled_users($c1ctx));
+
+ // Reset executed from import option.
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => $c1->shortname, 'reset' => 0);
+ $importoptions = array('reset' => 1, 'canreset' => true);
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data, array(), $importoptions);
+
+ $g1 = $this->getDataGenerator()->create_group(array('courseid' => $c1->id));
+ $this->getDataGenerator()->create_group_member(array('groupid' => $g1->id, 'userid' => $u1->id));
+ $this->assertEquals(1, $DB->count_records('groups', array('courseid' => $c1->id)));
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+ $this->assertFalse($DB->record_exists('groups', array('id' => $g1->id)));
+ }
+
+ public function test_create_bad_category() {
+ $this->resetAfterTest(true);
+
+ // Ensure fails when category cannot be resolved upon creation.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => 'c1', 'summary' => 'summary', 'fullname' => 'FN', 'category' => 'Wrong cat');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors());
+
+ // Ensure fails when category cannot be resolved upon update.
+ $c1 = $this->getDataGenerator()->create_course();
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => $c1->shortname, 'category' => 'Wrong cat');
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertFalse($co->prepare());
+ $this->assertArrayHasKey('couldnotresolvecatgorybyid', $co->get_errors());
+ }
+
+ public function test_enrolment_data() {
+ $this->resetAfterTest(true);
+
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+ $data = array('shortname' => 'c1', 'summary' => 'S', 'fullname' => 'FN', 'category' => '1');
+ $data['enrolment_1'] = 'manual';
+ $data['enrolment_1_role'] = 'teacher';
+ $data['enrolment_1_startdate'] = '2nd July 2013';
+ $data['enrolment_1_enddate'] = '2nd August 2013';
+ $data['enrolment_1_enrolperiod'] = '10 days';
+ $co = new tool_uploadcourse_course($mode, $updatemode, $data);
+ $this->assertTrue($co->prepare());
+ $co->proceed();
+
+ // Enrolment methods.
+ $enroldata = array();
+ $instances = enrol_get_instances($co->get_id(), false);
+ foreach ($instances as $instance) {
+ $enroldata[$instance->enrol] = $instance;
+ }
+
+ $this->assertNotEmpty($enroldata['manual']);
+ $this->assertEquals(ENROL_INSTANCE_ENABLED, $enroldata['manual']->status);
+ $this->assertEquals(strtotime($data['enrolment_1_startdate']), $enroldata['manual']->enrolstartdate);
+ $this->assertEquals(strtotime('1970-01-01 GMT + ' . $data['enrolment_1_enrolperiod']), $enroldata['manual']->enrolperiod);
+ $this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate);
+ }
+
+ public function test_idnumber_problems() {
+ $this->resetAfterTest(true);
+
+ $c1 = $this->getDataGenerator()->create_course(array('shortname' => 'sntaken', 'idnumber' => 'taken'));
+ $c2 = $this->getDataGenerator()->create_course();
+
+ // Create with existing ID number.
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = 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());
+ }
+
+}
--- /dev/null
+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
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing tests for the 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));
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * File containing tests for the 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();
+ }
+
+}
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Authorize.Net enrolment plugin version specification.
+ * Plugin version info.
*
- * @package enrol_authorize
- * @copyright 2010 Eugene Venter
- * @author Eugene Venter
+ * @package tool_uploadcourse
+ * @copyright 2011 Piers Harding
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2013050100; // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires = 2013050100; // Requires this Moodle version
-$plugin->component = 'enrol_authorize'; // Full name of the plugin (used for diagnostics)
-$plugin->cron = 180;
+$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).
function get_plugin_type($dirpath) {
global $CFG;
$dirpath = $CFG->dirroot.$dirpath;
- $plugintypes = get_plugin_types();
+ // Reverse order so that we get subplugin matches.
+ $plugintypes = array_reverse(core_component::get_plugin_types());
foreach ($plugintypes as $plugintype => $pluginbasedir) {
if (substr($dirpath, 0, strlen($pluginbasedir)) == $pluginbasedir) {
return $plugintype;
// Prepare the list of capabilities to choose from
$systemcontext = context_system::instance();
- $allcapabilities = fetch_context_capabilities($systemcontext);
+ $allcapabilities = $systemcontext->get_capabilities();
$capabilitychoices = array();
$capabilitychoices['norequiredcapability'] = get_string('norequiredcapability',
'webservice');
$params = array(
'name' => $data->name,
'description' => $data->description,
- 'image' => 0,
'timecreated' => $this->apply_date_offset($data->timecreated),
'timemodified' => $this->apply_date_offset($data->timemodified),
'usercreated' => $data->usercreated,
} else {
// This backup does not include the files - they should be available in moodle filestorage already.
- // Even if a file has been deleted since the backup was made, the file metadata will remain in the
- // files table, and the file will not be moved to the trashdir.
- // Files are not cleared from the files table by cron until several days after deletion.
- if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash))) {
- // Only grab one of the foundfiles - the file content should be the same for all entries.
- $foundfile = reset($foundfiles);
- $fs->create_file_from_storedfile($file_record, $foundfile->id);
- } else {
- // A matching existing file record was not found in the database.
- $result = new stdClass();
- $result->code = 'file_missing_in_backup';
- $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
- $result->level = backup::LOG_WARNING;
- $results[] = $result;
- continue;
+ // Create the file in the filepool if it does not exist yet.
+ if (!$fs->file_exists($newcontextid, $component, $filearea, $rec->newitemid, $file->filepath, $file->filename)) {
+
+ // Even if a file has been deleted since the backup was made, the file metadata will remain in the
+ // files table, and the file will not be moved to the trashdir.
+ // Files are not cleared from the files table by cron until several days after deletion.
+ if ($foundfiles = $DB->get_records('files', array('contenthash' => $file->contenthash))) {
+ // Only grab one of the foundfiles - the file content should be the same for all entries.
+ $foundfile = reset($foundfiles);
+ $fs->create_file_from_storedfile($file_record, $foundfile->id);
+ } else {
+ // A matching existing file record was not found in the database.
+ $result = new stdClass();
+ $result->code = 'file_missing_in_backup';
+ $result->message = sprintf('missing file %s%s in backup', $file->filepath, $file->filename);
+ $result->level = backup::LOG_WARNING;
+ $results[] = $result;
+ continue;
+ }
}
}
$fordb->timemodified = $now;
$fordb->usercreated = $USER->id;
$fordb->usermodified = $USER->id;
- $fordb->image = 0;
$fordb->issuername = $data->issuername;
$fordb->issuerurl = $data->issuerurl;
$fordb->issuercontact = $data->issuercontact;
$fordb->timemodified = time();
$fordb->usercreated = $user->id;
$fordb->usermodified = $user->id;
- $fordb->image = 0;
$fordb->issuername = "Test issuer";
$fordb->issuerurl = "http://issuer-url.domain.co.nz";
$fordb->expiredate = null;
$newid = $badge->make_clone();
$cloned_badge = new badge($newid);
- $this->assertEquals($badge->image, $cloned_badge->image);
$this->assertEquals($badge->description, $cloned_badge->description);
$this->assertEquals($badge->issuercontact, $cloned_badge->issuercontact);
$this->assertEquals($badge->issuername, $cloned_badge->issuername);
+++ /dev/null
-/**
- * The dock namespace: Contains all things dock related
- * @namespace
- */
-M.core_dock = {
- count : 0, // The number of dock items currently
- totalcount : 0, // The number of dock items through the page life
- items : [], // An array of dock items
- earlybinds : [], // Events added before the dock was augmented to support events
- Y : null, // The YUI instance to use with dock related code
- initialised : false, // True once thedock has been initialised
- delayedevent : null, // Will be an object if there is a delayed event in effect
- preventevent : null, // Will be an eventtype if there is an eventyoe to prevent
- holdingarea : null
-};
-/**
- * Namespace containing the nodes that relate to the dock
- * @namespace
- */
-M.core_dock.nodes = {
- dock : null, // The dock itself
- body : null, // The body of the page
- panel : null // The docks panel
-};
-/**
- * Configuration parameters used during the initialisation and setup
- * of dock and dock items.
- * This is here specifically so that themers can override core parameters and
- * design aspects without having to re-write navigation
- * @namespace
- */
-M.core_dock.cfg = {
- buffer:10, // Buffer used when containing a panel
- position:'left', // position of the dock
- orientation:'vertical', // vertical || horizontal determines if we change the title
- spacebeforefirstitem: 10, // Space between the top of the dock and the first item
- removeallicon: M.util.image_url('t/dock_to_block', 'moodle')
-};
-/**
- * CSS classes to use with the dock
- * @namespace
- */
-M.core_dock.css = {
- dock:'dock', // CSS Class applied to the dock box
- dockspacer:'dockspacer', // CSS class applied to the dockspacer
- controls:'controls', // CSS class applied to the controls box
- body:'has_dock', // CSS class added to the body when there is a dock
- buttonscontainer: 'buttons_container',
- dockeditem:'dockeditem', // CSS class added to each item in the dock
- dockeditemcontainer:'dockeditem_container',
- dockedtitle:'dockedtitle', // CSS class added to the item's title in each dock
- activeitem:'activeitem' // CSS class added to the active item
-};
-/**
- * Augments the classes as required and processes early bindings
- */
-M.core_dock.init = function(Y) {
- if (this.initialised) {
- return true;
- }
- var css = this.css;
- this.initialised = true;
- this.Y = Y;
- this.nodes.body = Y.one(document.body);
-
- // Give the dock item class the event properties/methods
- Y.augment(this.item, Y.EventTarget);
- Y.augment(this, Y.EventTarget, true);
- /**
- * A 'dock:actionkey' Event.
- * The event consists of the left arrow, right arrow, enter and space keys.
- * More keys can be mapped to action meanings.
- * actions: collapse , expand, toggle, enter.
- *
- * This event is subscribed to by dockitems.
- * The on() method to subscribe allows specifying the desired trigger actions as JSON.
- *
- * This event can also be delegated if needed.
- * Todo: This could be centralised, a similar Event is defined in blocks/navigation/yui/navigation/navigation.js
- */
- Y.Event.define("dock:actionkey", {
- // Webkit and IE repeat keydown when you hold down arrow keys.
- // Opera links keypress to page scroll; others keydown.
- // Firefox prevents page scroll via preventDefault() on either
- // keydown or keypress.
- _event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
-
- _keys: {
- //arrows
- '37': 'collapse',
- '39': 'expand',
- //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
- '32': 'toggle',
- '13': 'enter'
- },
-
- _keyHandler: function (e, notifier, args) {
- if (!args.actions) {
- var actObj = {collapse:true, expand:true, toggle:true, enter:true};
- } else {
- var actObj = args.actions;
- }
- if (this._keys[e.keyCode] && actObj[this._keys[e.keyCode]]) {
- e.action = this._keys[e.keyCode];
- notifier.fire(e);
- }
- },
-
- on: function (node, sub, notifier) {
- // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
- if (sub.args == null) {
- //no actions given
- sub._detacher = node.on(this._event, this._keyHandler,this, notifier, {actions:false});
- } else {
- sub._detacher = node.on(this._event, this._keyHandler,this, notifier, sub.args[0]);
- }
- },
-
- detach: function (node, sub, notifier) {
- //detach our _detacher handle of the subscription made in on()
- sub._detacher.detach();
- },
-
- delegate: function (node, sub, notifier, filter) {
- // subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
- if (sub.args == null) {
- //no actions given
- sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, {actions:false});
- } else {
- sub._delegateDetacher = node.delegate(this._event, this._keyHandler,filter, this, notifier, sub.args[0]);
- }
- },
-
- detachDelegate: function (node, sub, notifier) {
- sub._delegateDetacher.detach();
- }
- });
- // Publish the events the dock has
- this.publish('dock:beforedraw', {prefix:'dock'});
- this.publish('dock:beforeshow', {prefix:'dock'});
- this.publish('dock:shown', {prefix:'dock'});
- this.publish('dock:hidden', {prefix:'dock'});
- this.publish('dock:initialised', {prefix:'dock'});
- this.publish('dock:itemadded', {prefix:'dock'});
- this.publish('dock:itemremoved', {prefix:'dock'});
- this.publish('dock:itemschanged', {prefix:'dock'});
- this.publish('dock:panelgenerated', {prefix:'dock'});
- this.publish('dock:panelresizestart', {prefix:'dock'});
- this.publish('dock:resizepanelcomplete', {prefix:'dock'});
- this.publish('dock:starting', {prefix: 'dock',broadcast: 2,emitFacade: true});
- this.fire('dock:starting');
- // Re-apply early bindings properly now that we can
- this.applyBinds();
- // Check if there is a customisation function
- if (typeof(customise_dock_for_theme) === 'function') {
- try {
- // Run the customisation function
- customise_dock_for_theme();
- } catch (exception) {
- // Do nothing at the moment
- }
- }
-
- var dock = Y.one('#dock');
- if (!dock) {
- // Start the construction of the dock
- dock = Y.Node.create('<div id="dock" role="menubar" class="'+css.dock+' '+css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation+'"></div>')
- .append(Y.Node.create('<div class="'+css.buttonscontainer+'"></div>')
- .append(Y.Node.create('<div class="'+css.dockeditemcontainer+'"></div>')));
- this.nodes.body.append(dock);
- } else {
- dock.addClass(css.dock+'_'+this.cfg.position+'_'+this.cfg.orientation);
- }
- this.holdingarea = Y.Node.create('<div></div>').setStyles({display:'none'});
- this.nodes.body.append(this.holdingarea);
- if (Y.UA.ie > 0 && Y.UA.ie < 7) {
- // Adjust for IE 6 (can't handle fixed pos)
- dock.setStyle('height', dock.get('winHeight')+'px');
- }
- // Store the dock
- this.nodes.dock = dock;
- this.nodes.buttons = dock.one('.'+css.buttonscontainer);
- this.nodes.container = this.nodes.buttons.one('.'+css.dockeditemcontainer);
-
- if (Y.all('.block.dock_on_load').size() == 0) {
- // Nothing on the dock... hide it using CSS
- dock.addClass('nothingdocked');
- } else {
- this.nodes.body.addClass(this.css.body).addClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
- }
-
- this.fire('dock:beforedraw');
-
- // Add a removeall button
- // Must set the image src seperatly of we get an error with XML strict headers
- var removeall = Y.Node.create('<img alt="'+M.str.block.undockall+'" title="'+M.str.block.undockall+'" tabindex="0"/>');
- removeall.setAttribute('src',this.cfg.removeallicon);
- removeall.on('removeall|click', this.remove_all, this);
- removeall.on('dock:actionkey', this.remove_all, this, {actions:{enter:true}});
- this.nodes.buttons.appendChild(Y.Node.create('<div class="'+css.controls+'"></div>').append(removeall));
-
- // Create a manager for the height of the tabs. Once set this can be forgotten about
- new (function(Y){
- return {
- enabled : false, // True if the item_sizer is being used, false otherwise
- /**
- * Initialises the dock sizer which then attaches itself to the required
- * events in order to monitor the dock
- * @param {YUI} Y
- */
- init : function() {
- M.core_dock.on('dock:itemschanged', this.checkSizing, this);
- Y.on('windowresize', this.checkSizing, this);
- },
- /**
- * Check if the size dock items needs to be adjusted
- */
- checkSizing : function() {
- var dock = M.core_dock;
- var possibleheight = dock.nodes.dock.get('offsetHeight') - dock.nodes.dock.one('.controls').get('offsetHeight') - (dock.cfg.buffer*3) - (dock.items.length*2);
- var totalheight = 0;
- for (var id in dock.items) {
- var dockedtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle);
- if (dockedtitle) {
- if (this.enabled) {
- dockedtitle.setStyle('height', 'auto');
- }
- totalheight += dockedtitle.get('offsetHeight') || 0;
- }
- }
- if (totalheight > possibleheight) {
- this.enable(possibleheight);
- }
- },
- /**
- * Enables the dock sizer and resizes where required.
- */
- enable : function(possibleheight) {
- var dock = M.core_dock;
- var runningcount = 0;
- var usedheight = 0;
- this.enabled = true;
- for (var id in dock.items) {
- var itemtitle = Y.one(dock.items[id].title).ancestor('.'+dock.css.dockedtitle);
- if (!itemtitle) {
- continue;
- }
- var itemheight = Math.floor((possibleheight-usedheight) / (dock.count - runningcount));
- var offsetheight = itemtitle.get('offsetHeight');
- itemtitle.setStyle('overflow', 'hidden');
- if (offsetheight > itemheight) {
- itemtitle.setStyle('height', itemheight+'px');
- usedheight += itemheight;
- } else {
- usedheight += offsetheight;
- }
- runningcount++;
- }
- }
- };
- })(Y).init();
-
- // Attach the required event listeners
- // We use delegate here as that way a handful of events are created for the dock
- // and all items rather than the same number for the dock AND every item individually
- Y.delegate('click', this.handleEvent, this.nodes.dock, '.'+this.css.dockedtitle, this, {cssselector:'.'+this.css.dockedtitle, delay:0});
- Y.delegate('mouseenter', this.handleEvent, this.nodes.dock, '.'+this.css.dockedtitle, this, {cssselector:'.'+this.css.dockedtitle, delay:0.5, iscontained:true, preventevent:'click', preventdelay:3});
- //Y.delegate('mouseleave', this.handleEvent, this.nodes.body, '#dock', this, {cssselector:'#dock', delay:0.5, iscontained:false});
- this.nodes.dock.on('mouseleave', this.handleEvent, this, {cssselector:'#dock', delay:0.5, iscontained:false});
-
- this.nodes.body.on('click', this.handleEvent, this, {cssselector:'body', delay:0});
- this.on('dock:itemschanged', this.resizeBlockSpace, this);
- this.on('dock:itemschanged', this.checkDockVisibility, this);
- this.on('dock:itemschanged', this.resetFirstItem, this);
- // Inform everyone the dock has been initialised
- this.fire('dock:initialised');
- return true;
-};
-/**
- * Get the panel docked blocks will be shown in and initialise it if we havn't already.
- */
-M.core_dock.getPanel = function() {
- if (this.nodes.panel === null) {
- // Initialise the dockpanel .. should only happen once
- this.nodes.panel = (function(Y, parent){
- var dockpanel = Y.Node.create('<div id="dockeditempanel" class="dockitempanel_hidden"><div class="dockeditempanel_content"><div class="dockeditempanel_hd"></div><div class="dockeditempanel_bd"></div></div></div>');
- // Give the dockpanel event target properties and methods
- Y.augment(dockpanel, Y.EventTarget);
- // Publish events for the dock panel
- dockpanel.publish('dockpanel:beforeshow', {prefix:'dockpanel'});
- dockpanel.publish('dockpanel:shown', {prefix:'dockpanel'});
- dockpanel.publish('dockpanel:beforehide', {prefix:'dockpanel'});
- dockpanel.publish('dockpanel:hidden', {prefix:'dockpanel'});
- dockpanel.publish('dockpanel:visiblechange', {prefix:'dockpanel'});
- // Cache the content nodes
- dockpanel.contentNode = dockpanel.one('.dockeditempanel_content');
- dockpanel.contentHeader = dockpanel.contentNode.one('.dockeditempanel_hd');
- dockpanel.contentBody = dockpanel.contentNode.one('.dockeditempanel_bd');
- // Set the x position of the panel
- //dockpanel.setX(parent.get('offsetWidth'));
- dockpanel.visible = false;
- // Add a show event
- dockpanel.show = function() {
- this.fire('dockpanel:beforeshow');
- this.visible = true;
- this.removeClass('dockitempanel_hidden');
- this.fire('dockpanel:shown');
- this.fire('dockpanel:visiblechange');
- };
- // Add a hide event
- dockpanel.hide = function() {
- this.fire('dockpanel:beforehide');
- this.visible = false;
- this.addClass('dockitempanel_hidden');
- this.fire('dockpanel:hidden');
- this.fire('dockpanel:visiblechange');
- };
- // Add a method to set the header content
- dockpanel.setHeader = function(content) {
- this.contentHeader.setContent(content);
- if (arguments.length > 1) {
- for (var i=1;i < arguments.length;i++) {
- this.contentHeader.append(arguments[i]);
- }
- }
- };
- // Add a method to set the body content
- dockpanel.setBody = function(content) {
- this.contentBody.setContent(content);
- };
- // Add a method to set the top of the panel position
- dockpanel.setTop = function(newtop) {
- if (Y.UA.ie > 0 && Y.UA.ie < 7) {
- this.setY(newtop);
- } else {
- this.setStyle('top', newtop.toString()+'px');
- }
- return;
- };
- /**
- * Increases the width of the panel to avoid horizontal scrolling
- * if possible.
- */
- dockpanel.correctWidth = function() {
- var bd = this.one('.dockeditempanel_bd');
-
- // Width of content
- var w = bd.get('clientWidth');
- // Scrollable width of content
- var s = bd.get('scrollWidth');
- // Width of content container with overflow
- var ow = this.get('offsetWidth');
- // The new width
- var nw = w;
- // The max width (80% of screen)
- var mw = Math.round(this.get('winWidth') * 0.8);
-
- // If the scrollable width is more than the visible width
- if (s > w) {
- // Content width
- // + the difference
- // + any rendering difference (borders, padding)
- // + 10px to make it look nice.
- nw = w + (s-w) + ((ow-w)*2) + 10;
- }
-
- // Make sure its not more then the maxwidth
- if (nw > mw) {
- nw = mw;
- }
-
- // Set the new width if its more than the old width.
- if (nw > ow) {
- this.setStyle('width', nw+'px');
- }
- }
- // Put the dockpanel in the body
- parent.append(dockpanel);
- // Return it
- return dockpanel;
- })(this.Y, this.nodes.dock);
- this.nodes.panel.on('panel:visiblechange', this.resize, this);
- this.Y.on('windowresize', this.resize, this);
- this.fire('dock:panelgenerated');
- }
- return this.nodes.panel;
-};
-/**
- * Handles a generic event within the dock
- * @param {Y.Event} e
- * @param {object} options Event configuration object
- */
-M.core_dock.handleEvent = function(e, options) {
- var item = this.getActiveItem();
- if (options.cssselector == 'body') {
- if (!this.nodes.dock.contains(e.target)) {
- if (item) {
- item.hide();
- }
- }
- } else {
- var target;
- if (e.target.test(options.cssselector)) {
- target = e.target;
- } else {
- target = e.target.ancestor(options.cssselector);
- }
- if (!target) {
- return true;
- }
- if (this.preventevent !== null && e.type === this.preventevent) {
- return true;
- }
- if (options.preventevent) {
- this.preventevent = options.preventevent;
- if (options.preventdelay) {
- setTimeout(function(){M.core_dock.preventevent = null;}, options.preventdelay*1000);
- }
- }
- if (this.delayedevent && this.delayedevent.timeout) {
- clearTimeout(this.delayedevent.timeout);
- this.delayedevent.event.detach();
- this.delayedevent = null;
- }
- if (options.delay > 0) {
- return this.delayEvent(e, options, target);
- }
- var targetid = target.get('id');
- if (targetid.match(/^dock_item_(\d+)_title$/)) {
- item = this.items[targetid.replace(/^dock_item_(\d+)_title$/, '$1')];
- if (item.active) {
- item.hide();
- } else {
- item.show();
- }
- } else if (item) {
- item.hide();
- }
- }
- return true;
-};
-/**
- * This function delays an event and then fires it providing the cursor if either
- * within or outside of the original target (options.iscontained=true|false)
- * @param {Y.Event} event
- * @param {object} options
- * @param {Y.Node} target
- * @return bool
- */
-M.core_dock.delayEvent = function(event, options, target) {
- var self = this;
- self.delayedevent = (function(){
- return {
- target : target,
- event : self.nodes.body.on('mousemove', function(e){
- self.delayedevent.target = e.target;
- }),
- timeout : null
- };
- })(self);
- self.delayedevent.timeout = setTimeout(function(){
- self.delayedevent.timeout = null;
- self.delayedevent.event.detach();
- if (options.iscontained == self.nodes.dock.contains(self.delayedevent.target)) {
- self.handleEvent(event, {cssselector:options.cssselector, delay:0, iscontained:options.iscontained});
- }
- }, options.delay*1000);
- return true;
-};
-/**
- * Corrects the orientation of the title, which for the default
- * dock just means making it vertical
- * The orientation is determined by M.str.langconfig.thisdirectionvertical:
- * ver : Letters are stacked rather than rotated
- * ttb : Title is rotated clockwise so the first letter is at the top
- * btt : Title is rotated counterclockwise so the first letter is at the bottom.
- * @param {string} title
- */
-M.core_dock.fixTitleOrientation = function(item, title, text) {
- var Y = this.Y;
-
- var title = Y.one(title);
-
- if(M.core_dock.cfg.orientation != 'vertical') {
- // If the dock isn't vertical don't adjust it!
- title.setContent(text);
- return title
- }
-
- if (Y.UA.ie > 0 && Y.UA.ie < 8) {
- // IE 6/7 can't rotate text so force ver
- M.str.langconfig.thisdirectionvertical = 'ver';
- }
-
- var clockwise = false;
- switch (M.str.langconfig.thisdirectionvertical) {
- case 'ver':
- // Stacked is easy
- return title.setContent(text.split('').join('<br />'));
- case 'ttb':
- clockwise = true;
- break;
- case 'btt':
- clockwise = false;
- break;
- }
-
- if (Y.UA.ie == 8) {
- // IE8 can flip the text via CSS but not handle transform. IE9+ can handle the CSS3 transform attribute.
- title.setContent(text);
- title.setAttribute('style', 'writing-mode: tb-rl; filter: flipV flipH;display:inline;');
- title.addClass('filterrotate');
- return title;
- }
-
- // We need to fix a font-size - sorry theme designers.
- var fontsize = '11px';
- var transform = (clockwise) ? 'rotate(90deg)' : 'rotate(270deg)';
- var test = Y.Node.create('<h2><span class="transform-test-node" style="font-size:'+fontsize+';">'+text+'</span></h2>');
- this.nodes.body.insert(test, 0);
- var width = test.one('span').get('offsetWidth') * 1.2;
- var height = test.one('span').get('offsetHeight');
- test.remove();
-
- title.setContent(text);
- title.addClass('css3transform');
-
- // Move the title into position
- title.setStyles({
- 'margin' : '0',
- 'padding' : '0',
- 'position' : 'relative',
- 'fontSize' : fontsize,
- 'width' : width,
- 'top' : width/2
- });
-
- // Positioning is different when in RTL mode.
- if (right_to_left()) {
- title.setStyle('left', width/2 - height);
- } else {
- title.setStyle('right', width/2 - height);
- }
-
- // Rotate the text
- title.setStyles({
- 'transform' : transform,
- '-ms-transform' : transform,
- '-moz-transform' : transform,
- '-webkit-transform' : transform,
- '-o-transform' : transform
- });
-
- var container = Y.Node.create('<div></div>');
- container.append(title);
- container.setStyle('height', width + (width / 4));
- container.setStyle('position', 'relative');
- return container;
-
- return title;
-};
-/**
- * Resizes the space that contained blocks if there were no blocks left in
- * it. e.g. if all blocks have been moved to the dock
- * @param {Y.Node} node
- */
-M.core_dock.resizeBlockSpace = function(node) {
-
- if (this.Y.all('.block.dock_on_load').size()>0) {
- // Do not resize during initial load
- return;
- }
- var blockregions = [];
- var populatedblockregions = 0;
- this.Y.all('.block-region').each(function(region){
- var hasblocks = (region.all('.block').size() > 0);
- if (hasblocks) {
- populatedblockregions++;
- }
- blockregions[region.get('id')] = {hasblocks: hasblocks, bodyclass: region.get('id').replace(/^region\-/, 'side-')+'-only'};
- });
- var bodynode = M.core_dock.nodes.body;
- var showregions = false;
- if (bodynode.hasClass('blocks-moving')) {
- // open up blocks during blocks positioning
- showregions = true;
- }
-
- var noblocksbodyclass = 'content-only';
- var i = null;
- if (populatedblockregions==0 && showregions==false) {
- bodynode.addClass(noblocksbodyclass);
- for (i in blockregions) {
- bodynode.removeClass(blockregions[i].bodyclass);
- }
- } else if (populatedblockregions==1 && showregions==false) {
- bodynode.removeClass(noblocksbodyclass);
- for (i in blockregions) {
- if (!blockregions[i].hasblocks) {
- bodynode.removeClass(blockregions[i].bodyclass);
- } else {
- bodynode.addClass(blockregions[i].bodyclass);
- }
- }
- } else {
- bodynode.removeClass(noblocksbodyclass);
- for (i in blockregions) {
- bodynode.removeClass(blockregions[i].bodyclass);
- }
- }
-};
-/**
- * Adds a dock item into the dock
- * @function
- * @param {M.core_dock.item} item
- */
-M.core_dock.add = function(item) {
- item.id = this.totalcount;
- this.count++;
- this.totalcount++;
- this.items[item.id] = item;
- this.items[item.id].draw();
- this.fire('dock:itemadded', item);
- this.fire('dock:itemschanged', item);
-};
-/**
- * Appends a dock item to the dock
- * @param {YUI.Node} docknode
- */
-M.core_dock.append = function(docknode) {
- this.nodes.container.append(docknode);
-};
-/**
- * Initialises a generic block object
- * @param {YUI} Y
- * @param {int} id
- */
-M.core_dock.init_genericblock = function(Y, id) {
- if (!this.initialised) {
- this.init(Y);
- }
- new this.genericblock(id).initialise_block(Y, Y.one('#inst'+id));
-};
-/**
- * Removes the node at the given index and puts it back into conventional page sturcture
- * @function
- * @param {int} uid Unique identifier for the block
- * @return {boolean}
- */
-M.core_dock.remove = function(uid) {
- if (!this.items[uid]) {
- return false;
- }
- this.items[uid].remove();
- delete this.items[uid];
- this.count--;
- this.fire('dock:itemremoved', uid);
- this.fire('dock:itemschanged', uid);
- return true;
-};
-/**
- * Ensures the the first item in the dock has the correct class
- */
-M.core_dock.resetFirstItem = function() {
- this.nodes.dock.all('.'+this.css.dockeditem+'.firstdockitem').removeClass('firstdockeditem');
- if (this.nodes.dock.one('.'+this.css.dockeditem)) {
- this.nodes.dock.one('.'+this.css.dockeditem).addClass('firstdockitem');
- }
-};
-/**
- * Removes all nodes and puts them back into conventional page sturcture
- * @function
- * @return {boolean}
- */
-M.core_dock.remove_all = function(e) {
- for (var i in this.items) {
- this.remove(i);
- }
- return true;
-};
-/**
- * Hides the active item
- */
-M.core_dock.hideActive = function() {
- var item = this.getActiveItem();
- if (item) {
- item.hide();
- }
-};
-/**
- * Checks wether the dock should be shown or hidden
- */
-M.core_dock.checkDockVisibility = function() {
- if (!this.count) {
- this.nodes.dock.addClass('nothingdocked');
- this.nodes.body.removeClass(this.css.body)
- .removeClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
- this.fire('dock:hidden');
- } else {
- this.fire('dock:beforeshow');
- this.nodes.dock.removeClass('nothingdocked');
- this.nodes.body.addClass(this.css.body)
- .addClass(this.css.body+'_'+this.cfg.position+'_'+this.cfg.orientation);
- this.fire('dock:shown');
- }
-};
-/**
- * This smart little function allows developers to attach event listeners before
- * the dock has been augmented to allows event listeners.
- * Once the augmentation is complete this function will be replaced with the proper
- * on method for handling event listeners.
- * Finally applyBinds needs to be called in order to properly bind events.
- * @param {string} event
- * @param {function} callback
- */
-M.core_dock.on = function(event, callback) {
- this.earlybinds.push({event:event,callback:callback});
-};
-/**
- * This function takes all early binds and attaches them as listeners properly
- * This should only be called once augmentation is complete.
- */
-M.core_dock.applyBinds = function() {
- for (var i in this.earlybinds) {
- var bind = this.earlybinds[i];
- this.on(bind.event, bind.callback);
- }
- this.earlybinds = [];
-};
-/**
- * This function checks the size and position of the panel and moves/resizes if
- * required to keep it within the bounds of the window.
- */
-M.core_dock.resize = function() {
- this.fire('dock:panelresizestart');
- var panel = this.getPanel();
- var item = this.getActiveItem();
- if (!panel.visible || !item) {
- return;
- }
-
- if (this.cfg.orientation=='vertical') {
- var buffer = this.cfg.buffer;
- var screenheight = parseInt(this.nodes.body.get('winHeight'))-(buffer*2);
- var docky = this.nodes.dock.getY();
- var titletop = item.nodes.docktitle.getY()-docky-buffer;
- var containery = this.nodes.container.getY();
- var containerheight = containery-docky+this.nodes.buttons.get('offsetHeight');
- var scrolltop = panel.contentBody.get('scrollTop');
- panel.contentBody.setStyle('height', 'auto');
- panel.removeClass('oversized_content');
- var panelheight = panel.get('offsetHeight');
-
- if (this.Y.UA.ie > 0 && this.Y.UA.ie < 7) {
- panel.setTop(item.nodes.docktitle.getY());
- } else if (panelheight > screenheight) {
- panel.setTop(buffer-containerheight);
- panel.contentBody.setStyle('height', (screenheight-panel.contentHeader.get('offsetHeight'))+'px');
- panel.addClass('oversized_content');
- } else if (panelheight > (screenheight-(titletop-buffer))) {
- var difference = panelheight - (screenheight-titletop);
- panel.setTop(titletop-containerheight-difference+buffer);
- } else {
- panel.setTop(titletop-containerheight+buffer);
- }
-
- if (scrolltop) {
- panel.contentBody.set('scrollTop', scrolltop);
- }
- }
-
- if (this.cfg.position=='right') {
- panel.setStyle('left', -panel.get('offsetWidth')+'px');
-
- } else if (this.cfg.position=='top') {
- var dockx = this.nodes.dock.getX();
- var titleleft = item.nodes.docktitle.getX()-dockx;
- panel.setStyle('left', titleleft+'px');
- }
-
- this.fire('dock:resizepanelcomplete');
- return;
-};
-/**
- * Returns the currently active dock item or false
- */
-M.core_dock.getActiveItem = function() {
- for (var i in this.items) {
- if (this.items[i].active) {
- return this.items[i];
- }
- }
- return false;
-};
-/**
- * This class represents a generic block
- * @class M.core_dock.genericblock
- * @constructor
- */
-M.core_dock.genericblock = function(id) {
- // Nothing to actually do here but it needs a constructor!
- if (id) {
- this.id = id;
- }
-};
-M.core_dock.genericblock.prototype = {
- Y : null, // A YUI instance to use with the block
- id : null, // The block instance id
- cachedcontentnode : null, // The cached content node for the actual block
- blockspacewidth : null, // The width of the block's original container
- skipsetposition : false, // If true the user preference isn't updated
- isdocked : false, // True if it is docked
- /**
- * This function should be called within the block's constructor and is used to
- * set up the initial controls for swtiching block position as well as an initial
- * moves that may be required.
- *
- * @param {YUI} Y
- * @param {YUI.Node} node The node that contains all of the block's content
- * @return {M.core_dock.genericblock}
- */
- initialise_block : function(Y, node) {
- M.core_dock.init(Y);
-
- this.Y = Y;
- if (!node) {
- return false;
- }
-
- var commands = node.one('.header .title .commands');
- if (!commands) {
- commands = this.Y.Node.create('<div class="commands"></div>');
- if (node.one('.header .title')) {
- node.one('.header .title').append(commands);
- }
- }
-
- // Must set the image src seperatly of we get an error with XML strict headers
- var moveto = Y.Node.create('<input type="image" class="moveto customcommand requiresjs" />');
- var header = node.one('.header .title h2');
- moveto.setAttribute('alt', Y.Escape.html(M.util.get_string('addtodock', 'block')));
- if (header) {
- moveto.setAttribute('title', Y.Escape.html(M.util.get_string('dockblock', 'block', header.getHTML())));
- } else {
- moveto.setAttribute('title', Y.Escape.html(M.util.get_string('addtodock', 'block')));
- }
-
- var icon = 't/block_to_dock';
- if (right_to_left()) {
- icon = 't/block_to_dock_rtl';
- }
- moveto.setAttribute('src', M.util.image_url(icon, 'moodle'));
- moveto.on('movetodock|click', this.move_to_dock, this, commands);
-
- var blockaction = node.one('.block_action');
- if (blockaction) {
- blockaction.prepend(moveto);
- } else {
- commands.append(moveto);
- }
-
- // Move the block straight to the dock if required
- if (node.hasClass('dock_on_load')) {
- node.removeClass('dock_on_load');
- this.skipsetposition = true;
- this.move_to_dock(null, commands);
- }
- return this;
- },
-
- /**
- * This function is reponsible for moving a block from the page structure onto the
- * dock
- * @param {event}
- */
- move_to_dock : function(e, commands) {
- if (e) {
- e.halt(true);
- }
-
- var Y = this.Y;
- var dock = M.core_dock;
-
- var node = Y.one('#inst'+this.id);
- var blockcontent = node.one('.content');
- if (!blockcontent) {
- return;
- }
-
- // Disable the skip anchor when docking
- var skipanchor = node.previous();
- if (skipanchor.hasClass('skip-block')) {
- skipanchor.hide();
- }
-
- var blockclass = (function(classes){
- var r = /(^|\s)(block_[a-zA-Z0-9_]+)(\s|$)/;
- var m = r.exec(classes);
- return (m)?m[2]:m;
- })(node.getAttribute('className').toString());
-
- this.cachedcontentnode = node;
-
- node.replace(Y.Node.getDOMNode(Y.Node.create('<div id="content_placeholder_'+this.id+'" class="block_dock_placeholder"></div>')));
- M.core_dock.holdingarea.append(node);
- node = null;
-
- var blocktitle = Y.Node.getDOMNode(this.cachedcontentnode.one('.title h2')).cloneNode(true);
-
- var blockcommands = this.cachedcontentnode.one('.title .commands');
- if (!blockcommands) {
- blockcommands = Y.Node.create('<div class="commands"></div>');
- this.cachedcontentnode.one('.title').append(blockcommands);
- }
-
- // Must set the image src seperatly of we get an error with XML strict headers
- var movetoimg = Y.Node.create('<img alt="'+Y.Escape.html(M.str.block.undockitem)+'" title="'+
- Y.Escape.html(M.util.get_string('undockblock', 'block', blocktitle.innerHTML)) +'" />');
- var icon = 't/dock_to_block';
- if (right_to_left()) {
- icon = 't/dock_to_block_rtl';
- }
- movetoimg.setAttribute('src', M.util.image_url(icon, 'moodle'));
- var moveto = Y.Node.create('<a class="moveto customcommand requiresjs"></a>').append(movetoimg);
- if (location.href.match(/\?/)) {
- moveto.set('href', location.href+'&dock='+this.id);
- } else {
- moveto.set('href', location.href+'?dock='+this.id);
- }
- blockcommands.append(moveto);
-
- // Create a new dock item for the block
- var dockitem = new dock.item(Y, this.id, blocktitle, blockcontent, blockcommands, blockclass);
- // Wire the draw events to register remove events
- dockitem.on('dockeditem:drawcomplete', function(e){
- // check the contents block [editing=off]
- this.contents.all('.moveto').on('returntoblock|click', function(e){
- e.halt();
- dock.remove(this.id);
- }, this);
- // check the commands block [editing=on]
- this.commands.all('.moveto').on('returntoblock|click', function(e){
- e.halt();
- dock.remove(this.id);
- }, this);
- // Add a close icon
- // Must set the image src seperatly of we get an error with XML strict headers
- var closeicon = Y.Node.create('<span class="hidepanelicon" tabindex="0"><img alt="'+M.str.block.hidepanel+'" title="'+M.str.block.hidedockpanel+'" /></span>');
- closeicon.one('img').setAttribute('src', M.util.image_url('t/dockclose', 'moodle'));
- closeicon.on('forceclose|click', this.hide, this);
- closeicon.on('dock:actionkey',this.hide, this, {actions:{enter:true,toggle:true}});
- this.commands.append(closeicon);
- }, dockitem);
- // Register an event so that when it is removed we can put it back as a block
- dockitem.on('dockeditem:itemremoved', this.return_to_block, this, dockitem);
- dock.add(dockitem);
-
- if (!this.skipsetposition) {
- // save the users preference
- M.util.set_user_preference('docked_block_instance_'+this.id, 1);
- } else {
- this.skipsetposition = false;
- }
-
- this.isdocked = true;
- },
- /**
- * This function removes a block from the dock and puts it back into the page
- * structure.
- * @param {M.core_dock.class.item}
- */
- return_to_block : function(dockitem) {
- var placeholder = this.Y.one('#content_placeholder_'+this.id);
-
- // Enable the skip anchor when going back to block mode
- var skipanchor = placeholder.previous();
- if (skipanchor.hasClass('skip-block')) {
- skipanchor.show();
- }
-
- if (this.cachedcontentnode.one('.header')) {
- this.cachedcontentnode.one('.header').insert(dockitem.contents, 'after');
- } else {
- this.cachedcontentnode.insert(dockitem.contents);
- }
-
- placeholder.replace(this.Y.Node.getDOMNode(this.cachedcontentnode));
- this.cachedcontentnode = this.Y.one('#'+this.cachedcontentnode.get('id'));
-
- var commands = dockitem.commands;
- if (commands) {
- commands.all('.hidepanelicon').remove();
- commands.all('.moveto').remove();
- commands.remove();
- }
- this.cachedcontentnode.one('.title').append(commands);
- this.cachedcontentnode = null;
- M.util.set_user_preference('docked_block_instance_'+this.id, 0);
- this.isdocked = false;
- return true;
- }
-};
-
-/**
- * This class represents an item in the dock
- * @class M.core_dock.item
- * @constructor
- * @param {YUI} Y The YUI instance to use for this item
- * @param {int} uid The unique ID for the item
- * @param {this.Y.Node} title
- * @param {this.Y.Node} contents
- * @param {this.Y.Node} commands
- * @param {string} blockclass
- */
-M.core_dock.item = function(Y, uid, title, contents, commands, blockclass){
- this.Y = Y;
- this.publish('dockeditem:drawstart', {prefix:'dockeditem'});
- this.publish('dockeditem:drawcomplete', {prefix:'dockeditem'});
- this.publish('dockeditem:showstart', {prefix:'dockeditem'});
- this.publish('dockeditem:showcomplete', {prefix:'dockeditem'});
- this.publish('dockeditem:hidestart', {prefix:'dockeditem'});
- this.publish('dockeditem:hidecomplete', {prefix:'dockeditem'});
- this.publish('dockeditem:itemremoved', {prefix:'dockeditem'});
- if (uid && this.id==null) {
- this.id = uid;
- }
- if (title && this.title==null) {
- this.titlestring = title.cloneNode(true);
- this.title = document.createElement(title.nodeName);
- this.title = M.core_dock.fixTitleOrientation(this, this.title, this.titlestring.firstChild.nodeValue);
- }
- if (contents && this.contents==null) {
- this.contents = contents;
- }
- if (commands && this.commands==null) {
- this.commands = commands;
- }
- if (blockclass && this.blockclass==null) {
- this.blockclass = blockclass;
- }
- this.nodes = (function(){
- return {docktitle : null, dockitem : null, container: null};
- })();
-};
-/**
- *
- */
-M.core_dock.item.prototype = {
- Y : null, // The YUI instance to use with this dock item
- id : null, // The unique id for the item
- name : null, // The name of the item
- title : null, // The title of the item
- titlestring : null, // The title as a plain string
- contents : null, // The content of the item
- commands : null, // The commands for the item
- active : false, // True if the item is being shown
- blockclass : null, // The class of the block this item relates to
- nodes : null,
- /**
- * This function draws the item on the dock
- */
- draw : function() {
- this.fire('dockeditem:drawstart');
-
- var Y = this.Y;
- var css = M.core_dock.css;
-
- this.nodes.docktitle = Y.Node.create('<div id="dock_item_'+this.id+'_title" role="menu" aria-haspopup="true" class="'+css.dockedtitle+'"></div>');
- this.nodes.docktitle.append(this.title);
- this.nodes.dockitem = Y.Node.create('<div id="dock_item_'+this.id+'" class="'+css.dockeditem+'" tabindex="0"></div>');
- this.nodes.dockitem.on('dock:actionkey', this.toggle, this);
- if (M.core_dock.count === 1) {
- this.nodes.dockitem.addClass('firstdockitem');
- }
- this.nodes.dockitem.append(this.nodes.docktitle);
- M.core_dock.append(this.nodes.dockitem);
- this.fire('dockeditem:drawcomplete');
- return true;
- },
- /**
- * This function toggles makes the item active and shows it
- */
- show : function() {
- M.core_dock.hideActive();
- var Y = this.Y;
- var css = M.core_dock.css;
- var panel = M.core_dock.getPanel();
- this.fire('dockeditem:showstart');
- panel.setHeader(this.titlestring, this.commands);
- panel.setBody(Y.Node.create('<div class="'+this.blockclass+' block_docked"></div>').append(this.contents));
- panel.show();
- panel.correctWidth();
-
- this.active = true;
- // Add active item class first up
- this.nodes.docktitle.addClass(css.activeitem);
- // Set aria-exapanded property to true.
- this.nodes.docktitle.set('aria-expanded', "true");
- this.fire('dockeditem:showcomplete');
- M.core_dock.resize();
- return true;
- },
- /**
- * This function hides the item and makes it inactive
- */
- hide : function() {
- var css = M.core_dock.css;
- this.fire('dockeditem:hidestart');
- // No longer active
- this.active = false;
- // Remove the active class
- this.nodes.docktitle.removeClass(css.activeitem);
- // Hide the panel
- M.core_dock.getPanel().hide();
- // Set aria-exapanded property to false
- this.nodes.docktitle.set('aria-expanded', "false");
- this.fire('dockeditem:hidecomplete');
- },
- /**
- * A toggle between calling show and hide functions based on css.activeitem
- * Applies rules to key press events (dock:actionkey)
- * @param {Event} e
- */
- toggle : function(e) {
- var css = M.core_dock.css;
- if (this.nodes.docktitle.hasClass(css.activeitem) && !(e.type == 'dock:actionkey' && e.action=='expand')) {
- this.hide();
- } else if (!this.nodes.docktitle.hasClass(css.activeitem) && !(e.type == 'dock:actionkey' && e.action=='collapse')) {
- this.show();
- }
- },
- /**
- * This function removes the node and destroys it's bits
- * @param {Event} e
- */
- remove : function () {
- this.hide();
- this.nodes.dockitem.remove();
- this.fire('dockeditem:itemremoved');
- }
-};
global $CFG;
$bc = new block_contents($this->html_attributes());
-
+ $bc->attributes['data-block'] = $this->name();
$bc->blockinstanceid = $this->instance->id;
$bc->blockpositionid = $this->instance->blockpositionid;
$bc->collapsible = block_contents::VISIBLE;
}
+ if ($this->instance_can_be_docked() && !$this->hide_header()) {
+ $bc->dockable = true;
+ }
+
$bc->annotation = ''; // TODO MDL-19398 need to work out what to say here.
return $bc;
$this->specialization();
}
+ /**
+ * Allows the block to load any JS it requires into the page.
+ *
+ * By default this function simply permits the user to dock the block if it is dockable.
+ */
function get_required_javascript() {
if ($this->instance_can_be_docked() && !$this->hide_header()) {
- $this->page->requires->js_init_call('M.core_dock.init_genericblock', array($this->instance->id));
user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT);
}
}
* Set the initial properties for the block
*/
function init() {
- global $CFG;
$this->blockname = get_class($this);
$this->title = get_string('pluginname', $this->blockname);
}
*/
function get_required_javascript() {
global $CFG;
- user_preference_allow_ajax_update('docked_block_instance_'.$this->instance->id, PARAM_INT);
- $this->page->requires->js_module('core_dock');
+ parent::get_required_javascript();
$limit = 20;
if (!empty($CFG->navcourselimit)) {
$limit = $CFG->navcourselimit;
'expansionlimit' => $expansionlimit
);
$this->page->requires->string_for_js('viewallcourses', 'moodle');
- $this->page->requires->yui_module(array('core_dock', 'moodle-block_navigation-navigation'), 'M.block_navigation.init_add_tree', array($arguments));
+ $this->page->requires->yui_module('moodle-block_navigation-navigation', 'M.block_navigation.init_add_tree', array($arguments));
}
/**
* @return object $this->content
*/
function get_content() {
- global $CFG, $OUTPUT;
// First check if we have already generated, don't waste cycles
if ($this->contentgenerated === true) {
return $this->content;
+/**
+ * Navigation block JS.
+ *
+ * This file contains the Navigation block JS..
+ *
+ * @module moodle-block_navigation-navigation
+ */
+
+/**
+ * This namespace will contain all of the contents of the navigation blocks
+ * global navigation and settings.
+ * @namespace M
+ * @class block_navigation
+ * @static
+ */
+M.block_navigation = M.block_navigation || {};
+/**
+ * The number of expandable branches in existence.
+ *
+ * @property expandablebranchcount
+ * @protected
+ * @static
+ */
+M.block_navigation.expandablebranchcount = 1;
+/**
+ * The maximum number of courses to show as part of a branch.
+ *
+ * @property courselimit
+ * @protected
+ * @static
+ */
+M.block_navigation.courselimit = 20;
+/**
+ * Add new instance of navigation tree to tree collection
+ *
+ * @method init_add_tree
+ * @static
+ * @param {Object} properties
+ */
+M.block_navigation.init_add_tree = function(properties) {
+ if (properties.courselimit) {
+ this.courselimit = properties.courselimit;
+ }
+ new TREE(properties);
+};
+
/**
* A 'actionkey' Event to help with Y.delegate().
* The event consists of the left arrow, right arrow, enter and space keys.
* This event is delegated to branches in the navigation tree.
* The on() method to subscribe allows specifying the desired trigger actions as JSON.
*
- * Todo: This could be centralised, a similar Event is defined in blocks/dock.js
+ * @namespace M.block_navigation
+ * @class ActionKey
*/
Y.Event.define("actionkey", {
- // Webkit and IE repeat keydown when you hold down arrow keys.
+ // Webkit and IE repeat keydown when you hold down arrow keys.
// Opera links keypress to page scroll; others keydown.
// Firefox prevents page scroll via preventDefault() on either
// keydown or keypress.
_event: (Y.UA.webkit || Y.UA.ie) ? 'keydown' : 'keypress',
+ /**
+ * The keys to trigger on.
+ * @method _keys
+ */
_keys: {
//arrows
'37': 'collapse',
'39': 'expand',
- //(@todo: lrt/rtl/M.core_dock.cfg.orientation decision to assign arrow to meanings)
'32': 'toggle',
'13': 'enter'
},
+ /**
+ * Handles key events
+ * @method _keyHandler
+ * @param {EventFacade} e
+ * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
+ * @param {Object} args
+ */
_keyHandler: function (e, notifier, args) {
var actObj;
if (!args.actions) {
}
},
+ /**
+ * Subscribes to events.
+ * @method on
+ * @param {Node} node The node this subscription was applied to.
+ * @param {Subscription} sub The object tracking this subscription.
+ * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
+ */
on: function (node, sub, notifier) {
// subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
if (sub.args === null) {
}
},
+ /**
+ * Detaches an event listener
+ * @method detach
+ */
detach: function (node, sub) {
//detach our _detacher handle of the subscription made in on()
sub._detacher.detach();
},
+ /**
+ * Creates a delegated event listener.
+ * @method delegate
+ * @param {Node} node The node this subscription was applied to.
+ * @param {Subscription} sub The object tracking this subscription.
+ * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
+ * @param {String|function} filter Selector string or function that accpets an event object and returns null.
+ */
delegate: function (node, sub, notifier, filter) {
// subscribe to _event and ask keyHandler to handle with given args[0] (the desired actions).
if (sub.args === null) {
}
},
+ /**
+ * Detaches a delegated event listener.
+ * @method detachDelegate
+ * @param {Node} node The node this subscription was applied to.
+ * @param {Subscription} sub The object tracking this subscription.
+ * @param {SyntheticEvent.Notifier} notifier The notifier used to trigger the execution of subscribers
+ * @param {String|function} filter Selector string or function that accpets an event object and returns null.
+ */
detachDelegate: function (node, sub) {
sub._delegateDetacher.detach();
}
});
var EXPANSIONLIMIT_EVERYTHING = 0,
- //EXPANSIONLIMIT_COURSE = 20,
- //EXPANSIONLIMIT_SECTION = 30,
+ EXPANSIONLIMIT_COURSE = 20,
+ EXPANSIONLIMIT_SECTION = 30,
EXPANSIONLIMIT_ACTIVITY = 40;
-/**
- * Mappings for the different types of nodes coming from the navigation.
- * Copied from lib/navigationlib.php navigation_node constants.
- * @type object
- */
+// Mappings for the different types of nodes coming from the navigation.
+// Copied from lib/navigationlib.php navigation_node constants.
var NODETYPE = {
- /** @type int Root node = 0 */
+ // @type int Root node = 0
ROOTNODE : 0,
- /** @type int System context = 1 */
+ // @type int System context = 1
SYSTEM : 1,
- /** @type int Course category = 10 */
+ // @type int Course category = 10
CATEGORY : 10,
- /** @type int MYCATEGORY = 11 */
+ // @type int MYCATEGORY = 11
MYCATEGORY : 11,
- /** @type int Course = 20 */
+ // @type int Course = 20
COURSE : 20,
- /** @type int Course section = 30 */
+ // @type int Course section = 30
SECTION : 30,
- /** @type int Activity (course module) = 40 */
+ // @type int Activity (course module) = 40
ACTIVITY : 40,
- /** @type int Resource (course module = 50 */
+ // @type int Resource (course module = 50
RESOURCE : 50,
- /** @type int Custom node (could be anything) = 60 */
+ // @type int Custom node (could be anything) = 60
CUSTOM : 60,
- /** @type int Setting = 70 */
+ // @type int Setting = 70
SETTING : 70,
- /** @type int User context = 80 */
+ // @type int User context = 80
USER : 80,
- /** @type int Container = 90 */
+ // @type int Container = 90
CONTAINER : 90
};
*
* This class establishes the tree initially, creating expandable branches as
* required, and delegating the expand/collapse event.
+ *
+ * @namespace M.block_navigation
+ * @class Tree
+ * @constructor
+ * @extends Y.Base
*/
var TREE = function() {
TREE.superclass.constructor.apply(this, arguments);
TREE.prototype = {
/**
* The tree's ID, normally its block instance id.
+ * @property id
+ * @type Int
+ * @protected
*/
id : null,
/**
* An array of initialised branches.
+ * @property branches
+ * @type Array
+ * @protected
*/
branches : [],
/**
* Initialise the tree object when its first created.
+ * @method initializer
+ * @param {Object} config
*/
initializer : function(config) {
- this.id = config.id;
+ Y.log('Initialising navigation block tree', 'note', 'moodle-block_navigation');
+
+ this.id = parseInt(config.id, 10);
var node = Y.one('#inst'+config.id);
Y.delegate('click', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
Y.delegate('actionkey', this.fire_branch_action, node.one('.block_tree'), '.tree_item.branch[data-expandable]', this);
}
-
- // Call the generic blocks init method to add all the generic stuff
- if (this.get('candock')) {
- this.initialise_block(Y, node);
- }
},
/**
* Fire actions for a branch when an event occurs.
+ * @method fire_branch_action
+ * @param {EventFacade} event
*/
fire_branch_action : function(event) {
var id = event.currentTarget.getAttribute('id');
/**
* This is a callback function responsible for expanding and collapsing the
* branches of the tree. It is delegated to rather than multiple event handles.
+ * @method toggleExpansion
+ * @param {EventFacade} e
+ * @return Boolean
*/
toggleExpansion : function(e) {
// First check if they managed to click on the li iteslf, then find the closest
// If this block can dock tell the dock to resize if required and check
// the width on the dock panel in case it is presently in use.
- if (this.get('candock')) {
- M.core_dock.resize();
- var panel = M.core_dock.getPanel();
- if (panel.visible) {
- panel.correctWidth();
- }
+ if (this.get('candock') && M.core.dock.notifyBlockChange) {
+ M.core.dock.notifyBlockChange(this.id);
}
+ return true;
+
}
};
// The tree extends the YUI base foundation.
Y.extend(TREE, Y.Base, TREE.prototype, {
NAME : 'navigation-tree',
ATTRS : {
- instance : {
- value : null
- },
+ /**
+ * True if the block can dock.
+ * @attribute candock
+ * @type Boolean
+ */
candock : {
validator : Y.Lang.isBool,
value : false
},
+ /**
+ * If set to true nodes will be opened/closed in an accordian fashion.
+ * @attribute accordian
+ * @type Boolean
+ */
accordian : {
validator : Y.Lang.isBool,
value : false
},
+ /**
+ * The nodes that get shown.
+ * @attribute expansionlimit
+ * @type Integer
+ */
expansionlimit : {
value : 0,
setter : function(val) {
- return parseInt(val, 10);
+ val = parseInt(val, 10);
+ if (val !== EXPANSIONLIMIT_EVERYTHING &&
+ val !== EXPANSIONLIMIT_COURSE &&
+ val !== EXPANSIONLIMIT_SECTION &&
+ val !== EXPANSIONLIMIT_ACTIVITY) {
+ val = EXPANSIONLIMIT_EVERYTHING;
+ }
+ return val;
}
}
}
});
-if (M.core_dock && M.core_dock.genericblock) {
- Y.augment(TREE, M.core_dock.genericblock);
-}
/**
- * The tree branch class.
+ * The Branch class.
+ *
* This class is used to manage a tree branch, in particular its ability to load
* its contents by AJAX.
+ *
+ * @namespace M.block_navigation
+ * @class Branch
+ * @constructor
+ * @extends Y.Base
*/
BRANCH = function() {
BRANCH.superclass.constructor.apply(this, arguments);
BRANCH.prototype = {
/**
* The node for this branch (p)
+ * @property node
+ * @type Node
+ * @protected
*/
node : null,
/**
* Initialises the branch when it is first created.
+ * @method initializer
+ * @param {Object} config
*/
initializer : function(config) {
var i,
}
}
// Get the node for this branch
- this.node = Y.one('#', this.get('id'));
- // Now check whether the branch is not expandable because of the expansionlimit
+ this.node = Y.one('#'+this.get('id'));
var expansionlimit = this.get('tree').get('expansionlimit');
var type = this.get('type');
if (expansionlimit !== EXPANSIONLIMIT_EVERYTHING && type >= expansionlimit && type <= EXPANSIONLIMIT_ACTIVITY) {
*
* This function creates a DOM structure for the branch and then injects
* it into the navigation tree at the correct point.
+ *
+ * @method draw
+ * @chainable
+ * @param {Node} element
+ * @return Branch
*/
draw : function(element) {
// Prepare the icon, should be an object representing a pix_icon
var branchicon = false;
var icon = this.get('icon');
- if (icon && (!isbranch || this.get('type') == NODETYPE.ACTIVITY)) {
+ if (icon && (!isbranch || this.get('type') === NODETYPE.ACTIVITY)) {
branchicon = Y.Node.create('<img alt="" />');
branchicon.setAttribute('src', M.util.image_url(icon.pix, icon.component));
branchli.addClass('item_with_icon');
},
/**
* Gets the UL element that children for this branch should be inserted into.
+ * @method getChildrenUL
+ * @return Node
*/
getChildrenUL : function() {
var ul = this.node.next('ul');
*
* This function calls ajaxProcessResponse with the result of the AJAX
* request made here.
+ *
+ * @method ajaxLoad
+ * @param {EventFacade} e
+ * @return Bool
*/
ajaxLoad : function(e) {
if (e.type === 'actionkey' && e.action !== 'enter') {
// We've already loaded this stuff.
return true;
}
+ Y.log('Loading navigation branch via AJAX: '+this.get('key'), 'note', 'moodle-block_navigation');
this.node.addClass('loadingbranch');
var params = {
/**
* Processes an AJAX request to load the content of this branch through
* AJAX.
+ *
+ * @method ajaxProcessResponse
+ * @param {Int} tid The transaction id.
+ * @param {Object} outcome
+ * @return Boolean
*/
ajaxProcessResponse : function(tid, outcome) {
this.node.removeClass('loadingbranch');
var coursecount = 0;
for (var i in object.children) {
if (typeof(object.children[i])==='object') {
- if (object.children[i].type == NODETYPE.COURSE) {
+ if (object.children[i].type === NODETYPE.COURSE) {
coursecount++;
}
this.addChild(object.children[i]);
}
}
- if ((this.get('type') == NODETYPE.CATEGORY || this.get('type') == NODETYPE.ROOTNODE || this.get('type') == NODETYPE.MYCATEGORY)
+ if ((this.get('type') === NODETYPE.CATEGORY || this.get('type') === NODETYPE.ROOTNODE || this.get('type') === NODETYPE.MYCATEGORY)
&& coursecount >= M.block_navigation.courselimit) {
this.addViewAllCoursesChild(this);
}
+ Y.log('AJAX loading complete.', 'note', 'moodle-block_navigation');
+ // If this block can dock tell the dock to resize if required and check
+ // the width on the dock panel in case it is presently in use.
+ if (this.get('tree').get('candock') && M.core.dock.notifyBlockChange) {
+ M.core.dock.notifyBlockChange(this.get('tree').id);
+ }
return true;
}
+ Y.log('AJAX loading complete but there were no children.', 'note', 'moodle-block_navigation');
} catch (ex) {
- // If we got here then there was an error parsing the result
+ // If we got here then there was an error parsing the result.
+ Y.log('Error parsing AJAX response or adding branches to the navigation tree', 'error', 'moodle-block_navigation');
}
// The branch is empty so class it accordingly
this.node.replaceClass('branch', 'emptybranch');
/**
* Turns the branch object passed to the method into a proper branch object
* and then adds it as a child of this branch.
+ *
+ * @method addChild
+ * @param {Object} branchobj
+ * @return Boolean
*/
addChild : function(branchobj) {
// Make the new branch into an object
var count = 0, i, children = branch.get('children');
for (i in children) {
// Add each branch to the tree
- if (children[i].type == NODETYPE.COURSE) {
+ if (children[i].type === NODETYPE.COURSE) {
count++;
}
if (typeof(children[i]) === 'object') {
branch.addChild(children[i]);
}
}
- if ((branch.get('type') == NODETYPE.CATEGORY || branch.get('type') == NODETYPE.MYCATEGORY)
+ if ((branch.get('type') === NODETYPE.CATEGORY || branch.get('type') === NODETYPE.MYCATEGORY)
&& count >= M.block_navigation.courselimit) {
this.addViewAllCoursesChild(branch);
}
/**
* Add a link to view all courses in a category
+ *
+ * @method addViewAllCoursesChild
+ * @param {BRANCH} branch
*/
addViewAllCoursesChild: function(branch) {
var url = null;
- if (branch.get('type') == NODETYPE.ROOTNODE) {
+ if (branch.get('type') === NODETYPE.ROOTNODE) {
if (branch.get('key') === 'mycourses') {
url = M.cfg.wwwroot + '/my';
} else {
Y.extend(BRANCH, Y.Base, BRANCH.prototype, {
NAME : 'navigation-branch',
ATTRS : {
+ /**
+ * The Tree this branch belongs to.
+ * @attribute tree
+ * @type TREE
+ * @required
+ * @writeOnce
+ */
tree : {
+ writeOnce : 'initOnly',
validator : Y.Lang.isObject
},
+ /**
+ * The name of this branch.
+ * @attribute name
+ * @type String
+ */
name : {
value : '',
validator : Y.Lang.isString,
return val.replace(/\n/g, '<br />');
}
},
+ /**
+ * The title to use for this branch.
+ * @attribute title
+ * @type String
+ */
title : {
value : '',
validator : Y.Lang.isString
},
+ /**
+ * The ID of this branch.
+ * The ID and Type should always form a unique pair.
+ * @attribute id
+ * @type String
+ */
id : {
value : '',
validator : Y.Lang.isString,